<

Bir Siparişin 14 Servislik Yolculuğu: Dağıtık İzleme Olmadan Kördük

Platformda CTO olarak ilk haftamdaydım. Akşam 20.40 sularında, yemek tarafında sipariş dönüşüm oranının yüzde 4 düştüğünü gösteren bir alarm geldi. Müşteri destek hattına "param çekildi, sipariş görünmüyor" çağrıları düşmeye başlamıştı. Savaş odasına girdiğimde altı mühendis, altı ayrı ekranda altı ayrı servisin logunu grep'liyordu. Birine sordum: "Şu müşterinin siparişi hangi serviste kayboldu?" Cevap, o gece hafızama kazındı: "Bilmiyoruz. Sipariş 14 servisten geçiyor ve her servis kendi log formatını kullanıyor. Sınırdan sınıra elle eşliyoruz." Sorunun kendisi 40 dakikalık bir konfigürasyon hatası çıktı. Bulması 6 saat 20 dakika sürdü. O gece not defterime tek cümle yazdım: Biz sistemi izlemiyoruz; sistemin anılarını topluyoruz.

On Dört Duraklı Bir Yolculuğun Haritasız Hali

Bir yemek siparişinin bizdeki yolculuğunu sonradan bir tahtaya çizdik: API gateway, oturum doğrulama, sepet, kampanya/kupon, fiyatlama, stok-menü, sipariş orkestrasyonu, ödeme, fraud kontrolü, restoran entegrasyonu, kurye atama, bildirim, fatura ve veri ambarı beslemesi. On dört servis; üçü PHP, beşi Java Spring Boot, dördü Node.js, ikisi harici sağlayıcı. Aralarında hem senkron HTTP hem Kafka var. O geceki hata, kampanya servisinin yeni açılan bir kupon tipinde fiyatlama servisine eksi tutarlı bir satır göndermesiydi; fiyatlama bunu reddediyor, sipariş orkestrasyonu ödemeyi çoktan almış olduğu için sipariş "askıda" kalıyordu. Zincirin iki ucundaki servisler sağlıklıydı, hata tam ortada, iki servisin arasındaki sözleşmedeydi. Servis bazlı dashboard'ların hepsi yeşildi — çünkü her servis kendi işini "doğru" yapıyordu.

İlk toplantıda beklediğim itiraz geldi: "Correlation ID'miz zaten var." Vardı, kağıt üzerinde. Gerçekte üç ayrı kuşaktan üç ayrı gelenek yaşıyordu: PHP servisleri X-Request-Id header'ı taşıyordu ama Kafka'ya yazarken düşürüyordu; Spring Boot servislerinin yarısı kendi trace ID'sini üretip gelen ID'yi eziyordu; Node.js tarafında ise async context yönetilmediği için ID, ilk await'ten sonra kayboluyordu. Ölçtük: uçtan uca aynı correlation ID ile loglanabilen sipariş oranı yüzde 23'tü. Yani dört siparişten üçünde zincir en az bir yerde kopuyordu ve en çok koptuğu yer, senkron dünyadan Kafka'ya geçilen sınırdı.

Önce Standart, Sonra Araç

Refleks olarak "hemen bir tracing aracı alalım" tartışması başladı. Bilerek yavaşlattım, çünkü bir önceki şirketimde tersini yaşamıştım: araç önce gelirse, herkes aracı kendi diyalektiyle konuşturur ve elinizde pahalı bir Babil kulesi kalır. Önce tek sayfalık bir sözleşme yazdık: bağlam yayılımı W3C Trace Context standardıyla yapılır (traceparent header'ı), Kafka mesajlarında aynı bilgi mesaj header'ında taşınır, hiçbir servis gelen trace ID'yi ezemez, her log satırı trace ID içerir. Araç olarak OpenTelemetry SDK'larını seçtik; üç dilde de resmi desteği olması bizim poliglot gerçekliğimizde tek makul yoldu. Toplayıcı olarak her node'da OTel Collector, arkada da açık kaynak bir backend (Tempo) ve mevcut Grafana'mız.

Yayılım planı bilinçli olarak "en değerli yol" üzerinden gitti: önce sipariş yolculuğundaki 14 servis, diğer 40 küsur servis sonra. Spring Boot tarafı en kolayıydı — Java agent ile üç servisi bir sprintte, kod değişikliği neredeyse sıfırla bağladık. Node.js tarafında auto-instrumentation çalıştı ama async context kaçakları iki hafta uğraştırdı; özellikle kendi yazdığımız eski bir Redis kuyruk sarmalayıcısı bağlamı yutuyordu. PHP en sancılısıydı: o dönemki resmi destek olgunlaşmamıştı, biz de PHP servislerinde tam otomatik enstrümantasyon yerine gateway'de span başlatıp header'ı elle taşıyan ince bir kütüphane yazdık. Mükemmel değildi ama zinciri koparmıyordu — ve bu işte "zincir kopmasın" her şeyden önce gelir.

Yüzde Yüz Örnekleme Hayali ve Faturası

İkinci büyük tartışma örnekleme oranıydı. Günde birkaç milyon sipariş ve sipariş başına ortalama 38 span üretiyorduk; head-based yüzde 100 örnekleme ile ilk haftada depolama projeksiyonumuz ayda terabaytları buluyordu. Ekipteki ilk eğilim "yüzde 1 örnekleyelim" oldu; buna da karşı çıktım çünkü tracing'in asıl değeri ortalama istekte değil, o binde birlik tuhaf istektedir — yüzde 1 örneklemeyle aradığınız hatalı trace'in elinizde olma ihtimali kumar. Orta yolu tail-based sampling ile bulduk: Collector, trace tamamlanana kadar bekleyip karar veriyor. Hatalı biten, 2 saniyeden uzun süren veya ödeme-iade akışına dokunan her trace yüzde 100 saklanıyor; sağlıklı ve sıradan trace'ler yüzde 5. Depolama, tahminin onda birine indi; buna karşılık savaş odasında "o siparişin trace'i var mı?" sorusunun cevabı fiilen hep "evet".

Bir de kimsenin baştan söylemediği kazanım: trace verisi, organizasyon haritası çıkarıyor. İlk ay sonunda span'lerden ürettiğimiz servis bağımlılık grafiği, mimari dokümanımızda olmayan 11 bağımlılık gösterdi. Bunlardan biri, bildirim servisinin fiyatlama servisine — hiçbir mantıklı sebep yokken — senkron çağrı yaptığıydı; yıllar önce bir kampanya için eklenmiş, unutulmuş. O çağrı, fiyatlama her yavaşladığında bildirimleri de yavaşlatıyordu. Kimse bilmiyordu, çünkü kimsenin görebileceği bir yerde yazmıyordu.

Kültürel direnci de saklamayayım. İlk haftalarda iki ekipten aynı itiraz geldi: "Zaten yetişemiyoruz, bir de enstrümantasyon işi mi?" Cevabımız zor kullanmak değil, tek bir olayda değeri göstermek oldu: şubatta bir ekibin kendi başına iki gün çözemediği aralıklı bir 500 hatasını, trace'i açık olan komşu servisin span'lerinden 20 dakikada köşeye sıkıştırdık — hata, bir bağlantı havuzunun belirli bir pod'da tükenmesiydi. O ekip, ertesi sprintte enstrümantasyonu kendisi istedi. Gözlemlenebilirlik yatırımı emirle değil, kıskançlıkla yayılır; bir ekibin savaş odasından beş dakikada çıktığını gören diğer ekip, aynı silahı ister.

Altı Saatten Dört Dakikaya

Dönüşümün sınavı mart başındaki bir cuma akşamı geldi: yine "param çekildi, sipariş yok" çağrıları, yine akşam piki. Bu kez nöbetçi mühendis Grafana'da hatalı trace'leri filtreledi, ilk trace'i açtı ve dördüncü dakikada cevabı yazdı: fraud servisinin çağırdığı harici bir skorlama API'sinin p99'u 18 saniyeye çıkmış, bizim timeout 20 saniye olduğu için istekler ölmüyor, kuyruğu şişiriyordu. Span'in üzerinde harici çağrının süresi, kırmızı bir çubuk olarak duruyordu. Müdahale: timeout'u 3 saniyeye çekip fallback'i açmak. Toplam etki 11 dakika sürdü ve müşteri destek hattı dalgayı hissetmedi bile. Ocak ayındaki teşhis süremiz 380 dakikaydı; bu olayda 4 dakika. Ortalama teşhis süremiz (MTTD diyelim) o çeyrekte 96 dakikadan 12 dakikaya indi.

Bu işten çıkardığım ders, tracing'in bir gözlemlenebilirlik aracı olmaktan önce bir dil birliği meselesi olduğu. On dört servis, üç programlama dili, iki iletişim tarzı — bunların ortak hafızası ancak zorunlu bir standartla kurulabiliyor ve o standart, araçtan önce gelmek zorunda. İkinci ders: enstrümantasyonu "en değerli iş akışından" başlatmak, her yerde birden başlatmaktan katbekat hızlı değer üretiyor. Mikroservis sayınız 10'u geçtiyse ve hâlâ log grep'leyerek olay çözüyorsanız, yaşadığınız her gece nöbeti aslında bir tercihtir. Bu tercihi değiştirmenin yol haritasını konuşmak isterseniz bana buradan ulaşabilirsiniz; ilk savaş odası gecemin tahta fotoğrafı hâlâ telefonumda, gerekirse oradan başlarız.

O tahtadaki 14 duraklı çizim bugün şirket wiki'sinin kapağında duruyor. Altındaki başlık benim değil, o gece nöbetçi olan mühendisin cümlesi: "Harita olmadan yolculuk yönetilmez; biz iki yıl haritasız taşımacılık yapmışız."

📅 Yayınlanma:  ·  Yakup Zengin