Üretimde veritabanı incident’larının en sinsi olanı şudur: CPU düşük, veritabanı “ayakta”, ama uygulama donmuş gibi davranır. Kök neden çoğu zaman query’nin yavaş olması değil; connection pool’un doygunluğu ve bunun ürettiği kuyruklanma döngüsüdür.
Bu yazı; özellikle PostgreSQL + uygulama pool (Hikari/pgx/SQLAlchemy vb.) ve/veya PgBouncer kullanan yapılarda, pool doygunluğunu “teknik detay” değil operasyonel kontrol noktası olarak ele alır.
1) Mental model: problem veritabanında değil, kuyruğun önünde başlar
Basit zincir:
- Trafik artar → daha çok istek DB’ye ulaşmak ister
- Pool dolar → thread/worker bekler
- Bekleyen istekler timeout olur → retry devreye girer
- Retry, DB’ye yeni baskı yaratır → latency daha da artar
Sonuç: latency geri besleme döngüsü (kuyruk büyürken sistem “daha çok deniyor”).
2) Triage: 10 dakikada pool doygun mu?
2.1 Uygulama metrikleri (en değerli sinyal)
Aradığın metrik sınıfları:
- Active connections (kullanım)
- Pending / wait queue (bekleyen)
- Acquire time / wait time (bağlantı alma süresi)
- Timeout count
Bu metrikler yoksa, incident anında ilk aksiyonlardan biri şudur: pool metriklerini standartlaştırmak.
2.2 PostgreSQL tarafı: oturum ve bekleme
-- aktif oturumların genel görünümü
select
state,
count(*) as sessions
from pg_stat_activity
group by 1
order by 2 desc;
Triage yorumu:
- Çok sayıda
activeoturum: DB gerçekten çalışıyor olabilir ama doygun - Çok sayıda
idle in transaction: uygulama transaction’ı açık bırakıyor (en pahalı hata)
Bekleme sınıflarını okumak için:
select
wait_event_type,
wait_event,
count(*) as sessions
from pg_stat_activity
where wait_event is not null
group by 1,2
order by 3 desc;
2.3 PgBouncer varsa: “asıl kuyruk burada olabilir”
PgBouncer admin DB üzerinden:
show pools;
show stats;
Aradığın sinyaller:
- Waiting client sayısı
- Server connection sayısı (DB’ye giden gerçek bağlantı)
3) Hızlı mitigasyon: kuyruğu küçült, retry’ı kes
Hedef: DB’yi “daha güçlü” yapmak değil; önce sistemi stabil yapmak.
3.1 Trafiği kontrollü azalt (shed load)
Uygulanabilir seçenekler:
- Rate limit / concurrency limit (gateway katmanında)
- Cache ile DB read baskısını düşür (kısa süreli bile olsa)
- İş kritik olmayan endpoint’leri degrade et
3.2 Retry budget uygula (kritik)
Incident sırasında:
- Retry sayısını düşür (1-2)
- Exponential backoff + jitter zorunlu
- Idempotent olmayan işlemlerde retry’ı kapat
3.3 Timeout’ları “kuyruk”a göre hizala
Sık hata: uygulama timeout’u 30s, DB statement timeout yok.
Pratik kural:
- Uygulama request timeout < pool acquire timeout < DB statement timeout
Örnek (yaklaşım):
- Request: 10s
- Pool acquire: 3s
- Statement: 2s (kritik sorgular için daha kısa)
4) Kalıcı tasarım: pool boyutu bir “kapasite kontratı”dır
4.1 Pool’u büyütmek çoğu zaman çözüm değildir
Pool’u büyütmek, DB’ye daha fazla concurrency iter. Eğer DB’nin CPU/IO kapasitesi sabitse:
- Latency artar
- Lock contention artar
- Tail latency patlar
Sonuçta “daha çok connection” yerine “daha az concurrency ama daha stabil throughput” daha iyi olur.
4.2 Transaction sınırını netleştir
En sık kök neden:
- Uygulama, transaction’ı gereksiz uzun tutar
- IO/HTTP çağrısı transaction içinde kalır
- “idle in transaction” birikir
Operasyonel kontrol:
- Transaction içinde network çağrısı yasak
- ORM lazy-load sürprizleri gözlenebilir olmalı
4.3 PgBouncer mod seçimi
PgBouncer kullanıyorsan:
transactionmode: en verimli, çoğu web iş yükü için iyisessionmode: bazı özellikler için gerekli ama kapasiteyi daha hızlı tüketir
Yanlış seçim, “pool var ama hiçbir şey akmıyor” incident’ına dönüşebilir.
5) Runbook kapanış: doğrulama ve iz
Stabilite doğrulaması:
- Pool wait queue düşüyor mu?
- Timeout sayısı düşüyor mu?
- DB wait event’leri normalleşiyor mu?
Postmortem için delil:
- Peak concurrency (app + pgbouncer + db)
- En pahalı sorgu sınıfları (p95/p99)
- Retry davranışı (hangi katman retry etti?)
Connection pool, uygulamanın DB’ye verdiği sözleşmedir. Bu sözleşme görünür değilse incident’lar “DB yavaş” diye başlar, ama asıl sorun kuyruk yönetimidir. Sağlam sistemler, kuyruğu saklamaz; ölçer, sınırlar ve runbook’a bağlar.