Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit ff9f111

Browse files
committed
Fix WAL replay in presence of an incomplete record
Physical replication always ships WAL segment files to replicas once they are complete. This is a problem if one WAL record is split across a segment boundary and the primary server crashes before writing down the segment with the next portion of the WAL record: WAL writing after crash recovery would happily resume at the point where the broken record started, overwriting that record ... but any standby or backup may have already received a copy of that segment, and they are not rewinding. This causes standbys to stop following the primary after the latter crashes: LOG: invalid contrecord length 7262 at A8/D9FFFBC8 because the standby is still trying to read the continuation record (contrecord) for the original long WAL record, but it is not there and it will never be. A workaround is to stop the replica, delete the WAL file, and restart it -- at which point a fresh copy is brought over from the primary. But that's pretty labor intensive, and I bet many users would just give up and re-clone the standby instead. A fix for this problem was already attempted in commit 515e3d8, but it only addressed the case for the scenario of WAL archiving, so streaming replication would still be a problem (as well as other things such as taking a filesystem-level backup while the server is down after having crashed), and it had performance scalability problems too; so it had to be reverted. This commit fixes the problem using an approach suggested by Andres Freund, whereby the initial portion(s) of the split-up WAL record are kept, and a special type of WAL record is written where the contrecord was lost, so that WAL replay in the replica knows to skip the broken parts. With this approach, we can continue to stream/archive segment files as soon as they are complete, and replay of the broken records will proceed across the crash point without a hitch. Because a new type of WAL record is added, users should be careful to upgrade standbys first, primaries later. Otherwise they risk the standby being unable to start if the primary happens to write such a record. A new TAP test that exercises this is added, but the portability of it is yet to be seen. This has been wrong since the introduction of physical replication, so backpatch all the way back. In stable branches, keep the new XLogReaderState members at the end of the struct, to avoid an ABI break. Author: Álvaro Herrera <alvherre@alvh.no-ip.org> Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com> Reviewed-by: Nathan Bossart <bossartn@amazon.com> Discussion: https://postgr.es/m/202108232252.dh7uxf6oxwcy@alvherre.pgsql
1 parent 2acb7cc commit ff9f111

File tree

9 files changed

+450
-7
lines changed

9 files changed

+450
-7
lines changed

src/backend/access/rmgrdesc/xlogdesc.c

+12
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
139139
xlrec.ThisTimeLineID, xlrec.PrevTimeLineID,
140140
timestamptz_to_str(xlrec.end_time));
141141
}
142+
else if (info == XLOG_OVERWRITE_CONTRECORD)
143+
{
144+
xl_overwrite_contrecord xlrec;
145+
146+
memcpy(&xlrec, rec, sizeof(xl_overwrite_contrecord));
147+
appendStringInfo(buf, "lsn %X/%X; time %s",
148+
LSN_FORMAT_ARGS(xlrec.overwritten_lsn),
149+
timestamptz_to_str(xlrec.overwrite_time));
150+
}
142151
}
143152

144153
const char *
@@ -178,6 +187,9 @@ xlog_identify(uint8 info)
178187
case XLOG_END_OF_RECOVERY:
179188
id = "END_OF_RECOVERY";
180189
break;
190+
case XLOG_OVERWRITE_CONTRECORD:
191+
id = "OVERWRITE_CONTRECORD";
192+
break;
181193
case XLOG_FPI:
182194
id = "FPI";
183195
break;

src/backend/access/transam/xlog.c

+149-5
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ static XLogRecPtr LastRec;
199199
static XLogRecPtr flushedUpto = 0;
200200
static TimeLineID receiveTLI = 0;
201201

202+
/*
203+
* abortedRecPtr is the start pointer of a broken record at end of WAL when
204+
* recovery completes; missingContrecPtr is the location of the first
205+
* contrecord that went missing. See CreateOverwriteContrecordRecord for
206+
* details.
207+
*/
208+
static XLogRecPtr abortedRecPtr;
209+
static XLogRecPtr missingContrecPtr;
210+
202211
/*
203212
* During recovery, lastFullPageWrites keeps track of full_page_writes that
204213
* the replayed WAL records indicate. It's initialized with full_page_writes
@@ -892,8 +901,11 @@ static void CheckRequiredParameterValues(void);
892901
static void XLogReportParameters(void);
893902
static void checkTimeLineSwitch(XLogRecPtr lsn, TimeLineID newTLI,
894903
TimeLineID prevTLI);
904+
static void VerifyOverwriteContrecord(xl_overwrite_contrecord *xlrec,
905+
XLogReaderState *state);
895906
static void LocalSetXLogInsertAllowed(void);
896907
static void CreateEndOfRecoveryRecord(void);
908+
static XLogRecPtr CreateOverwriteContrecordRecord(XLogRecPtr aborted_lsn);
897909
static void CheckPointGuts(XLogRecPtr checkPointRedo, int flags);
898910
static void KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo);
899911
static XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
@@ -2246,6 +2258,18 @@ AdvanceXLInsertBuffer(XLogRecPtr upto, bool opportunistic)
22462258
if (!Insert->forcePageWrites)
22472259
NewPage->xlp_info |= XLP_BKP_REMOVABLE;
22482260

2261+
/*
2262+
* If a record was found to be broken at the end of recovery, and
2263+
* we're going to write on the page where its first contrecord was
2264+
* lost, set the XLP_FIRST_IS_OVERWRITE_CONTRECORD flag on the page
2265+
* header. See CreateOverwriteContrecordRecord().
2266+
*/
2267+
if (missingContrecPtr == NewPageBeginPtr)
2268+
{
2269+
NewPage->xlp_info |= XLP_FIRST_IS_OVERWRITE_CONTRECORD;
2270+
missingContrecPtr = InvalidXLogRecPtr;
2271+
}
2272+
22492273
/*
22502274
* If first page of an XLOG segment file, make it a long header.
22512275
*/
@@ -4394,6 +4418,19 @@ ReadRecord(XLogReaderState *xlogreader, int emode,
43944418
EndRecPtr = xlogreader->EndRecPtr;
43954419
if (record == NULL)
43964420
{
4421+
/*
4422+
* When not in standby mode we find that WAL ends in an incomplete
4423+
* record, keep track of that record. After recovery is done,
4424+
* we'll write a record to indicate downstream WAL readers that
4425+
* that portion is to be ignored.
4426+
*/
4427+
if (!StandbyMode &&
4428+
!XLogRecPtrIsInvalid(xlogreader->abortedRecPtr))
4429+
{
4430+
abortedRecPtr = xlogreader->abortedRecPtr;
4431+
missingContrecPtr = xlogreader->missingContrecPtr;
4432+
}
4433+
43974434
if (readFile >= 0)
43984435
{
43994436
close(readFile);
@@ -7069,6 +7106,12 @@ StartupXLOG(void)
70697106
InRecovery = true;
70707107
}
70717108

7109+
/*
7110+
* Start recovery assuming that the final record isn't lost.
7111+
*/
7112+
abortedRecPtr = InvalidXLogRecPtr;
7113+
missingContrecPtr = InvalidXLogRecPtr;
7114+
70727115
/* REDO */
70737116
if (InRecovery)
70747117
{
@@ -7655,8 +7698,9 @@ StartupXLOG(void)
76557698

76567699
/*
76577700
* Kill WAL receiver, if it's still running, before we continue to write
7658-
* the startup checkpoint record. It will trump over the checkpoint and
7659-
* subsequent records if it's still alive when we start writing WAL.
7701+
* the startup checkpoint and aborted-contrecord records. It will trump
7702+
* over these records and subsequent ones if it's still alive when we
7703+
* start writing WAL.
76607704
*/
76617705
XLogShutdownWalRcv();
76627706

@@ -7689,8 +7733,12 @@ StartupXLOG(void)
76897733
StandbyMode = false;
76907734

76917735
/*
7692-
* Re-fetch the last valid or last applied record, so we can identify the
7693-
* exact endpoint of what we consider the valid portion of WAL.
7736+
* Determine where to start writing WAL next.
7737+
*
7738+
* When recovery ended in an incomplete record, write a WAL record about
7739+
* that and continue after it. In all other cases, re-fetch the last
7740+
* valid or last applied record, so we can identify the exact endpoint of
7741+
* what we consider the valid portion of WAL.
76947742
*/
76957743
XLogBeginRead(xlogreader, LastRec);
76967744
record = ReadRecord(xlogreader, PANIC, false);
@@ -7821,6 +7869,18 @@ StartupXLOG(void)
78217869
XLogCtl->ThisTimeLineID = ThisTimeLineID;
78227870
XLogCtl->PrevTimeLineID = PrevTimeLineID;
78237871

7872+
/*
7873+
* Actually, if WAL ended in an incomplete record, skip the parts that
7874+
* made it through and start writing after the portion that persisted.
7875+
* (It's critical to first write an OVERWRITE_CONTRECORD message, which
7876+
* we'll do as soon as we're open for writing new WAL.)
7877+
*/
7878+
if (!XLogRecPtrIsInvalid(missingContrecPtr))
7879+
{
7880+
Assert(!XLogRecPtrIsInvalid(abortedRecPtr));
7881+
EndOfLog = missingContrecPtr;
7882+
}
7883+
78247884
/*
78257885
* Prepare to write WAL starting at EndOfLog location, and init xlog
78267886
* buffer cache using the block containing the last record from the
@@ -7873,13 +7933,23 @@ StartupXLOG(void)
78737933
XLogCtl->LogwrtRqst.Write = EndOfLog;
78747934
XLogCtl->LogwrtRqst.Flush = EndOfLog;
78757935

7936+
LocalSetXLogInsertAllowed();
7937+
7938+
/* If necessary, write overwrite-contrecord before doing anything else */
7939+
if (!XLogRecPtrIsInvalid(abortedRecPtr))
7940+
{
7941+
Assert(!XLogRecPtrIsInvalid(missingContrecPtr));
7942+
CreateOverwriteContrecordRecord(abortedRecPtr);
7943+
abortedRecPtr = InvalidXLogRecPtr;
7944+
missingContrecPtr = InvalidXLogRecPtr;
7945+
}
7946+
78767947
/*
78777948
* Update full_page_writes in shared memory and write an XLOG_FPW_CHANGE
78787949
* record before resource manager writes cleanup WAL records or checkpoint
78797950
* record is written.
78807951
*/
78817952
Insert->fullPageWrites = lastFullPageWrites;
7882-
LocalSetXLogInsertAllowed();
78837953
UpdateFullPageWrites();
78847954
LocalXLogInsertAllowed = -1;
78857955

@@ -9365,6 +9435,53 @@ CreateEndOfRecoveryRecord(void)
93659435
LocalXLogInsertAllowed = -1; /* return to "check" state */
93669436
}
93679437

9438+
/*
9439+
* Write an OVERWRITE_CONTRECORD message.
9440+
*
9441+
* When on WAL replay we expect a continuation record at the start of a page
9442+
* that is not there, recovery ends and WAL writing resumes at that point.
9443+
* But it's wrong to resume writing new WAL back at the start of the record
9444+
* that was broken, because downstream consumers of that WAL (physical
9445+
* replicas) are not prepared to "rewind". So the first action after
9446+
* finishing replay of all valid WAL must be to write a record of this type
9447+
* at the point where the contrecord was missing; to support xlogreader
9448+
* detecting the special case, XLP_FIRST_IS_OVERWRITE_CONTRECORD is also added
9449+
* to the page header where the record occurs. xlogreader has an ad-hoc
9450+
* mechanism to report metadata about the broken record, which is what we
9451+
* use here.
9452+
*
9453+
* At replay time, XLP_FIRST_IS_OVERWRITE_CONTRECORD instructs xlogreader to
9454+
* skip the record it was reading, and pass back the LSN of the skipped
9455+
* record, so that its caller can verify (on "replay" of that record) that the
9456+
* XLOG_OVERWRITE_CONTRECORD matches what was effectively overwritten.
9457+
*/
9458+
static XLogRecPtr
9459+
CreateOverwriteContrecordRecord(XLogRecPtr aborted_lsn)
9460+
{
9461+
xl_overwrite_contrecord xlrec;
9462+
XLogRecPtr recptr;
9463+
9464+
/* sanity check */
9465+
if (!RecoveryInProgress())
9466+
elog(ERROR, "can only be used at end of recovery");
9467+
9468+
xlrec.overwritten_lsn = aborted_lsn;
9469+
xlrec.overwrite_time = GetCurrentTimestamp();
9470+
9471+
START_CRIT_SECTION();
9472+
9473+
XLogBeginInsert();
9474+
XLogRegisterData((char *) &xlrec, sizeof(xl_overwrite_contrecord));
9475+
9476+
recptr = XLogInsert(RM_XLOG_ID, XLOG_OVERWRITE_CONTRECORD);
9477+
9478+
XLogFlush(recptr);
9479+
9480+
END_CRIT_SECTION();
9481+
9482+
return recptr;
9483+
}
9484+
93689485
/*
93699486
* Flush all data in shared memory to disk, and fsync
93709487
*
@@ -10295,6 +10412,13 @@ xlog_redo(XLogReaderState *record)
1029510412

1029610413
RecoveryRestartPoint(&checkPoint);
1029710414
}
10415+
else if (info == XLOG_OVERWRITE_CONTRECORD)
10416+
{
10417+
xl_overwrite_contrecord xlrec;
10418+
10419+
memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_overwrite_contrecord));
10420+
VerifyOverwriteContrecord(&xlrec, record);
10421+
}
1029810422
else if (info == XLOG_END_OF_RECOVERY)
1029910423
{
1030010424
xl_end_of_recovery xlrec;
@@ -10462,6 +10586,26 @@ xlog_redo(XLogReaderState *record)
1046210586
}
1046310587
}
1046410588

10589+
/*
10590+
* Verify the payload of a XLOG_OVERWRITE_CONTRECORD record.
10591+
*/
10592+
static void
10593+
VerifyOverwriteContrecord(xl_overwrite_contrecord *xlrec, XLogReaderState *state)
10594+
{
10595+
if (xlrec->overwritten_lsn != state->overwrittenRecPtr)
10596+
elog(FATAL, "mismatching overwritten LSN %X/%X -> %X/%X",
10597+
LSN_FORMAT_ARGS(xlrec->overwritten_lsn),
10598+
LSN_FORMAT_ARGS(state->overwrittenRecPtr));
10599+
10600+
ereport(LOG,
10601+
(errmsg("sucessfully skipped missing contrecord at %X/%X, overwritten at %s",
10602+
LSN_FORMAT_ARGS(xlrec->overwritten_lsn),
10603+
timestamptz_to_str(xlrec->overwrite_time))));
10604+
10605+
/* Verifying the record should only happen once */
10606+
state->overwrittenRecPtr = InvalidXLogRecPtr;
10607+
}
10608+
1046510609
#ifdef WAL_DEBUG
1046610610

1046710611
static void

src/backend/access/transam/xlogreader.c

+39-1
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ XLogReadRecord(XLogReaderState *state, char **errormsg)
278278
total_len;
279279
uint32 targetRecOff;
280280
uint32 pageHeaderSize;
281+
bool assembled;
281282
bool gotheader;
282283
int readOff;
283284

@@ -293,6 +294,8 @@ XLogReadRecord(XLogReaderState *state, char **errormsg)
293294
state->errormsg_buf[0] = '\0';
294295

295296
ResetDecoder(state);
297+
state->abortedRecPtr = InvalidXLogRecPtr;
298+
state->missingContrecPtr = InvalidXLogRecPtr;
296299

297300
RecPtr = state->EndRecPtr;
298301

@@ -319,7 +322,9 @@ XLogReadRecord(XLogReaderState *state, char **errormsg)
319322
randAccess = true;
320323
}
321324

325+
restart:
322326
state->currRecPtr = RecPtr;
327+
assembled = false;
323328

324329
targetPagePtr = RecPtr - (RecPtr % XLOG_BLCKSZ);
325330
targetRecOff = RecPtr % XLOG_BLCKSZ;
@@ -415,6 +420,8 @@ XLogReadRecord(XLogReaderState *state, char **errormsg)
415420
char *buffer;
416421
uint32 gotlen;
417422

423+
assembled = true;
424+
418425
/*
419426
* Enlarge readRecordBuf as needed.
420427
*/
@@ -448,8 +455,25 @@ XLogReadRecord(XLogReaderState *state, char **errormsg)
448455

449456
Assert(SizeOfXLogShortPHD <= readOff);
450457

451-
/* Check that the continuation on next page looks valid */
452458
pageHeader = (XLogPageHeader) state->readBuf;
459+
460+
/*
461+
* If we were expecting a continuation record and got an
462+
* "overwrite contrecord" flag, that means the continuation record
463+
* was overwritten with a different record. Restart the read by
464+
* assuming the address to read is the location where we found
465+
* this flag; but keep track of the LSN of the record we were
466+
* reading, for later verification.
467+
*/
468+
if (pageHeader->xlp_info & XLP_FIRST_IS_OVERWRITE_CONTRECORD)
469+
{
470+
state->overwrittenRecPtr = state->currRecPtr;
471+
ResetDecoder(state);
472+
RecPtr = targetPagePtr;
473+
goto restart;
474+
}
475+
476+
/* Check that the continuation on next page looks valid */
453477
if (!(pageHeader->xlp_info & XLP_FIRST_IS_CONTRECORD))
454478
{
455479
report_invalid_record(state,
@@ -551,6 +575,20 @@ XLogReadRecord(XLogReaderState *state, char **errormsg)
551575
return NULL;
552576

553577
err:
578+
if (assembled)
579+
{
580+
/*
581+
* We get here when a record that spans multiple pages needs to be
582+
* assembled, but something went wrong -- perhaps a contrecord piece
583+
* was lost. If caller is WAL replay, it will know where the aborted
584+
* record was and where to direct followup WAL to be written, marking
585+
* the next piece with XLP_FIRST_IS_OVERWRITE_CONTRECORD, which will
586+
* in turn signal downstream WAL consumers that the broken WAL record
587+
* is to be ignored.
588+
*/
589+
state->abortedRecPtr = RecPtr;
590+
state->missingContrecPtr = targetPagePtr;
591+
}
554592

555593
/*
556594
* Invalidate the read state. We might read from a different source after

src/include/access/xlog_internal.h

+10-1
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ typedef XLogLongPageHeaderData *XLogLongPageHeader;
7676
#define XLP_LONG_HEADER 0x0002
7777
/* This flag indicates backup blocks starting in this page are optional */
7878
#define XLP_BKP_REMOVABLE 0x0004
79+
/* Replaces a missing contrecord; see CreateOverwriteContrecordRecord */
80+
#define XLP_FIRST_IS_OVERWRITE_CONTRECORD 0x0008
7981
/* All defined flag bits in xlp_info (used for validity checking of header) */
80-
#define XLP_ALL_FLAGS 0x0007
82+
#define XLP_ALL_FLAGS 0x000F
8183

8284
#define XLogPageHeaderSize(hdr) \
8385
(((hdr)->xlp_info & XLP_LONG_HEADER) ? SizeOfXLogLongPHD : SizeOfXLogShortPHD)
@@ -249,6 +251,13 @@ typedef struct xl_restore_point
249251
char rp_name[MAXFNAMELEN];
250252
} xl_restore_point;
251253

254+
/* Overwrite of prior contrecord */
255+
typedef struct xl_overwrite_contrecord
256+
{
257+
XLogRecPtr overwritten_lsn;
258+
TimestampTz overwrite_time;
259+
} xl_overwrite_contrecord;
260+
252261
/* End of recovery mark, when we don't do an END_OF_RECOVERY checkpoint */
253262
typedef struct xl_end_of_recovery
254263
{

src/include/access/xlogreader.h

+10
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ struct XLogReaderState
175175
XLogRecPtr ReadRecPtr; /* start of last record read */
176176
XLogRecPtr EndRecPtr; /* end+1 of last record read */
177177

178+
/*
179+
* Set at the end of recovery: the start point of a partial record at the
180+
* end of WAL (InvalidXLogRecPtr if there wasn't one), and the start
181+
* location of its first contrecord that went missing.
182+
*/
183+
XLogRecPtr abortedRecPtr;
184+
XLogRecPtr missingContrecPtr;
185+
/* Set when XLP_FIRST_IS_OVERWRITE_CONTRECORD is found */
186+
XLogRecPtr overwrittenRecPtr;
187+
178188

179189
/* ----------------------------------------
180190
* Decoded representation of current record

0 commit comments

Comments
 (0)