Postgres recovery_prefetch als schadelijk beschouwd en veroorzaker van outages

Na veel werk ontdekte ik dat dit gedeeltelijk een geval was waarbij iemand jaren geleden een e-mail naar de postgres hackers lijst had gestuurd met de vraag "hoe zit het hiermee?", waarop iedereen dacht "nee, het kan niet zo erg zijn"... en nu zien we productie-outages!

Bewijsstuk A: Februari 2021

Tijdens de ontwikkeling van de feature, anderhalf jaar voordat deze werd uitgebracht, schreef Stephen Frost:

"Het versturen van onnodige prefetches voor blokken waar we al een prefetch voor hebben verzonden, is waarschijnlijk een punt van zorg, zelfs als FPW's (Full Page Writes) uitstaan. Maar het voordeel van het prefetching zal dat bijna zeker compenseren, wat betekent dat we een manier om dit aan te pakken later als toekomstige verbetering kunnen implementeren. Ik zou daar geen probleem mee hebben. Het lijkt me alleen minder duidelijk wanneer we kijken naar het geval waarbij FPW is ingeschakeld. Uiteindelijk, als jij, Andres en Munro zich er geen zorgen over maken en niemand anders iets zegt, dan zal ik geen ophef maken over het commiten ervan, maar zoals je hierboven zei, leek het me een goed punt om aan te stippen zodat iedereen erover na kan denken."
Bron: postgresql.org

Bewijsstuk B: Maart 2025

Een duidelijk rapport uit de praktijk van Michael Zhilin (Postgres Pro):

"Een klant klaagde dat de physical standby een aanzienlijke replay lag heeft (enkele uren) op moderne, krachtige bare metal servers. Volgens de output van 'top' gebruikten het startup-proces en enkele kernel workers CPU, voornamelijk in de kernel space. Het incident is opgelost door recovery_prefetch uit te schakelen na een snelle analyse van de perf- en strace-outputs. De feitelijke XLog prefetching-code houdt er geen rekening mee dat de shared buffer kan worden geladen vanuit WAL FPI in plaats van vanaf de schijf, en maakt in dergelijke gevallen fadvise-aanroepen."
Bron: postgresql.org

Beide e-mails waren op het juiste spoor, maar hadden het volledige plaatje nog niet.

Ten eerste: FPI/FPW is tegenwoordig alomtegenwoordig. Hoewel het qua prestaties erg duur is, is het de enige manier waarop Postgres zich kan beschermen tegen torn writes bij OS-restarts. Als gevolg hiervan hebben de meeste productie-installaties van de community-versie van Postgres dit ingeschakeld.

Ten tweede: synchrone replicatie wordt gebruikelijker. Er zijn eigenlijk maar twee manieren om de community-versie van open source Postgres volledig te beschermen tegen dataverlies bij hardwarestoringen:

  1. Replicatie op storage-niveau met open source technologie zoals DRBD of dure propriëtaire storage- en clusteringtechnologie.
  2. Bescherming op databaseniveau met Postgres synchrone replicatie. Deze tweede optie is de normale oplossing bij Kubernetes en commodity-hardware.

Ten derde: iets dat voorheen niet is gediagnosticeerd of gerapporteerd – het probleem met recoveryprefetch gaat niet alleen over FPI's, maar ook over het feit dat wanneer de primaire instantie grote sequentiële I/O uitvoert, recoveryprefetch deze operaties omzet in 8k random reads. Voor hetzelfde aantal gelezen bytes vergroot dit de IOPS op replica's met wel 16x. Dit kan de beschikbare IOPS gemakkelijk uitputten, waardoor replica's aanzienlijk achterlopen na schrijfactiviteit.

De versterking van de lees-IOPS in combinatie met het ontwerp van Postgres voor synchrone replicatie is wat recovery_prefetch verandert in een dodelijke veroorzaker van outages. En het blijkt triviaal om dit te reproduceren zodra je de stappen kent.

Reproductiestappen

  1. Start Postgres op met synchrone replicatie naar een standby, op kleine servers met goedkope harde schijven (bijvoorbeeld: cloud disk met 500 IOPS, 2 CPU's, 8GB geheugen).
  2. Initialiseer pgbench met scale 640 voor ongeveer 10GB aan data.
  3. Schakel autovacuum uit op pgbench_accounts zodat dit het experiment niet verstoort.
  4. Draai pgbench op 5 TPS (fixed) in een terminal.
  5. Update 5% van de tabel (Checkpoint en update pgbench_accounts where aid%20 = 0). Dit genereert een burst van schrijfactiviteit. Pgbench zal vertragen tot dit is voltooid. Wacht tot dit klaar is.
  6. Kill het walreceiver-proces om een netwerk- of systeemhang/instabiliteit te simuleren die deze verbinding verbreekt.

Het resultaat: de TPS zakt naar nul en alle writes hangen voor meer dan 30 minuten (dit kan uren duren bij een grotere pgbench-tabel).

Het is echt zo eenvoudig om deze outage consistent te reproduceren: github.com/ardentperf-labs/walreceiver-outage-repro

Voor wat het waard is: VACUUM zelf is ook prima in staat om schrijfactiviteit te produceren. Het recept voor een outage is simpelweg "genoeg pech": een netwerkglitch die streaming replicatie onderbreekt op hetzelfde moment als een burst van schrijfactiviteit.

Waarom het systeem vastloopt

Om te begrijpen waarom het hele systeem hangt, komt het neer op twee eenvoudige zaken: (1) synchrone replicatie en (2) de kern-"state machine" die een Postgres Standby Server definieert.

Onthoud dat we bij synchrone replicatie afhankelijk zijn van ten minste één gezonde standby-server; anders kunnen we geen writes accepteren. (We willen duurzaamheid aan cliënten garanderen en ervoor zorgen dat elke transactie op een externe machine staat vóór de commit.)

De kern van de standby-server state machine is gedefinieerd in src/backend/access/transam/xlogrecovery.c:

WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess,
bool fetching_ckpt, XLogRecPtr tliRecPtr,
TimeLineID replayTLI, XLogRecPtr replayLSN,
bool nonblocking)
{
static TimestampTz last_fail_time = 0;
TimestampTz now;
bool		streaming_reply_sent = false;
/*-------
* Standby mode is implemented by a state machine:
*
* 1. Read from either archive or pg_wal (XLOG_FROM_ARCHIVE), or just
*	  pg_wal (XLOG_FROM_PG_WAL)
* 2. Check for promotion trigger request
* 3. Read from primary server via walreceiver (XLOG_FROM_STREAM)
* 4. Rescan timelines
* 5. Sleep wal_retrieve_retry_interval milliseconds, and loop back to 1.
*
* Failure to read from the current source advances the state machine to
* the next state.
...
...

Fundamenteel ziet het er zo uit: wanneer een Postgres standby overschakelt naar WAL-restore modus, zal deze niet terugkeren naar streaming totdat hij volledig is ingehaald (caught up).

Een burst van schrijfactiviteit, gecombineerd met IOPS-versterking op alle replica's die een aanzienlijke lag veroorzaakt, kan betekenen dat de database urenlang niet opnieuw begint met streamen.

De IOPS-versterking is zeer duidelijk. De disk stats voor de primary laten ongeveer 300 IOPS zien gedurende 5 minuten. De disk status voor de replica/standby in dezelfde periode laat echter 40 minuten lang de disklimiet van 500 IOPS zien. Hoewel dit niet direct uit de grafieken blijkt, is het aantal bytes dat van disk is gelezen ongeveer gelijk.

Wat gebeurt er als we recovery_prefetch uitschakelen?

(Let op: FPW's blijven ingeschakeld.) Variaties in het repo

Er is nog steeds een pauze tijdens de UPDATE, maar we zien geen lange pauze nadat de UPDATE is voltooid. Er is geen burst van lees-IOPS en de standby-server blijft in principe bijgehouden – waarbij de 5% update in minder dan 10 minuten wordt toegepast.

Op dit moment neig ik ernaar om de recovery_prefetch-instelling over de hele linie uit te schakelen, maar ik wil eerst zien waar de discussies heen gaan. Ik denk dat de voordelen dubieus zijn wanneer FPW's standaard zijn ingeschakeld.

---

Appendix: Postgres Standby Server Replay Process

Hier is een gedetailleerde herinnering aan de verschillende processen die betrokken zijn bij de kern van de restore- en replay-logica van een Postgres standby-server.

Belangrijk is de richting van de pijl tussen de WAL-sender en de WAL-receiver. Bij synchrone replicatie in Postgres is deze verbinding kritiek – de primary kan geen writes accepteren zonder deze. Het is ook erg fragiel, want als de verbinding wegvalt, kan de primary alleen maar wachten en alle writes blokkeren, in de hoop dat een standby snel opnieuw verbinding maakt.

Dit is een reden om twee standbys en quorum commit (ANY 1) te overwegen bij Postgres. Hoewel dit de kosten verhoogt (drie replica's in totaal in plaats van twee), verhoogt het de robuustheid van de database aanzienlijk.

---

Aanvullende opmerking (Commentaar van Matt)

Dit doet denken aan een gerelateerd probleem bij een klant op PostgreSQL 18.x. De trigger is anders dan recovery_prefetch, maar het lijkt dezelfde logica te betreffen rondom WaitForWALToBecomeAvailable en de standby state machine.

Setup: een hot standby aangemaakt met pgbasebackup met een physical replication slot, waarbij later recoveryminapplydelay = '7d' is toegevoegd om te kunnen herstellen van gebruikersfouten (bijv. een per ongeluk verwijdering op vrijdagavond die pas op maandag wordt opgemerkt). Na de eerste replica-restart werkte streaming zoals verwacht: WAL kwam in real-time aan en werd 7 dagen later gereplayed.

Na een OS-update en reboot startte de standby prima op, maar startte de walreceiver nooit. Omdat de vertraagde replay betekende dat alles wat nodig was al in de lokale pgwal stond, was er blijkbaar geen reden om meer op te halen. Ondertussen bleef de pgwal op de primary groeien, vastgehouden door de slot, en er werd niets gestreamd.

Er was geen manier gevonden om de walreceiver in deze setup te forceren te starten, behalve door te wachten tot de vertraagde replay alle replica pg_wal had geconsumeerd (tot 7 dagen).

Dit bevestigt dat bij replicatie sommige instellingen je op een onverwacht moment kunnen bijten.