Elasticsearch ile Log Saklama Maliyetini Optimize Etme
Bir Elasticsearch kümesi işletiyorsanız ve depolama maliyetleriniz her ay biraz daha şişiyorsa, yalnız değilsiniz. Geçen yıl bir e-ticaret müşterisinin ELK altyapısına baktığımda, günlük 80 GB log yazıyorlardı ve bunun yaklaşık %60’ı “belki lazım olur” mantığıyla tutulan nginx access loglarıydı. Retention policy yoktu, ILM yoktu, shard sayısı default’ta bırakılmıştı. Aylık storage maliyeti 4 katına çıkmıştı. Bu yazıda o projeden ve benzer deneyimlerden öğrendiklerimi aktaracağım.
Sorunun Köküne Bakmak
Elasticsearch’te maliyet optimizasyonu denilince akla ilk gelen şey “daha ucuz disk al” oluyor. Ama asıl mesele bu değil. Asıl mesele şu: neyi sakladığınızı ve neden sakladığınızı bilmiyorsanız, hiçbir donanım yatırımı sizi kurtarmaz.
Önce şu soruları yanıtlamanız gerekiyor:
- Hangi logları gerçekten analiz ediyorsunuz?
- Hangi loglar yalnızca compliance ya da audit amacıyla tutuluyor?
- 30 günden eski bir loga kaç kez baktınız?
- Duplicate field’larınız var mı? (Bunu sormak zorundaydım çünkü %80 ihtimalle var)
Bu soruları yanıtlamadan yapacağınız her optimizasyon, ilerleyen ay yeniden aynı problemi yaşatacak.
Index Lifecycle Management ile Başlayın
ILM, Elasticsearch’in en güçlü maliyet kontrol araçlarından biri. Doğru kurgulandığında indeksleri otomatik olarak hot -> warm -> cold -> delete aşamalarına taşıyabilirsiniz.
Tipik bir ILM policy örneği şöyle görünür:
curl -X PUT "localhost:9200/_ilm/policy/log-policy"
-H 'Content-Type: application/json'
-d '{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d"
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "3d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
},
"set_priority": {
"priority": 50
}
}
},
"cold": {
"min_age": "14d",
"actions": {
"freeze": {},
"set_priority": {
"priority": 0
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}'
Burada dikkat edilmesi gereken nokta: min_age değerleri rollover zamanından itibaren değil, indeks oluşturulma zamanından itibaren hesaplanır. Bu yüzden rollover sıklığınızı ve phase geçiş sürelerinizi birlikte planlamanız gerekiyor.
Policy’yi template’e bağlamayı da unutmayın:
curl -X PUT "localhost:9200/_index_template/logs-template"
-H 'Content-Type: application/json'
-d '{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1,
"index.lifecycle.name": "log-policy",
"index.lifecycle.rollover_alias": "logs"
}
}
}'
Shard Sayısını Doğru Ayarlamak
Bu konuda yıllar içinde gördüğüm en yaygın hata şu: insanlar default shard sayısını olduğu gibi bırakıyor ya da “daha fazla shard = daha iyi performans” yanılgısına düşüyor. Elasticsearch’te her shard bir Lucene instance’ıdır ve bellek tüketir. 1000 shard’lık bir küme, master node’unuzu felç edebilir.
Genel kural olarak her shard 10 GB ile 50 GB arasında olmalıdır. Günde 5 GB log yazıyorsanız, tek shard yeterlidir. Günde 200 GB yazıyorsanız, o zaman 4-5 shard mantıklı olabilir.
Mevcut shard durumunuzu görmek için:
curl -s "localhost:9200/_cat/shards?v&h=index,shard,prirep,state,docs,store,node"
| sort -k5 -rn
| head -20
Aşırı küçük shard’ları tespit etmek için şunu kullanabilirsiniz:
curl -s "localhost:9200/_cat/indices?v&h=index,pri,rep,docs.count,store.size,pri.store.size"
| awk '$6 != "" && $6 ~ /[0-9]+/ {print $0}'
| sort -k6 -h
Warm veya cold tier’daki indeksleri shrink etmek için de ILM’e güvenin, manuel yapmaya çalışmayın.
Mapping Optimizasyonu: En Çok Göz Ardı Edilen Alan
Her field için Elasticsearch varsayılan olarak hem text hem de keyword tipinde index oluşturur. Bu “dynamic mapping” özelliğidir ve kontrolsüz bırakıldığında disk kullanımını önemli ölçüde artırır.
Örneğin, bir nginx access log’unda request_uri field’ınız varsa ve bunu hiçbir zaman full-text search ile sorgulamıyorsanız, sadece keyword yeterlidir. Ama Elasticsearch ikisini de tutuyor.
Bunu kontrol altına almak için explicit mapping kullanın:
curl -X PUT "localhost:9200/_index_template/nginx-logs-template"
-H 'Content-Type: application/json'
-d '{
"index_patterns": ["nginx-logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"codec": "best_compression"
},
"mappings": {
"dynamic": "strict",
"properties": {
"@timestamp": { "type": "date" },
"status_code": { "type": "short" },
"response_time": { "type": "float" },
"client_ip": { "type": "ip" },
"method": { "type": "keyword" },
"request_uri": { "type": "keyword" },
"bytes_sent": { "type": "integer" },
"user_agent": { "type": "keyword" },
"message": { "type": "text", "index": false }
}
}
}
}'
Burada birkaç kritik nokta var:
"dynamic": "strict": Tanımlanmamış field gelirse hata fırlatır, sessizce yeni field açmaz."codec": "best_compression": DEFLATE yerine LZ4 sıkıştırma kullanır, disk kullanımını %30-40 azaltabilir."index": false:messagefield’ı saklanır ama index’e eklenmez. Eğer bu field üzerinde search yapmıyorsanız bu ayar işe yarar.
Logstash Tarafında Veri Temizleme
Elasticsearch’e ulaşmadan önce gereksiz veriyi temizlemek, hem depolama hem de ingest maliyetini düşürür. Logstash pipeline’ınıza birkaç basit kural eklemeniz yeterli.
# /etc/logstash/conf.d/nginx-filter.conf
filter {
if [type] == "nginx" {
# Sağlık kontrollerini at
if [request_uri] =~ /^/health/ or [request_uri] =~ /^/ping/ {
drop { }
}
# Static asset loglarını at (isteğe bağlı)
if [request_uri] =~ /.(css|js|png|jpg|ico|woff)(?.*)?$/ {
drop { }
}
# Gereksiz field'ları kaldır
mutate {
remove_field => [
"beat",
"input_type",
"offset",
"prospector",
"log",
"ecs",
"agent",
"tags"
]
}
# user_agent'ı sadece browser adına indirge
useragent {
source => "user_agent"
target => "ua"
}
mutate {
remove_field => ["user_agent"]
rename => { "[ua][name]" => "browser" }
remove_field => ["ua"]
}
}
}
Bu pipeline’da health check endpoint’lerine gelen istekleri tamamen siliyoruz. Kubernetes ortamında bu tek başına günlük log hacmini %10-15 azaltabilir. Liveness ve readiness probe’lardan gelen requestler Kubernetes cluster büyüklüğüne göre günde milyonlarca satıra ulaşabiliyor.
Rollup ve Data Stream Kullanımı
Eski veriler için aggregation yapmak istiyorsunuz ama ham veriyi tutmak istemiyorsunuz. İşte tam burada Rollup devreye giriyor.
30 günden eski nginx logları için saatlik bazda toplam request sayısı, ortalama response time ve toplam hata sayısı yeterli olabilir. Ham logları silip bu aggregated veriyi tutarsanız boyutu %99 küçültürsünüz.
curl -X PUT "localhost:9200/_rollup/job/nginx-hourly-rollup"
-H 'Content-Type: application/json'
-d '{
"index_pattern": "nginx-logs-*",
"rollup_index": "nginx-rollup",
"cron": "0 * * * * ?",
"page_size": 1000,
"groups": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": "1h",
"delay": "7d"
},
"terms": {
"fields": ["status_code", "method", "browser"]
}
},
"metrics": [
{
"field": "response_time",
"metrics": ["min", "max", "avg", "sum"]
},
{
"field": "bytes_sent",
"metrics": ["sum"]
}
]
}'
Rollup job’ı başlatmayı unutmayın:
curl -X POST "localhost:9200/_rollup/job/nginx-hourly-rollup/_start"
Data stream kullanıyorsanız (ki Elasticsearch 7.9+ için tavsiye edilir), ILM ile entegrasyonu çok daha temizdir ve rollover yönetimini otomatikleştirir.
Snapshot ile Soğuk Arşivleme
Cold tier veya delete yapmadan önce düşünülmesi gereken bir ara çözüm: S3 veya benzeri nesne depolamaya snapshot almak. Özellikle compliance gereksinimleri olan ortamlar için bu kritik.
# Önce repository tanımla
curl -X PUT "localhost:9200/_snapshot/s3-archive"
-H 'Content-Type: application/json'
-d '{
"type": "s3",
"settings": {
"bucket": "elasticsearch-log-archive",
"region": "eu-west-1",
"base_path": "snapshots",
"compress": true,
"chunk_size": "1gb",
"storage_class": "STANDARD_IA"
}
}'
# Sonra otomatik snapshot policy kur
curl -X PUT "localhost:9200/_slm/policy/monthly-archive"
-H 'Content-Type: application/json'
-d '{
"schedule": "0 30 2 1 * ?",
"name": "<monthly-snap-{now/M{yyyy.MM}}>",
"repository": "s3-archive",
"config": {
"indices": ["logs-*"],
"include_global_state": false,
"ignore_unavailable": true
},
"retention": {
"expire_after": "365d",
"min_count": 12,
"max_count": 24
}
}'
S3’te STANDARD_IA (Infrequent Access) veya GLACIER storage class kullanmak, hot S3 depolamaya kıyasla maliyeti ciddi ölçüde düşürür. Yılda 1-2 kez bakacağınız arşiv loglar için Glacier daha mantıklı bir tercih.
Küme Sağlığını Düzenli İzlemek
Tüm bu optimizasyonları yaptıktan sonra durumu izlemeye devam etmezseniz, zamanla yeniden aynı noktaya dönersiniz. Basit bir monitoring scripti işinizi görebilir:
#!/bin/bash
# /usr/local/bin/es-storage-report.sh
ES_HOST="localhost:9200"
DATE=$(date +%Y-%m-%d)
echo "=== Elasticsearch Storage Report: $DATE ==="
echo ""
echo "--- Top 10 Largest Indices ---"
curl -s "$ES_HOST/_cat/indices?v&h=index,docs.count,store.size,pri.store.size&s=store.size:desc"
| head -11
echo ""
echo "--- Shard Distribution ---"
curl -s "$ES_HOST/_cat/allocation?v&h=node,shards,disk.used,disk.avail,disk.percent"
echo ""
echo "--- ILM Policy Status ---"
curl -s "$ES_HOST/_cat/indices?v&h=index,ilm.policy,ilm.phase,ilm.action"
| grep -v "^$"
| head -20
echo ""
echo "--- Indices Older Than 25 Days ---"
curl -s "$ES_HOST/_cat/indices?v&h=index,creation.date.string,store.size"
| awk -v cutoff="$(date -d '25 days ago' +%Y-%m-%d)"
'$2 <= cutoff {print $0}'
Bu scripti cron’a ekleyin ve çıktısını bir yere loglayın. Haftalık bazda depolama büyümesini takip etmek, sürpriz maliyetlerin önüne geçer.
Gerçek Dünyadan Rakamlar
Yukarıda bahsettiğim e-ticaret projesinde uygulanan değişiklikler ve sonuçları:
- Mapping optimizasyonu +
best_compressioncodec: %28 depolama azalması - Health check ve static asset loglarının filtrelenmesi: %12 ingest azalması
- ILM policy (hot 3 gün, warm 14 gün, delete 30 gün): %65 toplam depolama azalması
- S3’e aylık snapshot + Elasticsearch’ten silme: Compliance gereksinimi karşılandı, küme boyutu sabit kaldı
Tek bir şeyin mucize yapmasını beklemeyin. Bu optimizasyonlar katmanlı çalışıyor; birbiriyle birleştiğinde etki çarpımsal oluyor.
Bir başka noktaya değinmek istiyorum: Elasticsearch’ü her şey için kullanmayın. Debug logları, trace logları, application event’leri hepsini aynı kümeye pompalayan mimariler görüyorum. Bazı log türleri için Loki çok daha uygun maliyetli bir alternatif. Özellikle yalnızca grep-style sorgu yapacağınız, aggregation gerekmeyecek log akışları için Elasticsearch overkill oluyor.
Monitoring Maliyetini Küçük Tutmak
Kibana Dashboard’larınız ve Watcher kurallarınız da kaynak tüketir. Özellikle yüksek frekanslı scheduled search’ler cluster üzerine ciddi yük bindiriyor.
Watcher yerine Kibana alerting kullanıyorsanız, check interval’ını dikkatli seçin. Her 1 dakikada çalışan 50 kural ile her 5 dakikada çalışan 50 kural arasında %80 fark var.
Kibana visualization’larında da “last 30 days” yerine “last 24 hours” ya da “last 7 days” default olarak ayarlanmış dashboard’lar, her açılışta çok daha az veri tarıyor. Bu küçük detay, yüzlerce kullanıcılı organizasyonlarda toplam query yükünü önemli ölçüde etkiliyor.
Sonuç
Elasticsearch log maliyetini optimize etmek tek seferlik bir proje değil, süregelen bir disiplin meselesi. Bugün yaptığınız düzenleme, altı ay sonra büyüyen veri hacimleri karşısında yetersiz kalabilir.
Önceliklendirme yapmanız gerekirse şu sırayla gidin:
- Önce ILM policy kurun, bu en hızlı kazancı verir
- Mapping’leri gözden geçirin ve
best_compressionaktif edin - Logstash veya Filebeat tarafında gereksiz verileri kaynak noktasında filtreleyin
- Eski veriler için snapshot politikası belirleyin
- Rollup ile uzun dönem aggregated veri tutun, ham veriyi silin
Depolama maliyeti kontrolden çıktıktan sonra düzeltmek zordur; çünkü hem retroaktif temizlik zahmetlidir hem de organizasyon içinde “silersek bir şey kaçırırız” korkusu yerleşmiştir. Doğru alışkanlıkları baştan kurmak, ilerleyen süreçte çok daha az başağrısı demektir.
