PostgreSQL: pgBouncer: pooling mode i problemy z ustawieniami sesji

9-Cze-2022

Był sobie użytkownik. Ten użytkownik łączył się do swojej bazy danych w PostgreSQL korzystając z pgBouncera. Tyle dobrego mówi się o zarządzaniu połączeniami przez pgBouncera, że hohoho, więc użytkownik robił dobrze, że robił tak jak robił.

Pojawił się jednak problem… czasami ni z tego ni z owego pojawiał się komunikat mówiący o tym, że zapytanie wykonywane na serwerze zostało anulowane z powodu przekroczenia statement_timeout. Nie byłoby w tym nic dziwnego, gdyby nie to, że użytkownik przysięgał, że nie ustawiał u siebie statement_timout. Kto by mu tam wierzył!?

Czasami z kolei, użytkownik narzekał, że zmienił mu się parametr search_path. Ten parametr pozwala ustalić jakie schematy mają być przeszukiwane, jeśli użytkownik napisze zapytanie do tabeli, nie podając jawnie, w jakim schemacie ta tabela się znajduje. Dlatego użytkownik od czasu do czasu dostawał błąd „nie ma takiej tabeli” i się strasznie denerwował. Oczywiście przysięgał, że sam tego parameteru nigdy nie zmieniał, ale …. kto by mu tam wierzył!?

Na dodatek, podobno, jeśli użytkownik łączył się bezpośrednio do serwera, a nie przez pgBouncer, to problemu nie było. Hmmm ciekawe, co?

O co chodzi?

PgBouncer może pracować w różnych trybach zarządzania połączeniami (connection pooling). Dwa najpopularniejsze to SESSION i TRANSACTION.

W SESSION, użytkownik otwiera połączenie do pgBouncer, a pgBouncer łączy się do PostgreSQL. Połączenie jest otwarte tak długo, jak długo trwa sesja użytkownika, co daje mniejsze oszczędności zasobów po stronie PostgreSQL. Za to, kiedy połączenie się kończy, to jest wykonywany reset ustawień takiej sesji. Wszystkie jej opcje jak np. wspomniany statement_timeout czy search_path wracają do pierwotnych ustawień. Odpowiada za to parametr pgBouncera o nazwie server_reset_query.  Domyślna jego wartość to DISCARD_ALL i ta nazwa właściwie mówi wszystko sama za siebie. Wprawdzie otwarte sesje pozostaną otwarte dość długo, ale jak już zostaną zwolnione, to pgBouncer nie musi ich na nowo budować, więc jednak jakaś oszczędność zasobów jest.

W TRANSACTION, użytkownik otwiera połączenie do pgBouncer, a pgBouncer łączy się do PostgreSQL. Połączenie jest otwarte między pgBouncerem a PostgreSQLem dłużej, ale użytkownik dostaje je do dyspozycji tylko na czas działania jego transakcji. Transakcje powinny być krótkie, więc jedno połączenie pomiędzy PostgreSQL, a pgBouncerem może być wykorzystywane wiele razy. Niestety, podczas przekazywania połączeń między użytkownikami, nie jest odświeżane środowisko sesji, czyli „dziedziczy się” ustawnienia takie jak statement_timeout, czy search_path .

No i wiesz już pewnie jakie ustawienie miał mój nieszczęsny użytkownik? Tak – TRANSACTION. Opcje ustawione w jednej sesji, zaczynały działać w innej i stąd cały problem, Na nieco zmodyfikowanych ustawieniach pgBouncera (tylko 1 połączenie per 1 użytkownik per 1 baza danych w modelu TRANSACTION wyglądało to mniej więcej tak:

1 – w skutek bardzo oszczędnych ustawień pgBouncera, obaj użytkownicy otrzymali ten sam backend id. Mamy 2 połączenia do pgbouncera i jedno między pgBouncerem i PostgreSQLem

2 – użytkownik po lewej stronie ma statement_timeout równe zero

3 – użytkownik po prawej stronie zmiena statement_timeout na 10 sekund

4 – zmiana jest widoczna dla użytkownika po lewej stronie

5 – bo obaj korzystają z tej samej sesji serwerowej, a sesja ta nie podlega resetowi podczas przekazywania między użytkownikami

Dodajmy, że w modelu TRANSACTION też można wymuszać resetowanie połączenia po jego oddawaniu do pooli dostępnych połączeń, ale trzeba wtedy ustawić parametr pgBouncera o nazwie server_reset_query_always. Ceną za ten parametr będzie nieco więcej czasu podczas przekazywania otwartego połączenia z rąk do rąk, ale przynajmniej to co się dostanie będzie czyste – powiedziałbym, że warto 🙂

Polecam wpis depesz-a:

https://www.depesz.com/2012/12/02/what-is-the-point-of-bouncing/

oraz dokumentację opcji pgBouncer:

PgBouncer config

Dodaj komentarz:

Autor: Rafał Kraik