Yüksek Yüzdelik Gecikme Tanılama: Kuyruklanma, Zamanlayıcı, GC, I/O ve Doygunluk
P95, P99 ve P99.9 gecikmelerini ortalama süreden ayırarak; kuyruklanma, kaynak doygunluğu, zamanlayıcı davranışı, çöp toplama, I/O ve bağımlılık zincirleri üzerinden tanılamaya odaklanır.
Bir servisin ortalama yanıt süresinin düşük olması, en yavaş isteklerin de kabul edilebilir sürede tamamlandığını göstermez. Dağılımın sağ ucunda kalan az sayıdaki istek P95, P99 veya P99.9 değerlerini belirgin biçimde yükseltebilir. Dağıtık ve yüksek trafikli sistemlerde bu olgu genellikle tail latency adıyla anılır; burada daha açıklayıcı olduğu için yüksek yüzdelik gecikme ifadesini kullanıyorum.
Bu tür bir gecikmeyi incelerken ilk baktığım yer tek tek metotların çalışma süresi değildir. Önce bekleme ile gerçek işlem süresini ayırmak gerekir. Bir istek CPU üzerinde 20 ms çalışıp kuyrukta 400 ms bekliyorsa kod profili tek başına sorunu göstermez.
İlk ayrım: hizmet süresi mi, bekleme süresi mi?
Bir isteğin uçtan uca süresini kabaca şu parçalara ayırmak mümkündür:
- kuyrukta bekleme,
- zamanlayıcı nedeniyle çalışma fırsatı bekleme,
- uygulama kodunun gerçek hizmet süresi,
- veritabanı veya uzak servis beklemeleri,
- dosya ve ağ I/O'su,
- GC duraklamaları,
- kilit veya eşzamanlılık beklemeleri.
Bu ayrım yapılmadan yalnızca ortalama CPU süresine veya tek bir profiler çıktısına bakmak yanıltıcı olabilir.
Kuyruk neden bir anda büyür?
Kaynak kullanımı kapasite sınırına yaklaştıkça küçük servis süresi değişimleri büyük kuyruklara dönüşebilir. Little Yasası bunu temel düzeyde \(L = \lambda W\) ilişkisiyle ifade eder. Sisteme geliş hızı işleme hızına yaklaşıyor veya onu geçiyorsa bekleyen iş miktarı artar.
Doygunluğa yaklaşmış bir sistemde şu belirtiler birlikte görülebilir:
- CPU kullanımının sürekli yüksek kalması,
- connection pool beklemelerinin artması,
- disk kuyruğunun uzaması,
- worker veya thread havuzunun dolması,
- isteğin birden fazla bağımlılıkta sırayla beklemesi.
Burada sorun çoğu zaman tek bir isteğin pahalı olması değil, sistemin yeterli boşluk bırakmadan çalıştırılmasıdır.
Zamanlayıcı etkisini kod yavaşlığıyla karıştırmamak gerekir
Bir thread çalışmaya hazır olduğu halde CPU alamıyorsa geçen süre uygulama kodunun içinde görünmeyebilir. Context switch sayısı, runnable thread miktarı, CPU steal zamanı, cgroup veya container kotası ve çekirdek başına yük birlikte değerlendirilmelidir.
Java tarafında JFR gibi olay kayıtları; thread durumu, kilitler, tahsis hızı, GC ve I/O olaylarını aynı zaman çizgisinde karşılaştırmak için yararlıdır. Yalnızca flame graph üzerinden ilerlemek, çalışmayan fakat bekleyen thread'leri gözden kaçırabilir.
GC her P99 artışının açıklaması değildir
Çöp toplama duraklamaları yüksek yüzdelik gecikmeyi büyütebilir; ancak her sıçramayı GC'ye bağlamak doğru değildir. Tahsis hızı, canlı nesne kümesi, heap boşluğu, kullanılan collector ve duraklama süreleri birlikte ölçülmelidir.
Örneğin P99 artarken GC duraklamaları değişmiyorsa araştırmayı bağlantı havuzu, kilitler, I/O veya uzak servis sürelerine kaydırmak daha anlamlıdır.
Bağımlılık zinciri gecikmeyi katlayabilir
Bir istek üç uzak servise ardışık gidiyorsa son kullanıcı gecikmesi yalnızca kendi uygulamanızın süresi değildir. Her bağımlılığın kendi P99 dağılımı vardır. Özellikle fan-out yapan yapılarda tek bir yavaş alt istek bütün üst isteğin tamamlanmasını geciktirebilir.
Bu nedenle izleme verisinde yalnızca toplam süre değil, aşama süreleri ve bağımlılık süreleri de ayrı tutulmalıdır.
Tanılama sırası
Üretim ortamında şu sıra genellikle daha güvenilir sonuç verir:
- P50, P95, P99 ve mümkünse P99.9'u aynı zaman aralığında karşılaştırmak.
- Uçtan uca süreyi kuyruk, işlem, veritabanı, ağ ve I/O aşamalarına ayırmak.
- Aynı zaman aralığında CPU, run queue, GC, pool kullanımı ve disk göstergelerini eşleştirmek.
- Gecikmenin bütün isteklerde mi, belirli bir endpoint veya partition üzerinde mi arttığını görmek.
- Değişiklik öncesi ve sonrası dağılımı aynı trafik koşullarında karşılaştırmak.
Burada amaç bir mikro-optimizasyon bulmak değil, gecikmenin hangi sınırda üretildiğini saptamaktır. Kod hızlı olduğu halde kuyruk yavaş olabilir; veritabanı sorgusu hızlı olduğu halde bağlantı havuzu dolu olabilir; model çıkarımı hızlı olduğu halde segment bekleme süresi kullanıcı gecikmesini belirleyebilir.
Bu ayrım gerçek zamanlı konuşma tanımada kapasite mühendisliği ve Java sistemlerinde çalışma zamanı optimizasyonu gibi üretim odaklı konuların da ortak temelidir.