gRPC, “HTTP üzerinden RPC” gibi görünür ama üretimde davranışı genelde uzun ömürlü HTTP/2 bağlantıları tarafından belirlenir. Bu detay; load balancer seçimi, idle timeout’lar, keepalive ayarları ve retry politikaları yanlış olduğunda, bir anda “kodu değiştirmeden” SLO kaybetmenize sebep olabilir.
Bu yazıda gRPC/HTTP2 trafiğinde en sık gördüğüm üretim problemlerini, operasyonel olarak güvenli bir tasarıma nasıl bağladığımı anlatıyorum: connection draining, keepalive, outlier detection ve en önemlisi retry budget.
gRPC/HTTP2’de “bağlantı” neden kritik?
HTTP/1.1 dünyasında her istek nispeten bağımsızdır. gRPC’de ise istemci genelde bir hedefe az sayıda HTTP/2 bağlantısı açar ve bu bağlantılar üzerinde çok sayıda stream taşır.
Bu şu anlama gelir:
- Bir bağlantının reset olması, tek bir isteği değil, aynı anda onlarca stream’i etkileyebilir.
- Load balancer’ın “yeniden dengeleme” davranışı, trafik dağılımını beklemediğiniz şekilde bozabilir.
- Idle timeout / NAT timeout / firewall state timeout gibi değerler “sessizce” bağlantıyı öldürüp client tarafında retry fırtınası başlatabilir.
Sık görülen semptomlar ve kök nedenler
Üretimde gRPC sorunları genelde şu sinyallerle gelir:
- İstemcide
UNAVAILABLE,DEADLINE_EXCEEDED,RST_STREAMartışı - L7 proxy’de
upstream_reset_before_response_started/stream resetbenzeri loglar - p95/p99 gecikmede ani sıçrama ama CPU/DB normal
- Belirli AZ/POP’de hata patlaması (network/NAT/idle timeout gibi)
Kök nedenler çoğu zaman şunlardır:
- Idle timeout uyumsuzluğu (LB/ingress/NAT/firewall farklı süreler)
- Keepalive yanlışlığı (çok agresif ping = gürültü / çok pasif = sessiz kopuş)
- Draining yapılmadan deploy (pod/VM kapanır, connection reset)
- Retry kontrolsüz (thundering herd + amplification)
- LB algoritması gRPC’ye uygun değil (sticky, connection-count, stream-aware vs)
Idle timeout zinciri: En zayıf halka sistemi belirler
HTTP/2 bağlantısı bir yerlerde stateful bir cihazdan geçiyorsa (LB/NAT/firewall), pratikte “en düşük idle timeout” bağlantının ömrünü belirler.
Örnek zincir:
| Katman | Tipik risk |
|---|---|
| Client → NAT | NAT state idle süresi düşükse sessiz drop |
| Edge LB | HTTP/2 idle timeout düşükse reset |
| Service mesh / sidecar | drain yoksa RST |
| Backend | pod restart = stream reset |
Keepalive: Sessiz kopuşu “gürültüye” çevirmeden çözmek
Keepalive iki problemi çözer:
- NAT/firewall gibi stateful cihazların idle state’i düşürmesini engeller
- “karşı uç yaşıyor mu?” sinyalini hızlandırır
Ama keepalive agresifse başka bir problem üretir: binlerce istemci ping üretir, LB/ingress üzerinde gereksiz paket ve CPU tüketir.
Pratik yaklaşım:
- Keepalive’ı “her yere aç” değil, riskli segmentlerde aç.
- Ping interval’ı, en kısa idle timeout’tan daha kısa ama makul olsun.
- Ping, “kötü network’ü” maskeler; bu yüzden bağlantı kopmalarını metrikleştir.
Retry: Dayanıklılık mı, amplifikasyon mu?
Retry, doğru kurulduğunda transient hatayı kullanıcıya yansıtmaz. Yanlış kurulduğunda ise en kritik anda sistemi öldürür.
En tipik yanlışlar:
- Her hataya retry (özellikle
DEADLINE_EXCEEDEDve bağlantı reset) - Backoff yok, jitter yok
- Concurrent retry’lar sınırsız
- “başarısızlık” sinyali gecikmeli (client, LB, backend birbirini suçlar)
Retry budget (deneyimle sabitlenen pratik sınır)
Retry budget, “toplam trafiğin yüzde kaçını retry’a ayırabilirim?” sorusunun cevabıdır.
Benim sahada işe yarayan çerçevem:
- Servis başına bir retry budget (ör. %5)
- Bu bütçe aşılıyorsa: retry kıs, timeout azalt, degrade et
- Retry budget metrikleri:
requests_total,retries_total,retries_ratio
Connection draining: Deploy sırasında “bağlantı ölümü” yönetimi
gRPC’de deploy etkisi “rolling” bile olsa ağır olabilir. Çünkü:
- Pod kapanır → TCP FIN/RST → stream’ler kesilir
- İstemci yeni bağlantıyı geç açarsa hata patlar
Draining için iki eksen:
- Server-side: graceful shutdown + drain süresi
- LB/proxy-side: connection draining + health state geçişi
Operasyonel hedef: “kapatmadan önce yeni stream alma, mevcut stream’leri bitir, sonra çık”.
Gözlemlenebilirlik: gRPC için minimum metrik seti
Ben gRPC trafiğini en az şu metriklerle yönetmeyi öneriyorum:
grpc_server_handled_total{code=...}(code dağılımı)- Latency: p50/p95/p99 (method bazlı)
active_connectionsveactive_streams- Retry ratio (client side mümkünse)
- Reset/GOAWAY sayısı (proxy/LB tarafı)
Log tarafında:
- Upstream reset nedeni (timeout mu, drain mi, overload mu?)
- Deadline (istemci deadline çok kısa mı?)
Incident runbook: “UNAVAILABLE patladı” anında ne yaparım?
- Bölgesel mi? (tek AZ/POP mi?)
- Timeout zinciri: LB/proxy idle timeout değişti mi? NAT/firewall policy güncel mi?
- Deploy etkisi: son 30 dk’da rollout var mı? drain çalışıyor mu?
- Retry amplifikasyonu: retries_ratio yükseldi mi?
- İzole et: canary client → tek backend → tek path
Hızlı “hasar azaltma” hamleleri (risk sırasıyla):
- Retry’ı düşür (budget ile), backoff+jitter zorunlu kıl
- Deadline’ları servis SLO’suna uygun yap (çok kısa deadline = sürekli retry)
- Draining’i düzelt (graceful shutdown)
- LB idle timeout’ları uyumlu hale getir
Sonuç
gRPC/HTTP2’de dayanıklılık; “daha çok retry” veya “daha büyük autoscale” ile değil, bağlantı yaşam döngüsünü doğru yönetmekle gelir. Idle timeout zincirini görünür kıl, keepalive’ı kontrollü kullan, draining’i standartlaştır ve retry’ı mutlaka budget ile sınırla.
Üretimde farkı yaratan şey; “gRPC çalışıyor” değil, “gRPC incident’ı yönetilebilir” olmalıdır.