PostgreSQL connection pooling na VPS: jak uniknąć OOM
Dowiedz się, dlaczego PostgreSQL wyczerpuje RAM przy limicie max_connections. Wyjaśniamy, jak pooler zapobiega błędowi FATAL: remaining connection slots are reserved i awariom OOM.
Dlaczego mały VPS wyczerpuje pamięć RAM przed osiągnięciem limitu max_connections
Pooling połączeń PostgreSQL na serwerze VPS nie służy jedynie zwiększeniu wydajności. Jest to mechanizm utrzymujący stabilność serwera z 4 GB pamięci RAM, ponieważ każde połączenie z PostgreSQL jest osobnym procesem systemu operacyjnego, który zajmuje własną pamięć prywatną. Pooler pozwala obsłużyć dużą liczbę tanich połączeń klienckich przy użyciu niewielkiej, stałej liczby rzeczywistych procesów backendowych.
Domyślna wartość max_connections wynosi 100. Jest to limit, a nie budżet zasobów. PostgreSQL nie sprawdza, czy maszyna jest w stanie obsłużyć 100 backendów wykonujących rzeczywiste zapytania, dlatego serwer ulega awarii jako pierwszy. Mechanizm OOM (Out of Memory) killera wybiera proces do zakończenia, a gdy wybierze proces backendowy, PostgreSQL restartuje cały klaster, aby zapewnić bezpieczeństwo pamięci współdzielonej. W dzienniku pojawia się server process (PID 1234) was terminated by signal 9: Killed, a następnie terminating any other active server processes. Wszystkie otwarte połączenia zostają zerwane, w tym te poprawne.
Serwer wyczerpuje pamięć, ponieważ każde połączenie jest osobnym procesem, a wartość work_mem jest przydzielana dla każdej operacji sortowania lub haszowania, a nie dla pojedynczego połączenia. Oba te czynniki potęgują zużycie zasobów.
Każde połączenie to proces, a każdy proces zużywa pamięć
PostgreSQL wykorzystuje jeden proces na każde połączenie. Proces postmaster tworzy (fork) proces backendowy w momencie nawiązania połączenia przez klienta, a proces ten istnieje aż do rozłączenia. Nie jest to wątek. Posiada on własne tablice stron, własne pamięci podręczne katalogu oraz własne plany zapytań. Pamięci te rosną w miarę, jak połączenie odwołuje się do większej liczby tabel i wykonuje bardziej zróżnicowane zapytania, dlatego długotrwałe połączenie w obciążonej aplikacji korzystającej z ORM kosztuje więcej niż nowe.
Pamięć współdzielona jest faktycznie współdzielona. shared_buffers to jedna alokacja dla całego klastra, zmapowana w każdym procesie backendowym. Pamięć prywatna nie jest współdzielona i dlatego top wprowadza w tym miejscu w błąd: wartość RSS (resident set size) procesu backendowego zawiera strony współdzielone, do których proces ten uzyskał dostęp, więc sumowanie RSS dla 50 procesów backendowych powoduje policzenie shared_buffers 50 razy.
Należy mierzyć część prywatną. Wartość PSS (proportional set size) dzieli każdą stronę współdzieloną przez liczbę procesów, które ją mapują, a USS (unique set size) zlicza tylko te strony, które należą wyłącznie do danego procesu.
sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'Kolumna USS wskazuje ilość pamięci, która zostałaby zwolniona, gdyby dany proces backendowy zakończył działanie. Jest to rzeczywisty koszt pojedynczego połączenia. Publikowane dane zazwyczaj określają zużycie pamięci przez bezczynny proces backendowy na kilka megabajtów, a przez proces, który wykonał złożone zapytania ORM, na kilkukrotnie więcej. Należy traktować te wartości jako typowe dane poglądowe, a nie jako własne wyniki. Jedyną wartością, na której warto opierać planowanie, jest ta uzyskana z własnego serwera przy własnym obciążeniu.
Łatwo przeoczyć alokację wykonywaną na każdą sesję. Parametr temp_buffers ma domyślną wartość 8MB i jest alokowany dla sesji w momencie, gdy sesja ta po raz pierwszy odwoła się do tabeli tymczasowej. Pamięć ta nie jest zwracana aż do zakończenia sesji.
Parametr work_mem jest przydzielany na operację, a nie na połączenie
W tym miejscu obliczenia często sprawiają trudności. Domyślna wartość work_mem wynosi 4MB, a dokumentacja PostgreSQL jasno określa, co to oznacza: „złożone zapytanie może wykonywać kilka operacji sortowania i haszowania jednocześnie, przy czym każdej operacji zazwyczaj wolno użyć tyle pamięci, ile określa ta wartość, zanim zacznie zapisywać dane do plików tymczasowych”. Plan zapytania z trzema węzłami sortowania może wykorzystać trzykrotność work_mem w ramach jednego procesu backendu w tym samym momencie.
Operacje haszowania zużywają więcej pamięci. Domyślna wartość hash_mem_multiplier wynosi 2.0, więc złączenie haszujące (hash join) lub agregacja haszująca (hash aggregate) mogą wykorzystać work_mem pomnożone przez dwa, co przy ustawieniach domyślnych daje 8MB. Zapytania równoległe zwiększają to zużycie jeszcze bardziej, ponieważ każdy proces roboczy (parallel worker) jest osobnym procesem z własnym limitem pamięci.
Przeanalizujmy liczby dla serwera VPS z 4 GB RAM. Ustawienie shared_buffers na 1 GB, pozostawienie work_mem na poziomie 4MB oraz 100 połączeń, z których każde wykonuje zapytanie z dwoma węzłami haszującymi, daje 100 razy 16MB. To 1.6 GB pamięci prywatnej ponad 1 GB współdzielonych buforów (shared buffers), jeszcze przed uwzględnieniem pamięci podręcznej stron (page cache) i innych procesów w systemie. Jeśli teraz zwiększysz work_mem do 64MB, ponieważ serwer ma wolną pamięć RAM, te same 100 połączeń zażąda 100 razy 256MB. System nie wyświetli żadnego ostrzeżenia. O problemie dowiesz się dopiero w momencie zadziałania mechanizmu OOM killer.
Zamiast zgadywać, możesz sprawdzić, czy work_mem jest zbyt małe. Ustaw log_temp_files = 0 w pliku postgresql.conf i przeładuj konfigurację. Każde zrzucenie danych na dysk spowoduje zapisanie wiersza z nazwą pliku i jego rozmiarem, na przykład temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520. Częste zrzuty oznaczają, że zwiększenie work_mem przyniesie korzyści. Brak zrzutów oznacza, że zwiększenie tej wartości nic nie daje, a jedynie niepotrzebnie zużywa pamięć, której możesz nie mieć.
Arytmetyka puli, która faktycznie sprawia problemy
Nikt nie planuje otwierania 240 połączeń. Administratorzy konfigurują pulę o rozmiarze 20, a następnie uruchamiają aplikację w więcej niż jednym miejscu.
The data behind this chart
[
{
"config": "1 worker",
"backends": 20
},
{
"config": "4 web workers",
"backends": 80
},
{
"config": "4 web + 2 background",
"backends": 120
},
{
"config": "2 hosts x 4 workers",
"backends": 160
},
{
"config": "3 hosts x 4 workers",
"backends": 240
}
]Cztery procesy robocze Gunicorn, z których każdy utrzymuje pulę 20 połączeń, wymagają łącznie 80 backendów. Dodanie dwóch procesów roboczych dla zadań w tle zwiększa tę liczbę do 120. Wzrost do 3 hosts x 4 workers powoduje, że aplikacja żąda 240 backendów przy limicie max_connections wynoszącym 100. Żadna z tych konfiguracji 5 nie jest błędna w żadnym pojedynczym punkcie. Pula jest przypisana do procesu, a żadna część aplikacji nie widzi sumy wszystkich połączeń.
Domyślne ustawienia bibliotek potęgują ten problem. SQLAlchemy QueuePool domyślnie ustawia pool_size=5 z max_overflow=10, co daje 15 połączeń na proces. HikariCP domyślnie ustawia 10. Django przed wersją 5.1 nie posiadało wbudowanej puli, oferując jedno połączenie na proces roboczy. Dlatego aplikacje Django napotykają ten problem później, a następnie odczuwają go gwałtownie, gdy ktoś ustawi CONN_MAX_AGE lub włączy nowszą opcję "pool": True. W przypadku uruchamiania aplikacji Django za Gunicorn i nginx, mnożnikiem jest liczba procesów roboczych Gunicorn, a nie liczba serwerów.
Co w praktyce zmienia pulowanie połączeń Postgres na VPS
Pooler to proces, który z jednej strony komunikuje się z aplikacją za pomocą protokołu PostgreSQL, a z drugiej utrzymuje niewielki zestaw rzeczywistych połączeń z serwerem. Nie przyspiesza on wykonywania zapytań. Zmienia natomiast to, kto ponosi koszt nawiązania połączenia oraz ile rzeczywistych backendów istnieje w danym momencie.
Dwie rzeczy ulegają poprawie. Nawiązywanie połączenia przestaje kosztować operację fork oraz przeszukiwanie katalogu, które wypełnia pustą pamięć podręczną backendu, ponieważ pooler samodzielnie odpowiada na żądanie połączenia klienta. Co ważniejsze, liczba rzeczywistych backendów przestaje być powiązana z liczbą połączeń aplikacji, dzięki czemu 500 klientów może współdzielić 20 backendów.
Kolejkowanie jest kluczową funkcją, choć często budzi opór. Bez poolera 500 jednoczesnych zapytań otrzymuje własny backend i wszystkie uruchamiają się jednocześnie na dwóch rdzeniach CPU, przez co każde z nich działa wolno, a cała pamięć zostaje zużyta w tym samym momencie. Z poolerem 20 zapytań jest przetwarzanych, a reszta czeka kilka milisekund, dzięki czemu każde uruchomione zapytanie otrzymuje realny przydział mocy procesora i kończy się szybciej. Kolejka przed małą pulą jest lepsza niż brak kolejki przed dużą.
Pooler nie ogranicza niczego innego na maszynie. Jeśli Postgres współdzieli VPS z serwerem aplikacji lub z bazą wektorową na tym samym VPS, pooler chroni Postgresa wyłącznie przed Twoją aplikacją. Należy również nałożyć twarde limity na sąsiednie procesy: można ograniczyć pamięć i CPU używane przez usługę za pomocą systemd, aby jeden niekontrolowany proces nie spowodował awarii bazy danych. Miejsce, w którym działa baza danych, wpływa na sposób ustawiania tych limitów, co stanowi praktyczną różnicę między uruchamianiem Postgresa w Dockerze lub bezpośrednio na hoście.
Pula sesji a pula transakcji
Jedno ustawienie determinuje całą resztę, a jest nim pool_mode.
W puli sesji połączenie z serwerem jest przypisywane do klienta na cały czas trwania jego połączenia i zwalniane dopiero w momencie rozłączenia klienta. Wszystko działa poprawnie, ponieważ program obsługujący pulę działa jak zwykłe proxy. Oszczędzasz jedynie koszt nawiązywania połączenia. Jeśli aplikacja otwiera 200 połączeń, nadal potrzebujesz 200 backendów.
W puli transakcji połączenie z serwerem jest przypisywane do klienta tylko na czas trwania jednej transakcji. Po wykonaniu COMMIT lub ROLLBACK połączenie wraca do puli i przejmuje je kolejny oczekujący klient. To właśnie ten mechanizm pozwala zamienić 500 klientów na 20 backendów. Jest to również przyczyna problemów, które wynikają bezpośrednio z konstrukcji tego rozwiązania: kolejne zapytanie może zostać wykonane na innym backendzie niż poprzednie.
Domyślnym ustawieniem PgBouncer jest pool_mode = session. Jeśli zainstalujesz go i nie zmienisz konfiguracji, otrzymasz jedynie mniej istotne korzyści bez pełnej wydajności. Trzeci tryb, statement, zwraca połączenie po każdym pojedynczym zapytaniu i odrzuca transakcje wielozdaniowe. Nie używaj go, chyba że dokładnie wiesz, dlaczego jest Ci potrzebny.
Który tryb transakcyjny ulega awarii i dlaczego
Wszystkie poniższe przypadki zawodzą z jednego powodu. Jest to stan przechowywany wewnątrz pojedynczego backendu, a pooling transakcyjny nie gwarantuje, że za każdym razem otrzymasz ten sam backend.
SETorazRESETna poziomie sesji.SET search_path,SET statement_timeout,SET TIME ZONEiSET ROLEtrafiają do backendu, który obsłużył dane zapytanie i znikają przed kolejną transakcją. UżywajSET LOCALwewnątrz jawnej transakcji, co ogranicza zasięg do tej transakcji i jest bezpieczne.LISTEN. Dostarczanie powiadomień przypisane jest do backendu, który wykonałLISTEN, a ten backend jest przekazywany innemu klientowi natychmiast po zakończeniu transakcji.NOTIFYnadal działa w trybie transakcyjnym, co czyni ten błąd mylącym: wysyłanie kończy się sukcesem, ale odbieranie nigdy nie następuje. Jeśli potrzebujeszLISTEN, otwórz dodatkowe połączenie bezpośrednio do portu 5432, z pominięciem poolera.- Blokady doradcze (advisory locks) na poziomie sesji.
pg_advisory_lock()jest utrzymywana przez sesję i zwalniana dopiero po jej zakończeniu. W trybie poolingu transakcyjnego polecenie odblokowania trafia do innego backendu, więc blokada pozostaje aktywna, dopóki PgBouncer nie wycofa połączenia z serwerem, co domyślnie następuje poserver_lifetime, czyli godzinie. Używajpg_advisory_xact_lock(), która jest zwalniana na końcu transakcji przez ten sam backend, który ją założył. PREPAREorazDEALLOCATE, czyli instrukcje SQL. Niedostępne w trybie transakcyjnym.- Kursory
WITH HOLDoraz każdy kursor po stronie serwera, który ma przetrwać transakcję. - Tabele tymczasowe, które mają przetrwać commit.
CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWSumieszcza tabelę w schemacie tymczasowym jednego backendu, a kolejna transakcja może trafić do innego backendu. LOAD.
Przygotowane instrukcje (prepared statements) na poziomie protokołu to jedyny element, który uległ zmianie. PgBouncer 1.21.0 dodał ich obsługę w trybie transakcyjnym, a 1.24.0 włączył ją domyślnie poprzez ustawienie max_prepared_statements na 200. Starsze wersje pozostawiają tę wartość na 0, co oznacza wyłączenie. Ubuntu 24.04 dostarcza PgBouncer 1.22.0, więc funkcja jest dostępna, ale należy samodzielnie ustawić max_prepared_statements. Jeśli nie masz pewności, jak działa Twoja wersja, bezpiecznym ustawieniem jest konfiguracja po stronie klienta: psycopg 3 przestaje używać przygotowanych instrukcji po stronie serwera, gdy ustawisz prepare_threshold na None.
Django posiada własną nazwę dla tego mechanizmu. Dokumentacja stwierdza, że "używanie poolera połączeń w trybie transakcyjnym (np. PgBouncer) wymaga wyłączenia kursorów po stronie serwera dla tego połączenia", ponieważ "kursory po stronie serwera są dostępne tylko w połączeniu, w którym zostały utworzone". Ustaw DISABLE_SERVER_SIDE_CURSORS na True we wpisie danej bazy danych, w przeciwnym razie każde wywołanie .iterator() stanie się sporadycznym błędem, który pojawia się tylko pod obciążeniem.
Tryb transakcyjny jest warty wdrożenia, ale stanowi pewną umowę. Przeczytaj listę, sprawdź pod tym kątem swój ORM oraz bibliotekę zadań w tle, a następnie przełącz tryb.
Instalacja PgBouncer i przekierowanie aplikacji
Poniższa konfiguracja jest przeznaczona do wdrożenia na własnym serwerze: Ubuntu 24.04 z PostgreSQL działającym na 127.0.0.1 na porcie 5432.
sudo apt update
sudo apt install -y pgbouncer
pgbouncer --versionUbuntu 24.04 zawiera pakiet PgBouncer 1.22.0. Wersja upstream na sierpień 2026 to 1.25.2. Należy sprawdzić posiadaną wersję, ponieważ zachowanie przygotowanych instrukcji (prepared statements) zależy od wydania.
Utwórz rolę, której jedynym zadaniem jest logowanie do konsoli administracyjnej PgBouncer, a następnie przygotuj plik haseł. PgBouncer wymaga sekretów SCRAM (salted challenge response authentication mechanism) z pg_authid, a dostęp do tej tabeli posiada wyłącznie superużytkownik.
sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
> /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txtKopiowanie sekretów zamiast ręcznego wpisywania haseł zapewnia poprawne działanie mechanizmu. PgBouncer może użyć sekretu SCRAM do logowania do PostgreSQL tylko wtedy, gdy klient również uwierzytelnił się za pomocą SCRAM, sekret w pliku jest identyczny z tym w pg_authid (ta sama sól i liczba iteracji, nie tylko to samo hasło), a wiersz [databases] nie zawiera wymuszenia user=. Dodanie user=appuser do tego wiersza sprawia, że PgBouncer wymaga hasła w postaci jawnej. Upewnij się, że właścicielem pliku jest konto, na którym działa usługa, używając systemctl show pgbouncer -p User. Zmiana hasła w PostgreSQL wymaga wygenerowania tego pliku na nowo, w przeciwnym razie kolejne połączenie zwróci password authentication failed.
Teraz utwórz /etc/pgbouncer/pgbouncer.ini.
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digitslisten_addr = 127.0.0.1 izoluje pooler od publicznego Internetu, co jest istotne, ponieważ pooler dostępny z zewnątrz stanowi punkt uwierzytelniania, którego nie należy wystawiać. max_client_conn określa liczbę połączeń aplikacji, które zaakceptuje PgBouncer; jest to parametr mało kosztowny, więc może przyjmować wysokie wartości. default_pool_size to liczba rzeczywistych połączeń z backendem dla pary baza-użytkownik i jest to parametr kosztowny. max_db_connections ogranicza całkowitą liczbę połączeń do bazy do 80, pozostawiając zapas w max_connections dla psql, kopii zapasowych i monitoringu. ignore_startup_parameters = extra_float_digits zapobiega odrzucaniu przez PgBouncer sterowników, w tym JDBC, które przesyłają ten parametr podczas nawiązywania połączenia.
sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pagerPoprawny start usługi jest potwierdzony wpisem w logach informującym, że PgBouncer nasłuchuje na 127.0.0.1:6432. Start zakończony niepowodzeniem z powodu pliku haseł loguje ścieżkę, której nie można było odczytać – zazwyczaj wynika to z problemów z uprawnieniami lub właścicielem pliku, a nie z błędnej składni. Następnie zmień ciąg połączeniowy aplikacji z portu 5432 na 6432 i zrestartuj ją. Żadne inne zmiany w aplikacji nie są wymagane.
Jak sprawdzić, czy pula działa poprawnie
PgBouncer posiada konsolę administracyjną dostępną poprzez wirtualną bazę danych o nazwie pgbouncer.
psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncerSHOW POOLS;
SHOW STATS;
SHOW CLIENTS;SHOW POOLS jest parametrem, który należy monitorować. cl_active to klienci aktualnie połączeni z serwerem, cl_waiting to klienci oczekujący na połączenie, sv_active oraz sv_idle to rzeczywiste backendy w użyciu i wolne, a maxwait to czas w sekundach, przez jaki klient z początku kolejki oczekuje na obsługę. Prawidłowe działanie przy standardowym obciążeniu oznacza cl_waiting na poziomie 0 oraz maxwait na poziomie 0. Wartość maxwait rosnąca powyżej sekundy lub dwóch oznacza, że pula jest zbyt mała lub zapytania są zbyt wolne; oba przypadki wymagają odmiennych działań naprawczych.
Przed zwiększeniem default_pool_size należy sprawdzić, który z tych czynników występuje.
SELECT state, count(*) FROM pg_stat_activity
WHERE backend_type = 'client backend' GROUP BY state;Jeśli większość backendów znajduje się w stanie idle in transaction, problemem nie jest rozmiar puli. Aplikacja otwiera transakcję, a następnie wykonuje w jej obrębie wolną operację, na przykład wywołanie HTTP, przez co każdy backend jest zajęty, mimo że nie wykonuje zapytania. idle_in_transaction_session_timeout przerwie takie połączenia, jednak właściwe rozwiązanie wymaga poprawy kodu aplikacji. Jeśli natomiast wszystkie backendy są w stanie active, pula jest faktycznie nasycona, a zapytania wymagają EXPLAIN (ANALYZE, BUFFERS), zanim zostaną przydzielone kolejne połączenia.
W kwestii doboru rozmiaru, najczęściej cytowanym punktem wyjścia jest heurystyka HikariCP, zakładająca dwukrotność liczby rdzeni plus jeden, co dla VPS z 2 rdzeniami daje 5. Należy traktować to jako wstępną wartość, ustawić default_pool_size w jej pobliżu i dokonywać korekt w oparciu o maxwait. Małe pule wydają się nieintuicyjne, ale zazwyczaj osiągają lepsze wyniki, ponieważ backend w kolejce nie generuje kosztów, podczas gdy działający backend zużywa CPU, pamięć i powoduje rywalizację o blokady.
Wybór między PgBouncer, PgDog a Pgpool-II
PgBouncer stanowi rozwiązanie dla typowych scenariuszy: jeden serwer PostgreSQL, jeden VPS oraz aplikacja otwierająca więcej połączeń, niż jest w stanie obsłużyć maszyna. Narzędzie to realizuje jedno zadanie, jego konfiguracja ogranicza się do pojedynczego pliku ini, a pakiety są dostępne w repozytoriach Debian i Ubuntu. Obsługa połączeń odbywa się w jednym wątku, co jest wystarczające dla obciążeń typowych dla VPS i staje się ograniczeniem dopiero na znacznie większych maszynach.
PgDog warto rozważyć, gdy decyzje o routingu mają zapadać na tym samym etapie sieciowym co pooling. Narzędzie to określa się jako proxy do skalowania PostgreSQL, jest napisane w języku Rust i oferuje pooling transakcji oraz sesji, a także podział ruchu na odczyt i zapis poprzez analizę zapytań. Obsługuje również sharding z routingiem wieloshardowym oraz protokół two-phase commit. Należy po nie sięgnąć w przypadku posiadania instancji głównej (primary) oraz jednej lub większej liczby replik, gdy wymagane jest kierowanie odczytów do replik bez konieczności modyfikacji aplikacji. Należy zachować dwie ostrożności. Licencja to AGPLv3, więc kwestię klauzuli dotyczącej wykorzystania sieci należy wyjaśnić z odpowiednim działem w firmie przed wdrożeniem na produkcję; stanowisko projektu głosi, że wewnętrzne użycie i prywatne modyfikacje nie nakładają obowiązku udostępniania kodu źródłowego. Projekt jest również młody, z cotygodniowymi wydaniami i numeracją wersji 0.x, dlatego zaleca się przypięcie konkretnego tagu wydania zamiast śledzenia main.
git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.tomlBudowanie ze źródeł wymaga aktualnego, stabilnego łańcucha narzędzi Rust, CMake oraz kompilatora C/C++. Na stronie wydań dostępne są również gotowe pliki binarne dla systemu Linux i pakiety Debian, a obraz kontenera znajduje się pod adresem ghcr.io/pgdogdev/pgdog. Konfiguracja jest podzielona na dwa pliki. Pierwszy zawiera ustawienia ogólne oraz po jednym wpisie na każdą bazę danych, zapisanym tutaj jako tablica TOML z tabelami wbudowanymi, co pozwala łatwo odróżnić obie formy.
databases = [
{ name = "appdb", host = "127.0.0.1" },
]
[general]
port = 6432
default_pool_size = 10Drugi plik zawiera po jednym wpisie na każdego użytkownika, w tej samej formie tablicowej.
users = [
{ name = "appuser", database = "appdb", password = "change-this" },
]PgDog domyślnie nasłuchuje na porcie 6432, czyli tym samym co PgBouncer, dlatego oba narzędzia nie mogą jednocześnie korzystać z portu domyślnego na tym samym hoście.
Pgpool-II, w wersji 4.7.2 na czerwiec 2026, oferuje pooling oraz równoważenie obciążenia (load balancing), wraz z mechanizmem watchdog do automatycznego przełączania awaryjnego (failover). Dodatkowe funkcje wprowadzają dodatkowe tryby awarii, a model poolingu jest kluczowy do zrozumienia przed podjęciem decyzji o wyborze. Pgpool-II tworzy przed uruchomieniem (pre-fork) num_init_children procesów potomnych, a każdy z nich buforuje do max_pool połączeń z serwerem, więc górny limit backendów wynosi num_init_children pomnożone przez max_pool. Każdy proces potomny obsługuje jednego klienta w danym czasie, więc liczba klientów, których można zaakceptować, jest równa num_init_children i jest ustalana przy starcie, a bezczynny klient nadal zajmuje proces potomny. Ustawienie num_init_children na 100 oraz max_pool na 4 oznacza autoryzację 400 backendów, co jest dokładnie tym problemem, który miał rozwiązać instalowany pooler. Pgpool-II należy wybrać, gdy wymagane są funkcje failover i routingu zapytań, pamiętając o dokładnym wykonaniu obliczeń. Jeśli celem jest jedynie zmniejszenie liczby backendów, narzędzie to jest zbyt rozbudowane w stosunku do potrzeb.
Kwestia zarządzanego proxy a jego odpowiednik z własnym hostingiem
Zarządzane platformy sprzedają to jako osobny produkt. AWS umieszcza RDS Proxy przed RDS, a Supabase umieszcza własny pooler, Supavisor, przed Supabase Postgres. Oba rozwiązania realizują opisane tu zadanie: tanio utrzymują połączenia klienckie i przydzielają mniejszą liczbę rzeczywistych backendów. Supavisor jest oprogramowaniem typu open source i można go hostować samodzielnie, więc wybór nie ogranicza się do rozwiązań własnościowych kontra darmowych.
Odpowiednik zarządzanego proxy z własnym hostingiem nie jest odmienną koncepcją. To ten sam pomysł, z plikiem konfiguracyjnym w Twoich rękach: PgBouncer w trybie transakcyjnym, na tym samym VPS co baza danych, nasłuchujący na 127.0.0.1. Dwie różnice są istotne. Zarządzane proxy znajduje się w odległości jednego przeskoku sieciowego, więc dodaje opóźnienie i utrzymuje połączenia klienckie, podczas gdy baza danych pod nim się restartuje. PgBouncer na hoście bazy danych dodaje przeskok przez pętlę zwrotną (loopback), co jest niemal bezkosztowe, ale przestaje działać, gdy pada ten host. Jeśli wymagasz zachowania połączeń podczas restartu, potrzebujesz również mechanizmu przełączania awaryjnego (failover), w którym to miejscu watchdog w Pgpool-II lub testy stanu (health checks) w PgDog zaczynają uzasadniać swoją złożoność.
Na liście znajduje się jeszcze jedna opcja. Jeśli liczba połączeń jest głównym czynnikiem komplikującym wdrożenie, wbudowana baza danych nie posiada modelu połączeń do pulowania, ponieważ jest biblioteką wewnątrz procesu, a nie serwerem na porcie. W przypadku pojedynczego serwera aplikacji o umiarkowanym wolumenie zapisu, uruchomienie SQLite w środowisku produkcyjnym na VPS eliminuje ten problem całkowicie, zamiast nim zarządzać. Gdy faktycznie potrzebujesz prawdziwego serwera, określ rozmiar puli przed określeniem rozmiaru maszyny.
FAQ
Czy nadal potrzebuję PgBouncer, jeśli moja aplikacja posiada już pulę połączeń?
Zazwyczaj tak, ponieważ pula aplikacji działa w obrębie pojedynczego procesu i nie ma wglądu w inne. Cztery procesy robocze Gunicorn, z których każdy utrzymuje pulę 20 połączeń 80, wraz z dwoma procesami działającymi w tle, generują łącznie 120 połączeń. PgBouncer jest jedynym komponentem, który widzi sumaryczną liczbę połączeń i może ją ograniczyć. Optymalna konfiguracja obejmuje oba rozwiązania: małą pulę wewnątrz każdego procesu roboczego, aby uniknąć kosztów nawiązywania połączeń TCP, oraz PgBouncer w trybie transakcyjnym, który zarządza rzeczywistymi połączeniami z bazą danych.
Co dokładnie przestaje działać po przełączeniu PgBouncer w tryb transakcyjny?
Wszystko, co utrzymuje stan wewnątrz jednego połączenia z bazą danych pomiędzy transakcjami. Zmienne sesyjne SET i RESET, kursory LISTEN, WITH HOLD, instrukcje SQL PREPARE i DEALLOCATE, blokady doradcze (advisory locks) na poziomie sesji, tabele tymczasowe, które muszą przetrwać zatwierdzenie transakcji (commit), oraz LOAD. NOTIFY nadal działa, co sprawia, że błędy w LISTEN wyglądają na problemy z dostarczaniem danych, a nie z pulowaniem połączeń. W Django należy ustawić DISABLE_SERVER_SIDE_CURSORS na True. W przypadku psycopg 3 należy ustawić prepare_threshold na None lub używać PgBouncer w wersji 1.22 lub nowszej z parametrem max_prepared_statements ustawionym na wartość powyżej 0. Zastąp pg_advisory_lock() przez pg_advisory_xact_lock().
Jak duży powinien być default_pool_size na VPS z 2 rdzeniami?
Mniejszy, niż mogłoby się wydawać. Powszechnie stosowana heurystyka HikariCP sugeruje dwukrotność liczby rdzeni plus jeden, czyli około 5 dla dwóch rdzeni, co stanowi punkt wyjścia, a nie ostateczną odpowiedź. Ustaw tę wartość, a następnie monitoruj maxwait oraz cl_waiting w SHOW POOLS pod rzeczywistym obciążeniem. Wartość zero dla obu parametrów oznacza, że pula jest wystarczająco duża. Rosnąca wartość maxwait oznacza, że klienci czekają w kolejce. Zanim zwiększysz liczbę połączeń, sprawdź pg_stat_activity: procesy utknięte w stanie idle in transaction zazwyczaj świadczą o błędzie w aplikacji, a zwiększenie liczby połączeń jedynie zamaskuje ten problem.
PgBouncer czy PgDog?
PgBouncer jest odpowiedni dla jednego serwera PostgreSQL na jednym VPS, co stanowi większość wdrożeń. Jest dostępny w pakietach Ubuntu, jego zachowanie jest dobrze udokumentowane, a cała konfiguracja mieści się w jednym pliku ini. PgDog jest właściwym wyborem, gdy wymagany jest podział ruchu na odczyt/zapis pomiędzy replikami lub sharding na tym samym etapie co pulowanie, dzięki czemu aplikacja nie musi znać topologii bazy danych. Przed wyborem PgDog należy wyjaśnić kwestie licencyjne AGPLv3 z działem prawnym oraz przypiąć konkretną wersję oprogramowania, ponieważ projekt znajduje się w fazie 0.x i publikuje nowe wydania co tydzień.