Distributed Trace Analizi için Pratik Desenler

Selamlar, bu yazıda distributed tracing verisini topladıktan sonra ne yapacağımıza odaklanacağız. Trace altyapısı kurmak görece çözülmüş bir iş, ama elinizde bir avuç span varken 'şimdi bunlardan ne çıkaracağız?' sorusu çoğu zaman havada kalıyor. Açıkçası ben de ilk başlarda Jaeger UI'da gezip 'kırmızı yere bakalım' modunda kalıyordum, sonra bir noktada tek tek bakarak gitmeyeceğini anladım. Bu yazıda kendi kullandığım analiz desenlerini paylaşayım.

Önce kritik yol, sonra geri kalanı

Bir trace'e baktığınızda ilk soracağınız şey 'bu isteğin toplam latency'sini hangi span zinciri belirliyor?' olmalı. Buna kritik yol (critical path) deniyor: root span'den yaprağa giden, en geç biten dalın oluşturduğu zincir. Diğer her şey paralel çalışmış olabilir, ama kritik yolu kısaltmadan toplam süreyi düşüremezsiniz.

Pratikte şuna dikkat ediyorum: Eğer kritik yoldaki tek bir span toplam sürenin yüzde 50'sinden fazlasını yiyorsa optimizasyon önceliği nettir. Yüzde 20-30 bandında dağılmış üç-dört span varsa iş zorlaşıyor; o zaman tek tek değil, mimari olarak bakmak gerek (bu çağrılar gerçekten sıralı mı olmalı, paralelleştirilebilir mi).

N+1 desenini span'lerden yakalamak

N+1 desenine herkes ORM'lerden aşinadır ama dağıtık sistemde aynı şey servisler arası çağrılarda da oluyor. Bir trace'te aynı span adının (db.query SELECT users veya http GET /products/{id}) onlarca kez tekrarlandığını görüyorsanız muhtemelen N+1'e bakıyorsunuz.

Tempo kullanıyorsanız TraceQL ile bunu sorgulamak çok rahat:

{ resource.service.name = "orders-api" } | count_over_time() by (name) > 20

Bu sorgu, orders-api servisinde tek bir trace içinde 20'den fazla aynı isimde span üreten istekleri çıkarıyor. Sayı eşiğini kendi sisteminize göre ayarlarsınız ama mantık aynı. Ben bunu bir alert kuralına bağlamayı tavsiye ederim, çünkü N+1 sorunu genelde production'da veri büyüdükçe ortaya çıkar; staging'de fark etmezsiniz.

Kayıp span tespiti

En sinsi sorunlardan biri eksik span. Beklediğiniz bir alt çağrı trace'te hiç görünmüyorsa iki ihtimal var: ya çağrı hiç yapılmamış (circuit breaker, cache hit, erken dönüş), ya da instrumentation kopmuş (context propagation patlamış, library auto-instrument edilmemiş).

Bunu yakalamak için baseline'a ihtiyacınız var. Ben bir hafta boyunca toplanan trace'lerden her endpoint için 'tipik' span listesi çıkarıyorum, sonra yeni gelenleri bununla karşılaştırıyorum. Pseudocode olarak şöyle:

expected = {"auth.verify", "db.users.select", "cache.get", "payment.charge"}
actual = {span.name for span in trace.spans}

missing = expected - actual
if missing:
    alert(f"trace {trace.id}: kayip span'ler -> {missing}")

Burada dikkat edilecek nokta şu: bazı span'ler doğal olarak opsiyoneldir (cache hit'te db.query olmayabilir). Baseline'ı endpoint bazında değil, request shape bazında çıkarmak daha doğru oluyor.

Latency anomali ve z-score

Bir span'in 'yavaş' olup olmadığını mutlak değerle anlamak zor. 200 ms bir DB query için kötü, bir external API çağrısı için normaldir. Bunun yerine her (servis, span_adi) çifti için baseline tutuyorum (mean ve standart sapma), sonra yeni gelen span'in z-score'unu hesaplıyorum:

z = (duration - baseline.mean) / baseline.stddev
if z > 3:
    flag_as_anomaly(span)

3 sigma genelde iyi bir başlangıç. Welford algoritması ile online olarak hesaplayabilirsiniz, bütün geçmişi tutmak gerekmez. Bence bu yaklaşımın en güzel tarafı, sistem yavaşladıkça baseline'ın da yavaşça kayması; siz threshold'u sabitlemek zorunda kalmıyorsunuz.

Exemplar ile metric-trace bağlama

Prometheus metric'lerinizde histogram_quantile ile p99 latency'nin yükseldiğini gördünüz, peki o tek bir kötü isteğin trace'ine nasıl ulaşacaksınız? Cevap exemplar. OpenTelemetry SDK'ları histogram bucket'larına o anki trace ID'yi exemplar olarak iliştiriyor; Grafana'da p99 noktasına tıklayınca doğrudan ilgili trace'e atlıyorsunuz.

Aktif etmek için collector tarafında şu yeterli:

processors:
  spanmetrics:
    histogram:
      explicit:
        buckets: [10ms, 50ms, 100ms, 500ms, 1s, 5s]
    exemplars:
      enabled: true

Bunu kurduktan sonra metric'ten trace'e geçiş bir tık meselesi. Şahsi kanaatim, exemplar olmadan trace altyapısı yarım kalıyor; metric uyarı veriyor ama nedeni bulmak için elinizde sadece servis adı oluyor.

Sık karşılaşılan hatalar

  • Tek trace'e bakıp sonuç çıkarmak: Bir trace anomali olabilir. Aynı endpoint'in en az 50-100 trace'inde aynı deseni görmeden 'çözdük' demeyin.
  • Sampling'i unutmak: Head-based sampling kullanıyorsanız zaten kötü trace'lerin çoğu çöpe gidiyor olabilir. Hata trace'leri için tail-based sampling şart.
  • Statik threshold'a yapışmak: 'p99 500 ms'i geçerse uyar' kuralı altı ay sonra anlamsız olur. Percentile bazlı, baseline'a göre kayan eşikler kullanın.
  • Servis adı yerine span adı bazında baseline tutmak: db.query adı her yerde aynı ama context tamamen farklı. Mutlaka (servis, span_adi) çifti.

Kapanış

Trace toplamak kolay, anlamlandırmak zor. Yukarıdaki desenlerin hiçbiri tek başına sihir değil ama beraber kurulduklarında reaktif debug'dan proaktif gözleme geçişi sağlıyorlar. Bana sorarsanız önce kritik yol ve N+1 ile başlayın, sonra exemplar'ı ekleyin; anomali tespiti baseline biriktikten sonra zaten kendiliğinden anlamlı hale geliyor. Umarım faydalı olur, görüşmek üzere.