Pasca hot_standby_feedback: ako opravíte repliku a pomaly zabijete primár
Toto som videl ako “pomalý výpadok”:
- Read replica ruší query kvôli recovery konfliktom.
- Niekto zapne
hot_standby_feedback=onako fix. - Rušenie query prestane. Všetci sú spokojní.
- O týždeň je primár pomalší, disk rastie, autovacuum nestíha.
- Príde latency incident, ktorý vyzerá nesúvisiaco… ale nie je.
Toto je runbook na túto pascu: ako dokázať, že hot_standby_feedback vám drží xmin horizon, prečo to vytvára bloat a čo v produkcii mením tak, aby replika bola užitočná bez toho, aby ticho ničila primár.
Testované na: PostgreSQL 15–17, physical streaming replication, mix OLTP + “niekto pustil analytiku na replike”.
Incident (anonymizovaný)
Mali sme vyťažený primár a jednu repliku na dashboard query. Počas peaku replika často rušila dlhšie query (recovery conflicts). Prišiel “fix”:
hot_standby_feedback = on
Rušenie query prestalo, ale ~10 dní neskôr:
- disk na primári rástol
- index bloat sa zväčšoval
- p95 latencia query na primári driftovala hore
- autovacuum bežal skoro stále, ale nikdy “nevyhrával”
Blast radius: degradácia primára, paging na latenciu (nie na replikáciu).
Constraint: nemohli sme zo dňa na deň zakázať analytické query. Potrebovali sme plán, ktorý udrží read workload funkčný bez toho, aby primár skončil ako bloat farma.
Timeline
- T-0: sťažnosti na rušenie query na replike.
- T+1h:
hot_standby_feedback=on; conflicts padnú. - T+7d: primár disk trenduje hore; vacuum aktivita rastie.
- T+10d: latency incident na primári; autovacuum je pozadu.
- T+11d: korelujeme bloat s
backend_xmindržaným replikou. - T+12d: mitigácia: vypnúť feedback na “serving” replike, vynútiť timeouts, pridať dedikovanú analytics repliku s guardrails.
- T+14d: bloat prestane rásť; latencia primára sa zotaví.
Mechanizmus: prečo hot_standby_feedback vytvára bloat
Konflikty vznikajú, lebo vacuum chce čistiť a replika chce snapshot
Standby vykonáva query nad snapshotom, zatiaľ čo replayuje WAL. Replay niekedy potrebuje odstrániť row verzie/locky, ktoré query ešte potrebuje → konflikt → standby zruší query (alebo odloží replay).
hot_standby_feedback otočí tradeoff
Keď je hot_standby_feedback zapnutý, replika posiela primáru xmin horizon. Primár potom vynechá vacuum cleanup, ktorý by rozbil snapshot na replike.
To zníži rušenie query, ale primár platí:
- dead tuples žijú dlhšie
- indexy držia referencie dlhšie
- autovacuum nedokáže reclaimovať priestor
- bloat rastie
Pri dlhých query alebo vysokom churne je to stabilný “bloat pump”.
Runbook: diagnostika xmin pinningu a bloat tlaku
Čo skontrolovať ako prvé
1) Ruší replika query kvôli konfliktom?
Na replike:
SELECT
datname,
confl_snapshot,
confl_lock,
confl_bufferpin,
confl_deadlock
FROM pg_stat_database_conflicts
ORDER BY (confl_snapshot + confl_lock + confl_bufferpin + confl_deadlock) DESC;
Ak rastie confl_snapshot, problém sú dlhé snapshoty.
2) Drží replika primár späť?
Na primári:
SELECT
application_name,
state,
sync_state,
write_lag,
flush_lag,
replay_lag,
backend_xmin
FROM pg_stat_replication
ORDER BY application_name;
Ak je backend_xmin non-null a dlhodobo “starý”, replika pinne cleanup.
3) Rastú dead tuples a veľkosť tabuliek?
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
round(100.0 * n_dead_tup / (n_live_tup + n_dead_tup + 1), 2) AS dead_pct
FROM pg_stat_all_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
A veľkosti:
SELECT
n.nspname,
c.relname,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
Ako potvrdiť hypotézu
“Smoking gun” pattern:
- dlhé query na replike
hot_standby_feedback=onbackend_xminje držané replikou- dead tuples rastú a vacuum ich nevie zraziť
- primárna latencia driftuje hore
Ak sedí všetko, nehádate. Platíte za feedback.
Mitigácie: bezpečné vs rizikové
Bezpečné mitigácie
- Timeouty na replike Dashboardy zvyčajne nepotrebujú 30-minútové query.
ALTER DATABASE appdb SET statement_timeout = '30s';
-
Vypnúť feedback na replike, ktorá nesmie škodiť primáru
hot_standby_feedbackvypnúť na “serving” replike. -
Dedikovaná analytics replika (voliteľné) Ak potrebujete dlhé query, dajte tomu vlastnú repliku a cenu budgetujte.
-
Použiť
max_standby_streaming_delayvedome Radšej povoliť replike trochu zaostávať, než pinovať primár donekonečna (bolesť sa presunie do staleness).
Rizikové mitigácie
- zapnúť
hot_standby_feedbackna všetkých replikách VACUUM FULLpočas peaku (locky + rewrite)- agresívny manuálny vacuum “všade” (IO a latency spikes)
Čo sme zmenili (konkrétne)
1) Prestali sme sa tváriť, že replika je “free analytika”
Rozdelili sme repliky podľa roly:
- Replika A (serving reads, bezpečná pre primár): feedback off, strict timeouts
- Replika B (analytics): feedback on, ale guardrails + očakávaná cena
2) Konfig zmeny (ilustratívne)
Replika A:
hot_standby_feedback = off
statement_timeout = 30s
Replika B (analytics):
hot_standby_feedback = on
statement_timeout = 2min
3) Autovacuum tuning na najhoršie tabuľky
Per-table tuning (nie globálne kladivo):
ALTER TABLE public.events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 5000
);
Ako verifikovať
backend_xminuž nie je pinovaný Na primári:
SELECT application_name, backend_xmin FROM pg_stat_replication;
Očakávanie:
- Replika A už nedrží xmin
- Replika B môže držať, ale vedome a budgetovane
-
Dead tuples klesajú (alebo aspoň prestanú explodovať) Sledujte
pg_stat_all_tablesv čase. -
Primárna latencia sa zotaví
- p95 query time klesne
- autovacuum vie reclaimovať priestor
- Konflikty na replike sa vrátia, ale v rámci politiky Občasné zrušenie dlhého query na Replike A je OK. Alternatíva je degradovať primár.
Prevencia / guardrails
- Kontrakt roly repliky
- serving replika nesmie dlho pinovať xmin
- analytics replika musí mať explicitné budgety + timeouts
- Bloat budget
- alert na dead tuple ratio pre top tabuľky
- alert na rýchly rast tabuliek/indexov
- Conflict budget
- rušenie query je akceptovateľné do N/deň na serving replike
- Runbook
- “Ak rastú konflikty: skracuj query, nezapínaj feedback naslepo”
Súvisiace čítanie
- PostgreSQL Read Replica Konflikty: Prečo sa vaše dotazy rušia
- 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 Replication Slot Bloat: Ako Jeden Neaktívny Slot Naplnil 500GB Disk
- Logical Replication Slot WAL Bloat: Keď Subscribery Odídu Offline
- PostgreSQL HOT Updates + FILLFACTOR: Ako Znížiť Index Bloat o 60%
- PostgreSQL checkpoint špičky: prečo p99 exploduje každých N minút
Súvisiace články
PostgreSQL XID wraparound: núdzový playbook pre vacuum freeze v incidente
PostgreSQL môže prejsť do read-only pri XID wraparound. Núdzový playbook: nájsť najstaršie tabuľky, odblokovať vacuum freeze a prevencia do budúcna.
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.
Logical Replication Slot WAL Bloat: Keď Subscribery Odídu Offline
Disk sa plní WAL súbormi. Príčina: logical replication slot consumer odišiel offline a PostgreSQL drží všetok WAL odvtedy pretože by mohol byť potrebný.
PostgreSQL Autovacuum SLO Tuning: Ako nastaviť vacuum pre 200M riadkov a 5k UPSERT/s
Autovacuum je buď ignorovaný alebo cargo-cult tunovaný. Ukážem ako ho premeniť na SLO-driven systém s konkrétnymi číslami, pg_stat metriky a reprodukovateľným testom.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.