PgBouncer'ı transaction moduna almak, connection storm'u çözmenin en etkili yoludur: yüzlerce uygulama bağlantısı, avuç dolusu gerçek PostgreSQL bağlantısına iner. Ama bu modda bir bağlantı yalnızca bir transaction boyunca aynı istemciye ait kalır — transaction bitince başka istemciye gider. Bu, session'a bağlı üç şeyi sessizce kırar. "Sessizce" kısmı en tehlikeli olanı.
Neden kırılıyor: session ≠ transaction
session modunda bir istemci bağlantıyı bırakana kadar aynı backend'i kullanır. transaction modunda ise iki ardışık sorgun farklı PostgreSQL backend'lerine düşebilir. Backend'e bağlı her durum — hazırlanmış sorgular, SET ile ayarlanmış değişkenler, session-level kilitler — bir sonraki sorguda kaybolur.
1. Prepared statements
Uygulama sürücün (JDBC, asyncpg, node-postgres…) genellikle server-side prepared statement kullanır: önce PREPARE, sonra EXECUTE. transaction mode'da PREPARE bir backend'de olur, EXECUTE başka bir backend'e düşerse:
ERROR: prepared statement "S_1" does not exist
Çözüm A (PgBouncer 1.21+): PgBouncer'ın kendi prepared statement desteğini aç:
# pgbouncer.ini
[pgbouncer]
pool_mode = transaction
max_prepared_statements = 200
Bu, PgBouncer'ın hazırlanmış sorguları backend'ler arasında yeniden oynatmasını sağlar.
Çözüm B: Sürücüde server-side prepare'i kapat (ör. JDBC prepareThreshold=0, node-postgres tarafında adsız sorgular). Basit ama plan cache avantajını kaybedersin.
max_prepared_statements PgBouncer 1.21 ile geldi. Daha eski bir sürümdeysen bu satır işe yaramaz; ya PgBouncer'ı yükselt ya da sürücüde prepare'i kapat. Sürüm kontrolü yapmadan config'e eklersen sorun çözülmüş sanırsın ama devam eder.
2. Session değişkenleri ve SET
SET statement_timeout = '5s' ya da SET search_path = ... gibi session-level ayarlar bir sonraki transaction'da başka bir backend'e düşünce kaybolur. Uygulaman "timeout ayarladım" sanır, gerçekte ayar yok.
Çözüm: Ayarı transaction'a kapsa. SET LOCAL sadece o transaction için geçerlidir ve transaction mode'la uyumludur:
BEGIN;
SET LOCAL statement_timeout = '5s';
-- sorgun
COMMIT;
Kalıcı search_path ihtiyacı varsa onu PgBouncer'ın connect_query'sine ya da veritabanı/rol varsayılanına (ALTER ROLE ... SET search_path) taşı — session'a değil, bağlantı kurulumuna bağla.
3. Advisory lock'lar
Session-level advisory lock (pg_advisory_lock) backend'e bağlıdır. transaction mode'da lock'u aldığın backend bir sonraki istekte başkasına gider — lock'u bırakamazsın, ya da yanlış backend'de bırakırsın. Sonuç: dağıtık kilit mantığın sessizce bozulur.
Çözüm: Transaction kapsamlı sürümü kullan — pg_advisory_xact_lock. Bu, transaction bitince otomatik serbest kalır ve transaction mode ile uyumludur:
BEGIN;
SELECT pg_advisory_xact_lock(42);
-- korunan iş
COMMIT; -- kilit burada otomatik serbest
Hızlı karar tablosu
| İhtiyaç | session mode | transaction mode |
|---|---|---|
| Maksimum bağlantı azaltma | Kısıtlı | En iyi |
| Server-side prepared statement | Sorunsuz | 1.21+ ya da kapat |
| SET ile session değişkeni | Çalışır | SET LOCAL kullan |
| Session advisory lock | Çalışır | _xact_ sürümü kullan |
Özet
transaction mode bedava değildir: bağlantı tasarrufunu, session durumundan vazgeçerek alırsın. Üç kırılma da çözülebilir — prepared statement için PgBouncer 1.21+, session değişkenleri için SET LOCAL, advisory lock için _xact_ varyantı. Geçmeden önce uygulama sürücünün bu modda ne yaptığını test ortamında doğrula; production'da "neden prepared statement does not exist" hatasıyla tanışmak istemezsin.