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
PostgreSQL | PgBouncer (transaction) | |
tps | 170 | 122,5 |
średnie opóźnienie | 5,88 ms | 8,16 ms |
czas inicjacji połączenia | 15,95 ms | 4,17 ms |
Drugi test:
- liczba klientów: 10
- czas: 5 sekund
- skrypt: TPC-B
PostgreSQL | PgBouncer (transaction) | |
tps | 246,76 | 182,59 |
średnie opóźnienie | 40,52 ms | 54,76 ms |
czas inicjacji połączenia | 171,12 ms | 30,01 ms |
Trzeci test:
- liczba klientów: 100
- czas: 5 sekund
- skrypt: TPC-B
PostgreSQL | PgBouncer (transaction) | |
tps | 168,89 | 163,72 |
średnie opóźnienie | 592,07 ms | 610,78 ms |
czas inicjacji połączenia | 1722,09 ms | 299,56 ms |
Czwarty test:
- liczba klientów: 500
- czas: 5 sekund
- skrypt: TPC-B
PostgreSQL | PgBouncer (transaction) | |
tps | 0 | 139,4 |
średnie opóźnienie | nd | 3586,89 ms |
czas inicjacji połączenia | nd | 1718,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
PostgreSQL | PgBouncer (transaction) | |
tps | 73,86 | 134,17 |
średnie opóźnienie | 6769,80 ms | 3726,68 ms |
czas inicjacji połączenia | 8187.13 ms | 1734,29 ms |
wykorzystana pamięć | 2113 MB | 1253 MB |
max. l poł. do bazy | 500 | 20 |
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
PostgreSQL | PgBouncer (transaction) | PgBouncer (session) | PgBouncer (session) | |
tps | 11,38 | 38,06 | 53,4 | 48,41 |
średnie opóźnienie | 351472,98 ms | 105073,70 ms | 74908,722 ms | 82613,25 ms |
czas inicjacji połączenia | 68986,58 ms | 15270,04 ms | 14686,64 ms | 15489,31 ms |
wykorzystana pamięć | 7702 MB | 1511 MB | b.d. | b.d. |
max. l poł. do bazy | 4000 | 100 / 10 | 10 / 4 | 100 |
max. liczba locków w bazie | 414 tys | 1,8 tys | b.d. | b.d. |
Test PgBouncera odbył się przy w ustawieniu przekazywania zapytań w 2 trybach:
- 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;
- 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
PostgreSQL | PgBouncer (transaction) | PgBouncer (session) | |
tps | 92,89 | 3416,25 | 2834,39 |
średnie opóźnienie | 43063,11 ms | 1170,87 ms | 1411,24 ms |
czas inicjacji połączenia | 118704,05 ms | 14082,64 ms | 13324,65 ms |
wykorzystana pamięć | 7724 MB | 1358 MB | 1250 MB |
max. l poł. do bazy | 4000 | 100 / 10 | 100 / 10 |
max. liczba locków w bazie | 0 | 0 | 0 |
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: