Zysk ten ma swój specyficzny koszt: tryb transakcji po cichu przerywa wszystko, co zakłada stabilne połączenie Postgres, od jednego żądania do drugiego. Sesja SET, LISTEN/NOTIFY, blokady doradcze, kursory, które przetrwają transakcję, nazwane przygotowane instrukcje. W tym artykule szczegółowo opisano mechanizm, wymieniono te ograniczenia wraz z ich dokładnymi objawami, a następnie pokazano, jak backend w środowisku produkcyjnym (nasz, zweryfikowany bezpośrednio w jego repozytorium) konfiguruje go, nie dając mu się złapać. Aby zapoznać się z metodą pomiaru związaną z przytoczonymi tutaj wartościami wydajności, zapoznaj się z naszą metodologią testów porównawczych .
Najważniejsze
- Tryb transakcyjny: połączenie z serwerem jest zwalniane po zakończeniu każdej transakcji, a nie po rozłączeniu się klienta. Jest to najbardziej efektywny tryb udostępniania krótkich połączeń typu REST API.
- Konstrukcja niekompatybilna z: sesją SET/RESET, LISTEN/NOTIFY, blokadą doradztwa sesji, kursorami WITH HOLD, tabelami tymczasowymi używanymi ponownie z jednego żądania do drugiego.
- Najczęstsza pułapka w praktyce: nazwane przygotowane instrukcje, które domyślnie aktywuje kilka sterowników (sqlx, asyncpg, sterownik JDBC pgjdbc), mogą zostać odtworzone na innym połączeniu z serwerem i wywołać błąd taki jak
prepared statement does not existpod obciążeniem. - Od wersji 1.21 PgBouncer może śledzić instrukcje przygotowane przez protokół w trybie transakcyjnym (pamięć podręczna LRU poprzez połączenie z serwerem). Nie zapobiega to wyłączeniu pamięci podręcznej po stronie klienta, jeśli aplikacja zmienia
search_pathprzy każdym żądaniu. - Zweryfikowano w kodzie Aurabase: pule dzierżawców działają z
statement_cache_capacity(0)i PgBouncer wpool_mode=transaction, podczas gdy PostgREST dobrowolnie pozostaje w bezpośrednim połączeniu w celu ponownego załadowania swojego schematu za pośrednictwem LISTEN/NOTIFY.
3 tryby łączenia PgBouncer
PgBouncer oferuje trzy tryby, które różnią się jedynie momentem, w którym połączenie z serwerem Postgres wraca do wspólnej puli. Oficjalna dokumentacja nazywa je session, transaction i statement (pgbouncer.org/features.html, sekcja „Tryby łączenia”, dostęp: 24 sierpnia 2026 r.).
| Moda | Połączenie z serwerem luźne | Zgodność sesji |
|---|---|---|
| sesja (domyślna) | W przypadku rozłączenia klienta | Razem: SET, LISTEN, kursory, wszystko działa jak na żywo |
| transakcja | Na koniec każdej transakcji (COMMIT/ROLLBACK) | Częściowe: tylko to, co pozostaje lokalne dla transakcji |
| oświadczenie | Po każdym indywidualnym zapytaniu | Minimalne: zabronione są jawne transakcje zawierające wiele zapytań |
Tryb sesji jest najbardziej liberalny, ale najmniej efektywny pod względem skalowalności: połączenie Postgres pozostaje zarezerwowane dla klienta tak długo, jak długo pozostaje połączone, nawet jeśli nie robi nic między dwoma żądaniami. Tryb instrukcji jest zarezerwowany dla bardzo specyficznych przypadków (proxy tylko do odczytu, kontrola stanu) i łamie nawet klasyczne, jawne transakcje. Tryb transakcyjny to kompromis dominujący w praktyce w przypadku interfejsu API REST: każde żądanie HTTP odpowiada zazwyczaj pojedynczej krótkiej transakcji Postgres.
Jak działa tryb transakcyjny, połączenie po połączeniu
W trybie transakcyjnym PgBouncer łączy połączenie z serwerem z klientem tylko wtedy, gdy ten otwiera transakcję i zwraca ją do puli po zatwierdzeniu lub wycofaniu. Pomiędzy dwiema transakcjami ten sam klient może zostać przeniesiony do zupełnie innego połączenia z serwerem.
Konkretnie, przy default_pool_size wynoszącym 20, PgBouncer może wchłonąć kilkuset jednoczesnych klientów, którzy w danym momencie mają tylko kilka transakcji w toku. To właśnie ten stosunek uzasadnia tryb transakcji w przypadku interfejsu API REST o dużym ruchu, ale krótkich transakcjach: rzadki zasób (połączenie Postgres, drogie w pamięci po stronie serwera) jest zajęty tylko przez czas ściśle niezbędny.
Ten ostatni komentarz podsumowuje sedno: tryb transakcji działa, ponieważ celowo przerywa połączenie między „moją sesją aplikacji” a „moim połączeniem Postgres”. Wszystko oparte na tym linku psuje się. W następnej sekcji szczegółowo opisano, co.
Co psuje się w trybie łączenia transakcji
Oficjalna dokumentacja PgBouncera wyraźnie wymienia funkcje PostgreSQL, które tracą znaczenie, gdy tylko połączenie z serwerem może zostać powtórzone między dwoma żądaniami od tego samego klienta.
| Dotknięta funkcjonalność | Dlaczego to się psuje | Typowy objaw |
|---|---|---|
| USTAW / USTAW SESJĘ | Ustawienie dotyczy połączenia, które można natychmiast poddać recyklingowi | Wydaje się, że parametr został losowo zapomniany pomiędzy dwoma żądaniami |
| POSŁUCHAJ / POWIADOM | Zakłada trwałe połączenie w celu otrzymywania powiadomień | Klient nie jest powiadamiany nigdy lub tylko sporadycznie |
| Blokady doradcze sesji | Blokada jest utrzymywana przez połączenie z serwerem, a nie przez klienta logicznego | Blokada zostaje zwolniona przed oczekiwanym zakończeniem lub nigdy nie zostaje zwolniona |
| Z suwakami HOLD | Musi przetrwać po transakcji, która go otworzyła | Błąd „kursor nie istnieje” podczas następnej iteracji |
| Stoły tymczasowe | Związane z sesją Postgres, a nie transakcją | Tabela „znika” przy następnym zapytaniu |
| Przygotowane zestawienia nazwane | Przygotowane na konkretnym połączeniu z serwerem, odtworzone na innym | „przygotowana instrukcja… nie istnieje” pod obciążeniem |
Większość tych ograniczeń nie objawia się w rozwoju lokalnym, gdzie jedno połączenie zazwyczaj obsługuje cały ruch. Pojawiają się przy rzeczywistym obciążeniu, gdy kilku klientów faktycznie dzieli pulę, a połączenie z serwerem faktycznie zmienia właściciela między dwoma żądaniami od tego samego klienta logicznego. Test dymu prawie nigdy ich nie ujawnia.
Przygotowane zestawienia: najbardziej niezrozumiany limit
Większość nowoczesnych sterowników Postgres domyślnie przygotowuje nazwane żądania po stronie protokołu, bez wyraźnego żądania kodu aplikacji. Właśnie to sprawia, że trudno jest przewidzieć tę pułapkę.
Instrukcja przygotowana przez protokół jest nazywana i buforowana na określonym połączeniu z serwerem w momencie Parse. W trybie transakcyjnym połączenie to można ponownie przypisać innemu klientowi pomiędzy dwoma żądaniami tego samego klienta logicznego. Jeśli następnie sterownik odtwarza tę samą nazwę instrukcji w połączeniu, w którym nigdy nie została przygotowana, Postgres odpowiada jawnym błędem, zwykle prepared statement "sqlx_s_N" does not exist dla klienta sqlx. Zachowanie to jest sporadyczne: zależy od tego, jak połączenia działają pod obciążeniem, a nie od deterministycznego błędu powtarzalnego przy każdym wywołaniu.
Korekta po stronie klienta jest taka sama niezależnie od języka: wyłącz pamięć podręczną nazwanych przygotowanych instrukcji lub wymuś nienazwane zapytania dla dowolnej puli, która przechodzi przez pulę w trybie transakcyjnym. W Rust z sqlx przechodzi przez statement_cache_capacity(0) w opcjach połączenia.
Od wersji 1.21 PgBouncer łagodzi część problemu po stronie serwera: może podążać za instrukcjami przygotowanymi przez protokół w trybie transakcyjnym i przygotowywać je na bieżąco w ramach przypisanego połączenia, z pamięcią podręczną LRU na połączenie, której rozmiar jest dostosowywany za pomocą max_prepared_statements. Zmniejsza to liczbę chybień, ale nie zwalnia Cię z wyłączania pamięci podręcznej klienta w puli z wieloma dzierżawcami, gdzie search_path zmienia się z jednego żądania na drugie: plan buforowany blokuje wewnętrzny identyfikator (OID) rozwiązanej tabeli w momencie Parse, a ponowne odtwarzanie go w ramach innego schematu może zwrócić dane od niewłaściwej dzierżawy, a nie zwykły błąd.
Jak Aurabase konfiguruje PgBouncer w trybie transakcyjnym
Repozytorium Aurabase wdraża PgBouncer jako pool_mode=transaction przed współdzieloną płaszczyzną danych (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) i identycznie skonfigurowaną Pooler CNPG przed każdą dedykowaną instancją Postgres dzierżawy (deploy/cnpg/tenant-pooler.yaml). Obie ścieżki stosują tę samą dyscyplinę opisaną powyżej.
Kod źródłowy dokumentuje konkretny powód bezpieczeństwa dla tego wyboru, a nie tylko powód stabilności. Pule Postgres współdzielone między dzierżawcami pozycjonują różne search_path na projekt w przypadku ponownie używanych połączeń. Przygotowana instrukcja przechowywana w pamięci podręcznej blokuje OID rozwiązanej tabeli w momencie Parse; odtworzenie go dla innego dzierżawcy w tym samym połączeniu spowodowałoby uruchomienie zapytania względem schematu pierwszego dzierżawcy, co oznacza obejście izolacji, a nie tylko błąd aplikacji. statement_cache_capacity(0) jest zatem stosowany bez wyjątku, także w przypadku dedykowanych instancji, które również przechodzą przez CNPG Pooler w trybie transakcyjnym.
Drugi środek higieny sesji: gdy każde połączenie powraca do puli, hak wykonuje DISCARD ALL (resetowanie ustawień, cofanie alokacji przygotowanych instrukcji po stronie serwera, zwalnianie blokad doradczych, czyszczenie kursorów i tabel tymczasowych). Bez tego zaczepu pozostałości sesji powstałe w wyniku poprzedniego żądania mogłyby wyciekać przy następnym żądaniu od innego dzierżawcy ponownie korzystającego z tego samego odzyskanego połączenia.
Zaakceptowano wyjątek: dedykowane instancje PostgREST pozostają w bezpośrednim połączeniu z instancją podstawową, bez przechodzenia przez moduł puli. Ponowne ładowanie schematu PostgREST opiera się na LISTEN/NOTIFY, które zakłada trwałe połączenie, dokładnie na funkcjonalności, którą psuje tryb transakcji (szczegóły udokumentowano już w naszym artykule na temat kompatybilności PostgREST w Aurabase). Ustawienia RLS na żądanie są przekazywane do SET LOCAL w ramach jawnej transakcji, co jest jedynym sposobem na zachowanie zgodności z pulą, która może zmieniać połączenia z serwerem przy dowolnym COMMIT (zobacz nasz artykuł na tematizolacji RLS dla wielu dzierżawców).
Włącz tryb transakcji bez przerywania aplikacji
Krótka lista kontrolna, mająca zastosowanie do każdego backendu przechodzącego z bezpośredniego połączenia Postgres do PgBouncera w trybie transakcyjnym.
- Audyt kodu aplikacji. Poszukaj nietransakcyjnych
SET,LISTEN/NOTIFY, blokad doradczych sesji, kursorówWITH HOLDi tabel tymczasowych używanych ponownie między zapytaniami. - Zamień zestawy sesji na LOKALNE zestawy w ramach jawnej transakcji. Jest to jedyne ustawienie, które prawidłowo przetrwa recykling połączenia, ponieważ jest czyszczone podczas COMMIT/ROLLBACK, a nie wycieka przy następnym połączeniu.
- Wyłącz pamięć podręczną instrukcji przygotowanych po stronie sterownika, jeśli pula przechodzi przez moduł puli i schemat lub rola zmieniają się z jednego żądania na drugie. Koszt wykonania jest realny, ale wymierny i znacznie niższy niż ryzyko wycieku pomiędzy najemcami.
- Izoluj połączenia, które naprawdę wymagają trybu sesji (migracje, skrypty administracyjne, wszystko, co zależy od LISTEN/NOTIFY) do bezpośredniego połączenia innego niż pula, zamiast rezygnować z trybu transakcji dla całego innego ruchu.
- Rozmiar
default_pool_sizeimax_client_connodnosi się do rzeczywistegomax_connectionsPostgres, a nie dowolna liczba skopiowana z innego projektu. - Testuj pod rzeczywistym obciążeniem, a nie tylko test dymu. Błędy przygotowanych instrukcji i wycieki ustawień sesji prawie nigdy nie pojawiają się w przypadku pojedynczego połączenia lokalnego.
- Monitoruj
SHOW POOLSiSHOW STATSz konsoli administracyjnej PgBouncer po uruchomieniu produkcyjnym, aby wykryć nasycenie puli, zanim stanie się ono widoczne po stronie klienta.
Czy zawsze należy wybierać tryb transakcyjny, a nie sesyjny?
Nie, ale jest to właściwy domyślny wybór dla zdecydowanej większości interfejsów API REST. Tryb sesji jest nadal preferowany w przypadku starszych aplikacji w dużym stopniu zależnych od funkcjonalności sesji, których nie można szybko zrefaktoryzować, lub w przypadku małego ruchu, gdzie zysk z łączenia nie rekompensuje wysiłku związanego z migracją.
PgBouncer nie jest jedyną implementacją tego modelu łączenia zasobów: Supavisor (Supabase) i PgCat to dwie najnowsze alternatywy, oferujące różne kompromisy w zakresie dystrybucji obciążenia i klastrowania. Zobacz nasze szczegółowe porównanie, PgBouncer vs Supavisor vs PgCat, aby wybrać pomiędzy trzema w zależności od topologii.
Często zadawane pytania
Pytania, które pojawiają się najczęściej po włączeniu trybu transakcyjnego na produkcji.