Ekim 4 2024

Cron İşlerini Bash ile Monitör Etme ve Alerting

Modern altyapılarda Kubernetes cronjob’ları, serverless scheduler’lar havada uçuşsa da, günün sonunda hepimizin bir yerlerde tıkır tıkır çalışan (ya da çalıştığını umduğu) emektar bir linux sunucusu ve içinde barınan klasik cron işleri vardır. Ancak cron’un en büyük problemi “sessiz ölüm” (silent failure) dediğimiz durumdur. Bir backup script’i patladığında, disk dolduğunda veya API key’in süresi geçtiğinde cron bunu size söylemez. Bu yazıda, kendi yazacağımız esnek bir bash wrapper script ile tüm cron işlerinizi merkezi olarak monitoring dünyasına entegre edecek, hata durumlarında anında alert üretecek ve üçüncü parti servislerle nasıl haberleştireceğimizi göreceğiz.

Klasik Cron Yaklaşımlarının Sorunu Nedir?

Hepimiz o yollardan geçtik. Bir cron işinin çıktısını almak için satırın sonuna efsanevi >> /var/log/myjob.log 2>&1 yönlendirmesini yazarız. Sonra ne mi olur? O log dosyası gigabaytlarca büyür, diski doldurur ve bir gün script hata verdiğinde kimsenin haberi olmaz. Çünkü logları aktif olarak okuyan kimse yoktur.

Eski usul Linux yöneticileri size “MTA (Mail Transfer Agent) kurun, cron hata aldığında lokal mail atsın” diyecektir. Yıl 2024+. ephemeral VM’lerin, cloud instance’larının cirit attığı bu çağda localhost mail kuyruklarıyla uğraşmak, postfix yapılandırmak ve spame düşen lokal mailleri takip etmek tam bir operasyonel işkencedir. Bizim daha proaktif, modern ve DevOps pratiklerine uygun bir çözüme ihtiyacımız var.

Wrapper Script Pattern Neden En İyi Çözümdür?

Elinizde 20 farklı cron script’i olduğunu düşünün. Her birinin içine tek tek Slack webhook entegrasyonu, hata yakalama (error handling) ve çalışma süresi ölçümü eklemek tam bir anti-pattern’dir. Kod tekrarı yaratır ve yarın öbür gün Slack yerine Teams’e geçildiğinde 20 farklı script’i update etmeniz gerekir.

Wrapper Script Pattern, çalıştırılacak asıl komutu sarmalayan, onun exit code’unu yakalayan, standart output (stdout) ve standart error (stderr) akışlarını yöneten merkezi bir aracıdır. Crontab içerisindeki tanımımız şuna benzer:

0 4 * * * /usr/local/bin/cron-wrapper.sh --job "db-backup" --cmd "/opt/scripts/backup.sh --force"

Bu yaklaşım sayesinde, asıl işi yapan script’in monitoring mantığından tamamen bağımsız (decoupled) kalmasını sağlarız. Script sadece işini yapar ve exit code döner; wrapper ise raporlamayı üstlenir.

Adım 1: Savaşçı Ruhlu Bash Wrapper Script

Lafı uzatmadan production ortamında güvenle kullanabileceğiniz, hata durumunda exit code’u ve logları yakalayan, zaman aşımı (timeout) kontrolü yapabilen wrapper script’imizi yazalım. Bu script’i /usr/local/bin/cron-wrapper.sh olarak kaydedebilirsiniz.

#!/usr/bin/env bash

# Robust Bash Ayarları
set -o nounset
set -o pipefail

# Değişken Tanımları
JOB_NAME=""
COMMAND=""
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-""}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-""}"

# Kullanım Yardımcısı
usage() {
    echo "Kullanım: $0 --job [JOB_NAME] --cmd [COMMAND]"
    exit 1
}

# Parametreleri Parse Etme
while [[ $# -gt 0 ]]; do
    case "$1" in
        --job)
            JOB_NAME="$2"
            shift 2
            ;;
        --cmd)
            COMMAND="$2"
            shift 2
            ;;
        *)
            usage
            ;;
    esac
done

if [[ -z "$JOB_NAME" || -z "$COMMAND" ]]; then
    usage
fi

# Geçici Log Dosyası Oluşturma (Güvenli yöntem)
LOG_FILE=$(mktemp /tmp/cron_wrapper_${JOB_NAME}.XXXXXX)

# Her durumda temizlik yapılması için trap kullanımı
cleanup() {
    rm -f "$LOG_FILE"
}
trap cleanup EXIT

# Zamanı Ölçmeye Başla
START_TIME=$(date +%s)

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job '$JOB_NAME' başlatılıyor..." >> "$LOG_FILE"
echo "Komut: $COMMAND" >> "$LOG_FILE"
echo "--------------------------------------------------" >> "$LOG_FILE"

# Komutu çalıştır ve tüm çıktıyı log dosyasına yönlendir
# set +e kullanıyoruz çünkü komut hata verirse wrapper script'in hemen çökmesini istemiyoruz,
# hatayı yakalayıp alert üreteceğiz.
set +e
eval "$COMMAND" >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
set -e

# Zamanı Hesapla
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

echo "--------------------------------------------------" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job bitti. Süre: ${DURATION}sn, Exit Code: $EXIT_CODE" >> "$LOG_FILE"

# Sonuçları Değerlendir
if [ $EXIT_CODE -eq 0 ]; then
    echo "Başarılı: $JOB_NAME sorunsuz tamamlandı."
    # Eğer tanımlıysa Healthcheck ping'i gönder (Mavi Tik)
    if [ -n "$HEALTHCHECK_URL" ]; then
        curl -fsS --retry 3 "$HEALTHCHECK_URL" > /dev/null 2>&1 || true
    fi
else
    echo "HATA: $JOB_NAME başarısız oldu! Exit code: $EXIT_CODE" >&2
    
    # Slack Alert Tetikleme fonksiyonunu çağır
    if [ -n "$SLACK_WEBHOOK_URL" ]; then
        # Log dosyasının son 20 satırını alalım ki Slack mesajı devasa olmasın
        LAST_LOGS=$(tail -n 20 "$LOG_FILE" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
        
        PAYLOAD=$(cat <<EOF
{
  "text": "🚨 *Cron Job Hatası!*",
  "attachments": [
    {
      "color": "danger",
      "fields": [
        { "title": "Job Adı", "value": "${JOB_NAME}", "short": true },
        { "title": "Exit Code", "value": "${EXIT_CODE}", "short": true },
        { "title": "Süre", "value": "${DURATION} saniye", "short": true },
        { "title": "Sunucu", "value": "$(hostname)", "short": true }
      ],
      "text": "*Son Loglar:*\n\`\`\`${LAST_LOGS}\`\`\`"
    }
  ]
}
EOF
)
        curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$SLACK_WEBHOOK_URL" > /dev/null 2>&1 || true
    fi
    
    # Wrapper kendisi de hata koduyla çıksın ki pipeline'lar veya üst sistemler anlasın
    exit $EXIT_CODE
fi

Neden “set +e” ve “set -o pipefail” Kullandık?

Bash’te hata yönetimi hassas bir konudur. set -o pipefail komutu, bir pipe zincirindeki herhangi bir komut hata verirse tüm satırın hata kodu dönmesini sağlar. Wrapper script’in başında bunu aktif ettik. Ancak, sarmaladığımız asıl komut hata verdiğinde script’imizin patlayıp Slack alert’ini gönderemeden sonlanmasını engellemek için, çalıştırma aşamasında geçici olarak set +e ile esneklik sağladık, exit code’u güvenli bir şekilde $? ile lokal değişkene aldık ve ardından set -e ile güvenli modumuza geri döndük.

Ayrıca mktemp kullanımı, paralel çalışan cron işlerinin aynı log dosyası üzerine yazarak yarış durumuna (race condition) girmesini engeller.

Adım 2: “Dead Man’s Snitch” Mantığı ile Healthcheck Entegrasyonu

Peki ya sunucunun kendisi çökerse? Ya da cron daemon durursa? Bu durumlarda “hata aldığımda Slack’e yaz” mantığı tamamen devre dışı kalır. Çünkü çalışmayan bir sistem hata mesajı da gönderemez. Buna monitoring dünyasında silent failure denir.

Çözüm: Inverted Monitoring (Ters İzleme) ya da bilinen adıyla Heartbeat / Dead Man’s Snitch pattern. Sistem çalışmayı durdurduğunda alarm çalmasını istiyorsak, sistemin düzenli aralıklarla “Ben hayattayım!” sinyali göndermesi gerekir. Eğer sinyal gelmezse, monitoring sunucumuz bizi uyarır.

Bu iş için healthchecks.io veya kendi host ettiğiniz open-source alternatiflerini (örneğin Uptime Kuma) kullanabilirsiniz. Script’imizdeki HEALTHCHECK_URL tam olarak bunu yapar. Cron başarılı biterse ilgili adrese bir curl isteği atar. Eğer o istek zamanında gitmezse, dış dünya sunucunun patladığını anlar.

Adım 3: Slack Webhook ile Gerçek Zamanlı Alerting

Yukarıdaki script’imizde Slack için JSON payload’unu dinamik olarak oluşturduk. Bash içinde JSON oluştururken kaçış karakterleri (escaping) her zaman baş ağrıtır. Bu yüzden log dosyasından okuduğumuz son 20 satırı (LAST_LOGS) temizlemek için sed ile yeni satır karakterlerini (\n) ve çift tırnakları escape ettik.

Slack’te görünecek mesaj, hata anında debug yapmanızı inanılmaz kolaylaştıracaktır. Hangi sunucuda, ne kadar sürede patladığını ve hatanın son satırlarını terminale girmeden telefonunuzdan görebilirsiniz:

Slack Notification Alerting

Adım 4: Crontab Yapılandırması

Şimdi yazdığımız bu sistemi gerçek bir senaryoda test edelim. Sunucumuzdaki env variable’ları crontab ortamına geçirmek bazen can sıkıcı olabilir. En temiz yöntem webhook url gibi hassas verileri wrapper script içine hardcode etmek yerine wrapper’ı çağırırken bir konfigürasyondan beslemek veya crontab’ın üst kısmında tanımlamaktır.

# /etc/cron.d/my-monitored-crons veya crontab -e

SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00/B00/X00"
HEALTHCHECK_URL="https://hc-ping.com/your-uuid-here"

# Her gece 03:00'te yedek al, hata olursa Slack'e bildir, başarılı olursa ping at
0 3 * * * root /usr/local/bin/cron-wrapper.sh --job "database-backup" --cmd "/opt/backups/pg_dump.sh"

Dikkat Edilmesi Gereken İpuçları

  • Mutlak Yol (Absolute Paths) Kullanın: Cron ortamında $PATH değişkeni kısıtlıdır. Hem wrapper script’in içinde çağırdığınız tool’ların (curl, sed vb.) hem de crontab içindeki komutların tam yollarını yazmaya özen gösterin.
  • Yetkilendirme: Wrapper script’in çalıştırılabilir olduğundan emin olun: chmod +x /usr/local/bin/cron-wrapper.sh
  • Log Rotasyonu: Biz script içinde trap kullanarak geçici logları işlem sonunda siliyoruz. Ancak işlem yarıda kesilirse (örneğin kill -9) loglar kalabilir. Periyodik olarak /tmp dizininin temizlendiğinden emin olun (çoğu modern Linux dağıtımı bunu otomatik yapar).

Özet

Cron işlerini kendi haline bırakmak, prod ortamında saatli bir bomba üzerinde oturmak gibidir. Bu makalede yazdığımız esnek bash wrapper script sayesinde, mevcut hiçbir altyapınızı bozmadan, tek bir merkezi mekanizma ile tüm linux cron işlerinizi modern birer microservice gibi monitoring ve alert süreçlerine dahil ettik. Unutmayın, en iyi sistem tıkır tıkır çalışan değil, bozulduğunda neresinin bozulduğunu size ilk söyleyen sistemdir.

Category: Genel | LEAVE A COMMENT
Eylül 27 2024

Linux Sistem Performans Analizi: perf, strace ve eBPF Araçları

Bir sabah Slack’ten gelen o korkunç alarm sesiyle uyandınız: “Production veritabanı sunucusunda latency uçtu, CPU %99!” Hemen sunucuya SSH ile bağlandınız, top komutunu çalıştırdınız ve evet, CPU gerçekten can çekişiyor. Ancak top veya htop gibi geleneksel araçlar size sadece “yangın olduğunu” söyler; yangının hangi odada, hangi kibritle başladığını göstermez. İşte bu noktada modern bir SRE (Site Reliability Engineer) gibi düşünmeli ve linux çekirdeğinin (kernel) derinliklerine inmeliyiz. Bu yazıda, modern altyapılarda performance sorunlarını iğne deliğinden geçirir gibi analiz etmenizi sağlayacak üç silahşörü inceleyeceğiz: perf, strace ve devrim niteliğindeki ebpf tabanlı bpftrace.

1. Donanımın Nabzını Tutmak: perf (CPU & Hardware Profiling)

perf, Linux çekirdeği ile doğrudan entegre çalışan inanılmaz güçlü bir profil çıkarma (profiling) aracıdır. CPU döngülerini (cycles), cache miss (önbellek kaçırma) oranlarını ve CPU talimatlarını (instructions) donanımsal sayaçları (PMU – Performance Monitoring Unit) kullanarak analiz eder.

Neden Sadece CPU Yüzdesine Bakmak Yanıltıcıdır?

Çoğu mühendis CPU %100 olduğunda uygulamanın çok yoğun hesaplama yaptığını düşünür. Ancak durum her zaman bu değildir. CPU, RAM’den veri beklerken de (stall cycles) %100 meşgul görünebilir. Biz buna Memory-Bound (bellek sınırlandırılmış) deriz. Eğer CPU gerçekten matematiksel işlemler yapıyorsa buna da Compute-Bound denir.

Bu ayrımı görmek için ilk yapmamız gereken şey perf stat komutunu çalıştırmaktır:

# Belirli bir PID'yi 5 saniye boyunca analiz edelim
perf stat -p 1234 sleep 5

Karşımıza şöyle bir çıktı gelecektir:

 Performance counter stats for process id '1234':

       2001.45 msec task-clock                #    0.400 CPUs utilized          
              1234  context-switches          #    0.617 K/sec                  
                45  cpu-migrations            #    0.022 K/sec                  
               105  page-faults               #    0.052 K/sec                  
        4002938102  cycles                    #    2.000 GHz                    
        2001467291  instructions              #    0.50  insn per cycle         
         120495839  branches                  #   60.204 M/sec                  
           4958102  branch-misses             #    4.11% of all branches        

       5.002345123 seconds time elapsed

Burada odaklanmamız gereken en kritik metrik insn per cycle (IPC) değeridir. IPC (Instructions Per Cycle), CPU’nun her döngüde kaç talimat çalıştırdığını gösterir.

  • IPC < 1.0 ise: Uygulamanız muhtemelen I/O veya memory erişimi bekliyor (Memory-Bound). CPU boşta (stall) bekliyor demektir.
  • IPC > 1.5 ise: CPU gerçekten yoğun şekilde kodunuzu çalıştırıyor demektir (Compute-Bound).

Sıcak Noktaları Bulmak: perf record ve perf report

Uygulamanın tam olarak hangi fonksiyonunun CPU’yu sömürdüğünü bulmak için örnekleme (sampling) yapmamız gerekir. Bu işlem production ortamlarında genellikle %1 ila %5 arasında çok düşük bir overhead (ek yük) ile yapılabilir.

# Saniyede 99 frekansla (overkill olmaması için idealdir) 10 saniye boyunca kayıt alalım
# -g parametresi call-graph (çağrı ağacı) kaydetmesini sağlar
perf record -F 99 -p 1234 -g -- sleep 10

Bu komut geçerli dizinde perf.data adında bir dosya oluşturur. Bu dosyayı analiz etmek için terminalden şu komutu veririz:

perf report -n --stdio

Karşınıza çıkan interaktif arayüzde hangi fonksiyonun (sembolün) CPU döngülerinin yüzde kaçını harcadığını hiyerarşik bir şekilde görebilirsiniz. Eğer fonksiyon isimleri yerine [hexadecimal] adresler görüyorsanız, uygulamanızın “debug symbols” (hata ayıklama sembolleri) eksiktir demektir. Go kullanıyorsanız binary’nizi strip etmeyin, C++/Rust kullanıyorsanız -g flag’i ile derlediğinizden emin olun.

2. Karanlıkta Kalan Sistem Çağrıları: strace

Uygulamanız çalışıyor ama hiçbir şey yapmıyor gibi mi görünüyor? Dosya okumaya çalışırken kilitlenmiş olabilir mi? Yoksa DNS çözümlemesi yaparken timeout mu yaşıyor? Bu gibi durumlarda, uygulamanın kernel ile olan iletişimini yani System Calls (syscalls) trafiğini izlememiz gerekir.

İşte strace bu işin mutfağıdır.

Production Uyarısı: strace’i Dikkatli Kullanın!

Geliştirme ortamında strace my_app yazıp geçmek harikadır ancak bunu canlı production ortamında sakın yapmayın! strace, izlediği process’in her syscall yaptığında durdurulmasını (ptrace ile) sağlar. Bu durum, uygulamanın performansını %100 ila %1000 oranında düşürebilir.

Bunun yerine, production dostu parametrelerle nokta atışı yapmalıyız:

# Uygulamanın en çok hangi sistem çağrısında ne kadar zaman harcadığını özetleyelim
# Bu işlem ham log akıtmaya göre çok daha az overhead yaratır
strace -c -p 1234

Çıktı bize muhteşem bir özet sunacaktır:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 89.45    1.204958         120     10041           read
 10.12    0.136120          13     10200      1050 openat
  0.43    0.005789          11       500           write
------ ----------- ----------- --------- --------- ----------------
100.00    1.346867                 20741      1050 total

Yukarıdaki tabloda net bir şekilde görüyoruz ki, uygulama zamanının %89’unu read sistem çağrısında harcıyor ve ciddi miktarda openat (dosya açma hatası) alıyor. Hangi dosyayı açamadığını bulmak için sadece hatalı çağrıları filtreleyebiliriz:

# Sadece başarısız olan (errors) openat çağrılarını göster
strace -e trace=openat -Z -p 1234

3. Modern Çağın Sihirli Değneği: eBPF ve bpftrace

Geleneksel araçların kısıtlamalarından sıkıldıysanız, sahneye eBPF (Extended Berkeley Packet Filter) çıkıyor. eBPF, Linux çekirdeğinin kodunu değiştirmeden veya kernel modülü yüklemeden, güvenli sandbox’lar içinde doğrudan çekirdekte kod çalıştırmamızı sağlar.

bpftrace ise eBPF dünyasının “awk” dilidir. Son derece hafif, pratik ve neredeyse sıfır overhead ile çalışan tek satırlık (one-liners) script’ler yazmamıza olanak tanır.

Örnek 1: Disk I/O Latency Analizi (Biolatency)

Diskinizin yavaş olduğunu düşünüyorsunuz ama hangi process’in ne kadarlık bir gecikmeye sebep olduğunu bulamıyorsunuz. Geleneksel iostat size sadece ortalama değerler verir. bpftrace ile gerçek zamanlı bir histogram çizelim:

# Disk I/O tamamlanma sürelerini mikrosaniye cinsinden histogram olarak gösterir
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; } 
kretprobe:vfs_read /@start[tid]/ { 
    @latency = hist((nsecs - @start[tid]) / 1000); 
    delete(@start[tid]); 
}'

Bu script, her vfs_read (sanal dosya sistemi okuma) başladığında bir zaman damgası alır ve bittiğinde aradaki farkı hesaplayıp logaritmik bir grafik çizer:

@latency: 
[2, 4)                12 |@@@@                                        |
[4, 8)                85 |@@@@@@@@@@@@@@@@@@@@@@@@                    |
[8, 16)              142 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[16, 32)              45 |@@@@@@@@@@@@                                |
[32, 64)               8 |@@                                          |

Bu grafiğe bakarak, okuma işlemlerinin büyük çoğunluğunun 8-16 mikrosaniye arasında tamamlandığını, yani diskimizin sağlıklı çalıştığını saniyeler içinde anlayabiliriz.

Örnek 2: Bellek Sızıntısı (Memory Leak) Avı

Sisteminizde gizemli bir bellek şişmesi var. Hangi uygulamanın sürekli malloc çağırıp free etmediğini bulmak istiyorsunuz. eBPF ile kullanıcı alanındaki (user-space) bellek allocation çağrılarını takip edebiliriz:

# libc içindeki malloc çağrılarının byte boyutlarına göre dağılımını izleyelim
bpftrace -e 'usdt:/lib/x86_64-linux-gnu/libc.so.6:libc:memory_malloc_retry { @[arg0] = count(); }'

Bu sayede production ortamında çalışan uygulamanıza hiçbir kütüphane enjekte etmeden veya onu durdurmadan bellek tüketim kalıplarını yakalayabilirsiniz.

Özet ve Doğru Aracı Seçme Rehberi

Sistem tıkandığında hangi aracı ne zaman kullanacağınıza karar vermek, tecrübeli bir mühendisi amatörden ayıran en önemli özelliktir. İşte size pratik bir başucu tablosu:

Senaryo / Belirti Kullanılacak Araç Neden?
Yüksek CPU kullanımı var, hangi kod satırının yavaş olduğunu bulmak istiyorum. perf record / report Düşük overhead ile CPU çağrı ağacını (call graph) çıkarır.
Uygulama kilitlendi (stuck), log üretmiyor, ne yaptığını göremiyorum. strace -p <PID> Çekirdeğe gönderdiği sistem çağrılarını (network, dosya erişimi) anlık listeler.
Disk performansından şüpheleniyorum ama genel istatistikler yetersiz kalıyor. bpftrace (eBPF) Kernel seviyesinde I/O kuyruk gecikmesini histogram olarak gösterir.
Sistemin donanımsal düzeyde (L1/L2 Cache, CPU Cycles) analizine ihtiyacım var. perf stat İşlemcinin donanımsal sayaçlarına doğrudan erişim sağlar.

Performans analizi yaparken her zaman en az invaziv (sisteme en az müdahale eden) yöntemden başlayın. Önce perf stat ve bpftrace ile genel resmi görün, gerekirse ve güvenliyse strace ile derinlemesine inceleme yapın. Unutmayın, iyi bir SRE sadece sorunu çözen değil, sorunu çözerken production sistemini ayakta tutabilen kişidir!

Category: Genel | LEAVE A COMMENT
Eylül 13 2024

Elastic Fleet ile Merkezi Agent Yönetimi: Elveda Config Karmaşası!

DevOps dünyasında monitoring ve log yönetimi denince akla gelen ilk isimlerden biri şüphesiz Elastic Stack. Ancak her sunucuya ayrı Filebeat, Metricbeat, Heartbeat kurup, Ansible playbook’ları içinde YAML indentasyon hatalarıyla boğuştuğumuz günler geride kaldı. Modern altyapılarda artık “single binary” felsefesi ve merkezi yönetim ön planda. İşte bu noktada sahneye elastic stack’in kurtarıcısı olan fleet ve onun her işe koşan askeri agent çıkıyor.

Bu makalede, production ortamlarında hayat kurtaran Elastic Fleet mimarisini, Fleet Server kurulumunu, policy yönetimini ve sisteme dinamik entegrasyonlar eklemeyi derinlemesine inceleyeceğiz. Çayınızı kahvenizi alın; Ansible playbook’larınızdaki Beats rollerini silmeye hazırlanıyoruz.

Neden Elastic Fleet? Eski Usul Beats vs. Modern Agent

Eski günleri hatırlayalım. Bir web sunucunuz var ve hem sistem metriklerini (CPU, RAM) hem de Nginx loglarını toplamak istiyorsunuz. Süreç kabaca şöyle işliyordu:

  • Sunucuya Metricbeat indir, kur, metricbeat.yml konfigüre et, servisi başlat.
  • Filebeat indir, kur, Nginx modülünü aktif et, filebeat.yml içinde Elasticsearch adresini gir, servisi başlat.
  • Elasticsearch cluster adresi veya şifresi değiştiğinde, tüm sunuculardaki o meşhur YAML dosyalarını CM (Configuration Management) araçlarıyla güncelle.

Bu yaklaşım ölçeklendikçe tam bir kabusa dönüşüyor. Elastic Agent ise bu karmaşayı tek bir binary altında topluyor. Agent arka planda gereken tüm “Beat” işlevlerini barındırıyor ve en önemlisi, konfigürasyonunu lokal bir dosyadan değil, doğrudan Kibana üzerinde koşan Fleet Server’dan dinamik olarak çekiyor. Siz Kibana arayüzünden tek bir tıkla yeni bir log kaynağı eklediğinizde, hedef sunucudaki Elastic Agent saniyeler içinde yeni policy’yi uyguluyor. Ne restart gerekiyor, ne de sunucuya SSH atmak.

Fleet Server Mimarisi: Olayın Arkasındaki Beyin

Fleet yapısını kurmadan önce mimariyi anlamak şart. Fleet, merkezi yönetim katmanıdır ve Kibana ile entegre çalışır. Ancak Kibana, binlerce agent ile doğrudan ham veri alışverişi yapacak şekilde tasarlanmamıştır. Bu yüzden araya Fleet Server girer.

Fleet Server, Elastic Agent’ların bağlandığı, policy güncellemelerini aldığı ve durum raporladığı bir kontrol merkezidir. Kendisi de aslında özel bir policy ile çalışan bir Elastic Agent’tır. Agent’lar Fleet Server’a HTTPS üzerinden bağlanır (genellikle port 8220). Fleet Server ise aldığı durumları ve metrikleri doğrudan Elasticsearch’e yazar.

Adım Adım Fleet Server Kurulumu (Self-Hosted)

Eğer Elastic Cloud kullanıyorsanız Fleet Server sizin için otomatik olarak yönetilir. Ancak gerçek DevOps mühendisleri olarak biz self-hosted yapıları severiz (veya mecbur kalırız). Gelin, kendi altyapımızda Fleet Server’ı Docker üzerinde ayağa kaldıralım.

Adım 1: Elasticsearch ve Kibana Hazırlığı

Öncelikle halihazırda HTTPS korumalı bir Elasticsearch cluster’ınız ve Kibana’nız olduğunu varsayıyoruz. Fleet Server’ın Elasticsearch ile haberleşebilmesi için bir “Service Token” üretmemiz gerekiyor. Kibana Console (Dev Tools) üzerinden şu komutla token’ımızı alalım:

POST _security/service/elastic/fleet-server/token/my-fleet-token

Dönen yanıttaki value değerini güvenli bir yere kaydedin. Bu bizim FLEET_SERVER_ELASTICSEARCH_SERVICE_TOKEN değerimiz olacak.

Adım 2: Fleet Server’ı Docker ile Çalıştırma

Aşağıdaki Docker komutu ile Fleet Server’ı ayağa kaldırabiliriz. Üretim ortamında mutlaka geçerli SSL sertifikaları kullanmalısınız, ancak test ortamı için doğrulama adımlarını esnetebiliriz.

docker run -d \
  --name fleet-server \
  -p 8220:8220 \
  -e FLEET_SERVER_ENABLE=1 \
  -e FLEET_SERVER_ELASTICSEARCH_HOST=https://elasticsearch.local:9200 \
  -e FLEET_SERVER_ELASTICSEARCH_SERVICE_TOKEN=AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuLTE2N... \
  -e FLEET_URL=https://fleet-server.local:8220 \
  -e FLEET_SERVER_CERT=/usr/share/elastic-agent/certs/fleet.crt \
  -e FLEET_SERVER_KEY=/usr/share/elastic-agent/certs/fleet.key \
  -v /path/to/certs:/usr/share/elastic-agent/certs \
  docker.elastic.co/beats/elastic-agent:8.11.1

Bu komutla Fleet Server, 8220 portundan agent bağlantılarını kabul etmeye hazır hale gelecektir. Kibana arayüzünde Management > Fleet sekmesine gittiğinizde Fleet Server’ın “Healthy” durumuna geçtiğini görmelisiniz.

Elastic Agent Kurulumu ve Fleet’e Enroll Etme

Sırada log ve metrik toplayacağımız hedef sunuculara (örneğin bir Linux VM) Elastic Agent kurmak var. Agent kurulumu son derece basittir çünkü tek bir binary indirip çalıştırmaktan ibarettir.

Adım 1: Agent Paketini İndirme

Hedef sunucuda terminale çıkıp işletim sistemimize uygun paketi indiriyoruz:

curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-8.11.1-linux-x86_64.tar.gz
tar xzvf elastic-agent-8.11.1-linux-x86_64.tar.gz
cd elastic-agent-8.11.1-linux-x86_64

Adım 2: Enrollment (Kayıt) İşlemi

Agent’ı sisteme kurarken onu Fleet Server’a “enroll” etmemiz, yani kaydetmemiz gerekir. Bunun için Kibana > Fleet > Enrollment Tokens sekmesinden hedef policy için üretilmiş token’ı alıyoruz. Ardından şu komutla kurulumu tetikliyoruz:

sudo ./elastic-agent install \
  --url=https://fleet-server.local:8220 \
  --enrollment-token=U0ZVMWhvMEJGUnp6eE5iNUp0eXU6cUpnZXA3V... \
  --certificate-authorities=/path/to/ca.crt

Bu komut arka planda şunları yapar:

  • Agent’ı sistem servisi olarak kaydeder (systemd).
  • Gerekli dizin yapılarını (/etc/elastic-agent, /var/log/elastic-agent) oluşturur.
  • Fleet Server ile el sıkışarak ilk policy’sini indirir ve servisi başlatır.

Servisin durumunu doğrulamak için:

sudo systemctl status elastic-agent

Policy Yönetimi: Tek Merkezden Gücü Dağıtmak

Elastic Fleet’in en güçlü yanı policy tabanlı yönetimdir. Bir policy; hangi logların toplanacağını, hangi metriklerin hangi sıklıkla çekileceğini belirten bir kurallar bütünüdür. Sunucularınızı rollerine göre gruplayabilirsiniz (örn: web-servers-policy, db-servers-policy).

Bir sunucunun davranışını değiştirmek istediğinizde, tek yapmanız gereken Kibana arayüzünden ilgili policy’yi editlemektir. Örneğin, tüm web sunucularında Apache log takibini kapatıp yerine Nginx log takibini açmak istiyorsunuz. Bu değişikliği policy üzerinde yaptığınız an, o policy’ye bağlı olan 500 sunucudaki Elastic Agent saniyeler içinde kendini günceller.

Pratik Örnek: Nginx Entegrasyonu Ekleme

Gelin, çalışan bir Elastic Agent’a Kibana üzerinden nasıl dinamik entegrasyon ekleyeceğimize bakalım:

  1. Kibana > Fleet > Agent Policies sayfasına gidin.
  2. Agent’ınızın bağlı olduğu policy’yi seçin.
  3. Add Integration butonuna tıklayın.
  4. Arama çubuğuna Nginx yazın ve entegrasyonu seçin.
  5. Nginx log dosyalarının yollarını (varsayılan: /var/log/nginx/access.log) ve metrik endpoint’ini (stub_status) konfigüre edin.
  6. Save Integration butonuna basarak kaydedin.

Arkanıza yaslanın. Hedef sunucudaki agent, konfigürasyon değişikliğini algılayacak, arka planda Nginx loglarını okumaya başlayacak ve Kibana’da hazır gelen “Nginx Overview” dashboard’larını verilerle dolduracaktır. Sıfır konfigürasyon dosyası editlendi, sıfır servis restart edildi!

Prodüksiyon Tavsiyeleri ve Savaş Hikayeleri

Her şey kağıt üzerinde harika görünse de, production ortamlarında Fleet koştururken dikkat etmeniz gereken bazı can alıcı noktalar var:

1. Resource Limits (Kaynak Sınırları)

Elastic Agent, eski Beats’lere göre biraz daha fazla memory tüketebilir çünkü arka planda birden fazla süreci yönetir. Özellikle çok sayıda log dosyası (wildcard ile binlerce log) izleniyorsa, agent memory limitlerine takılabilir. Sunucularınızda elastic-agent‘ın CPU ve RAM tüketimini mutlaka monitor edin.

2. SSL/TLS Sertifikaları

Fleet Server ile Agent arasındaki iletişim kesinlikle HTTPS olmalıdır. Self-signed sertifika kullanıyorsanız, agent kurulumu sırasında --certificate-authorities parametresiyle CA sertifikanızı belirtmeyi unutmayın. Aksi takdirde agent’lar “x509: certificate signed by unknown authority” hatasıyla kaydolmayacaktır.

3. Fleet Server Ölçekleme

Tek bir Fleet Server kabaca 1000 ila 2000 agent’a kadar sorunsuz hizmet verebilir. Ancak binlerce agent’ın olduğu büyük altyapılarda, Fleet Server’ı arkasında bir Load Balancer (HAProxy, AWS ALB vb.) olacak şekilde multi-node mimaride kurmalısınız.

Fleet Server’ları ölçeklerken şu parametre ile JVM heap boyutunu ve max connection limitlerini optimize edebilirsiniz:

# Örnek environment değişkeni
-e FLEET_SERVER_MAX_CONNECTIONS=5000

Özet

Elastic Fleet ve Agent ikilisi, modern DevOps dünyasındaki monitoring operasyonlarını büyük ölçüde kolaylaştırıyor. Altyapınızı kodla yönetirken (IaC), sunuculara sadece tek bir agent kurup geri kalan tüm konfigürasyonu UI veya Fleet API’leri üzerinden yönetmek, operasyonel yükü ciddi oranda azaltır. Eğer hala eski usul Beats konfigürasyonlarıyla uğraşıyorsanız, Fleet’e geçiş planını hemen sprint’inize eklemenizi öneririm.

Category: Genel | LEAVE A COMMENT
Eylül 6 2024

Logstash Pipeline Tasarımı: Filter ve Grok Desenlerinde Uzmanlık

Production ortamında koşan yüzlerce mikroservisin loglarını merkezi bir elk stack’e toplamak işin sadece başlangıcıdır. Gerçek kaos, o anlamsız string yığınlarını anlamlı metrikler ve aranabilir alanlar haline getirmek istediğinizde, yani log parsing aşamasında başlar. Bu yazıda, logstash üzerinde performans canavarı bir pipeline nasıl tasarlanır, karmaşık log formatları grok desenleri ile CPU’yu ağlatmadan nasıl dize getirilir ve parsing hatalarıyla nasıl başa çıkılır derinlemesine inceleyeceğiz. Kahvenizi alın, çünkü regex (regular expressions) derinliklerine iniyoruz.

Grok ile Savaşmak: Regex Cehenneminden Nasıl Kurtuluruz?

Logstash dünyasında Grok, yapılandırılmamış (unstructured) metin verilerini yapılandırılmış (structured) JSON formatına dönüştürmek için kullandığımız en güçlü silahtır. Ancak yanlış ellerde bu silah, pipeline üzerinde ciddi darboğazlara (bottleneck) yol açabilir. Grok aslında arkada regex çalıştırır. Eğer yazdığınız regex deseni verimsiz ise, Logstash JVM heap limitlerini zorlar ve CPU tüketiminiz tavan yapar.

Farz edelim ki elimizde şöyle karmaşık, custom bir Java uygulama logu var:

2023-11-23T14:32:10.123Z [thread-pool-4] INFO  c.k.service.UserService [TX-98123] - User login failed for user 'kertenkerem_devops' from IP 192.168.1.50 - Reason: Invalid credentials

Bu logu parse etmek için yazacağımız klasik, hantal ve performansı düşük Grok filtresi genelde şuna benzer:

# TAVSİYE EDİLMEYEN YÖNTEM (Yavaş ve hantal)
filter {
  grok {
    match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{DATA:thread}\] %{LOGLEVEL:level}  %{NOTSPACE:class} \[%{DATA:tx_id}\] - %{GREEDYDATA:log_message}" }
  }
}

Neden tavsiye etmiyoruz? Çünkü %{DATA} ve %{GREEDYDATA} desenleri regex motorunu çok yorar. Logstash, eşleşme bulabilmek için metnin sonuna kadar gider, bulamazsa geri döner (backtracking). Bu da milyonlarca log satırında CPU fanlarınızın jet motoru gibi bağırmasına neden olur.

Doğru ve Optimize Grok Tasarımı

Aynı logu daha spesifik desenlerle, sınırları (anchors) belirleyerek parse edelim. Ayrıca kendi custom pattern’lerimizi tanımlayarak okunabilirliği artıralım:

filter {
  grok {
    pattern_definitions => {
      "THREAD_NAME" => "[a-zA-Z0-9-]+"
      "TX_ID" => "TX-[0-9]+"
    }
    match => { 
      "message" => "^%{TIMESTAMP_ISO8601:timestamp} \[%{THREAD_NAME:thread}\] %{LOGLEVEL:level}\s+%{NOTSPACE:class} \[%{TX_ID:tx_id}\] - (?<action>[a-zA-Z\s]+) '%{USER:username}' from IP %{IP:client_ip} - Reason: %{GREEDYDATA:reason}$" 
    }
  }
}

Neden bu daha iyi?

  • Satır başı (^) ve satır sonu ($) işaretleyicilerini kullanarak regex motorunun tüm satırı gereksiz yere tekrar tekrar taramasını engelledik.
  • %{DATA} yerine özel olarak tanımladığımız %{THREAD_NAME} ve regex limitli capture gruplarını kullandık.
  • \s+ ile boşluk miktarı değişse bile eşleşmenin kırılmamasını sağladık.

Conditional Filters: Akıllı Pipeline Yönlendirmesi

Logstash pipeline’ınızda tüm log türlerini aynı Grok filtresinden geçirmek tam bir kaynak israfıdır. Nginx logu ile Java logu aynı pipeline’da karşılaşabilir. Çözüm, conditional (koşullu) filtreler kullanarak logları doğru parser’lara yönlendirmektir.

Aşağıdaki örnekte log kaynağına göre (örneğin Kubernetes namespace veya log tipi) nasıl filtreleme yapacağımızı görebilirsiniz:

filter {
  # Kubernetes metadata'sından gelen namespace bilgisine göre ayırma
  if [kubernetes][namespace] == "production" {
    mutate { add_tag => [ "prod_environment" ] }
    
    if [kubernetes][container_name] == "nginx-ingress" {
      grok {
        match => { "message" => "%{NGINXACCESS}" }
        remove_tag => [ "_grokparsefailure" ] # Başarılıysa default failure tag'ini ez
      }
    } else if [kubernetes][container_name] == "payment-api" {
      grok {
        match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:msg}" }
      }
    }
  } else {
    # Non-prod veya diğer loglar için hafif bir parser
    mutate { add_tag => [ "non_prod" ] }
  }
}

Bu yaklaşım sayesinde, sadece ilgili konteynerden gelen loglar ilgili Grok desenine maruz kalır. Boş yere tüm regex kuralları her log satırı için koşturulmaz.

Dead Letter Queue (DLQ) ve Parse Hatalarının Yönetimi

Günün birinde yazılımcı ekibinden biri log formatını değiştirir (çünkü neden değiştirmesinler ki?) ve pipeline patlar. Logstash bu durumda _grokparsefailure tag’ini basar ve logu Elasticsearch’e hatalı haliyle yazar. Eğer kritik bir audit logu parse edilemediği için kaybolursa başınız ağrıyabilir.

Logstash, teslim edilemeyen veya işlenemeyen event’ler için bir Dead Letter Queue (DLQ) mekanizması sunar. Bu mekanizmayı aktifleştirmek için logstash.yml dosyanızda şu ayarı yapmanız gerekir:

# logstash.yml
dead_letter_queue.enable: true
dead_letter_queue.max_bytes: 1024mb
path.dead_letter_queue: "/var/lib/logstash/dead_letter_queue"

DLQ’ya düşen bu bozuk logları okumak ve analiz etmek için ayrı bir Logstash pipeline’ı tasarlayabilirsiniz. Örneğin, hatalı logları tespit edip Slack’e alert atan veya bunları incelemek üzere ayrı bir “failed-logs” index’ine basan bir pipeline kurabiliriz:

# dlq_pipeline.conf
input {
  dead_letter_queue {
    path => "/var/lib/logstash/dead_letter_queue"
    commit_offsets => true
    pipeline_id => "main"
  }
}

output {
  elasticsearch {
    hosts => ["https://elasticsearch.kertenkerem.local:9200"]
    index => "logstash-dlq-%{+YYYY.MM.dd}"
    user => "logstash_writer"
    password => "highly_secure_password"
    ssl => true
    cacert => "/etc/logstash/certs/ca.crt"
  }
}

Logstash Performans Optimizasyonu: canavar gibi çalışan pipeline’lar

Eğer saniyede on binlerce log event’i işliyorsanız, Logstash tuning kaçınılmaz hale gelir. İşte production ortamlarında bizzat uyguladığımız hayat kurtaran performans tüyoları:

1. Grok Yerine Dissect Kullanın

Log formatınız belirli ve sabit bir ayırıcıya (delimiter) sahipse (örneğin Virgülle ayrılmış CSV, Tab veya Pipe | karakterleri), Grok kullanmayın! Dissect plugini, regex kullanmadan sadece string operasyonları ile parse işlemi yapar ve Grok’a göre neredeyse 10 kat daha hızlıdır.

# Dissect Örneği: Log formatı [Thread] LEVEL - Message şeklindeyse
filter {
  dissect {
    mapping => {
      "message" => "[%{thread}] %{level} - %{msg}"
    }
  }
}

2. Pipeline Workers Ayarı

Logstash varsayılan olarak CPU çekirdek sayınız kadar worker thread oluşturur. Ancak yoğun I/O işlemlerinde bu yetmeyebilir. logstash.yml içinde bu değeri optimize edin:

pipeline.workers: 8 # CPU core sayınızın 1.5 - 2 katı civarında tutabilirsiniz
pipeline.batch.size: 250 # Tek seferde Elasticsearch'e gönderilecek event sayısı

Batch size değerini artırmak throughput (iş çıkarma yeteneği) artışına sebep olurken, RAM tüketimini artırır. 125 ila 500 arasında ideal noktanızı test ederek bulmalısınız.

3. ECS (Elastic Common Schema) Uyumluluğu

Modern ELK mimarisinde log alanlarının (fields) standartlaştırılması çok önemlidir. Logstash filtrelerinizde yazdığınız output’ların ECS uyumlu olması, Kibana üzerinde dashboard yaparken veya SIEM kuralları yazarken hayatınızı kurtarır. Örneğin client_ip yerine source.ip, username yerine user.name kullanmaya özen gösterin.

Özet ve Kapanış

Verimli bir logstash pipeline tasarımı, sadece logları parse etmekle kalmaz; production ortamınızın gözlemleyebilirliğini (observability) artırırken altyapı maliyetlerinizi de düşürür. Grok yazarken regex sınırlarına dikkat etmek, conditional filter’lar ile akıllı routing yapmak ve DLQ ile hata yönetimini otomatikleştirmek sizi sıradan bir sistem yöneticisinden uzman bir DevOps mühendisine dönüştürür. Loglarınız temiz, kuyruklarınız boş olsun!

Category: Genel | LEAVE A COMMENT
Ağustos 30 2024

Elasticsearch ILM (Index Lifecycle Management) ile Disk Tasarrufu

Her devops mühendisinin kabusudur: Gece yarısı gelen ve uykuyu piç eden o meşhur “Disk Space Low” uyarısı. Hele ki bu uyarı, log analizi için canla başla koşturduğumuz, saniyede on binlerce satır log yutan bir elasticsearch cluster’ından geliyorsa durum daha da can sıkıcı olur. Sürekli büyüyen log yığınlarını yönetmek, sadece daha fazla ve daha pahalı disk satın almakla çözülmez; akıllıca bir ilm (Index Lifecycle Management) stratejisi kurmak gerekir. Bu yazımızda, bütçe dostu ve yüksek performanslı hot-warm-cold-delete mimarisini nasıl kuracağımızı, shrink ve rollover operasyonlarının perde arkasını pratik örneklerle inceleyeceğiz.

Neden Sadece “Daha Büyük Disk” Çözüm Değil?

Kapasite sorunuyla karşılaştığımızda ilk refleks genellikle disk boyutunu artırmak veya cluster’a yeni node’lar eklemek olur. Ancak bu yaklaşım sürdürülebilir değildir. Neden mi?

  • Maliyet: Üretim ortamlarında hızlı yazma (indexing) performansı için NVMe SSD diskler kullanırız. 90 günlük log verisinin tamamını bu pahalı disklerde tutmak bütçeyi sarsar.
  • Performans: Eski ve nadiren sorgulanan loglar, aktif olarak yazılan güncel index’ler ile aynı kaynakları (özellikle heap memory) tüketerek sorgu performansını düşürür.
  • Yönetilebilirlik: Tek bir devasa index yerine, yönetilebilir boyutlarda shard’lara bölünmüş bir yapı hem yedekleme (snapshot) hem de cluster kurtarma süreçlerinde hayat kurtarır.

Hot-Warm-Cold-Delete Mimarisi Nedir?

Verinin yaşlandıkça değerini kaybetmesi felsefesine dayanan bu mimari, veriyi yaşam döngüsüne göre farklı donanım özelliklerine sahip node gruplarına dağıtır:

  • Hot Phase (Sıcak Evre): Yeni logların yazıldığı, yoğun CPU ve NVMe SSD disklere sahip node’lardır. Veri tazedir ve sürekli sorgulanır.
  • Warm Phase (Ilık Evre): Veri artık yazılmaz (read-only), ancak sorgulanmaya devam eder. Daha ucuz SATA SSD diskler barındıran node’lara taşınır. Burada shard sayısı azaltılarak (shrink) sistem üzerindeki yük hafifletilir.
  • Cold Phase (Soğuk Evre): Nadiren sorgulanan, arşiv niteliğindeki verilerdir. HDD tipi ucuz depolama birimlerinde saklanır. Arama performansı düşüktür ama maliyeti minimumdur.
  • Delete Phase (Silme Evresi): Belirlenen saklama süresi (retention period) dolan verilerin güvenli bir şekilde yok edildiği evredir.

Adım Adım Elasticsearch ILM Kurulumu

Gelin, teoriyi pratiğe dökelim. Senaryomuzda günlük uygulama loglarımızı toplayacağız, bunları 50 GB veya 30 gün sınırlamasıyla yeni index’lere böleceğiz (rollover), ardından shrink işlemiyle shard sayısını azaltıp daha ucuz disklere taşıyacağız ve 90 günün sonunda sileceğiz.

1. Node Rollerinin Tanımlanması

Öncelikle Elasticsearch cluster’ınızdaki node’ların hangi katmanda yer alacağını elasticsearch.yml dosyalarında belirtmeniz gerekir. Modern Elasticsearch versiyonlarında (7.10+) veri katmanları (data tiers) kullanılır:

# Hot Node konfigürasyonu
node.roles: [ master, data_content, data_hot ]

# Warm Node konfigürasyonu
node.roles: [ data_warm ]

# Cold Node konfigürasyonu
node.roles: [ data_cold ]

2. ILM Policy Tanımlama

Şimdi Elasticsearch API’sini kullanarak ILM politikamızı tanımlayalım. Bu politikada hot, warm, cold ve delete fazlarındaki geçiş kurallarını ve yapılacak operasyonları belirtiyoruz.

PUT _ilm/policy/kertenkerem_log_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "50gb",
            "max_age": "30d"
          }
        }
      },
      "warm": {
        "min_age": "0d",
        "actions": {
          "shrink": {
            "number_of_shards": 1
          },
          "forcemerge": {
            "max_num_segments": 1
          },
          "allocate": {
            "number_of_replicas": 1
          }
        }
      },
      "cold": {
        "min_age": "60d",
        "actions": {
          "readonly": {}
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

Buradaki “Neden?” Sorularını Cevaplayalım:

  • Neden max_primary_shard_size? Toplam index boyutu yerine tek bir primary shard boyutunu baz almak en doğru yaklaşımdır. Replica shard’lar hesaba katılmaz ve dengeli bir shard yapısı korunur.
  • Neden warm fazında shrink ve forcemerge yapıyoruz? Yazma işlemi bittiği için artık çoklu shard yapısına ihtiyacımız yok. Shard sayısını 1’e düşürerek (shrink) overhead’i azaltıyoruz. forcemerge ile segment sayısını 1’e indirip disk alanından ciddi tasarruf sağlıyor ve arama hızını optimize ediyoruz.

3. Index Template Oluşturma

Oluşturduğumuz ILM politikasının yeni açılacak log index’lerine otomatik olarak uygulanması için bir index template tanımlıyoruz. Burada dikkat etmemiz gereken en önemli nokta index.lifecycle.rollover_alias tanımıdır.

PUT _index_template/kertenkerem_logs_template
{
  "index_patterns": ["kertenkerem-app-logs-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "kertenkerem_log_policy",
      "index.lifecycle.rollover_alias": "kertenkerem-app-logs",
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  }
}

4. İlk Index’in Tetiklenmesi (Bootstrap)

Rollover mekanizmasının doğru çalışabilmesi için ilk index’i bizim manuel olarak oluşturmamız ve yazma iznini (write index) vermemiz gerekir. Index isminin sonunun sıralı bir sayı formatında (örn: -000001) bitmesi şarttır.

PUT kertenkerem-app-logs-000001
{
  "aliases": {
    "kertenkerem-app-logs": {
      "is_write_index": true
    }
  }
}

Bu adımdan sonra log toplayıcı araçlarınızın (Logstash, Fluentd, Vector vb.) doğrudan spesifik index ismine değil, sadece kertenkerem-app-logs alias’ına (takma ad) veri göndermesi gerekir. Elasticsearch, arka planda ILM kurallarına göre yeni index’ler açarak trafiği otomatik yönlendirecektir.

Shrink ve Rollover Operasyonlarının Kritik Detayları

Sistem tıkır tıkır çalışırken bazen arka planda işler karışabilir. DevOps mühendislerinin bu süreçte bilmesi gereken bazı “under the hood” detaylar şunlardır:

Shrink İşlemi Nasıl Gerçekleşir?

Bir index’i shrink edebilmek için Elasticsearch öncelikle o index’e ait tüm primary shard’ları cluster’daki tek bir node üzerinde toplamak zorundadır. ILM bunu otomatik olarak yönetir. Ancak o node üzerinde yeterli disk alanı yoksa shrink işlemi askıda (blocking) kalabilir. Bu yüzden warm node’larınızın disk kapasitesini planlarken bu geçici yoğunlaşmayı hesaba katmalısınız.

Neden Günlük Index (Daily Index) Yerine Rollover?

Eski usul logstash-YYYY.MM.DD yapısında, hafta sonu az log gelen günlerde de, hafta içi devasa log gelen günlerde de aynı sayıda index açılırdı. Bu durum cluster içinde “over-sharding” (aşırı shard birikmesi) sorununa yol açar ve master node’un canını okurdu. Rollover ise boyuta göre tetiklendiği için her shard’ın ideal boyut olan 30-50 GB aralığında kalmasını garanti eder.

Sonuç ve “DevOps Tavsiyeleri”

Bu makalede kurduğumuz ILM yapısı sayesinde, pahalı SSD disklerimizi sadece aktif olarak yazılan taze veriler için rezerve etmiş olduk. Yaşlanan logları daha ucuz disklere taşıyarak, shrink operasyonuyla cluster üzerindeki shard yükünü hafifleterek ve nihayetinde eski verileri silerek disk maliyetlerinde %60’a varan tasarruf sağlayabilirsiniz.

Unutmayın, iyi bir DevOps mühendisi sadece çalışan sistemler kurmaz; aynı zamanda şirketin bulut faturasını da optimize eden kişidir. Loglarınızı kendi haline bırakmayın, ILM ile onları disipline edin!

Category: Genel | LEAVE A COMMENT
Ağustos 23 2024

Bash Script’lerinde Kurşun Geçirmez Hata Yönetimi: set -euo pipefail ve Ötesi

Hepimiz oradaydık: Gece yarısı gelen bir PagerDuty alarmı, yarıda kesilmiş bir CI/CD pipeline’ı ve sessizce hata fırlatıp “başarılı” (exit 0) olarak sonlanan bir bash script’i. Linux sistemlerde scripting yaparken, özellikle modern devops ve otomasyon süreçlerinde, hata yönetimi lüks değil bir zorunluluktur. Varsayılan olarak Bash, adeta bir nihilist gibi davranır: Adımlardan biri başarısız olsa bile, hiçbir şey olmamış gibi bir sonraki satıra geçer. Bu makalede, bu vurdumduymazlığı nasıl dizginleyeceğimizi ve prod-ready scriptler yazacağımızı inceleyeceğiz.

5+ yıllık deneyime sahip bir mühendis olarak muhtemelen set -e komutunu duymuş, hatta şablonlarınıza eklemişsinizdir. Ancak bu buzdağının sadece görünen kısmı. Gelin, işi profesyonel seviyeye taşıyalım.

Sihirli Üçlü: set -euo pipefail

Bash script’lerinizin başına ekleyeceğiniz bu sihirli satır, hata yönetiminizin temel taşıdır. Peki ama neden bu üç parametre birlikte kullanılmalı? Her birinin arka planda çözdüğü spesifik problemi inceleyelim.

#!/usr/bin/env bash
set -euo pipefail

1. set -e (errexit)

Herhangi bir komut sıfırdan farklı bir exit code ile dönerse, script’in çalışmasını anında durdurur.

Neden gerekli? Varsayılan senaryoda, diskte yer kalmadığı için başarısız olan bir tar komutundan sonra script çalışmaya devam eder ve bir sonraki adımdaki silme işlemini tetikleyebilir. set -e bunu engeller. Ancak alt kabuklarda (subshell) veya mantıksal operatörlerde (&&, ||) her zaman beklediğiniz gibi davranmaz. Bu yüzden tek başına kurtarıcı değildir.

2. set -u (nounset)

Tanımlanmamış bir değişken kullanılmaya çalışıldığında script’i hata vererek durdurur.

Neden gerekli? Şu felaket senaryosunu düşünün:

# set -u OLMADAN
TARGET_DIR="" # Bir hata sonucu boş kaldı
rm -rf "$TARGET_DIR/*" # Tebrikler, kök dizini (root) sildiniz.

Eğer set -u aktif olsaydı, Bash daha rm komutuna geçmeden TARGET_DIR is not set hatasıyla işlemi sonlandıracaktı.

3. set -o pipefail

Pipeline (boru hattı) içerisindeki herhangi bir komut hata aldığında, tüm pipeline’ın exit code’unu en son hata alan komutun kodu olarak belirler.

Neden gerekli? Bash varsayılan olarak sadece pipeline’ın en sonundaki komutun exit code’una bakar.

# set -o pipefail OLMADAN
non_existent_command | echo "Merhaba"
echo $? # Çıktı: 0 (Çünkü echo başarılı oldu!)

Yukarıdaki örnekte ilk komut crash olsa bile pipeline başarılı sayılır. set -o pipefail ile bu durum engellenir ve ilk komutun hatası tüm hattı başarısız kılar.

İstisnaları Yönetmek: “Peki ya hata almasını bekliyorsam?”

set -e kullandığınızda, bazen hata vermesi normal olan komutlar da script’inizi sonlandırır. Örneğin, bir portun açık olup olmadığını nc ile kontrol etmek istiyorsunuz:

#!/usr/bin/env bash
set -euo pipefail

# nc başarısız olursa script burada ölecektir.
nc -z -w3 10.0.0.5 80

echo "Bu satıra asla ulaşılamayacak."

Bunu aşmanın “temiz” yolu, komutun sonuna mantıksal || true eklemek veya hata durumunu inline olarak handle etmektir:

# Güvenli yaklaşım 1: || true ile bypass etmek
nc -z -w3 10.0.0.5 80 || true

# Güvenli yaklaşım 2: Hata durumunda alternatif aksiyon almak
if ! nc -z -w3 10.0.0.5 80; then
    echo "Port kapalı, alternatif akışa geçiliyor..."
    # Burası script'i durdurmaz çünkü if bloğu içindeyiz
fi

Trap Mekanizması ile Graceful Degradation ve Cleanup

Script’iniz yarıda kesildiğinde (ister hata sebebiyle, ister kullanıcı CTRL+C’ye bastığında), arkasında çöp bırakmamalıdır. Geçici dosyalar (temporary files), kilit mekanizmaları (lock files) veya açık SSH tünelleri mutlaka temizlenmelidir. İşte burada trap devreye girer.

Bash’teki sinyalleri (signals) yakalayarak, script sonlanırken mutlaka çalıştırılacak bir cleanup fonksiyonu tanımlayabiliriz:

#!/usr/bin/env bash
set -euo pipefail

# Geçici bir dizin oluşturalım
TMP_DIR=$(mktemp -d)
echo "Geçici dizin oluşturuldu: ${TMP_DIR}"

# Cleanup fonksiyonumuz
cleanup() {
    local exit_code=$?
    echo "Temizlik yapılıyor: ${TMP_DIR} siliniyor..."
    rm -rf "${TMP_DIR}"
    
    if [ $exit_code -ne 0 ]; then
        echo "Script HATA ile sonlandı! Exit code: ${exit_code}"
    else
        echo "Script başarıyla tamamlandı."
    fi
    exit $exit_code
}

# EXIT sinyalini yakala (Hata olsun ya da olmasın, script çıkarken çalışır)
trap cleanup EXIT

# İş yükü simülasyonu
echo "İşlem yapılıyor..."
touch "${TMP_DIR}/important_data.tmp"

# Hata simülasyonu (Burada script patlayacak ama trap çalışacak!)
grep "olmayan_pattern" "${TMP_DIR}/important_data.tmp"

Standartlara Uygun Exit Code Kullanımı

Gelişigüzel exit 1 yazıp geçmek, üst seviye bir otomasyonda kabul edilemez. Script’inizin çağıran sisteme (örneğin Jenkins, GitHub Actions veya bir cron job) neyin yanlış gittiğini net bir şekilde söylemesi gerekir.

POSIX standartlarına ve /usr/include/sysexits.h yapısına uygun olarak şu kodları tercih edin:

  • 0: Başarılı operasyon.
  • 64 (EX_USAGE): Yanlış CLI argüman kullanımı.
  • 69 (EX_UNAVAILABLE): Servis veya kaynak ulaşılamaz durumda.
  • 70 (EX_SOFTWARE): İçsel yazılım hatası (Internal error).
  • 74 (EX_IOERR): Giriş/Çıkış (I/O) hatası (Disk dolu, dosya yazılamadı).
# Örnek kullanım
if [[ $# -lt 2 ]]; then
    echo "Hata: Eksik parametre. Kullanım: $0  " >&2
    exit 64
fi

Script’lerinizi Test Edin: BATS (Bash Automated Testing System)

SRE dünyasında “test edilmeyen kod çalışmıyordur” kuralı geçerlidir. Bash script’lerinizi manuel test etmek yerine, declarative testler yazmak için bats-core kullanabilirsiniz.

Önce basit bir script’imiz olsun (deploy.sh):

#!/usr/bin/env bash
# deploy.sh
set -euo pipefail

check_env() {
    if [[ -z "${APP_ENV:-}" ]]; then
        echo "Hata: APP_ENV tanımlı değil!" >&2
        return 64
    fi
}

check_env
echo "Deploying to ${APP_ENV}"

Şimdi bu script için yazacağımız BATS test dosyası (deploy.bats):

#!/usr/bin/env bats

# Test 1: APP_ENV tanımlı değilse script 64 koduyla hata vermeli
@test "APP_ENV set edilmediğinde script hata fırlatmalı" {
  run ./deploy.sh
  [ "$status" -eq 64 ]
  [[ "$output" =~ "Hata: APP_ENV tanımlı değil!" ]]
}

# Test 2: APP_ENV tanımlı olduğunda başarılı olmalı
@test "APP_ENV set edildiğinde script başarılı olmalı" {
  export APP_ENV="production"
  run ./deploy.sh
  [ "$status" -eq 0 ]
  [[ "$output" =~ "Deploying to production" ]]
}

Bu testleri CI pipeline’ınızda bats deploy.bats komutuyla çalıştırarak, script’lerinizin gelecekteki değişikliklerde kırılmasını engelleyebilirsiniz.

Özet

Bash’te hata yönetimi, sadece set -e yazıp şansımıza güvenmekten ibaret değildir. Profesyonel otomasyon süreçlerinde, pipefail ile boru hatlarını güvenceye almak, trap ile arkamızı temizlemek, standart exit code’lar ile çağıran sistemlere anlamlı yanıtlar dönmek ve nihayetinde BATS ile bu davranışları test etmek gerekir. Unutmayın, iyi bir DevOps mühendisi sadece çalışan kod değil, güvenle patlayan kod yazar.

Category: Genel | LEAVE A COMMENT
Ağustos 23 2024

Grafana Tempo ve OpenTelemetry ile Distributed Tracing

Gece saat 03:00. Telefonunuz çalıyor, PagerDuty çığlık çığlığa. Prod ortamındaki o kritik ödeme servisi yavaşlamış, hata oranları tavan yapmış durumda. Hemen bilgisayarınızı açıp loglara bakıyorsunuz: Dev bir 500 Internal Server Error yığını. Ancak bu hata, o servisin kendisinden mi kaynaklanıyor, yoksa arkada çağırdığı 15 farklı microservice’ten birinin database sorgusunun takılmasından mı? İşte bu noktada loglar yetersiz, metrikler ise dilsiz kalır. İhtiyacımız olan şey, bir isteğin sistemdeki tüm yolculuğunu uçtan uca görebilmektir.

Bu yazıda, modern gözlemlenebilirlik (observability) dünyasının kutsal üçlüsünden biri olan distributed tracing konusunu ele alacağız. Eski nesil, yönetmesi ve ölçeklemesi tam bir operasyonel kabus olan tracing çözümlerini bir kenara bırakıp; Grafana, Tempo ve OpenTelemetry (OTel) kullanarak, production-ready, maliyet dostu ve yüksek performanslı bir tracing hattını nasıl kuracağımızı adım adım inceleyeceğiz. Üstelik bunu yaparken, microservices mimarimizin performans darboğazlarını nasıl saniyeler içinde tespit edeceğimizi göreceğiz.

Neden Grafana Tempo? Elasticsearch’e RAM Yetiştiremeyenler Kulübü

Eğer daha önce Jaeger veya Zipkin kullanarak bir distributed tracing altyapısı kurduysanız, arka planda devasa bir Elasticsearch veya Cassandra cluster’ı yönetmenin ne kadar sancılı olduğunu bilirsiniz. Sadece trace datalarını indekslemek için harcanan RAM ve CPU miktarı, bazen uygulamanın kendisinden fazla kaynak tüketebilir.

Grafana Tempo, bu probleme radikal bir mühendislik yaklaşımı getiriyor: “No-index” architecture. Tempo, trace datalarını indekslemez. Bunun yerine, trace’leri sadece Trace ID’lerine göre anahtarlayarak nesne depolama servislerinde (Object Storage – AWS S3, Google Cloud Storage veya lokalde MinIO) bloklar halinde saklar. Peki, indeksleme yoksa arama nasıl yapılıyor? Tempo, metrikler (Prometheus) ve loglar (Loki) arasındaki korelasyonu kullanır. Logunuzda bir Trace ID bulursunuz ve Tempo bu ID’yi doğrudan nesne deposundan milisaniyeler içinde çeker. Sonuç? Sıfıra yakın operasyonel maliyet ve inanılmaz ucuz storage faturaları.

Mimarinin Tasarımı: Veri Nasıl Akacak?

Uygulamamızdan çıkan trace verilerinin Tempo’ya ulaşması için endüstri standardı olan OpenTelemetry protokolünü (OTLP) kullanacağız. Doğrudan uygulamadan Tempo’ya yazmak yerine, araya bir OpenTelemetry Collector koyacağız. Neden mi? Çünkü collector, uygulamalarımızın üzerindeki yükü alır, verileri buffer’lar, batch halinde gönderir ve gerekirse hassas verileri (PII) maskeleme işini üstlenir. Akış tam olarak şöyle olacak:

Uygulama (Go SDK) --[OTLP/gRPC]--> OTel Collector --[OTLP/gRPC]--> Grafana Tempo <-- Grafana (UI)

Adım 1: Altyapıyı Ayağa Kaldıralım (Docker Compose)

Lafı uzatmadan pratik tarafa geçelim. Lokalinizde bu yapıyı test edebilmeniz için hazırladığım, içinde Tempo, OTel Collector ve Grafana bulunan docker-compose.yml dosyamızı oluşturalım.

version: '3.8'

services:
  # Grafana Tempo (Trace Deposu)
  tempo:
    image: grafana/tempo:2.3.0
    command: [ "-config.file=/etc/tempo.yaml" ]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml
    ports:
      - "3200:3200"   # HTTP API
      - "4317:4317"   # OTLP gRPC receiver

  # OpenTelemetry Collector (Veri Toplayıcı ve Dağıtıcı)
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.90.0
    command: [ "--config=/etc/otel-collector-config.yaml" ]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4318:4318"   # OTLP HTTP receiver
    depends_on:
      - tempo

  # Grafana (Görselleştirme)
  grafana:
    image: grafana/grafana:10.2.0
    volumes:
      - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
    ports:
      - "3000:3000"
    depends_on:
      - tempo

Şimdi Tempo’nun konfigürasyon dosyası olan tempo.yaml dosyasını oluşturalım. Burada lokal diskimizi bir object storage gibi simüle edeceğiz:

stream_over_http: true
server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317

ingester:
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo/traces
    wal:
      path: /tmp/tempo/wal

Sırada OTel Collector konfigürasyonumuz (otel-collector-config.yaml) var. Burası gelen trace verisini karşılayıp Tempo’ya paslayacak olan ana santralimiz:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 256

exporters:
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp]

Son olarak Grafana’nın ayağa kalktığında otomatik olarak Tempo veri kaynağını tanıması için grafana-datasources.yaml dosyasını hazırlayalım:

apiVersion: 1

datasources:
  - name: Tempo
    type: tempo
    access: proxy
    orgId: 1
    url: http://tempo:3200
    basicAuth: false
    isDefault: true
    uid: tempo-datasource

Adım 2: Uygulama Enstrümantasyonu (Go ve OpenTelemetry)

Altyapımız hazır olduğuna göre, artık kod seviyesine inebiliriz. Distributed tracing’in en kritik kavramı Context Propagation‘dır. Yani bir istek dışarıdan geldiğinde oluşan Trace ID’nin, alt servislere yapılan HTTP veya gRPC çağrılarına taşınması gerekir. Eğer bu zinciri kırarsanız, trace bütünlüğünüz kaybolur.

Aşağıdaki Go örneğinde, hem bir OpenTelemetry Tracer’ı nasıl initialize edeceğimizi, hem de manuel olarak nasıl span üreteceğimizi göreceğiz. Bu örnek, prod ortamındaki mikroservisleriniz için harika bir şablon olacaktır.

package main

import (
	"context"
	"log"
	"net/http"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/grpc"
)

const (
	serviceName = "payment-gateway"
	collectorAddr = "localhost:4317"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
	// gRPC üzerinden OTel Collector'a bağlanacak exporter
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithInsecure(),
		otlptracegrpc.WithEndpoint(collectorAddr),
		otlptracegrpc.WithDialOption(grpc.WithBlock()),
	)
	if err != nil {
		return nil, err
	}

	// Servis kimlik bilgileri
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String(serviceName),
			semconv.ServiceVersionKey.String("1.0.0"),
		),
	)
	if err != nil {
		return nil, err
	}

	// Tracer Provider tanımı (Samplers burada belirlenebilir)
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()), // Prod için ParentBased(AlwaysSample) önerilir
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)

	otel.SetTracerProvider(tp)
	return tp, nil
}

func main() {
	ctx := context.Background()
	tp, err := initTracer(ctx)
	if err != nil {
		log.Fatalf("Tracer başlatılamadı: %v", err)
	}
	defer func() {
		if err := tp.Shutdown(ctx); err != nil {
			log.Printf("Tracer kapatılırken hata: %v", err)
		}
	}()

	tracer := otel.Tracer("http-server")

	http.HandleFunc("/checkout", func(w http.ResponseWriter, r *http.Request) {
		// HTTP isteğinden gelen context'i ve trace parent bilgisini alıyoruz
		ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
		
		ctx, span := tracer.Start(ctx, "CheckoutProcess", trace.WithSpanKind(trace.SpanKindServer))
		defer span.End()

		// DB Sorgusunu simüle eden bir alt span oluşturalım
		_, dbSpan := tracer.Start(ctx, "QueryUserBalance")
		time.Sleep(120 * time.Millisecond) // Veritabanı gecikmesi
		dbSpan.SetAttributes(semconv.DBSystemPostgreSQL)
		dbSpan.End()

		// Üçüncü parti API çağrısını simüle edelim
		_, apiSpan := tracer.Start(ctx, "CallStripeAPI")
		time.Sleep(350 * time.Millisecond) // Dış servis gecikmesi
		apiSpan.End()

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status": "success"}`))
	})

	log.Println("Server 8080 portunda çalışıyor...")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Adım 3: Grafana Explore’da Trace Görselleştirme

Artık her şey hazır! Sistemimizi ayağa kaldırmak için terminalden şu komutu çalıştıralım:

docker compose up -d

Ardından Go uygulamamızı çalıştıralım ve test isteği gönderelim:

go run main.go
curl -i http://localhost:8080/checkout

Şimdi tarayıcınızdan http://localhost:3000 adresine giderek Grafana’ya giriş yapın (varsayılan kullanıcı adı/şifre: admin/admin). Sol menüden Explore sekmesine tıklayın ve veri kaynağı olarak Tempo‘yu seçin.

Burada Search sekmesine gelip “payment-gateway” servisimizi seçtiğimizde, az önce attığımız isteğin trace verisini göreceğiz. Trace’e tıkladığınızda karşınıza çıkacak olan Gantt şeması benzeri görselleştirme, size şu altın bilgileri sunacak:

  • İsteğin toplamda ne kadar sürdüğünü (örn: 470ms)
  • Hangi alt işlemin (QueryUserBalance vs CallStripeAPI) bu sürenin ne kadarını yediğini (Darboğaz tespiti!)
  • Eğer işlem sırasında bir hata oluştuysa, o hatanın tam olarak hangi span üzerinde patladığını ve hata detaylarını (Exception stack trace)

Pro Tip: Loglar ve Trace’leri Birleştirmek (Derived Fields)

Kıdemli bir DevOps mühendisinin fark yaratacağı yer burasıdır. Sadece trace izlemek yetmez, loglar ile trace’ler arasında köprü kurmalısınız. Grafana Loki konfigürasyonunuza ekleyeceğiniz bir derived fields kuralı ile log satırındaki trace_id değerini otomatik olarak Tempo’ya yönlendiren tıklanabilir linklere dönüştürebilirsiniz. Böylece hata loguna bakan bir yazılımcı, tek bir tıkla o hatanın oluştuğu trace’e sıçrayabilir. Buna observability dünyasında “correlating signals” denir.

Sonuç ve Production Tavsiyeleri

Uygulamalarınız büyüdükçe ve microservices sayınız arttıkça, distributed tracing lüks olmaktan çıkıp bir hayatta kalma aracına dönüşür. Grafana Tempo ve OpenTelemetry ikilisi, size hem açık standartlara dayalı (vendor lock-in olmadan) hem de son derece düşük maliyetli bir gözlemlenebilirlik platformu sunar.

Üretim ortamına geçişte şu noktalara dikkat etmenizi öneririm:

  • Sampling (Örnekleme): Her isteğin trace’ini saklamak disk alanınızı hızla tüketebilir. OTel Collector üzerinde tail-based sampling kullanarak sadece hatalı (5xx) veya uzun süren (latency > 2s) istekleri %100 saklayıp, başarılı isteklerin sadece %1’ini örnekleyebilirsiniz.
  • Resource Constraints: OTel Collector için mutlaka memory_limiter processor’ını aktif edin. Yoğun yük altında collector’ın memory sızıntısı yapmasını engellemiş olursunuz.

Sistemlerinizi izlenebilir kılın, geceleri rahat uyuyun!

Category: Genel | LEAVE A COMMENT
Ağustos 16 2024

Prometheus Alerting Rules ve Recording Rules Yazma Rehberi

Gece saat 03:00. Telefonunuz acı acı çalıyor. Gözlerinizi kırpıştırarak ekrana bakıyorsunuz: “CPU Usage is High!”. Kalbiniz küt küt atarak bilgisayarı açıyor, Kubernetes cluster’ına bağlanıyor ve aslında her şeyin normal olduğunu, sadece anlık bir cron job’ın CPU’yu tetiklediğini görüyorsunuz. Tebrikler, “Alert Fatigue” (alarm yorgunluğu) kulübüne hoş geldiniz! Modern devops ve sre dünyasında, yanlış yapılandırılmış bir prometheus ve alerting altyapısı kadar ekipleri yıpratan çok az şey vardır. Bu rehberde, uykularınızı kaçırmayacak, gerçekten aksiyon alınabilir alert kuralları yazmayı ve recording rules kullanarak Prometheus sunucunuzun CPU’sunu nasıl rahatlatacağınızı konuşacağız.

1. Alert Felsefesi: “Semptom” vs. “Sebep”

Bir alert kuralı yazmadan önce kendimize sormamız gereken ilk soru şudur: “Bu alarm çaldığında birinin yataktan kalkıp hemen bir şey yapması gerekiyor mu?” Cevap hayır ise, o bir alert değil, sadece bir metric veya en fazla bir dashboard grafiğidir.

SRE dünyasında alert’leri ikiye ayırırız:

  • Semptom Tabanlı (Symptom-based): “Kullanıcılar 500 hatası alıyor.” (Hemen müdahale edilmeli!)
  • Sebep Tabanlı (Cause-based): “Node CPU kullanımı %92.” (Belki de Kubernetes bunu zaten scale edecek, neden uyandırılıyorum?)

Her zaman semptomlara alert kurmaya çalışın. CPU yükselmesi tek başına bir felaket değildir; eğer bu durum latency (gecikme) artışına veya hata oranının (error rate) yükselmesine sebep olmuyorsa, bırakın Prometheus sessizce metric toplamaya devam etsin.

2. Etkin Prometheus Alerting Rules Yazmak

Prometheus üzerinde alert tanımlarken PromQL dilinin gücünden faydalanırız. Ancak verimsiz yazılan sorgular hem Prometheus’u yorar hem de yanlış alarmlara (false positive) yol açar.

Kötü Bir Örnek ve Analizi

- alert: HighCPUUsage
  expr: node_cpu_seconds_total{mode="idle"} < 10
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "CPU usage is high on {{ $labels.instance }}"

Bu kural neden kötü?

  1. Çok kısa süre (for: 1m): 1 dakikalık bir CPU yükselmesi geçici bir spike (anlık sıçrama) olabilir. Hemen alarm tetiklemek anlamsızdır.
  2. Ham metric kullanımı: `node_cpu_seconds_total` doğrudan kullanılmamalı, rate() veya irate() fonksiyonları ile normalize edilmelidir.
  3. Aksiyon yok: “CPU yüksek” bilgisini alan bir mühendis ne yapmalı? Annotation içinde bir runbook linki yok.

Daha İyi Bir Yaklaşım

- alert: NodeCPUUtilizationHigh
  expr: (100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)) > 85
  for: 15m
  labels:
    severity: warning
    team: platform
  annotations:
    summary: "Node {{ $labels.instance }} CPU utilization is above 85% for 15 minutes."
    description: "High CPU might impact application performance. Check Pod distribution."
    runbook_url: "https://wiki.kertenkerem.net/ops/runbooks/high-node-cpu"

Burada neyi değiştirdik? Sorguyu 5 dakikalık pencerelerde `rate` alacak şekilde optimize ettik, bekleme süresini (`for`) 15 dakikaya çıkararak geçici spike’ları eledik ve en önemlisi, alarmı alan mühendis için bir `runbook_url` ekledik.

3. Prometheus’u Coşturmak: Recording Rules

Prometheus her `evaluation_interval` süresinde (genelde 15-30 saniye) tüm alert kurallarını sıfırdan hesaplar. Eğer yukarıdaki gibi ağır matematiksel işlemler içeren yüzlerce kuralınız ve binlerce node/pod içeren bir cluster’ınız varsa, Prometheus sunucunuzun CPU ve Memory kullanımı tavan yapacaktır. İşte burada sahneye recording rules çıkıyor.

Recording rules, karmaşık ve sık çalıştırılan PromQL sorgularının sonuçlarını önceden hesaplayıp yeni bir metric olarak kaydetmenizi sağlar. Böylece alert kuralları veya Grafana dashboard’ları bu karmaşık sorguyu her seferinde çalıştırmak yerine, önceden hesaplanmış hazır veriyi okur.

Yavaş Sorgu Örneği (Kubernetes Pod CPU Utilization)

Grafana dashboard’unuzda veya alert kuralınızda her 15 saniyede bir şu sorgunun çalıştığını hayal edin:

sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{container!=""}[5m]))

Bunu optimize etmek için bir recording rule tanımlayalım:

groups:
  - name: kertenkerem.cpu.rules
    rules:
      - record: namespace_pod:container_cpu_usage:rate5m
        expr: sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{container!=""}[5m]))

Artık Prometheus arkada bu sorguyu sessizce çalıştırıp sonucu `namespace_pod:container_cpu_usage:rate5m` isimli yeni ve tertemiz bir metric olarak kaydedecek. Şimdi alert kuralımızı bu yeni metric üzerinden yazalım:

- alert: PodCPULimitApproaching
  expr: namespace_pod:container_cpu_usage:rate5m > 0.90
  for: 10m
  labels:
    severity: warning

Prometheus artık her sorgulamada diskten gigabaytlarca veri okuyup CPU hesaplaması yapmak zorunda kalmayacak. Doğrudan hazır hesaplanmış tek bir metric değerine bakacak. Disk I/O ve CPU tüketiminizde inanılmaz bir düşüş göreceksiniz.

4. Alertmanager ile Gürültüyü Engelleme: Inhibition ve Silence

Prometheus alert’i üretir, ancak bu alert’lerin nereye (Slack, PagerDuty, Webhook) nasıl gideceğini yöneten yer Alertmanager bileşenidir. Alert gürültüsünü kesmek için iki can kurtaran silahımız var: Inhibition Rules ve Silences.

Inhibition Rules (Engelleme Kuralları)

Bir veri merkezindeki tüm sunucuların bağlı olduğu ana switch çöktüğünde ne olur? Muhtemelen o switch arkasındaki 50 sunucu ve üzerindeki 500 pod için ayrı ayrı “Node Down” ve “Pod Down” alarmı alırsınız. Telefonunuz bildirim yağmurundan kilitlenir.

Inhibition rules, belirli bir ana alarm aktifken, onunla ilişkili alt alarmların susturulmasını sağlar. Klasik bir Alertmanager konfigürasyon örneği:

inhibit_rules:
  - source_matchers: [alertname="NodeNetworkDown"]
    target_matchers: [alertname="InstanceDown"]
    equal: ['node', 'instance']

Yukarıdaki kural der ki: Eğer bir node için `NodeNetworkDown` alarmı zaten tetiklendiyse, aynı node üzerindeki servisler için `InstanceDown` (servis ulaşılamaz) alarmı gönderme, onu block’la. Mühendis zaten ağın gittiğini biliyor, servislerin de gitmiş olacağını tahmin etmek için dahi olmaya gerek yok!

Silencing (Susturma) ve Maintenance

Planlı bir bakım çalışması (maintenance window) yapıyorsunuz ve Kubernetes cluster’ındaki bazı node’ları restart edeceksiniz. Bu sırada Slack kanalınızın “CRITICAL” loglarıyla dolmasını istemezsiniz.

Bunun için Alertmanager UI üzerinden veya `amtool` CLI aracını kullanarak hızlıca geçici “silence” tanımları yapabilirsiniz:

amtool silence add alertname="NodeCPUUtilizationHigh" node="node-01" --duration=2h --comment="Planli kernel upgrade calismasi"

Bu komut, `node-01` üzerindeki yüksek CPU alarmlarını 2 saat boyunca askıya alır. Bakım bittiğinde sistem otomatik olarak normal haline döner.

Sonuç: Temiz Alarmlar, Huzurlu Geceler

Prometheus ile monitoring kurmak kolaydır; ancak akıllıca tasarlanmış bir alarm yapısı kurmak gerçek bir mühendislik disiplini gerektirir. Özetlemek gerekirse:

  1. Sadece aksiyon almanız gereken durumlarda telefonunuzu çaldırın.
  2. Ağır PromQL sorgularınızı recording rules ile önceden hesaplayarak Prometheus’u rahatlatın.
  3. Alertmanager’ın inhibition kurallarını kullanarak kriz anlarında alarm bombardımanına tutulmaktan kaçının.

Bir sonraki yazımızda görüşmek üzere, metrics’iniz bol, geceleriniz sessiz olsun!

Category: Genel | LEAVE A COMMENT
Ağustos 9 2024

Grafana Loki ile Merkezi Log Yönetimi: ELK’nın Hantallığından Kaçış Rehberi

Sistem yöneticilerinin ve DevOps mühendislerinin ortak rüyası nedir? Tabii ki gece yarısı “Disk Full” uyarısıyla uyanmamak. Ancak modern mikroservis mimarilerinde observability sağlamak ve devasa log yığınları arasında arama yapmak tam bir karın ağrısı olabiliyor. Yıllarca ELK (Elasticsearch, Logstash, Kibana) üçlüsünün JVM bellek canavarlığıyla mücadele ettikten sonra, hafif, dinamik ve bütçe dostu bir alternatif arıyorsanız doğru yerdesiniz. Bu rehberde, grafana ekosisteminin parlayan yıldızı loki ile nasıl ölçeklenebilir ve az maliyetli bir log management altyapısı kuracağımızı adım adım inceleyeceğiz.

Neden Loki? (Yani, Neden Elasticsearch Değil?)

Elasticsearch harika bir arama motorudur; bunu kimse inkar edemez. Ancak log yönetimi söz konusu olduğunda, Elasticsearch her şeyi (log satırının tamamını) indexler. Bu durum devasa index boyutlarına ve dolayısıyla korkunç bir RAM tüketimine yol açar. Kendi sunucularında ELK koşturanlar, Elasticsearch pod’unun 16GB RAM’i “kahvaltı niyetine” tükettiğini çok iyi bilir.

Loki ise farklı bir felsefe benimser: “Prometheus, ama loglar için.”

Loki, log içeriğinin tamamını indexlemez. Sadece logların metadata’sını (yani label/etiketlerini; örneğin app="nginx", env="production") indexler. Log satırlarının kendisini ise sıkıştırılmış chunk’lar halinde doğrudan nesne depolama servislerinde (AWS S3, Google Cloud Storage veya lokalde MinIO) saklar. Bu yaklaşım bize ne kazandırır?

  • Düşük Altyapı Maliyeti: RAM tüketimi MB’lar seviyesine düşer, disk maliyeti ise S3 kullanımı sayesinde neredeyse devede kulak kalır.
  • Kolay Korelasyon: Prometheus metriklerinizle aynı etiketleri (labels) kullanarak metrikten loga tek tıkla geçiş yapabilirsiniz.
  • Kolay Ölçeklenme: Loki’nin mikroservis mimarisi sayesinde yazma (ingester) ve okuma (querier) katmanlarını birbirinden bağımsız olarak ölçekleyebilirsiniz.

Sistem Mimarisi: Parçaları Birleştirelim

Loki tek başına çalışmaz. Log yönetim sistemimiz üç temel bileşenden oluşur:

  1. Promtail (Agent): Logları toplar, etiketler (labeling) ve Loki’ye push eder (Kubernetes dünyasında her node üzerinde bir DaemonSet olarak koşar).
  2. Loki (Veri Deposu): Gelen logları alır, indeksler ve depolar.
  3. Grafana (Görselleştirme): Loki’yi bir datasource olarak ekler, LogQL sorguları yazar ve şık dashboard’lar oluştururuz.

Adım Adım Kurulum: Kubernetes Üzerinde Loki ve Promtail

Lafı fazla uzatmadan klavyenin başına geçelim. Kurulum için en pratik yol Helm kullanmaktır. Kubernetes cluster’ınızın hazır olduğunu varsayarak işlemlere başlıyoruz.

1. Helm Depolarını Ekleme

İlk olarak Grafana’nın resmi Helm reposunu sistemimize ekleyelim ve güncelleyelim:

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

2. Loki Kurulumu

Üretim ortamı için Loki’yi S3 entegrasyonu ile kurmak en doğrusudur. Ancak bu rehberde pratiklik açısından lokal disk (StatefulSet) kullanan basit bir kurulum yapacağız. Aşağıdaki loki-values.yaml dosyasını oluşturalım:

loki:
  commonConfig:
    replication_factor: 1
  storage:
    type: 'filesystem'
  auth_enabled: false

singleBinary:
  replicas: 1

persistence:
  enabled: true
  size: 10Gi
  storageClassName: "standard" # Kendi storage class'ınızla değiştirin

Şimdi Loki’yi bu konfigürasyonla kuralım:

helm install loki grafana/loki -f loki-values.yaml --namespace logging --create-namespace

3. Promtail Kurulumu

Promtail, Kubernetes cluster’ındaki pod loglarını otomatik olarak keşfedecek şekilde konfigüre edilmiştir. promtail-values.yaml dosyamızı oluşturalım. Burada en kritik nokta Promtail’e Loki’nin adresini doğru vermektir:

config:
  clients:
    - url: http://loki-gateway.logging.svc.cluster.local/loki/api/v1/push

# Kubernetes namespace ve pod etiketlerini otomatik olarak loglara eklemesi için relabeling kuralları hazır gelir.

Promtail’i kuralım:

helm install promtail grafana/promtail -f promtail-values.yaml --namespace logging

LogQL 101: Loglar İçinde Kaybolmadan Arama Yapmak

Artık loglarımız Loki’ye akıyor. Peki bunları nasıl sorgulayacağız? Karşınızda LogQL. Eğer Prometheus’un sorgu dili olan PromQL’e aşinaysanız, LogQL size çocuk oyuncağı gelecektir. LogQL sorguları iki ana bölümden oluşur: Log Stream Selector ve Filter Expression.

Basit Filtreleme

Belirli bir namespace altındaki, belirli bir uygulamanın loglarını çekmek ve içinde “error” geçen satırları yakalamak için:

{namespace="production", app="payment-api"} |= "error"

Eğer “timeout” içeren ama “connection” içermeyen logları istiyorsak zincirleme filtre kullanabiliriz:

{app="nginx"} |= "timeout" != "connection"

Parser ve Yapılandırılmış (Structured) Loglar

Eğer uygulamanız JSON formatında log üretiyorsa, Loki bu logları çalışma anında (on-the-fly) ayrıştırabilir. Bu, Loki’nin en güçlü özelliklerinden biridir:

{app="auth-service"} | json | status = "500"

Yukarıdaki sorguda | json ifadesi, log satırını JSON olarak parse eder ve içerideki tüm JSON anahtarlarını geçici etiketlere (labels) dönüştürür. Sonrasında status = "500" filtresi ile sadece HTTP 500 hatası veren istekleri süzebiliriz.

Loglardan Metrik Üretmek

Diyelim ki son 5 dakikada Nginx loglarındaki HTTP 500 hatalarının saniyedeki oranını (rate) görmek istiyorsunuz. LogQL bunu yapabilir:

sum(rate({app="nginx"} | json | status =~ "5.." [5m]))

Bu sorguyu Grafana’da bir Graph panelinde çalıştırarak doğrudan loglardan türetilmiş canlı metrik grafikleri elde edebilirsiniz. Prometheus’a yük bindirmeden mükemmel bir alarm altyapısı!

Grafana ile Log Görselleştirme

Logları sorgulamak için Grafana arayüzünü kullanacağız. İşte izlemeniz gereken adımlar:

  1. Grafana arayüzüne gidin ve sol menüden Connections -> Data Sources yolunu izleyin.
  2. Add data source butonuna tıklayın ve listeden Loki‘yi seçin.
  3. Connection URL kısmına Loki servisinizin adresini yazın: http://loki-gateway.logging.svc.cluster.local (Eğer Grafana aynı cluster içindeyse).
  4. Sayfanın altındaki Save & Test butonuna tıklayın. Yeşil onay kodunu gördüyseniz bağlantı tamamdır!

Şimdi sol menüden Explore sekmesine gidin. Üst taraftan veri kaynağı olarak Loki‘yi seçin. Log browser üzerinden etiketlerinizi seçerek veya yukarıda paylaştığımız LogQL sorgularını yazarak loglarınızı canlı olarak (Live tailing) izlemeye başlayabilirsiniz.

Grafana Dashboard Visualization

Üretim Ortamı İçin Altın Tavsiyeler (Pro-Tips)

Loki’yi production ortamında koştururken canınızın sıkılmasını istemiyorsanız şu üç kurala mutlaka dikkat edin:

  1. Yüksek Cardinality’den Kaçının (High Cardinality): Loglarınıza asla user_id, ip_address veya request_id gibi binlerce benzersiz değere sahip dinamik etiketler (labels) eklemeyin. Bu, Loki’nin indeks yapısını şişirir ve sorgu performansını yerle bir eder. Bu tarz dinamik verileri log satırının içinde bırakın; aramak için LogQL filtrelerini (|= veya | json) kullanın.
  2. S3 Entegrasyonu: Lokal disk yerine her zaman AWS S3 veya MinIO gibi nesne depolama çözümlerini tercih edin. Bu sayede log retention (logların silinme süresi) politikanızı kolayca yönetebilir ve disk dolma riskini sıfıra indirebilirsiniz.
  3. Query Frontend Kullanımı: Yoğun log sorgulaması yapılan ortamlarda Loki’yi mikroservis modunda kurup query-frontend bileşenini aktif edin. Bu bileşen, büyük zaman aralıklarındaki sorguları parçalara ayırarak paralel çalıştırır ve caching mekanizması sunar.

Özet

Grafana Loki, modern DevOps pratiklerinde log yönetimi için ezber bozan bir araç. ELK’nın getirdiği o devasa operasyonel yük ve maliyet olmadan, sadece ihtiyacımız olanı indexleyerek harika bir observability altyapısı kurduk. Promtail ile logları topladık, LogQL ile derinlemesine analiz ettik ve Grafana ile taçlandırdık.

Siz de production ortamlarınızda kaynak tüketimini optimize etmek istiyorsanız, Loki’ye bir şans verin. Bir sonraki yazımızda görüşmek üzere, loglarınız temiz, sistemleriniz ayakta kalsın!

Category: Genel | LEAVE A COMMENT
Temmuz 26 2024

Zabbix’te LLD ile Dinamik Monitoring Otomasyonu: Custom Script ve Trigger Entegrasyonu

Modern altyapılarda statik konfigürasyon dosyalarıyla veya arayüzden tek tek host ekleyerek hayatta kalmak imkansız. Hele ki her gün onlarca microservice’in, disk partition’ının veya network interface’inin ayağa kalkıp kapandığı dinamik bir Kubernetes ya da bare-metal cluster yönetiyorsanız, manuel izleme (monitoring) tam bir kabusa dönüşür. Bu noktada imdadımıza yetişen en güçlü mekanizmalardan biri, şüphesiz Zabbix LLD (Low-Level Discovery) tabanlı dinamik monitoring otomasyonu mimarisidir.

Bu makalede, teorik tanımları bir kenara bırakıp doğrudan production ortamında çalışan, özelleştirilmiş (custom) bir LLD senaryosunu sıfırdan inşa edeceğiz. Amacımız, host üzerindeki dinamik Docker container’larını otomatik keşfetmek, bunlara ait metrikleri toplamak ve dinamik trigger prototipleri ile akıllı alarmlar üretmek.

Neden Standart Discovery Değil de LLD?

Zabbix’te iki tip keşif mekanizması bulunur: Network Discovery ve Low-Level Discovery (LLD). Network Discovery, ağdaki IP bloklarını tarayıp “Burada yeni bir sunucu var mı?” sorusuna yanıt ararken; LLD, “Bu sunucunun içinde izlemem gereken ve sürekli değişen hangi alt bileşenler var?” sorusunu yanıtlar. Diskler, network arayüzleri, CPU çekirdekleri, veritabanı instance’ları veya Docker container’ları LLD’nin ana hedefidir.

LLD’nin arkasındaki sihir JSON formatındadır. Zabbix Agent veya bir script, Zabbix Server’a belirli bir yapıda JSON döndürür. Server bu JSON’ı parse eder, içindeki makroları (`{#MACRO}`) ayıklar ve tanımladığınız prototiplere göre otomatik olarak Item, Trigger ve Graph oluşturur.

Adım 1: Custom LLD Scripti Yazımı (JSON Formatı)

Zabbix’in bir yapıyı LLD ile keşfedebilmesi için çıktının mutlaka `data` array’i içeren veya doğrudan düz bir JSON array formatında olması gerekir. Biz, sunucuda çalışan aktif Docker container’larını adlarıyla keşfedecek bir Python scripti yazacağız. Bu scripti Zabbix Agent’ın kurulu olduğu hedef makinede konumlandıracağız.

Aşağıdaki scripti hedef sunucuda /etc/zabbix/scripts/discover_containers.py olarak kaydedin:

#!/usr/bin/env python3
import json
import subprocess

def get_containers():
    try:
        # Çalışan container adlarını alıyoruz
        cmd = "docker ps --format '{{.Names}}'"
        result = subprocess.check_output(cmd, shell=True).decode('utf-8')
        containers = [line.strip() for line in result.split('\n') if line.strip()]
        
        # Zabbix LLD formatına uygun JSON şeması oluşturuyoruz
        lld_data = []
        for container in containers:
            lld_data.append({
                "{#CONTAINERNAME}": container
            })
        
        # Zabbix 5.x ve sonraki sürümler doğrudan array kabul eder
        print(json.dumps(lld_data))
    except Exception as e:
        print(json.dumps([]))

if __name__ == "__main__":
    get_containers()

Scriptin çalıştırılabilir olduğundan emin olun:

chmod +x /etc/zabbix/scripts/discover_containers.py

Adım 2: Zabbix Agent Konfigürasyonu (UserParameter)

Yazdığımız scripti Zabbix Server’ın tetikleyebilmesi için Zabbix Agent üzerinde bir UserParameter tanımlamamız gerekiyor. Neden böyle yapıyoruz? Çünkü Agent’ın yeteneklerini dışarıdan güvenli bir şekilde genişletmenin en performanslı yolu budur.

/etc/zabbix/zabbix_agentd.d/userparameter_docker.conf dosyasını oluşturun ve şu satırı ekleyin:

UserParameter=docker.container.discovery,/etc/zabbix/scripts/discover_containers.py
UserParameter=docker.container.cpu[*],docker stats --no-stream --format "{{.CPUPerc}}" $1 | cut -d'%' -f1
UserParameter=docker.container.status[*],docker inspect -f '{{.State.Running}}' $1

Burada ne yaptık?
1. docker.container.discovery anahtarı ile LLD JSON çıktısını sağladık.
2. docker.container.cpu[*] parametrik anahtarı ile keşfettiğimiz container’ın anlık CPU tüketimini çekeceğiz.
3. docker.container.status[*] ile de container’ın çalışıp çalışmadığını (true/false) denetleyeceğiz.

Değişikliklerin devreye girmesi için Zabbix Agent servisini yeniden başlatın:

systemctl restart zabbix-agent

Şimdi Zabbix Server veya proxy üzerinden bu mekanizmanın çalışıp çalışmadığını test edelim:

zabbix_get -s 127.0.0.1 -k docker.container.discovery

Eğer her şey doğru yapılandırıldıysa, ekranda şu tarz bir JSON görmelisiniz:

[{"{#CONTAINERNAME}": "nginx-prod"}, {"{#CONTAINERNAME}": "postgres-db"}]

Adım 3: Zabbix UI Üzerinde Template ve LLD Rule Tanımlama

Sistem katmanındaki hazırlığımız bitti. Şimdi Zabbix Web UI üzerinde bu veriyi işleyecek şablonu (Template) hazırlayalım.

1. Discovery Rule Oluşturma

Zabbix UI’da Configuration > Templates adımına gidin ve yeni bir template oluşturun (Örn: Template App Docker Custom).

  • Oluşturduğunuz template içinde Discovery rules sekmesine gelin ve Create discovery rule butonuna tıklayın.
  • Name: Docker Container Discovery
  • Type: Zabbix agent (veya agent active)
  • Key: docker.container.discovery
  • Update interval: 1m (Geliştirme aşamasında hızlı görmek için 1 dakika, production’da 10m veya 30m idealdir).

2. Item Prototypes (Öğe Prototipleri) Oluşturma

Keşfedilen her bir container için hangi metrikleri toplayacağımızı burada tanımlıyoruz. Discovery rule içindeki Item prototypes sekmesine geçin ve Create item prototype deyin.

CPU Metriği İçin:

  • Name: Container {#CONTAINERNAME}: CPU Usage
  • Key: docker.container.cpu[{#CONTAINERNAME}]
  • Type: Zabbix agent
  • Type of information: Numeric (float)
  • Units: %

Status Metriği İçin:

  • Name: Container {#CONTAINERNAME}: Status
  • Key: docker.container.status[{#CONTAINERNAME}]
  • Type: Zabbix agent
  • Type of information: Character (veya boolean/numeric dönüşümü yapabilirsiniz, inspect çıktımız ‘true’/’false’ döneceği için Character seçip trigger’da regex ile kontrol etmek pratik bir yoldur).

Adım 4: Dinamik Trigger Prototipleri ile Akıllı Alarmlar

Sadece veri toplamak yetmez, bu verilerin anomali durumlarında nöbetçi mühendisi ayağa kaldırması gerekir. Ancak her yeni container için manuel alarm yazamayız. LLD burada devreye girerek her container için bağımsız trigger’lar üretir.

Discovery rule altındaki Trigger prototypes sekmesine gelin ve Create trigger prototype butonuna tıklayın.

Senaryo: Container Stop Olduysa Alarm Üret

  • Name: Container {#CONTAINERNAME} is not running on {HOST.NAME}
  • Expression:
    last(/Template App Docker Custom/docker.container.status[{#CONTAINERNAME}])="false"
  • Severity: High veya Average

Bu ifade sayesinde, sistemde yeni bir container (örneğin redis-cache) ayağa kalktığı anda Zabbix bunu LLD ile keşfedecek, ardından docker.container.status[redis-cache] item’ını oluşturacak ve eğer bu container stop durumuna düşerse otomatik olarak “Container redis-cache is not running on Server-01” başlığıyla alarm üretecektir.

Production İpuçları ve “Neden Böyle?” Analizi

Büyük ölçekli ortamlarda LLD kullanırken sistem kaynaklarını tüketmemek ve false-positive alarmların önüne geçmek için şu mimari kararları uygulamalısınız:

Keep Lost Resources Period (Kayıp Kaynakları Tutma Süresi)

LLD rule konfigürasyonunda Keep lost resources period adında bir parametre göreceksiniz. Varsayılan değeri 30 gündür. Eğer dinamik ortamlarda (örn. Jenkins agent’ları gibi sürekli açılıp kapanan container’lar) çalışıyorsanız, bu değeri 30 gün bırakırsanız Zabbix veritabanınız çöpe döner. Silinen container’ların monitoring öğelerinin sistemden hızlıca temizlenmesi için bu değeri 1h (1 saat) veya en fazla 1d (1 gün) olarak ayarlayın.

Discard Unchanged (Değişmeyen Veriyi Yoksayma)

Container status metriği her dakika “true” dönecektir. Veritabanında her dakika aynı veriyi yazarak I/O tüketmemek için Item Prototype ayarlarındaki Preprocessing sekmesine gidin. Buraya Discard unchanged with heartbeat ekleyin ve süresini 1h yapın. Böylece durum değişmediği sürece veritabanına sadece saatte bir yazılır, ancak durum “false” olduğu an hemen kayıt düşülür.

Zabbix LLD, altyapınızı kodla yönettiğiniz (Infrastructure as Code) modern dünyada izleme katmanının da bu hıza ayak uydurmasını sağlayan en kritik araçtır. Doğru kurgulanmış bir LLD şablonu, operasyonel iş yükünüzü kalıcı olarak azaltır.

Category: Genel | LEAVE A COMMENT