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

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