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:

vbnet
PG 17
ERROR: prepared statement "S_1" does not exist

Çözüm A (PgBouncer 1.21+): PgBouncer'ın kendi prepared statement desteğini aç:

ini
PG 17
# 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.

Dikkat

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:

sql
PG 17
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:

sql
PG 17
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.