Späť na blog

PostgreSQL Read Replica Konflikty: Prečo sa vaše dotazy rušia

“ERROR: canceling statement due to conflict with recovery” - ak prevádzkujete read repliky v PostgreSQL, toto ste už videli. Dotaz bol v poriadku. Replika je zdravá. Ale PostgreSQL zrušil váš dotaz, pretože WAL replay potreboval niečo, čo váš dotaz používal.

Frustrujúce je, že chybová správa nehovorí ktorý typ konfliktu to spôsobil. Existuje päť rôznych typov konfliktov, každý s inými príčinami a riešeniami. Slepé nastavenie max_standby_streaming_delay = -1 (nekonečno) môže vyriešiť váš okamžitý problém, ale vytvorí oveľa horší.

Prostredie: PostgreSQL 12+, streaming replikácia, read repliky obsluhujúce produkčné dotazy

Päť typov konfliktov

PostgreSQL sleduje konflikty v pg_stat_database_conflicts. Toto je váš diagnostický východiskový bod:

SELECT datname,
       confl_tablespace,
       confl_lock,
       confl_snapshot,
       confl_bufferpin,
       confl_deadlock
FROM pg_stat_database_conflicts
WHERE datname = current_database();

Každý stĺpec reprezentuje iný typ konfliktu:

┌─────────────────────┬─────────────────────────────────────────────────────┐
│ Typ konfliktu       │ Čo sa deje                                          │
├─────────────────────┼─────────────────────────────────────────────────────┤
│ confl_snapshot      │ VACUUM odstránil riadky, ktoré váš dotaz potrebuje  │
│ confl_lock          │ Exkluzívny zámok potrebný pre WAL replay            │
│ confl_bufferpin     │ WAL replay potrebuje buffer, ktorý používa dotaz    │
│ confl_tablespace    │ Tablespace zmazaný počas prístupu dotazu            │
│ confl_deadlock      │ Deadlock medzi dotazom a recovery procesom          │
└─────────────────────┴─────────────────────────────────────────────────────┘

V 90% prípadov je to confl_snapshot. Ale skontrolujte svoje čísla - riešenie pre každý typ je iné.

Typ konfliktu 1: Snapshot konflikty (confl_snapshot)

Toto je najčastejšie. Tu je čo sa deje:

Časová os:

Primary:
T+0:00   Transakcia vkladá riadok (id=100)
T+0:05   Transakcia commitne
T+5:00   VACUUM beží, odstraňuje mŕtve verzie riadkov
T+5:01   WAL záznam odoslaný: "odstráň riadky zo stránky X"

Replika:
T+0:00   Dotaz štartuje: SELECT * FROM big_table (trvá 10 minút)
         Dotaz má snapshot z T+0:00
T+5:02   WAL replay chce aplikovať VACUUM zmeny
         Ale dotaz stále potrebuje tie verzie riadkov!
         → confl_snapshot, dotaz zrušený

Replika nemôže aplikovať WAL záznam VACUUM-u, pretože váš dlho-bežiaci dotaz drží snapshot, ktorý referencuje riadky, ktoré VACUUM chce odstrániť.

Riešenie: hot_standby_feedback

-- Na replike, v postgresql.conf
hot_standby_feedback = on

Toto hovorí primary “nevacuumuj riadky, ktoré moje dotazy stále potrebujú.” VACUUM na primary preskočí riadky, ktoré akákoľvek replika stále používa.

Kompromis: Nafukovanie tabuliek na primary. Ak replika beží 6-hodinový dotaz, primary nemôže vyčistiť žiadne mŕtve riadky 6 hodín. Monitorujte pg_stat_user_tables.n_dead_tup.

-- Monitorovanie nafukovania na primary
SELECT schemaname, relname, n_live_tup, n_dead_tup,
       round(n_dead_tup::numeric / nullif(n_live_tup, 0) * 100, 2) as dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;

Alternatíva: max_standby_streaming_delay

-- Na replike
max_standby_streaming_delay = 300s  -- Počkaj až 5 minút pred zrušením

Toto odloží WAL replay namiesto zrušenia dotazov. Nastavenie na -1 znamená “čakaj navždy” - ale potom vaša replika zaostáva neobmedzene počas dlhých dotazov.

Rozhodovací strom pre Snapshot konflikty

Je trvanie dotazu predvídateľné?
├─ Áno, dotazy < 5 minút
│   └─ Nastav max_standby_streaming_delay = 300s
│      (Akceptovateľný replikačný lag)

├─ Áno, ale niektoré dotazy bežia hodiny
│   └─ hot_standby_feedback = on
│      + Monitoruj nafukovanie primary
│      + Najprv zvážiť optimalizáciu dotazov

└─ Nie, nepredvídateľné ad-hoc dotazy
    └─ hot_standby_feedback = on
       + Nastav statement_timeout na replike
       + Akceptuj určité nafukovanie primary

Typ konfliktu 2: Lock konflikty (confl_lock)

Menej časté, ale zložitejšie. Toto sa stane, keď WAL replay potrebuje exkluzívny zámok, ktorý váš dotaz blokuje.

Primary:
T+0:00   ALTER TABLE users ADD COLUMN email_verified boolean;
         (Získa AccessExclusiveLock, odošle vo WAL)

Replika:
T+0:00   Dotaz: SELECT * FROM users WHERE ... (drží AccessShareLock)
T+0:01   WAL replay chce aplikovať ALTER TABLE
         Potrebuje AccessExclusiveLock - blokovaný dotazom
         Po max_standby_streaming_delay → dotaz zrušený

Riešenie: Vyhnite sa DDL počas špičky

Lock konflikty pochádzajú z DDL operácií (ALTER TABLE, dokončenie CREATE INDEX CONCURRENTLY, atď.). Replika nemá inú možnosť ako čakať alebo zrušiť.

-- Skontrolujte aké zámky recovery čaká
SELECT relation::regclass, mode, granted, pid
FROM pg_locks
WHERE NOT granted
  AND pid = (SELECT pid FROM pg_stat_activity WHERE backend_type = 'startup');

Best practice: Spúšťajte DDL počas nízko-záťažových období. Neexistuje konfigurácia, ktorá toto magicky vyrieši bez zrušenia dotazov alebo oneskorenia replikácie.

Typ konfliktu 3: Buffer Pin konflikty (confl_bufferpin)

Toto sa stane, keď WAL replay potrebuje modifikovať stránku bufferu, ktorú dotaz aktívne číta:

Replika:
Dotaz skenuje stránku 12345 tabuľky X (buffer pripnutý)
WAL replay: "Potrebujem modifikovať stránku 12345"
→ Nemôže pokračovať kým je buffer pripnutý
→ Po timeoutu, zruší dotaz

Riešenie: max_standby_streaming_delay

Buffer pin konflikty sa vyriešia rýchlo - zvyčajne v milisekundách. Mierne oneskorenie ich zvládne:

max_standby_streaming_delay = 30s

Ak vidíte vysoký confl_bufferpin, príčinou je zvyčajne extrémne zápis-intenzívna záťaž na primary + dlhé sekvenčné skeny na replike.

Typ konfliktu 4: Tablespace konflikty (confl_tablespace)

Zriedkavé. Stáva sa len keď spustíte DROP TABLESPACE kým dotaz pristupuje k objektom v ňom.

-- Nerobte toto počas produkčných hodín
DROP TABLESPACE old_tablespace;

Riešenie: Nemažte tablespace-y kým bežia dotazy. Koordinujte údržbové okná.

Typ konfliktu 5: Deadlock konflikty (confl_deadlock)

Extrémne zriedkavé. Stáva sa keď recovery a dotaz vytvoria deadlock cyklus. PostgreSQL ho vyrieši zrušením dotazu.

Ak tieto vidíte, deje sa niečo neobvyklé. Skontrolujte:

  • Extrémne dlho-bežiace dotazy držiace veľa bufferov
  • Veľmi vysokú rýchlosť zápisov na primary
  • Zle nakonfigurovaný max_standby_streaming_delay

Pasca hot_standby_feedback

hot_standby_feedback = on vyzerá ako perfektné riešenie. Ale má nepríjemný vedľajší efekt, ktorý dokumentácia podceňuje:

Scenár: Replika má únikový dotaz

T+0:00   Dotaz na replike štartuje (možno zabudnutá psql session)
T+1:00   Dotaz stále beží... primary nemôže VACUUM-ovať
T+2:00   Dotaz stále beží... mŕtve tuples sa hromadia
...
T+24:00  Niekto si všimne, že primary má 500GB nafúknutia

-- Dotaz repliky zabránil VŠETKÉMU čisteniu na primary
-- Dokonca aj pre tabuľky, ktorých sa dotaz nedotýkal!

Zmiernenie:

-- Na replike, nastavte agresívny statement timeout
SET statement_timeout = '30min';

-- Alebo per-user
ALTER ROLE reporting_user SET statement_timeout = '30min';

Vždy párujte hot_standby_feedback = on so statement_timeout.

Špecifiká manažovaného PostgreSQL

AWS RDS

hot_standby_feedback: Nekonfigurovateľné na read replikách (predvolene off)
max_standby_streaming_delay: Konfigurovateľné cez parameter group

-- RDS workaround: Zvýšte delay
max_standby_streaming_delay = 300

RDS nevystavuje hot_standby_feedback, pretože Amazon nechce, aby zákaznícke repliky nafukovali primary. Ste limitovaní na ladenie max_standby_streaming_delay.

Google Cloud SQL

hot_standby_feedback: Konfigurovateľné cez flagy
max_standby_streaming_delay: Konfigurovateľné cez flagy

-- Cloud SQL povoľuje oboje, ale varuje pred nafukovaním

Azure Database for PostgreSQL

hot_standby_feedback: Konfigurovateľné
max_standby_streaming_delay: Konfigurovateľné

-- Flexible Server dáva plnú kontrolu

Monitorovanie a alertovanie

-- Vytvorte view pre monitorovanie konfliktov
CREATE VIEW replica_conflicts AS
SELECT datname,
       confl_snapshot + confl_lock + confl_bufferpin +
       confl_tablespace + confl_deadlock as total_conflicts,
       confl_snapshot,
       confl_lock,
       confl_bufferpin,
       confl_tablespace,
       confl_deadlock
FROM pg_stat_database_conflicts
WHERE datname = current_database();
# Prometheus alert
groups:
  - name: postgresql-replica
    rules:
      - alert: HighReplicaConflicts
        expr: |
          rate(pg_stat_database_conflicts_confl_snapshot[5m]) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Vysoká miera snapshot konfliktov na replike"

      - alert: ReplicaLagDuringConflict
        expr: |
          pg_replication_lag_seconds > 300
          and rate(pg_stat_database_conflicts_confl_snapshot[5m]) > 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Replika zaostáva kvôli konfliktom dotazov"

Skutočné riešenie: Optimalizácia dotazov

Pred ladením replikačných parametrov sa pýtajte: prečo dotazy bežia tak dlho?

-- Nájdite dlho-bežiace dotazy na replike
SELECT pid, now() - query_start as duration, query
FROM pg_stat_activity
WHERE state = 'active'
  AND query_start < now() - interval '1 minute'
ORDER BY query_start;

Dotaz, ktorý beží 30 minút, môže bežať 30 sekúnd so správnym indexovaním. Opravte dotaz a problém konfliktov zmizne.

-- Identifikujte chýbajúce indexy
SELECT schemaname, relname, seq_scan, seq_tup_read,
       idx_scan, seq_tup_read / nullif(seq_scan, 0) as avg_seq_tup
FROM pg_stat_user_tables
WHERE seq_scan > 100
  AND seq_tup_read / nullif(seq_scan, 0) > 10000
ORDER BY seq_tup_read DESC;

Checklist

## Riešenie PostgreSQL Replica konfliktov

### Diagnostika
- [ ] Skontrolujte pg_stat_database_conflicts pre distribúciu typov konfliktov
- [ ] Identifikujte dlho-bežiace dotazy na replike
- [ ] Monitorujte replikačný lag počas konfliktov
- [ ] Skontrolujte nafukovanie tabuliek na primary (n_dead_tup)

### Rýchle riešenia
- [ ] Zvýšte max_standby_streaming_delay (ak akceptovateľný lag)
- [ ] Povoľte hot_standby_feedback (ak akceptovateľné nafukovanie)
- [ ] Nastavte statement_timeout na replike (prevencia únikových dotazov)

### Koreňová príčina
- [ ] Optimalizujte pomalé dotazy (indexy, prepísanie dotazov)
- [ ] Naplánujte ťažkú analytiku počas mimo-špičky
- [ ] Zvážte dedikovanú analytickú repliku s inými nastaveniami

### Monitorovanie
- [ ] Alert na nárast miery konfliktov
- [ ] Alert na replikačný lag
- [ ] Alert na nafukovanie primary (mŕtve tuples)

Záver

“Canceling statement due to conflict with recovery” znamená, že PostgreSQL robí svoju prácu - chráni konzistenciu replikácie. Päť typov konfliktov má každý iné príčiny:

  1. Snapshot konflikty (najčastejšie): VACUUM vs dlhé dotazy → hot_standby_feedback alebo dlhší delay
  2. Lock konflikty: DDL na primary → plánujte DDL opatrne
  3. Buffer pin konflikty: kolízia prístupu k stránke → mierny delay zvyčajne stačí
  4. Tablespace konflikty: zmazaný tablespace → nemažte počas dotazov
  5. Deadlock konflikty: zriedkavý edge case → vyšetrujte ak sa vyskytuje

Kľúčový poznatok: neexistuje obed zadarmo. hot_standby_feedback vymieňa stabilitu repliky za nafukovanie primary. max_standby_streaming_delay vymieňa stabilitu dotazov za replikačný lag. Najlepším riešením je často oprava pomalých dotazov, ktoré konflikty spúšťajú.

Najprv skontrolujte pg_stat_database_conflicts. Vedzte ktorý typ konfliktu riešite predtým, než si vyberiete zbraň.

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "PostgreSQL Read Replica Konflikty: Prečo sa vaše dotazy rušia". https://www.michal-drozd.com/sk/blog/postgresql-read-replica-conflicts/ (Publikované 28. januára 2025).