总结
-
开源 PostgreSQL 启动时,会先从 pg_control 读取最近一次 checkpoint 的位置,再读取对应的 checkpoint record 得到 checkpoint.redo;如果需要 crash recovery,就从 checkpoint.redo 开始向后 redo WAL,直到最后一条完整有效的 WAL record,然后把这条最后 WAL record的起始位置作为后续新 WAL 的 prev_lsn / xl_prev 来源,把它的结束位置作为后续新 WAL record 的写入起点。
- 关键位点:checkpoint.redo = 停机checkpoint位点 或 上一个在线chkpt的开始位置(在线chkpt之保证起始位置前的数据)
- 关键位点:prev_lsn = 最后一个完整wal的开始位置。
- 关键位点:后续wal写入起点 = 最后一个完整wal的结束位置。
-
Neon primary Compute 在执行 basebackup 之前,会先通过 postgres --sync-safekeepers 或相关检查,让 Safekeepers 同步/确认出一个安全 LSN,例如 lsn=100;然后 compute_ctl 向 Pageserver 请求 basebackup@100,Pageserver 生成 PGDATA 压缩包,其中 neon.signal 记录 prev_lsn,也就是 100 之前最后一条 WAL record 的起始位置,同时生成的 pg_control.checkPointCopy.redo 被设置为 100 这个 basebackup LSN;PostgreSQL 启动时看到 neon.signal 后进入 Neon recovery 分支,不会从本地 WAL redo,而是"假装"已经读完了 prev_lsn -> 100 这条上一条 WAL record,用 prev_lsn 初始化后续新 WAL 的 xl_prev 来源,用 100 初始化后续新 WAL 的写入起点,然后正常启动。
- 关键位点:checkpoint.redo = 100 = 请求basebackup时的LSN = safekeeper共识点
- 关键位点:prev_lsn = 100位置上一条wal的起始位置。
- 关键位点:后续wal写入起点 = 100。
-
核心代码位置:
c
// neon/vendor/postgres-v17/src/backend/access/transam/xlogrecovery.c
else if (NeonRecoveryRequested)
{
/*
* Neon hacks to spawn compute node without WAL. Pretend that we
* just finished reading the record that started at 'neonLastRec'
* and ended at checkpoint.redo
*/
elog(LOG, "starting with neon basebackup at LSN %X/%X, prev %X/%X",
LSN_FORMAT_ARGS(ControlFile->checkPointCopy.redo),
LSN_FORMAT_ARGS(neonLastRec));
CheckPointLoc = neonLastRec;
CheckPointTLI = ControlFile->checkPointCopy.ThisTimeLineID;
RedoStartLSN = ControlFile->checkPointCopy.redo;
/* make basebackup LSN available for walproposer */
SetRedoStartLsn(RedoStartLSN);
memcpy(&checkPoint, &ControlFile->checkPointCopy, sizeof(CheckPoint));
...
}
1. 什么时候会进入这个分支
进入条件是:
c
NeonRecoveryRequested == true
NeonRecoveryRequested 是在 StartupXLOG() 一开始读取 neon.signal 时设置的:
c
readNeonSignalFile();
readNeonSignalFile() 会尝试打开 PGDATA 下的:
text
neon.signal
如果文件存在,就解析其中内容:
text
PREV LSN: <lsn>
PREV LSN: none
PREV LSN: invalid
然后设置:
c
NeonRecoveryRequested = true;
因此,进入该分支的直接前提是:
text
PGDATA 中存在 neon.signal 文件。
这个文件由 Pageserver 在生成 basebackup tarball 时写入:
rust
for signalfilename in ["neon.signal", "zenith.signal"] {
self.ar.append(...);
}
完整链路是:
text
compute_ctl 请求 Pageserver basebackup
↓
Pageserver 生成 PGDATA tarball
↓
tarball 里包含 neon.signal
↓
PostgreSQL StartupXLOG() 看到 neon.signal
↓
readNeonSignalFile() 设置 NeonRecoveryRequested = true
↓
xlogrecovery.c 进入 NeonRecoveryRequested 分支
所以,这个分支通常发生在:
text
Neon Compute 从 Pageserver basebackup 启动 / 重启 PGDATA 时。
它不是开源 PostgreSQL 普通从旧 PGDATA 原地 crash recovery 的路径。
2. 该分支处于什么启动逻辑中
这段代码所在位置大致是在选择 recovery 起点。逻辑结构可以简化成:
c
if (backup_label exists)
{
// 开源 PostgreSQL base backup / archive recovery 路径
}
else if (NeonRecoveryRequested)
{
// Neon 从 Pageserver basebackup 启动路径
}
else
{
// 普通 PostgreSQL 从 pg_control + pg_wal 启动路径
}
所以 NeonRecoveryRequested 分支表示:
text
不走开源 PG 的 backup_label basebackup recovery;
也不走普通本地 pg_control + pg_wal crash recovery;
而是走 Neon 自己的 basebackup 启动路径。
3. 这个分支要解决的问题
核心问题是:
text
Neon Compute 启动时没有完整的本地 WAL 历史。
开源 PostgreSQL 启动时,可以从本地 WAL 中知道:
text
上一条 WAL record 从哪里开始;
上一条 WAL record 在哪里结束;
下一条 WAL record 应该从哪里开始写。
但是 Neon Compute 是从 Pageserver 拉了一个快照。PGDATA 中的 pg_wal 只是 Pageserver 生成的 dummy/bootstrap WAL segment,不是完整历史 WAL。
然而 PostgreSQL 写新 WAL record 时必须维护 WAL record 链:
text
record_N.xl_prev = record_N-1 的起始 LSN
因此 Neon 必须给 PostgreSQL 两个信息:
text
1. basebackup LSN:下一条 WAL 应该从哪里开始写;
2. prev_lsn:上一条 WAL record 的起始 LSN。
这个分支就是把这两个信息灌入 PostgreSQL 的 recovery 状态中。
4. 逐行解释
4.1 打印启动日志
c
elog(LOG, "starting with neon basebackup at LSN %X/%X, prev %X/%X",
LSN_FORMAT_ARGS(ControlFile->checkPointCopy.redo),
LSN_FORMAT_ARGS(neonLastRec));
这里打印两个关键 LSN:
text
ControlFile->checkPointCopy.redo = basebackup LSN
neonLastRec = prev_lsn
ControlFile->checkPointCopy.redo 来自 Pageserver 生成的 global/pg_control。
Pageserver 在生成 pg_control 时,会把 checkpoint 的 redo 设置为这次 basebackup 的启动 LSN:
rust
checkpoint.redo = normalize_lsn(lsn, WAL_SEGMENT_SIZE).0;
neonLastRec 来自 neon.signal 中的 PREV LSN。
4.2 设置 CheckPointLoc
c
CheckPointLoc = neonLastRec;
在开源 PostgreSQL 中,CheckPointLoc 通常表示 checkpoint record 的位置。
但在 Neon 这个分支中,它被设置为:
text
上一条 WAL record 的起始 LSN。
也就是 prev_lsn。
这样做是为了复用后续 PostgreSQL 的通用逻辑。后面 FinishWalRecovery() 会把 CheckPointLoc 作为 lastRec,最终在 StartupXLOG() 中初始化:
c
Insert->PrevBytePos = XLogRecPtrToBytePos(endOfRecoveryInfo->lastRec);
Insert->CurrBytePos = XLogRecPtrToBytePos(EndOfLog);
所以这行本质是在告诉 PostgreSQL:
text
上一条 WAL record 的起始位置是 neonLastRec。
后续第一条新 WAL record 的 xl_prev 就能正确指向它。
4.3 设置 timeline
c
CheckPointTLI = ControlFile->checkPointCopy.ThisTimeLineID;
设置当前 checkpoint / recovery 相关的 timeline ID。
该 timeline 信息来自 Pageserver 生成的 pg_control.checkPointCopy。
4.4 设置 RedoStartLSN
c
RedoStartLSN = ControlFile->checkPointCopy.redo;
在开源 PostgreSQL 中,RedoStartLSN 通常表示:
text
从这个 LSN 开始 redo WAL。
但在 Neon 这个分支中,它不是传统含义。
Neon 生成的 pg_control 中:
text
checkpoint.redo = basebackup LSN
所以这里实际表达的是:
text
RedoStartLSN = Compute 要启动到的 LSN
= 下一条 WAL record 准备开始写的位置
而不是说真的要从这个 LSN 向后重放本地 WAL。
4.5 通知 walproposer
c
SetRedoStartLsn(RedoStartLSN);
源码注释是:
c
/* make basebackup LSN available for walproposer */
Neon Compute 写 WAL 时,需要通过 walproposer 和 Safekeepers 交互。
这里把 basebackup/start LSN 暴露给 walproposer,让 walproposer 知道:
text
当前 Compute 从哪个 LSN 开始;
后续 WAL 要从哪里接上。
4.6 拷贝 checkpoint
c
memcpy(&checkPoint, &ControlFile->checkPointCopy, sizeof(CheckPoint));
把 controlfile 中的 checkpoint copy 拷贝到本地变量 checkPoint。
后续通用 recovery/startup 代码还会使用 checkPoint 中的字段,例如:
text
nextXid
nextOid
nextMulti
oldestXid
ThisTimeLineID
redo
...
这些信息由 Pageserver 从历史 WAL/checkpoint 元数据中持久化,并在 basebackup 中生成出来。
5. 后面的 wasShutdown 为什么这样设置
紧接着这个分支后面有:
c
if (StandbyModeRequested &&
PrimaryConnInfo != NULL && *PrimaryConnInfo != '\0')
{
wasShutdown = false;
}
else
wasShutdown = true;
源码注释说:
c
When a primary Neon compute node is started, we pretend that it
started after a clean shutdown and no recovery is needed.
也就是说,Neon primary 启动时会"假装":
text
这是一次 clean shutdown 后的启动,不需要传统 WAL recovery。
原因是:
text
数据一致性不依赖 Compute 本地 WAL redo;
Pageserver 已经能够按页提供目标 LSN 上的一致数据。
但是如果这是一个会跟随 primary 的 read-only replica:
text
StandbyModeRequested && PrimaryConnInfo != NULL
则不能设置 wasShutdown = true。
因为 wasShutdown = true 隐含语义是:
text
启动 LSN 上没有 running transactions。
但 replica 从某个 LSN 启动时,该 LSN 可能有正在进行中的事务。Hot standby 的 known-assigned XIDs 机制需要正确处理这些 running transactions,所以这里必须:
c
wasShutdown = false;
否则只读跟随节点可能错误地认为没有 in-progress transaction。
6. 这个分支不会做传统 WAL redo
开源 PostgreSQL 普通启动逻辑大概是:
text
CheckPointLoc = ControlFile->checkPoint
RedoStartLSN = ControlFile->checkPointCopy.redo
ReadCheckpointRecord()
PerformWalRecovery()
FinishWalRecovery()
也就是它会真的从本地 pg_wal 读 checkpoint record,并根据需要 redo WAL。
Neon 这个分支则是:
text
CheckPointLoc = neonLastRec
RedoStartLSN = basebackup LSN
checkPoint = generated checkpoint
pretend clean shutdown
它的目的不是:
text
从 RedoStartLSN 开始读 WAL 并 redo。
而是:
text
绕过本地 WAL replay;
把 PostgreSQL 的内部状态设置成"已经恢复到 basebackup LSN";
并准备从 basebackup LSN 继续写 WAL。
后面在 FinishWalRecovery() 中还有 Neon 特殊逻辑:
c
if (!NeonRecoveryRequested)
{
XLogPrefetcherBeginRead(xlogprefetcher, lastRec);
ReadRecord(...);
}
endOfLog = xlogreader->EndRecPtr;
如果是 Neon recovery:
text
不会真的 ReadRecord(lastRec),因为本地没有完整 WAL 可以读。
然后又有:
c
if (NeonRecoveryRequested)
{
/*
* When starting from a neon base backup, we don't have WAL.
* Initialize the WAL page where we will start writing new records from scratch.
*/
}
它会手工初始化最后一个 WAL page 的 header,让后续可以从 endOfLog 继续写新 WAL。
7. checkpoint.redo 在这个分支中的真实含义
在这个 Neon 分支中:
c
ControlFile->checkPointCopy.redo
不要按开源 PostgreSQL 习惯理解为"需要 redo 的起点"。
在 Neon basebackup 里,它更像是:
text
basebackup_lsn
也就是 Compute 当前数据视图对应的 LSN
也是下一条 WAL record 准备写的位置
来源在 Pageserver:
rust
checkpoint.redo = normalize_lsn(lsn, WAL_SEGMENT_SIZE).0;
所以近似可以理解为:
text
ControlFile->checkPointCopy.redo == send_basebackup_tarball 里的 lsn / req_lsn
8. neonLastRec 是什么
neonLastRec 来自 neon.signal:
text
PREV LSN: <lsn>
它表示:
text
basebackup_lsn 前一条 WAL record 的起始 LSN。
也就是 Pageserver basebackup.rs 中的 prev_record_lsn,或者 API 入参中的 prev_lsn。
例如:
text
neonLastRec = 0/16B6A50
ControlFile->checkPointCopy.redo = 0/16B6AF8
Neon 启动时会"假装":
text
刚刚读完了一条 WAL record:
start = 0/16B6A50
end = 0/16B6AF8
然后下一条 WAL record 从:
text
0/16B6AF8
开始写,并且它的 xl_prev 会指向:
text
0/16B6A50
9. 和开源 PostgreSQL 分支的区别
开源 PostgreSQL 普通分支会做:
c
CheckPointLoc = ControlFile->checkPoint;
RedoStartLSN = ControlFile->checkPointCopy.redo;
record = ReadCheckpointRecord(xlogprefetcher, CheckPointLoc, CheckPointTLI);
也就是说它会真的去本地 WAL 中读取 checkpoint record。
Neon 分支不会这样做:
text
不会去 pg_wal 中读取 CheckPointLoc = neonLastRec 的 record;
不会从 RedoStartLSN 做传统 redo;
而是直接使用 Pageserver 生成的 checkpoint/control 信息。
原因是:
text
Neon Compute 本地 pg_wal 不是历史 WAL 的权威来源;
权威 WAL 在 Safekeepers;
数据页版本在 Pageserver。
10. 图示
10.1 Neon Compute 从 Pageserver basebackup 启动
假设 Pageserver 给 Compute 的 basebackup 位于 LSN B:
text
previous WAL record
A --------------------------------> B
^ ^
| |
prev_lsn / neonLastRec basebackup_lsn / checkpoint.redo
上一条 WAL record 起点 下一条 WAL record 写入位置
Neon 启动时本地没有这条 WAL record 的完整内容,但它知道:
text
A = neonLastRec
B = checkpoint.redo
于是该分支告诉 PostgreSQL:
text
你可以假装刚刚读完了 A -> B 这条 WAL record。
现在从 B 继续写。
之后新 WAL 链条为:
text
old record new record
A -------------------------------> B -------------------------------> C
^ ^ ^
prev_lsn req_lsn/basebackup_lsn new end
new_record.xl_prev = A
注意:
text
new_record.xl_prev 指向上一条 record 的起点 A,
而不是 B。
Neon 场景下,A -> B 这条 previous WAL record 不一定完整存在于 Compute 本地 pg_wal 中。Compute 只是通过 neon.signal 知道 A,通过生成的 pg_control 知道 B,然后伪造出"已经读完上一条 record"的状态。
10.2 开源 PostgreSQL 普通启动 / crash recovery
开源 PostgreSQL 中,本地 pg_wal 是完整且权威的 WAL 来源。启动时 PostgreSQL 会读取 global/pg_control,找到 checkpoint,然后根据需要从 checkpoint 的 redo 位点向后读取并重放 WAL。
简化图如下:
text
pg_control
|
| ControlFile->checkPoint
v
checkpoint record at CKPT
|
| checkpoint.redo
v
R -----------------------> A -----------------------> B
^ ^ ^
| | |
redo start lastRec EndOfLog
开始 redo 的位置 最后一条完整 WAL record 起点 下一条 WAL record 写入位置
开源 PG 启动时会从本地 WAL 中真实读到这些信息:
text
1. 从 pg_control 找到 checkpoint record;
2. 从 checkpoint record 得到 checkpoint.redo;
3. 如果需要 crash recovery,则从 redo start 开始 replay WAL;
4. replay / scan 到 WAL 末尾,得到:
- lastRec = 最后一条完整 WAL record 的起始 LSN;
- EndOfLog = 最后一条完整 WAL record 的结束 LSN,也就是下一条 WAL 的写入位置。
然后初始化 WAL insert 状态:
text
Insert->PrevBytePos = lastRec
Insert->CurrBytePos = EndOfLog
之后新 WAL record 为:
text
last existing WAL record new WAL record
A --------------------------------> B -------------------------------> C
^ ^ ^
| | |
lastRec EndOfLog new end
new_record.xl_prev = A
也就是说,开源 PG 的 lastRec 和 EndOfLog 是通过本地 pg_wal 真实扫描 / replay 得到的。
10.3 两者对比
text
开源 PostgreSQL:
本地 pg_control + 本地 pg_wal
|
v
读取 checkpoint -> 从 redo 位点 replay/scan WAL -> 得到 lastRec 和 EndOfLog
|
v
Insert->PrevBytePos = lastRec
Insert->CurrBytePos = EndOfLog
Neon Compute:
Pageserver 生成的 pg_control + neon.signal
|
v
checkpoint.redo = basebackup LSN = B
neonLastRec = prev_lsn = A
|
v
不从本地 pg_wal replay A -> B
而是假装已经读完 A -> B
|
v
Insert->PrevBytePos = A
Insert->CurrBytePos = B
核心区别:
text
开源 PG:
A 和 B 是通过本地 WAL 真实读取 / replay / scan 得到的。
Neon:
A 由 neon.signal 提供;
B 由 Pageserver 生成的 pg_control 提供;
Compute 本地不需要有完整的 A -> B WAL 内容。
11. 总结
xlogrecovery.c 中的 NeonRecoveryRequested 分支在 Neon Compute 从 Pageserver basebackup 启动、PGDATA 中存在 neon.signal 时进入。
它的本质是:
text
用 Pageserver 给出的 basebackup LSN 和 prev_lsn,
伪造/初始化 PostgreSQL recovery 结束状态,
让 PostgreSQL 认为自己已经位于一个可以继续写 WAL 的位置。
关键对应关系是:
text
ControlFile->checkPointCopy.redo
= basebackup LSN
= 下一条 WAL record 起始位置
= Insert->CurrBytePos 的来源
neonLastRec
= prev_lsn
= 上一条 WAL record 起始位置
= Insert->PrevBytePos 的来源
后续通用代码会初始化:
c
Insert->PrevBytePos = XLogRecPtrToBytePos(endOfRecoveryInfo->lastRec);
Insert->CurrBytePos = XLogRecPtrToBytePos(EndOfLog);
在 Neon 场景下大致就是:
text
Insert->PrevBytePos = neonLastRec
Insert->CurrBytePos = ControlFile->checkPointCopy.redo
于是第一条新 WAL record 的 header 会包含:
text
xl_prev = neonLastRec
从而把 Neon Compute 新生成的 WAL 接到已有 WAL 链上。