PRODSuwerenna europejska platforma BaaSOtwórz Panel →

Wydajność · 10 min odczytu

PgBouncer objaśnienie łączenia transakcji

Affane Daylami · Fondateur · 12 czerwca 2026

Powrót do bloga

Tryb transakcyjny PgBouncera zwalnia połączenie PostgreSQL po zakończeniu każdej transakcji, a nie po rozłączeniu się klienta. To właśnie umożliwia obsługę tysięcy klientów HTTP z kilkudziesięciu rzeczywistych połączeń z serwerem i jest to zalecany tryb dla każdego interfejsu API REST z krótkimi zapytaniami.

Ten tekst w języku angielskim został wygenerowany automatycznie na podstawie francuskiego oryginału i nie był jeszcze recenzowany.
Ta strona została przetłumaczona automatycznie. Wersja angielska jest miarodajna.

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 exist pod 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_path przy każdym żądaniu.
  • Zweryfikowano w kodzie Aurabase: pule dzierżawców działają z statement_cache_capacity(0) i PgBouncer w pool_mode=transaction, podczas gdy PostgREST dobrowolnie pozostaje w bezpośrednim połączeniu w celu ponownego załadowania swojego schematu za pośrednictwem LISTEN/NOTIFY.
#
Koncepcje

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.).

ModaPołączenie z serwerem luźneZgodność sesji
sesja (domyślna)W przypadku rozłączenia klientaRazem: SET, LISTEN, kursory, wszystko działa jak na żywo
transakcjaNa koniec każdej transakcji (COMMIT/ROLLBACK)Częściowe: tylko to, co pozostaje lokalne dla transakcji
oświadczeniePo każdym indywidualnym zapytaniuMinimalne: 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.

#
Mechanizm

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.

pgbouncer.iniini
[pgbouncer]
listen_port = 5432

; La connexion serveur est libérée dès la fin de chaque transaction
pool_mode = transaction

default_pool_size = 80
max_client_conn = 1000

; Ne pas utiliser avec des sessions stateful (SET, advisory locks, LISTEN/NOTIFY)

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.

#
Limity

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ę psujeTypowy objaw
USTAW / USTAW SESJĘUstawienie dotyczy połączenia, które można natychmiast poddać recyklingowiWydaje się, że parametr został losowo zapomniany pomiędzy dwoma żądaniami
POSŁUCHAJ / POWIADOMZakłada trwałe połączenie w celu otrzymywania powiadomieńKlient nie jest powiadamiany nigdy lub tylko sporadycznie
Blokady doradcze sesjiBlokada jest utrzymywana przez połączenie z serwerem, a nie przez klienta logicznegoBlokada zostaje zwolniona przed oczekiwanym zakończeniem lub nigdy nie zostaje zwolniona
Z suwakami HOLDMusi przetrwać po transakcji, która go otworzyłaBłąd „kursor nie istnieje” podczas następnej iteracji
Stoły tymczasoweZwiązane z sesją Postgres, a nie transakcjąTabela „znika” przy następnym zapytaniu
Przygotowane zestawienia nazwanePrzygotowane na konkretnym połączeniu z serwerem, odtworzone na innym„przygotowana instrukcja… nie istnieje” pod obciążeniem
Pułapka nie zawsze jest natychmiastowa

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.

#
Powszechna pułapka

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.

pool.rsrust
let connect_options = url
    .parse::<PgConnectOptions>()?
    .statement_cache_capacity(0);

// Odpowiedniki: asyncpg -> Statement_cache_size=0, pgjdbc -> przygotowanieThreshold=0

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.

#
Sprawdzone w kodzie

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).

#
Praktyczny przewodnik

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.

  1. Audyt kodu aplikacji. Poszukaj nietransakcyjnych SET, LISTEN/NOTIFY, blokad doradczych sesji, kursorów WITH HOLD i tabel tymczasowych używanych ponownie między zapytaniami.
  2. 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.
  3. 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.
  4. 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.
  5. Rozmiar default_pool_size i max_client_conn odnosi się do rzeczywistego max_connectionsPostgres, a nie dowolna liczba skopiowana z innego projektu.
  6. 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.
  7. Monitoruj SHOW POOLS i SHOW STATS z konsoli administracyjnej PgBouncer po uruchomieniu produkcyjnym, aby wykryć nasycenie puli, zanim stanie się ono widoczne po stronie klienta.
#
Decyzja

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

Często zadawane pytania

Pytania, które pojawiają się najczęściej po włączeniu trybu transakcyjnego na produkcji.

Co to jest tryb łączenia transakcji PgBouncer?+
Jest to jeden z 3 trybów PgBouncera (z sesją i instrukcją), w którym połączenie Postgres jest ponownie przypisywane innemu klientowi po zakończeniu każdej transakcji, a nie po rozłączeniu się klienta. Dzięki temu możliwa jest obsługa znacznie większej liczby konkurencyjnych klientów, niż faktycznie jest otwartych połączeń Postgres.
Dlaczego moje przygotowane wyciągi ulegają awarii w trybie transakcji?+
Nazwana przygotowana instrukcja jest przygotowywana na określonym połączeniu z serwerem. W trybie transakcyjnym połączenie to można przypisać innemu klientowi pomiędzy dwoma żądaniami. Jeśli sterownik odtwarza nazwę instrukcji w przypadku połączenia, w którym nigdy nie została ona przygotowana, Postgres zwróci błąd przypominający, że przygotowana instrukcja nie istnieje. Poprawka polega na wyłączeniu pamięci podręcznej przygotowanych wyciągów po stronie sterownika (statement_cache_capacity(0) z sqlx, Statement_cache_size=0 z asyncpg).
Czy możemy używać LISTEN/NOTIFY za PgBouncerem w trybie transakcyjnym?+
Nie, niezawodnie. LISTEN/NOTIFY zakłada trwałe połączenie w celu otrzymywania powiadomień, czego tryb transakcyjny nie gwarantuje. Standardową praktyką jest przekazywanie komponentów zależnych od LISTEN/NOTIFY (na przykład PostgREST) ​​przez bezpośrednie połączenie z Postgres, poza pulą.
Czy powinniśmy używać SET LOCAL zamiast SET w trybie transakcji?+
Tak, systematycznie dla dowolnego ustawienia, które musi mieć zastosowanie do danego żądania. SET LOCAL jest czyszczony automatycznie po zatwierdzeniu lub ROLLBACK, dzięki czemu jest bezpieczny w przypadku połączenia z serwerem, które może zmieniać się między dwiema transakcjami. Klasyczny SET może wyciekać do następnego klienta, który odzyskuje to samo odzyskane połączenie z serwerem.
Czy tryb transakcji działa z zabezpieczeniami na poziomie wiersza (RLS)?+
Tak, pod warunkiem, że oświadczenia JWT lub zmienne sesji używane przez zasady RLS są ustawione w zestawie LOCAL SET wewnątrz transakcji, a nie w zestawie sesji. Jest to wzorzec opisany w naszym artykule na temat izolacji RLS dla wielu dzierżawców.
PgBouncer, Supavisor, PgCat: który wybrać?+
Cała trójka wdraża podobny model łączenia zasobów, z różnicami w klastrowaniu, rozkładzie obciążenia i ekosystemie (Supavisor jest rozwijany przez Supabase, PgCat jest napisany w Rust). Wybór zależy przede wszystkim od topologii wdrożenia i istniejących ograniczeń operacyjnych: szczegółowe informacje można znaleźć w naszym dedykowanym porównaniu.

GOTOWY DO WDROŻENIA?

Twój backend w pięć minut.

Karta kredytowa nie jest wymagana · 500 MB za darmo · 50 000 MAU