Elasticsearch Performans Optimizasyonu: Hız ve Verimlilik İçin En İyi Yöntemler

Prodüksiyonda Elasticsearch kümesi yönetmek, ilk başta masum görünen ama zamanla sizi gece 2’de uyandıran cinsten bir iş. Ben de bu yazıyı tam olarak o yüzden yazıyorum: gecenin bir yarısı “heap kullanımı %95’i geçti” alarmıyla uyandıktan sonra öğrendiklerimi paylaşmak için. ELK Stack kuruyorsunuz, loglar akıyor, Kibana’da güzel dashboardlar yapıyorsunuz ve her şey yolunda görünüyor. Sonra bir gün trafik artıyor ya da log hacmi patliyor ve Elasticsearch size “yeter artık” diyor. İşte bu noktaya gelmemek, ya da gelince ne yapacağınızı bilmek için bu yazıyı okuyun.

Temel Performans Metriklerini Anlamak

Optimizasyona başlamadan önce neyi ölçeceğinizi bilmeniz gerekiyor. Elasticsearch’ün kendi API’leri bu konuda oldukça zengin.

# Cluster sağlık durumu
curl -X GET "localhost:9200/_cluster/health?pretty"

# Node bazlı istatistikler
curl -X GET "localhost:9200/_nodes/stats?pretty"

# Hangi index ne kadar kaynak tüketiyor
curl -X GET "localhost:9200/_cat/indices?v&s=store.size:desc"

# Shard dağılımını görmek için
curl -X GET "localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node,store"

Cluster durumu yeşil değilse optimizasyondan önce bunu düzeltin. Sarı veya kırmızı cluster’da yapılan optimizasyon çalışmaları genellikle boşa gider. Shard allocation sorunları, node’lar arası dengesizlikler ve disk doluluğu ilk bakmanız gereken şeyler.

_cat/indices çıktısında docs.deleted değerine dikkat edin. Bu değer docs.count‘un %20-30’unu geçiyorsa segment merge işlemi yapmanız gerekiyor demektir. Deleted document’lar disk alanı tüketir ve arama performansını düşürür.

JVM Heap Ayarları: En Çok Yapılan Hatalar

Elasticsearch performans sorunlarının büyük çoğunluğu yanlış JVM heap konfigürasyonundan kaynaklanır. İki temel kural var ve bu ikisini aklınızdan çıkarmayın:

Kural 1: Heap size’ı toplam RAM’in %50’sini geçirmeyin. Geri kalan %50, OS file system cache için kritik önem taşır. Elasticsearch, Lucene indexlerini okumak için bu cache’e çok güvenir.

Kural 2: Heap size’ı 32GB’ın altında tutun. Bu sınırın üzerinde JVM’in compressed object pointer optimizasyonu devreye girmez ve bellek kullanımı verimsizleşir.

# /etc/elasticsearch/jvm.options dosyasını düzenleyin
# 64GB RAM'li bir sunucu için
-Xms31g
-Xmx31g

Xms ve Xmx değerlerini her zaman eşit tutun. Farklı değerler verirseniz JVM heap’i büyütmeye çalışırken pause yaşar ve bu production’da ciddi gecikmelere yol açar.

GC loglarını mutlaka aktif edin ve izleyin:

# jvm.options dosyasına ekleyin
-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m

GC pause süresi 500ms’yi geçiyorsa ciddi bir sorun var demektir. 1 saniyenin üzerindeyse artık emergency durumundasınız. Bu durumda genellikle heap boyutunu küçültmek paradoksal ama işe yarıyor; çünkü daha küçük heap daha hızlı toplanır.

Index Ayarları ve Shard Stratejisi

Shard sayısı belki de Elasticsearch’te en çok yanlış yapılandırılan şey. “Daha fazla shard daha fazla parallelism” mantığıyla hareket edip her index için 10-20 shard açmak, small cluster’larda felakete davet etmektir.

Genel bir kural olarak, her shard için en az 1 GB, ideal olarak 10-50 GB arası veri düşünün. Bir node üzerinde çok fazla küçük shard bulunması, overhead’i artırır ve arama performansını düşürür.

# Yeni index için optimum shard ayarı
curl -X PUT "localhost:9200/uygulama-logs-2024" -H 'Content-Type: application/json' -d'
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "index.refresh_interval": "30s",
    "index.translog.durability": "async",
    "index.translog.sync_interval": "30s"
  }
}'

Log yönetimi senaryolarında refresh_interval değerini artırmak ciddi yazma performansı kazancı sağlar. Default değer olan 1 saniye, gerçek zamanlı uygulama için güzel ama log analizi için gereğinden sık. 30 saniye ya da 1 dakita yapmak, indexing throughput’unu %30-40 artırabilir.

translog.durability değerini async yapmanın riski şu: node beklenmedik şekilde kapanırsa son birkaç saniyelik veri kaybolabilir. Log senaryolarında bu genellikle kabul edilebilir bir trade-off. Finansal veriler için bu ayarı yapmayın.

Index Template Kullanmak

Her index’i tek tek yapılandırmak yerine template kullanın. Özellikle ILM (Index Lifecycle Management) ile birlikte kullanıldığında hayat kurtarır:

curl -X PUT "localhost:9200/_index_template/uygulama-logs-template" -H 'Content-Type: application/json' -d'
{
  "index_patterns": ["uygulama-logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 2,
      "number_of_replicas": 1,
      "index.refresh_interval": "30s",
      "index.codec": "best_compression"
    },
    "mappings": {
      "dynamic": "strict",
      "properties": {
        "@timestamp": { "type": "date" },
        "level": { "type": "keyword" },
        "message": { "type": "text", "norms": false },
        "service": { "type": "keyword" },
        "host": { "type": "keyword" }
      }
    }
  }
}'

dynamic: strict ayarı önemli. Mapping explosion denen olayı engelliyor. Beklenmedik field’lar geldiğinde Elasticsearch bunları otomatik olarak yeni field olarak eklemek yerine hata döndürüyor. Böylece index içinde yüzlerce gereksiz field birikmesinin önüne geçiyorsunuz.

norms: false ayarı, TF/IDF skor hesaplaması için kullanılan normalizasyon verilerini depolamaktan vazgeçiyor. Log mesajlarında full-text relevance scoring yapmadığınız için bu alanı kapatmak shard başına megabytes tasarruf sağlıyor.

Mapping Optimizasyonu

Yanlış mapping, hem disk hem de memory açısından ciddi israf kaynağı. Gerçek bir senaryodan bahsedeyim: Bir e-ticaret şirketinin Nginx access logları için açılan index’te request_id field’ı text olarak maplenmiş. Bu field üzerinde hiçbir full-text arama yapılmıyor, sadece exact match lazım. keyword yerine text kullanmak, inverted index yapısı nedeniyle gereksiz yere 3-4 kat fazla disk alanı tüketiyor.

# Mevcut mapping'i kontrol et
curl -X GET "localhost:9200/uygulama-logs-2024/_mapping?pretty"

# Analiz edilmesi gerekmeyen fakat sorgulanan field'lar için keyword kullan
# Sayısal field'lar için doğru tipi seç
curl -X PUT "localhost:9200/uygulama-logs-optimized" -H 'Content-Type: application/json' -d'
{
  "mappings": {
    "properties": {
      "status_code": { "type": "short" },
      "response_time_ms": { "type": "integer" },
      "bytes_sent": { "type": "long" },
      "request_id": { "type": "keyword", "doc_values": false, "index": false },
      "url_path": { "type": "keyword" },
      "user_agent": { 
        "type": "text", 
        "fields": {
          "keyword": { "type": "keyword", "ignore_above": 256 }
        }
      }
    }
  }
}'

doc_values: false ve index: false kombinasyonu, sadece log tutma amacıyla kullandığınız ve üzerinde hiç aggregation ya da filtering yapmadığınız field’lar için kullanın. request_id gibi correlation için kullandığınız ama sıralama yapmadığınız field’larda doc_values: false disk tasarrufu sağlar.

Query Optimizasyonu

İyi konfigüre edilmiş bir cluster bile kötü sorgularla çökebilir. Kibana üzerinden çalışan analistler bazen masum görünen ama cluster’ı dizlerinin üstüne çökerten sorgular yazabiliyor.

# Slow query log'u aktif et
curl -X PUT "localhost:9200/uygulama-logs-*/_settings" -H 'Content-Type: application/json' -d'
{
  "index.search.slowlog.threshold.query.warn": "5s",
  "index.search.slowlog.threshold.query.info": "2s",
  "index.search.slowlog.threshold.fetch.warn": "1s",
  "index.indexing.slowlog.threshold.index.warn": "2s"
}'

Slow query logları aktifleştirildikten sonra /var/log/elasticsearch/ altındaki *_index_search_slowlog.json dosyalarını izleyin. Burada gördüğünüz sorguları Profile API ile analiz edin:

# Sorgu performansını profile API ile analiz et
curl -X GET "localhost:9200/uygulama-logs-*/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "profile": true,
  "query": {
    "bool": {
      "filter": [
        { "term": { "level": "ERROR" } },
        { "range": { "@timestamp": { "gte": "now-1h" } } }
      ]
    }
  },
  "aggs": {
    "services": {
      "terms": { "field": "service", "size": 20 }
    }
  }
}'

must yerine filter kullanmak, skor hesaplamasını devre dışı bırakır ve sonuçların cache’lenmesini sağlar. Log sorgularında relevance score’a genellikle ihtiyaç duymazsınız, filter context her zaman tercih edin.

Wildcard ve regex sorgularından mümkün olduğunca kaçının. Özellikle ile başlayan wildcard (error* gibi) tüm term’leri taramak zorunda kalır ve çok yavaş çalışır. Bunu yapmanız gerekiyorsa, index oluşturulurken o field için analyzer yapılandırın.

ILM ile Veri Yaşam Döngüsü Yönetimi

Performans optimizasyonunun gözden kaçan ama belki de en önemli parçası, eski verilerden zamanında kurtulmak. Terabaytlarca eski log tutan bir cluster hem yavaş hem pahalıdır.

# ILM policy oluştur
curl -X PUT "localhost:9200/_ilm/policy/log-lifecycle-policy" -H 'Content-Type: application/json' -d'
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "30gb",
            "max_age": "1d"
          },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "3d",
        "actions": {
          "forcemerge": { "max_num_segments": 1 },
          "shrink": { "number_of_shards": 1 },
          "allocate": { "require": { "data": "warm" } },
          "set_priority": { "priority": 50 }
        }
      },
      "cold": {
        "min_age": "14d",
        "actions": {
          "allocate": { "require": { "data": "cold" } },
          "set_priority": { "priority": 0 }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}'

Bu ILM policy ile veriler şu şekilde hareket ediyor: Aktif yazma olan hot node’lardan 3 gün sonra warm node’lara geçiyor, 14 gün sonra cold node’lara, 30 gün sonra siliniyor. Warm aşamasında forcemerge ile tek segment’e düşürülüyor ve shrink ile shard sayısı azaltılıyor. Bu konfigürasyon, hot node’larındaki SSD disk kullanımını dramatik biçimde azaltıyor.

Thread Pool ve Circuit Breaker Ayarları

# Thread pool durumunu izle
curl -X GET "localhost:9200/_cat/thread_pool?v&h=node_name,name,active,rejected,completed"

rejected kolonunda artan değerler görüyorsanız o thread pool dolup taşıyor demektir. search thread pool’u için rejected değerleri artıyorsa, sorgu yükü fazla ya da sorgular çok uzun sürüyor anlamına gelir.

Circuit breaker ayarları, Elasticsearch’ün OOM (Out of Memory) ile çökmesini önleyen güvenlik mekanizması:

# elasticsearch.yml içine eklenecek ayarlar
# (config dosyasını düzenleyin, API üzerinden de yapılabilir)

curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
  "persistent": {
    "indices.breaker.total.limit": "70%",
    "indices.breaker.fielddata.limit": "40%",
    "indices.breaker.request.limit": "40%",
    "network.breaker.inflight_requests.limit": "100%"
  }
}'

FieldData breaker özellikle önemli. Text field’lar üzerinde aggregation yapmaya çalışırsanız fielddata yüklenir ve bu ciddi memory tüketimi demektir. Bu yüzden aggregation yapacağınız field’ların mapping’inde keyword kullandığınızdan emin olun.

Disk I/O ve Depolama Optimizasyonu

Elasticsearch’ün I/O’ya en çok binen operasyonları segment merge işlemleridir. Bunu kontrol altına almak için:

# Merge scheduling throttling
curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
  "persistent": {
    "indices.store.throttle.max_bytes_per_sec": "200mb"
  }
}'

# Index açıkken manuel segment merge (off-peak saatlerinde)
curl -X POST "localhost:9200/uygulama-logs-2024.01.15/_forcemerge?max_num_segments=1&only_expunge_deletes=true"

only_expunge_deletes=true parametresi sadece silinmiş dökümanlardan kalan segmentleri temizler, tüm index’i yeniden yazmaz. Aktif yazma olmayan eski indexler için max_num_segments=1 ile tam merge yapabilirsiniz. Bu işlem sonrasında hem disk alanı kazanırsınız hem de arama hızlanır.

best_compression codec’i index template’lerinizde mutlaka açın. Varsayılan LZ4 compression ile karşılaştırıldığında disk kullanımını %30-40 azaltabilir, bunun karşılığında biraz daha CPU kullanır. Log verileri genellikle tekrarlı karakter yapısı nedeniyle çok iyi sıkışır.

Monitoring ve Alerting Kurulumu

Kendi kendini izlemeyen bir cluster, sizi sabah 2’de uyandırır. Elasticsearch’ün built-in monitoring’ini aktifleştirip Kibana üzerinden alarm kurun:

# Cluster monitoring'i aktifleştir
curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
  "persistent": {
    "xpack.monitoring.collection.enabled": true,
    "xpack.monitoring.elasticsearch.collection.enabled": true
  }
}'

İzlemeniz gereken kritik metrikler:

  • JVM Heap Kullanımı: %75 üzeri sarı alarm, %85 üzeri kırmızı alarm
  • CPU Kullanımı: 5 dakika ortalaması %70 üzeri dikkat, %85 üzeri aksiyon gerektirir
  • Disk Kullanımı: %80 üzeri Elasticsearch otomatik readonly moda geçer, bunu beklemeden %70’de müdahale edin
  • GC Duration: Dakikada toplam 10 saniyeyi geçen GC süreleri ciddi performans sorunu işareti
  • Search Latency: P99 arama süresi baseline’ınızın 2 katını geçiyor mu?
  • Indexing Rate: Düşüyorsa indexing bottleneck’i araştırın
  • Pending Tasks: Cluster state güncellemesi bekleyen görevler birikirken cluster ağırlaşır

Gerçek Dünya Senaryosu: Logstash Pipeline Optimizasyonu

Elasticsearch’e veri gönderen Logstash pipeline’ını da optimize etmezseniz, Elasticsearch tarafını ne kadar iyi yaparsanız yapın dar boğaz oraya kayar. Bir Logstash konfigürasyon örneği:

# /etc/logstash/conf.d/elasticsearch-output.conf

output {
  elasticsearch {
    hosts => ["es-node1:9200", "es-node2:9200", "es-node3:9200"]
    index => "uygulama-logs-%{+YYYY.MM.dd}"
    template_name => "uygulama-logs-template"
    template_overwrite => false
    
    # Bulk indexing ayarları
    bulk_max_size => 5000
    flush_size => 5000
    
    # Connection pool
    pool_max => 1000
    pool_max_per_route => 100
    
    # Retry ayarları
    retry_on_conflict => 3
    
    # Compression
    http_compression => true
    
    # Workers
    workers => 4
  }
}

bulk_max_size ve flush_size değerlerini sisteminizin document boyutuna göre ayarlayın. Küçük document’lar için bu değeri artırabilirsiniz. 5000 document başlangıç için iyi bir nokta ama 10MB bulk request boyutunu geçmemeye dikkat edin.

http_compression => true ayarı, Logstash ile Elasticsearch arasındaki network trafiğini azaltır. Log verisi gibi metin bazlı içeriği çok iyi sıkıştırır ve ağ darboğazı olan ortamlarda belirgin fark yaratır.

Sonuç

Elasticsearch optimizasyonu tek seferlik bir işlem değil, sürekli izleme ve ayarlama gerektiren bir süreç. Ama bu yazıda anlattıklarını sırayla uygularsanız çoğu ortamda performansın ciddi biçimde iyileştiğini göreceksiniz.

Öncelik sırasıyla yapmanızı önerdiklerim şunlar: Önce mevcut durumu ölçün, neyi optimize edeceğinizi bilmeden başlamayın. Sonra JVM heap’i doğru ayarlayın, bu tek başına birçok problemi çözer. Mapping’lerinizi gözden geçirin, yanlış tipler her şeyi etkiliyor. ILM politikası yoksa hemen kurun, büyüyen veri en büyük düşmanınız. Son olarak slow query log’larını aktifleştirin ve kötü sorguları bulup düzeltin.

Prodüksiyonda bir şeyi değiştirmeden önce staging ortamında test edin. Bazı ayarlar özellikle shard ve mapping değişiklikleri mevcut data’yı doğrudan etkiler ve geri alınması çok zor olabilir. Yedek almayı ve index snapshot’larını ihmal etmeyin. Geceleri rahat uyumak istiyorsanız monitoring olmadan tek bir prodüksiyon Elasticsearch cluster’ı çalıştırmayın.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir