PostgreSQL XID wraparound: núdzový playbook pre vacuum freeze v incidente
Vidíte chybu typu “database is not accepting commands to avoid wraparound data loss” (alebo sa k tomu nebezpečne blížite). Prvá reakcia je pustiť niečo ťažké a dúfať.
Takto si viete zmeniť riešiteľnú situáciu na vlastný výpadok.
Tento playbook je pre 3:00 ráno, keď:
- XID age je vysoké
- autovacuum nestíha
- máte veľké tabuľky
- a potrebujete sa zachrániť bez toho, aby ste zhoršili IO a locking
Testované na: PostgreSQL 15–17 (self-managed + managed ponuky s rovnakou semantikou), multi‑GB až multi‑TB dataset, mix OLTP workloadov.
Incident (anonymizovaný)
SaaS cluster bežal na multi-tenant Postgrese s vysokým write volume. Autovacuum bol nastavený na throughput, ale tím mal dve slepé miesta:
- dlhé “idle in transaction” sessions blokovali vacuum
- veľké append-heavy tabuľky rýchlo “stárli”
Monitoring zachytil rastúce age(datfrozenxid) neskoro. Keď incident začal, “normálny” vacuum nestíhal a hrozilo prekročenie wraparound bezpečnostnej hranice.
Blast radius: produkčná DB riskovala forced read-only / shutdown správanie.
Constraint: minimálne downtime; vyhnúť sa ťažkým lockom; udržať repliku zdravú.
Timeline
- T-0: alert: XID age sa blíži ku kritickému thresholdu.
- T+10m: identifikácia najhorších DB podľa XID age.
- T+20m: identifikácia najhorších tabuliek podľa
age(relfrozenxid). - T+30m: nájdenie long-running tx a idle-in-tx, ktoré blokujú vacuum.
- T+40m: ukončenie blockerov; spustenie cieleného
VACUUM (FREEZE)na najhorších tabuľkách. - T+2h: XID age začína klesať; autovacuum dobieha; incident končí.
- T+1d: guardrails: alerty, timeouty, per-table autovacuum freeze tuning.
Mechanizmus: čo wraparound naozaj je
Postgres prideľuje transaction ID (XID). XID sa časom wrapne. Ak staré row verzie nie sú “frozen”, wraparound spôsobí nejednoznačnosť a riziko nekonzistencie dát.
Prevencia je pravidelný vacuum, ktorý freeze-ne staré tuplíky. Keď sa systém príliš priblíži k wraparound, Postgres zvolí správnosť pred dostupnosťou.
Ak freezing nevie napredovať, Postgres bude chrániť correctness aj za cenu dostupnosti.
Prečo sa vacuum freeze zasekne
Aj keď autovacuum beží, freezing sa vie zastaviť, ak:
- long-running transakcie držia staré
xmin - idle-in-tx drží snapshot otvorený
- vysoký write churn tvorí nekonečný backlog
- IO budget je príliš nízky (autovacuum nestíha)
Runbook: núdzová reakcia
Čo skontrolovať ako prvé (10 min)
1) Ako blízko sme?
SELECT
datname,
age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
Pozrite aj thresholdy:
SELECT
current_setting('autovacuum_freeze_max_age') AS freeze_max_age,
current_setting('autovacuum_freeze_table_age') AS freeze_table_age;
Mentálny model: nie “absolútne číslo”, ale “vzdialenosť k vášmu max”.
2) Najhoršie tabuľky podľa relfrozenxid
SELECT
n.nspname,
c.relname,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
age(c.relfrozenxid) AS xid_age
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','m')
ORDER BY age(c.relfrozenxid) DESC
LIMIT 20;
3) Blokujú nás long-running transakcie?
SELECT
pid,
usename,
state,
now() - xact_start AS xact_age,
wait_event_type,
wait_event,
left(query, 120) AS query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start ASC
LIMIT 20;
“idle in transaction” s veľkým xact_age je typický blocker.
Ako potvrdiť hypotézu
A. Vacuum progress
SELECT
pid,
datname,
relid::regclass AS relation,
phase,
heap_blks_total,
heap_blks_scanned,
heap_blks_vacuumed,
index_vacuum_count
FROM pg_stat_progress_vacuum;
Ak progress stojí a máte long transactions, pravdepodobne máte snapshot blocker.
Bezpečné mitigácie (v poradí)
- Zastaviť blockerov
- ukončiť idle-in-tx sessions
- potom ukončiť skutočne runaway long tx (opatrne)
SELECT pg_terminate_backend(<pid>);
- Cielené
VACUUM (FREEZE)na najhoršie tabuľky
VACUUM (FREEZE, VERBOSE) public.big_table;
- Dočasne zvýšiť autovacuum kapacitu
- viac workerov
- rozumné zvýšenie maintenance memory
- uistiť sa, že disk/IO to dá
vacuumdbparalelne (off-peak)
vacuumdb --all --freeze --jobs=4
Rizikové mitigácie
VACUUM FULL
- silnejšie locky, rewrite tabuľky → často horšia dostupnosť
- Vypnúť autovacuum
- prehráte preteky
Čo sme zmenili (konkrétne)
1) Timeouty proti snapshot blockerom
Na úrovni databázy:
ALTER DATABASE appdb SET idle_in_transaction_session_timeout = '60s';
ALTER DATABASE appdb SET statement_timeout = '30s';
Hodnoty prispôsobte workloadu; pointa je: žiadne “idle in transaction navždy”.
2) Autovacuum tuning na freeze debt
Diff (ilustratívny postgresql.conf):
-autovacuum_max_workers = 3
-autovacuum_naptime = 60s
+autovacuum_max_workers = 6
+autovacuum_naptime = 20s
Per-table nastavenia pre najhoršie append-heavy tabuľky:
ALTER TABLE public.big_table SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.05
);
Per-table tuning je často bezpečnejší než agresívny cluster-wide tuning.
3) Wraparound alerty ako SLO
Alertujeme:
max(age(relfrozenxid))pre top tabuľkyage(datfrozenxid)per DB- idle-in-transaction sessions staršie než X
Ako verifikovať (merateľné)
- XID age klesá:
SELECT datname, age(datfrozenxid) FROM pg_database ORDER BY 2 DESC;
- Najhoršie tabuľky sa zlepšujú:
SELECT relname, age(relfrozenxid) FROM pg_class WHERE relkind='r' ORDER BY 2 DESC LIMIT 10;
- Vacuum progress ide dopredu:
SELECT * FROM pg_stat_progress_vacuum;
- Dopad na aplikáciu je kontrolovaný:
- p95 DB latencia v akceptovateľnom pásme
- replication lag bounded (ak relevantné)
Prevencia / guardrails
Kontrakty
- Žiadny “idle in transaction” dlhší než X sekúnd.
- Wraparound budget: alert, keď XID age spotrebuje > Y% z vášho freeze max.
- Veľké tabuľky musia mať explicitne reviewnuté autovacuum nastavenia.
Checklist alertov
age(datfrozenxid)rastie príliš rýchlo- najstaršie
relfrozenxidnad thresholdom - idle-in-tx sessions nad limitom (počet + vek)
- vacuum nepostupuje (stuck phase)
Súvisiace čítanie
- PostgreSQL Autovacuum SLO Tuning: Ako nastaviť vacuum pre 200M riadkov a 5k UPSERT/s
- PostgreSQL Idle in Transaction: Núdzový Playbook pre Zaseknuté Spojenia
- PostgreSQL checkpoint špičky: prečo p99 exploduje každých N minút
- PostgreSQL OOM by Design: work_mem × Parallel Workers × Plan Nodes
- Logical Replication Slot WAL Bloat: Keď Subscribery Odídu Offline
- PostgreSQL Replication Slot Bloat: Ako Jeden Neaktívny Slot Naplnil 500GB Disk
- Zero-downtime migrácie PostgreSQL: Expand/Contract, backfill a rollback stratégie
Súvisiace články
Pasca hot_standby_feedback: ako opravíte repliku a pomaly zabijete primár
hot_standby_feedback zastaví rušenie query na replike, ale vie nafúknuť primár v dňoch. Diagnostika xmin pinningu, bezpečné mitigácie a guardrails.
PostgreSQL logical replication lag: veľké transakcie a reorder buffer spilly
Jedna obrovská transakcia vie pripnúť logical replication na hodiny. Runbook na rýchlu identifikáciu, bezpečné tunenie decodingu a kontrakt na bounded transakcie.
Pod zaseknutý v Terminating: produkčný rozhodovací strom pre finalizery, volume a mŕtve nody
Konzervatívny runbook na bezpečné odblokovanie Terminating Podov: finalizery, CSI/volume cleanup, mŕtve nody a kedy (a ako) použiť force delete.
Redis AOF fsync latency špičky: keď sa durabilita stane tvojím p99
Redis AOF vie spraviť z durability p99 špičky: fsync tlak a BGREWRITEAOF fork CoW. Runbook na dôkaz, bezpečné mitigácie a guardrails.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.