PostgreSQL / EDB

PgBouncer – lekki connection pooler dla PostgreSQL

2023-01-11
Podziel się

Z  artykułu dowiesz się między innymi, czy:

  • warto stosować zarządzanie połączeniami w PostgreSQL;
  • PgBouncer jest jedynym słusznym connection poolerem;
  • tryb “session” zawsze będzie wolniejszy od “transaction”;
  • lepiej ustawić 100 czy 10 połączeń do bazy danych.

Z  artykułu dowiesz się między innymi, czy:

  • warto stosować zarządzanie połączeniami w PostgreSQL;
  • PgBouncer jest jedynym słusznym connection poolerem;
  • tryb “session” zawsze będzie wolniejszy od “transaction”;
  • lepiej ustawić 100 czy 10 połączeń do bazy danych

Pracując dla klientów, często słyszę, że obecnie nie używa się już narzędzi do zarządzania połączeniami w bazie danych PostgreSQL. Stwierdzenie to argumentowane jest faktem, iż aktualnie tworzone oprogramowanie posiada już wbudowane takie rozwiązania.

Teoretycznie twierdzenie to jest prawdziwe. Jeżeli po stronie aplikacji jest sprawnie działający connection pooler to nie ma potrzeby stosowania dodatkowo takiego rozwiązania po stronie bazy danych.

Dlaczego “teoretycznie” i co oznacza “sprawnie działający”?

Zdarza się, że takie aplikacje nie są już zainstalowane na maszynach wirtualnych, ale zamknięte w kontenerach. Na etapie tworzenia i wdrażania aplikacji możemy się umówić na maksymalną liczbę połączeń, jaką będą nawiązywać. Sprawne zarządzanie połączeniami jest zaszyte w takim kontenerze, więc mamy wszystko, co potrzeba.

Wyobraźmy sobie taki scenariusz:

  • konfiguracja poda: trzyma 10 połączeń na stałe, maksymalnie 50 połączeń;
  • system zakłada skalowalność do 10 podów;
  • musimy skonfigurować minimum 500 połączeń do takiej bazy danych + zapas + połączenia dodatkowe, dajemy 600 (popularnym jest obliczanie ilości możliwych połączeń na podstawie ilości wątków procesora w stosunku 3-4 połączenia na wątek, czyli 600/4=150 – tyle wątków powinniśmy zapewnić maszynie);
  • pół roku (albo dowolny inny czas po wdrożeniu) pada decyzja: baza danych świetnie sobie radzi a aplikacja jest wąskim gardłem, więc podwajamy możliwą liczbę podów.

Czy teraz powinniśmy zwiększyć liczbę dostępnych połączeń i procesorów 2x? A może skonfigurować wszystkie pody, aby konsumowały mniej połączeń?

Nie zagłębiając się w tajniki tuningu PostgreSQL, (bo nie taki cel przyświeca temu wpisowi) odpowiem ‘to zależy’:

  • nie zawsze do obsługi 600 połączeń potrzebujemy aż 150 wątków procesora, ale czasem na 96 możemy obsłużyć 96 połączeń i zwiększenie ich liczby wpłynie negatywnie na wydajność systemu, wszystko zależy od charakteru ruchu;
  • może się zdarzyć taka sytuacja, że mamy moce przerobowe procesora, ale kłopotem może być dostępna pamięć operacyjna;
  • może się również zdarzyć taka sytuacja, że kłopotem będzie większa ilość locków w bazie związana ze wzrostem ilości transakcji;
  • kłopotem może być również zła jakość kodu odwołującego się do bazy danych lub nieoptymalna konfiguracja connection poolera wbudowanego w aplikację.

Przeprowadziłem proste testy, mające na celu sprawdzenie:

  • jak ilość połączeń wpływa na wydajność bazy danych;
  • jaki efekt można uzyskać stosując pgBouncera.

Testy nie są skomplikowane i nie mają na celu dokładnego pomiaru wydajności serwera, a jedynie wskazanie ogólnych możliwości zastosowania technologii. Również wyniki testów mogą nie być satysfakcjonujące z punktu widzenia wydajności. Głównym ich celem jest wychwycenie możliwości optymalizacji poprzez zarządzanie połączeniami.

Każdy test został przeprowadzony 3 do 5 razy, aby wyeliminować grube błędy. Przedstawiony został wynik najbardziej zbliżony średniej. Ostatnie testy (dla 4000 połączeń) miały na celu porównanie zużycia pamięci dla dużej ilości połączeń w przypadku dwóch scenariuszy – z obsługą połączeń i bez niej.

Konfiguracja maszyny:

  • 4 CPU
  • 8GB RAM
  • 40 GB SSD
  • Centos 7
  • PostgreSQL 14 (port 5432)
  • PgBouncer (port 6432)

Testy były prowadzone narzędziem pgbench a weryfikowane za pomocą psql i pg_top.

Pierwszy test:

  • liczba klientów: 1 
  • czas: 5 sekund
  • skrypt: TPC-B
PgBouncer pierwszy test
PostgreSQLPgBouncer (transaction)
tps170122,5
średnie opóźnienie5,88 ms8,16 ms
czas inicjacji połączenia15,95 ms4,17 ms

Drugi test:

  • liczba klientów: 10
  • czas: 5 sekund
  • skrypt: TPC-B
PgBouncer drugi test
PostgreSQLPgBouncer (transaction)
tps246,76182,59
średnie opóźnienie40,52 ms54,76 ms
czas inicjacji połączenia171,12 ms30,01 ms

Trzeci test:

  • liczba klientów: 100
  • czas: 5 sekund
  • skrypt: TPC-B
PgBouncer trzeci test
PostgreSQLPgBouncer (transaction)
tps168,89163,72
średnie opóźnienie592,07 ms610,78 ms
czas inicjacji połączenia1722,09 ms299,56 ms

Czwarty test:

  • liczba klientów: 500
  • czas: 5 sekund
  • skrypt: TPC-B
PgBouncer czwarty test
PostgreSQLPgBouncer (transaction)
tps0139,4
średnie opóźnieniend3586,89 ms
czas inicjacji połączeniand1718,43 ms

W powyższym teście widać, że przy tej ilości bezpośrednich połączeń do bazy danych czas oczekiwania był na tyle duży, że pgbench nie zdążył wykonać żadnej operacji w trakcie 5 sekund trwania testu. Kolejny test zakłada już wykonanie określonej liczby operacji, a nie czas działania.

Piąty test:

  • liczba klientów: 500
  • liczba operacji: 50
  • skrypt: TPC-B
PgBouncer piąty test
PostgreSQLPgBouncer (transaction)
tps73,86134,17
średnie opóźnienie6769,80 ms3726,68 ms
czas inicjacji połączenia8187.13 ms1734,29 ms
wykorzystana pamięć2113 MB1253 MB
max. l poł. do bazy50020

W trakcie wykonywania tego testu warto już było zwrócić uwagę na wykorzystanie pamięci. Brałem tu pod uwagę raportowany przez pg_top stan pamięci – zawiera się w nim zarówno zużycie bazy, connection poolera, jak i narzędzia do testów.

Szósty test:

  • liczba klientów: 4000
  • liczba transakcji: 5 sekund
  • skrypt: TPC-B
PgBouncer szósty test
PostgreSQLPgBouncer (transaction)PgBouncer (session)PgBouncer (session)
tps11,3838,0653,448,41
średnie opóźnienie351472,98 ms105073,70 ms74908,722 ms82613,25 ms
czas inicjacji połączenia68986,58 ms15270,04 ms14686,64 ms15489,31 ms
wykorzystana pamięć7702 MB1511 MBb.d.b.d.
max. l poł. do bazy4000100 / 1010 / 4100
max. liczba locków w bazie414 tys1,8 tysb.d.b.d.

Test PgBouncera odbył się przy w ustawieniu przekazywania zapytań w 2 trybach:

  1. transakcji: wszystkie zapytania w obrębie jednej transakcji są kierowane do tego samego połączenia z bazą danych, ale różne transakcje w obrębie tego samego połączenia mogą trafić do różnych połączeń z bazą danych;
  2. sesji: wszystkie zapytania w obrębie połączenia przekazywane są do tego samego połączenia bazy danych.

Najszybszy w tym wypadku okazał się wariant przekazywania sesji z najmniejszą liczbą połączeń do bazy danych.

Siódmy test:

  • liczba klientów: 4000
  • liczba transakcji: 5
  • skrypt: Select
PgBouncer siódmy test
PgBouncer siódmy test
PostgreSQLPgBouncer (transaction)PgBouncer (session)
tps92,893416,252834,39
średnie opóźnienie43063,11 ms1170,87 ms1411,24 ms
czas inicjacji połączenia118704,05 ms14082,64 ms13324,65 ms
wykorzystana pamięć7724 MB1358 MB1250 MB
max. l poł. do bazy4000100 / 10100 / 10
max. liczba locków w bazie000

Ten test został wykonany w 2 wariantach sposobu przekazania połączeń oraz ilości finalnych połączeń do bazy danych.

Podsumowanie

Baza PostgreSQL do obsługi każdego połączenia powołuje osobny proces systemowy. Jak każde rozwiązanie ma ono swoje dobre i złe strony. Minusem takiej sytuacji jest przede wszystkim zwiększenie konsumpcji zasobów w stosunku do modelu opartego o wątki. PgBouncer natomiast jest jednym procesem, który zarządza połączeniami poprzez wątki.

Różnice w wykorzystaniu zasobów

-> Użycie pamięci

Na podstawie testów widać ogromną różnicę w zużyciu pamięci. Różnica zajętości pamięci przy obsłudze 500 połączeń 2113 MB (Postgres) i 1253 MB (PgBouncer) w porównaniu z różnicą zajętości pamięci przy liczbie 4000 połączeń 7702 MB (Postgres) i 1511 MB (PgBouncer) pozwala określić, że sama baza na obsługę jednego połączenia konsumuje ilości pamięci liczone w MB, natomiast PgBouncer w KB.

-> Locki w bazie danych

PgBouncer został tak skonfigurowany, aby jednocześnie obsługiwał 100 połączeń do bazy danych, a co za tym idzie, wykonywał 100 zapytań jednocześnie, a pozostałe czekają w kolejce.

To podejście zaowocowało między innymi powstaniem 230 razy mniejszej ilości blokad (do 1,8 tys.) w bazie danych w porównaniu do obsługiwania jednocześnie 4000 połączeń (do 414 tys.).

Dzięki temu została osiągnięta wydajność  3,5 razy większa przy teście, w którym na jedną transakcję przypada: 1xSELECT, 3xUPDATE, 1xINSERT.

-> Użycie procesora

W sytuacji kiedy mamy 4000 połączeń i 4-ry wątki procesora musi następować podział czasu procesora na ich obsługę. Ten przypadek idealnie obrazuje test numer 7 – seria niewymagających dla bazy zapytań, ale za to wymagająca szybkiego przełączania pomiędzy obsługiwanymi połączeniami.

W wypadku czystego PostgreSQL przełączenie procesora na obsługę kolejnego połączenia zabiera kilka cykli procesora, natomiast w przypadku PgBouncera jest to przełączenie  adresu pamięci.

-> Czas łączenia

Sumaryczny czas inicjalizacji połączenia jest wyraźnie wyższy w scenariuszu bez wykorzystania connection poolera. Spowodowane jest to tym, że dłużej trwa uruchomienie kolejnego procesu w systemie operacyjnym niż wątku w PgBouncerze.

-> Liczba transakcji na sekundę

Wszystkie powyżej wymienione różnice mają bezpośrednie odzwierciedlenie w wydajności bazy danych – TPS, czyli liczbie transakcji na sekundę.

Z testów wynika, że dla mniejszych ilości połączeń (w tym wypadku do 100, ale dla innych systemów ta wartość może być różna) lepsze wyniki baza osiąga bez dodatkowego zarządzania połączeniami. Wraz ze wzrostem ilości jednoczesnych połączeń wzrastają korzyści z zarządzania nimi.

Efekt najbardziej jest widoczny w scenariuszu z szeregiem prostych zapytań typu SELECT, gdzie nie występują blokady w bazie danych – samo zastosowanie PgBadgera spowodowało 36-krotny wzrost wydajności.

Celem artykułu nie było udowodnienie, że PgBouncer jest jedynym słusznym wyborem. Najlepszym rozwiązaniem jest używanie takiego zarządzania połączeniami, które będzie odpowiednie do danej sytuacji.

Czasem będzie to właśnie PgBouncer, w innym wypadku HikariCP lub inne narzędzie. Są też systemy, gdzie wiele różnych aplikacji korzysta z bazy danych i jedyną możliwością zarządzania połączeniami jest to przy bazie danych.

Pamiętajmy, że lepsze wyniki bazy danych w testach nie wynikają z faktu, że connection pooler wykonał za nią jakąś część pracy szybciej, ale z faktu, że było włączone zarządzanie połączeniami – w związku z tym baza danych miała ruch rozłożony w czasie zamiast pojedynczego piku.

Dodatkowo, w przedstawionych testach zastosowana została prosta konfiguracja zarządzania połączeniami – ile połączeń nie przyjdzie ustaw wszystkie w kolejkę i wykonuj partiami po 100 lub 10 transakcji, a możliwości konfiguracji są znacznie większe.

Powyższych wyników nie należy traktować jako wyrocznię, ale jako wskazówkę jak planować testy na własnym systemie.

Zastosowane skrypty:

PgBadgera zastosowane skrypty
Zobacz również