PostgreSQL — Transaction ID Exhaustion (Wraparound)

TL;TR O co chodzi i co trzeba zrobić jeśli zobaczysz
autovacuum: VACUUM public.x (to prevent wraparound)
Historia jak wiele innych, zaplanowana przez deweloperów poranna wrzuta przeciąga się w nieskończoność. Większość operacji na bazie trwa niemiłosiernie długo. Ktoś wpadł na pomysł, żeby ubić proces autovacuum i wyłączyć je całkowicie. Niestety, proces “VACUUM (to prevent wraparound)” pojawia się ponownie, co z tym zrobić ?
Kilka słów wstępu, PostgreSQL używa mechanizmu zwanego MVCC ( MultiVersion Concurrency Control ), dzięki któremu kontroluje które transakcje są widoczne dla którego użytkownika. Wyobraźmy sobie sytuacje, w której USER 1, wykonuje polecenie “BEGIN, update ….. “, jego transakcja jeszcze się nie zakończyła, a już USER2 wywołuje to samo polecenie tylko z innymi parametrami. Skad PostgreSQL wie, które dane są poprawne dla którego użytkownika ? Właśnie dzięki MVCC. O MVCC można by napisać całą książkę, dlatego jeden przykład. Na dowolnej tabeli w twojej bazie wywołaj
SELECT x,y, xmin, xmax from tabela;
id | name | xmin | xmax
----+------------------+------------+------
15 |Jakies dane | 1880828026 | 0
xmin i xmax to są właśnie wartości wykorzystywane przez MVCC. Ilość ID transakcji jest ograniczona, wynosi 32 bity. Jeśli dalej jesteś ciekaw, polecam zapoznać się z tqual.c w kodzie PostgreSQL.
Wracając do naszego problemu, co możemy zrobić ? Za to, kiedy uruchomi się autovacuum(to prevent wraparound) odpowiada zmienna autovacuum_freeze_max_age, zmiana tego parametru wymaga restartu instancji. W opisanej sytuacji, zmieniłem wartość z 200.000.000 na 300.000.000. Co pozwoliło dokończyć rozpoczętą już wrzutę. Następnego dnia, uruchomiłem vacuum i przywróciłem poprzednie wartości.
Jako rozwiązanie docelowe polecam regularne uruchamianie vacuumdb z opcją freeze.
vacuumdb --all --freeze --jobs=2 --echo --analyze
Poniżej znajdziecie kilka przydatnych zapytań, które można dodać do monitoringu i pomogą wam wcześniej reagować.
SELECT datname
, age(datfrozenxid)
, current_setting('autovacuum_freeze_max_age')
FROM pg_database
ORDER BY 2 DESC;
Tutaj trochę więcej szczegółów, wyniki są ograniczone do bazy w której obecnie jesteśmy.
SELECT c.oid::regclass
, age(c.relfrozenxid)
, pg_size_pretty(pg_total_relation_size(c.oid))
FROM pg_class c
JOIN pg_namespace n on c.relnamespace = n.oid
WHERE relkind IN ('r', 't', 'm')
AND n.nspname NOT IN ('pg_toast')
ORDER BY 2 DESC LIMIT 100;
Do swojego monitoringu dorzuciłem zapytanie, które pokazuje procentową najwyższa wartość , do tego alert przy 80% i można spać spokojnie.
SELECT round((age(datfrozenxid)::float *100.0 ) / current_setting('autovacuum_freeze_max_age')::float) FROM pg_database ORDER BY 1 DESC limit 1;

Tomasz Gintowt jest Architektem/DevOps/DBA, głównie skupiony na dostarczaniu rozwiązań składowania i przetwarzania danych. Nie są mu obce wszelkiej maści bazy danych, systemy real-time data i streamingu. Obecnie pracuje z Apache Kafka, RabbitMQ, Elastic Stack i PostgreSQL. Prywatnie fan ciasta marchewkowego.