时间线与时间线历史
时间线(timeline)是 PostgreSQL WAL 体系中最容易被忽略、却在关键时刻决定成败的概念。它给数据库的「历史」分出了分支:每次备库提升、每次 PITR 恢复后选择提升,都会产生一条新的时间线。归档目录里 00000001 与 00000002 开头的 WAL 段并存、恢复时报「时间线不存在」、pg_rewind 依赖分叉点对齐——这些现象的根源都是时间线。
很多生产事故的本质是「不认识时间线」:运维人员看到归档目录里同时有两套 WAL 段以为是异常,把旧时间线的文件当垃圾清理掉;做 PITR 时没有显式指定 recovery_target_timeline,恢复到了错误的分支上;归档脚本按「24 位十六进制」过滤文件,把 8 位的时间线历史文件漏掉了,导致后续恢复与 pg_rewind 全部失败。
本页系统讲清时间线的概念体系:它的编号规则、WAL 段文件名中的位置、历史文件的格式、如何查看当前时间线、如何在恢复与流复制中选择与跟随时间线,以及归档与多级分叉的工程化管理。时间线是 pg_rewind 的前置知识,也是理解整个故障转移善后流程的钥匙。
前置知识
建议先阅读:pg_rewind 时间线回退
基础概念
时间线相关的概念不多,但彼此咬合得很紧。先把表格里的对应关系建立起来,后面看文件名、.history 内容、控制文件输出时就能「对上号」。
| 概念 | 英文 / 记法 | 说明 |
|---|---|---|
| 时间线 | timeline | WAL 历史的分支标识,用递增整数表示,初始为 1 |
| 时间线 ID | timeline ID (TLI) | 时间线的编号,8 位十六进制出现在 WAL 文件名前缀 |
| 时间线历史文件 | timeline history file | .history 结尾的文件,记录该时间线的「出身」 |
| 父时间线 | parent timeline | 当前时间线从哪条时间线分叉而来 |
| 分叉点 | switchpoint / fork point | 分叉发生时的 WAL 位置 |
| 提升时切换 | new timeline at promote | 提升备库/完成 PITR 时创建新时间线的动作 |
| 跟随 | follow | 恢复或复制中从一条时间线切换到另一条的行为 |
recovery_target_timeline | 参数 | 恢复时选择跟随哪条时间线 |
时间线的定义
时间线可以理解为「WAL 历史的版本号」。刚 initdb 出来的集群处于时间线 1。只要集群「结束过一次恢复并开始自己产生 WAL」,就会创建新的时间线,编号在父时间线的基础上递增。
具体来说,以下动作会触发新时间线:
| 动作 | 是否产生新时间线 |
|---|---|
备库执行 pg_ctl promote / pg_promote() | 是 |
PITR 恢复中到达目标并按 recovery_target_action = promote 提升 | 是 |
PITR 恢复后直接结束不提升(shutdown / pause) | 否(提升时才产生) |
| 备库正常重启 | 否 |
| 主库正常重启 | 否 |
| 崩溃后自动恢复 | 否 |
关键区分:普通的崩溃恢复不产生新时间线,因为崩溃恢复重放的是「同一条历史」;而提升产生新时间线,因为它宣告「从此以后是一条与旧主库不同的新历史」。
时间线与 WAL 段文件名
每个 WAL 段文件名是 24 位十六进制,前三段各 8 位:
000000010000000000000001
└──┬───┘└───┬──────┘└──┬──┘
时间线 日志文件号 段号
(8 位) (8 位) (8 位)时间线 ID 占前 8 位。00000001 开头是时间线 1 的 WAL,00000002 开头是时间线 2 的 WAL。这意味着同一时刻,不同时间线的 WAL 在文件系统里天然并存、互不干扰——这正是时间线机制要解决的问题:如果提升后的新主库继续用时间线 1,归档目录里就会出现「同名文件、两份内容」的致命冲突。
时间线历史文件
每条时间线(除了初始的时间线 1)都有一个对应的 .history 文件,例如 00000002.history。它记录这条时间线的「出身」:从哪条时间线、在哪个 WAL 位置分叉而来。这个文件同时存在于归档目录与主库(或备库)的 pg_wal 中,是恢复与 pg_rewind 进行时间线交叉的依据。
时间线的「跟随」
恢复进程(PITR 或备库)重放到一条时间线的末尾时,不会停在那里,而是去读该时间线的 .history,确认「下一条时间线是谁」,然后继续重放下一条时间线的 WAL。这个行为叫跟随(follow)。跟随哪条时间线、何时跟随,由 recovery_target_timeline 参数控制。
语法与用法
时间线没有「执行语法」,它的用法体现在三类操作上:查看当前时间线、配置恢复时的时间线选择、检查文件系统中的时间线痕迹。
查看当前时间线
SQL 方式(PostgreSQL 10 及以上):
SELECT timeline_id FROM pg_control_checkpoint(); timeline_id
-------------
2
(1 row)命令行方式(适用于任何版本,也适用于实例停止状态):
pg_controldata /var/lib/postgresql/18/main | grep -i timelineLatest checkpoint's TimeLineID: 2
Latest checkpoint's PrevTimeLineID: 1第三种方式是看当前 WAL 段文件名的前 8 位:
SELECT pg_walfile_name(pg_current_wal_lsn()) AS current_walfile; current_walfile
--------------------------
0000000200000000000000A5
(1 row)00000002 即当前时间线。三种方式中,pg_control_checkpoint() 最适合放进监控脚本。
配置恢复时的时间线选择
# postgresql.conf / postgresql.auto.conf(PITR 或备库场景)
# 默认值 latest:跟随归档/目标目录中能找到的最新时间线
recovery_target_timeline = 'latest'
# 或者明确跟随某条时间线(写入具体的编号)
recovery_target_timeline = '1'
# 或者只在基础备份所属的时间线上恢复,不跨时间线跟随
recovery_target_timeline = 'current'检查文件系统中的时间线痕迹
# 归档目录中:WAL 段与历史文件并存
ls -1 /mnt/wal_archive/ | sort0000000100000000000000A4
0000000100000000000000A5
00000001.history
0000000200000000000000A5
0000000200000000000000A6
00000002.history# 查看历史文件内容
cat /mnt/wal_archive/00000002.history1 0/3000A5B8 no recovery target specified详细说明
时间线为什么必须存在
设想没有时间线的世界:主库在某个 WAL 位置被提升成了新主库,随后旧主库复活也在同一位置继续写 WAL。两份 WAL 使用完全相同的文件名(都是时间线 1、同一日志号、同一段号),但内容不同。归档目录里第二个文件会覆盖第一个,或者拒绝写入,无论哪种都是灾难——你永远无法确定归档里的那个段属于哪条历史。
时间线把「历史分支」编码进了文件名:提升后的一切写入都带新时间线前缀,归档目录里两条历史并存而不冲突。恢复时通过 .history 文件明确「这条时间线从哪里分出来」,从而既能跟随到最新历史,也能精确回到分叉前的旧历史。
一句话总结:时间线存在的唯一理由,就是让「分叉的历史」可以被安全地保存、识别和选择。
WAL 段文件名中的时间线段
WAL 文件名前三段的语义在流复制与 PITR 相关页面中已有铺垫,这里聚焦时间线段:
- 时间线 ID 用 8 位十六进制表示,实际数值是普通整数(1、2、3……),最大可表示的编号远超实际需要;
- 时间线递增时,新 WAL 段的文件名前缀整体切换(
00000001...→00000002...); - 分叉点附近的文件名有讲究:分叉点所在的段文件,新旧时间线各自都有一份,内容在分叉点之前相同、之后不同。上例中
0000000100000000000000A5与0000000200000000000000A5的并存就是这个原因。
通过文件名快速判断时间线,是最朴素的运维技巧:
# 列出归档目录中所有时间线编号
ls /mnt/wal_archive/ | cut -c1-8 | sort -u00000001
00000002.history 文件的内容格式
时间线历史文件是纯文本,每行三列,以制表符分隔:
parentTLI<TAB>switchpoint<TAB>reason| 列 | 含义 |
|---|---|
parentTLI | 父时间线编号,说明本时间线从哪条分出来 |
switchpoint | 分叉点的 WAL 位置(LSN) |
reason | 分叉原因的文字说明,如 no recovery target specified(提升时未指定恢复目标) |
多条记录(多级时间线)时从上到下按父时间线递增排列。以 # 开头的行是注释。一个三级时间线的 00000003.history 可能长这样:
1 0/3000A5B8 no recovery target specified
2 0/40001A60 promotion解读:时间线 3 的直接父级是时间线 2,分叉点在 0/40001A60;而时间线 2 又是从时间线 1 的 0/3000A5B8 分出来的。恢复进程正是靠逐级读这些文件,才能把「时间线 3 的完整祖先链」拼出来。
.history 文件在提升的那一刻生成并立即进入归档,且永远不会被改写。这是它作为「历史锚点」可靠性的来源。
pg_control 与 pg_controldata 中的时间线
控制文件(pg_control)中记录了最近一次检查点的时间线信息,pg_controldata 可以查看:
pg_controldata /var/lib/postgresql/18/main | grep -i timelineLatest checkpoint's TimeLineID: 2
Latest checkpoint's PrevTimeLineID: 1两个字段的含义:
| 字段 | 含义 |
|---|---|
Latest checkpoint's TimeLineID | 最近检查点所在的时间线,即当前时间线 |
Latest checkpoint's PrevTimeLineID | 检查点之前的时间线(用于崩溃恢复时判断是否发生了时间线切换) |
PrevTimeLineID 的一个实际用途:崩溃恢复时,如果检查点之后发生过来时间线切换,恢复进程需要知道上一条时间线是谁,以便正确处理切换边界处的 WAL。
实例停止时也能用 pg_controldata 查看时间线,这在「数据目录不能启动但需要判断状态」的排障场景里非常有用。
pg_control_checkpoint() 函数
PostgreSQL 10 起提供了 SQL 函数 pg_control_checkpoint(),把控制文件中检查点相关的内部信息暴露出来:
SELECT timeline_id, prev_timeline_id, checkpoint_lsn
FROM pg_control_checkpoint(); timeline_id | prev_timeline_id | checkpoint_lsn
-------------+------------------+----------------
2 | 1 | 0/3000C100
(1 row)这个函数的价值在于把时间线纳入了 SQL 监控体系:监控脚本可以像采集 pg_stat_replication 一样采集时间线,在提升事件发生时自动记录「何时、从几变成几」。注意它是 PostgreSQL 10+ 才有的函数,老版本只能用 pg_controldata。
recovery_target_timeline 的取值与选择
recovery_target_timeline 决定恢复时跟随哪条时间线,三个取值对应三种策略:
| 取值 | 行为 | 典型场景 |
|---|---|---|
latest(默认) | 跟随归档中能找到的最新时间线 | 绝大多数常规恢复:恢复到「已知历史的最新分支」 |
具体编号(如 '1') | 只恢复到指定时间线上的目标点 | 需要回到分叉之前的旧历史(例如分叉后的数据是坏数据) |
current | 不跟随,只恢复基础备份所属时间线 | 基础备份来自旧时间线,且明确不想跨到新时间线 |
选错时间线的典型事故:主库被误操作写坏后做 PITR,操作者用默认 latest,结果恢复到了「写坏数据之后又被新主库提交」的那条时间线上——表面恢复成功,坏数据还在。正确的做法是显式指定分叉前的时间线编号,把恢复锚定在干净的历史上。
latest 并不总是「正确」
recovery_target_timeline = 'latest' 的语义是「归档里编号最大的时间线」,而不是「数据最完好的时间线」。归档里最新时间线上的数据未必是你想要的。涉及数据修复的 PITR,建议总是显式指定时间线。
流复制中的时间线跟随与传播
备库跟随主库的时间线,有一套自动机制:
- 正常跟随:备库重放主库推送的 WAL,时间线与主库一致;
- 跟随切换:当主库发生提升(时间线递增)时,备库通过复制协议向主库请求新的时间线历史文件,自动切换到新时间线继续重放。这就是「主库提升后,其余备库无需重建即可继续跟随」的机制基础;
- 重新接入:旧主库 rewind 后作为备库重接入,pg_rewind 已把它的数据目录对齐到新时间线,启动后正常跟随。
时间线信息在复制协议中是显式传递的:IDENTIFY_SYSTEM 命令的返回中就包含 timeline 字段,备库用它确认自己与源端是否在同一条历史线上:
psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" systemid | timeline | xlogpos | dbname
---------------------+----------+-----------+--------
7230697861213143552 | 2 | 0/3000C100 |
(1 row)备库重放日志中也会出现时间线切换的痕迹:
LOG: fetching timeline history file for timeline 2 from primary server
LOG: new timeline 2 is active, resuming standby from 0/3000A5B8时间线切换事件的日志判读
时间线切换在日志中留有一套固定的痕迹词汇,排障时按图索骥即可还原事件序列。备库视角的典型日志序列:
LOG: fetching timeline history file for timeline 2 from primary server
LOG: new timeline 2 is active, resuming standby from 0/3000A5B8
LOG: restarted WAL streaming at 0/3000A5B8 on timeline 2提升时刻(新主库视角)与 PITR 恢复(归档恢复视角)的日志又各有特征:
# 提升时刻(新主库)
LOG: selected new timeline ID: 2
LOG: archive recovery complete
# 归档恢复 + 提升(PITR)
LOG: restored log file "0000000100000000000000A5" from archive
LOG: selected new timeline ID: 3判读要点:
selected new timeline ID: N是「本实例将成为时间线 N」的宣告,只出现在提升路径上;new timeline N is active出现在跟随方(备库),表示它接受了对端的时间线切换;- 如果备库日志反复出现「请求时间线历史失败」「时间线不存在」类错误,优先检查归档 / 源端是否缺
.history文件; - 排查脑裂疑点时,把所有节点的日志按时间排列,找出
selected new timeline ID出现的次数——出现两次(未被工具或人工记录在案)就是重大警报。
归档目录中的多时间线并存
一次故障转移之后,归档目录会同时保存至少两条时间线的 WAL。这是正常状态,不要清理:
| 文件 | 归属 | 作用 |
|---|---|---|
0000000100000000...(时间线 1 的段) | 旧历史 | 回到分叉前恢复的依据;新时间线祖先链的一环 |
00000001.history | 时间线 1 的出身记录 | 通常不存在(初始时间线无历史文件),存在则说明更早的分叉 |
00000002...(时间线 2 的段) | 当前历史 | 常规恢复使用 |
00000002.history | 时间线 2 的出身记录 | 恢复与 pg_rewind 交叉时间线的依据 |
清理旧时间线 WAL 的问题在于:新时间线的「血缘」经过旧时间线。虽然日常恢复通常不需要旧时间线的完整 WAL,但以下场景会用到:
- 在新时间线上恢复到很早的目标点,且该点恰好落在分叉点之前;
- 时间线历史文件本身引用了旧时间线;
- pg_rewind 需要双方时间线历史可交叉。
稳妥的运维策略:旧时间线的 WAL 保留策略与当前时间线一致,或至少永久保留全部 .history 文件。
分叉点与 pg_rewind
时间线历史文件里记录的 switchpoint,正是 pg_rewind 用来定位分叉点的核心数据。pg_rewind 把源库与目标库的时间线历史交叉比对:
源库(时间线 2): 1 ──●────────────────►
└── 2 ──────B──────►
目标库(时间线 1 + 本地写):1 ──●────A────A──►
↑
分叉点 0/3000A5B8(来自 00000002.history)pg_rewind 读取 00000002.history,得知时间线 2 从时间线 1 的 0/3000A5B8 分出,于是把目标库回拨到该位置。如果 .history 文件缺失(归档脚本漏归档、手工清理),pg_rewind 无法交叉,只能报错——这就是为什么归档 .history 文件是 pg_rewind 可用性的隐性前提。
时间线分叉的更多机制细节,见上一页 pg_rewind 时间线回退。
pg_waldump 查看时间线
pg_waldump 按 WAL 段文件解析日志内容,时间线信息蕴含在文件名中,解析输出的每条记录都属于该文件名前缀对应的时间线:
pg_waldump -p /mnt/wal_archive 0000000100000000000000A5rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/0A500028, prev 0/0A500000, desc: RUNNING_XACTS nextXid 743 latestCompletedXid 742 oldestRunningXid 743
rmgr: Transaction len (rec/tot): 46/ 46, tx: 743, lsn: 0/0A500060, prev 0/0A500028, desc: COMMIT 2026-09-30 10:00:00.123456+08
pg_waldump: fatal: error in WAL record at 0/0A500090: no record at 0/0A500090最后的 fatal 值得注意:如果解析的是「分叉点所在段」,段内分叉点之后的部分对时间线 1 而言没有记录(记录属于时间线 2),pg_waldump 走到该位置会报「没有记录」——这不是文件损坏,而是读到了分叉边界。理解这一点可以避免把分叉边界误判为 WAL 损坏。
排查时间线问题时,pg_waldump 的典型用法是:确认「旧时间线在分叉点之后是否还有业务提交记录」(判断旧主库是否接受过写入),这直接决定善后策略是 rewind 还是数据比对。
归档脚本对 .history 文件的处理
这是时间线体系里最容易被工程实践忽略的一环。archive_command 收到的 %f 参数既可能是 24 位的 WAL 段文件名,也可能是 8 位的时间线历史文件名(如 00000002.history)。使用通用写法的归档命令天然覆盖两种情况:
# 正确:不关心文件名形态,来了就归档
archive_command = 'test ! -f /mnt/wal_archive/%f && cp %p /mnt/wal_archive/%f'危险的写法是按文件名长度过滤:
# 错误示范:只归档 24 位文件名,.history 文件会被拒绝归档
# (test 判断导致 .history 归档失败,WAL 归档链路随之卡住).history 归档失败的连锁反应比 WAL 段归档失败更隐蔽:时间线历史文件很小、更新频率极低,失败可能几个月都无人察觉,直到某次恢复或 pg_rewind 需要它时才爆发。验收归档脚本时,必须实际验证过一次 .history 文件的归档(最简单的办法就是提升一次备库,然后检查归档目录)。
多级时间线的管理复杂性
集群经历多次故障转移后,时间线会形成一条链:1 → 2 → 3 → 4……每次提升都在链条上增加一环。管理上的复杂性体现在:
- 历史文件逐级引用:时间线 4 的恢复需要
00000002.history、00000003.history、00000004.history全部可用,缺一环则祖先链断裂; - 同一段文件可能存在多份:分叉点附近的段文件在相邻时间线上各有副本,归档容量随之增长;
- 时间线编号与「当前活跃历史」脱节:归档里编号最大的时间线未必是实际业务所在的分支(例如某次误提升后回退);
- 自动化工具的状态记录成为权威:多级分叉之后,人工已经很难凭目录推断历史,Patroni / repmgr 的集群状态记录是更可靠的依据。
应对策略:把「时间线变更记录」作为运维文档的一部分(何时提升、提升到几、为什么),并定期核对归档中 .history 文件链的完整性。
实战示例
示例一:建立时间线基线
在新集群交付时记录初始状态,作为后续所有时间线变更的对照。
SELECT timeline_id, prev_timeline_id, checkpoint_lsn
FROM pg_control_checkpoint(); timeline_id | prev_timeline_id | checkpoint_lsn
-------------+------------------+----------------
1 | 1 | 0/3000C100
(1 row)初始集群 timeline_id = 1、prev_timeline_id = 1。把这个状态记进交付文档。
示例二:提升备库,观察时间线变化
# 提升前:确认是备库
psql -c "SELECT pg_is_in_recovery(), timeline_id FROM pg_control_checkpoint();" pg_is_in_recovery | timeline_id
-------------------+-------------
t | 1
(1 row)pg_ctl promote -D /var/lib/postgresql/18/mainwaiting for server to promote.... done
server promoted-- 提升后再查
SELECT timeline_id, prev_timeline_id FROM pg_control_checkpoint(); timeline_id | prev_timeline_id
-------------+------------------
2 | 1
(1 row)同时观察日志:
LOG: selected new timeline ID: 2
LOG: archive recovery completetimeline_id 从 1 变为 2,prev_timeline_id 记住了出身。这就是提升事件的完整时间线足迹。
示例三:解读 .history 文件
# 提升后,数据目录与归档目录中出现 00000002.history
cat /var/lib/postgresql/18/main/pg_wal/00000002.history1 0/3000A5B8 no recovery target specified解读:时间线 2 从时间线 1 的 0/3000A5B8 分出;分叉原因是提升时未指定恢复目标(普通提升的默认原因)。这个 LSN 同时也是 pg_rewind 未来对齐时要用的分叉点,值得记录。
# 确认归档目录中也已归档
ls -l /mnt/wal_archive/00000002.history-rw------- 1 postgres postgres 60 Sep 30 10:12 /mnt/wal_archive/00000002.history如果这步发现归档目录里没有 .history,立即修复归档脚本——这是最容易埋雷的地方。
示例四:PITR 中显式指定时间线
场景:主库在时间线 2 上被写坏了业务数据,需要恢复到分叉之前(时间线 1)的干净时刻。
# 停实例、恢复基础备份之后:
touch /var/lib/postgresql/18/main/recovery.signal# postgresql.auto.conf
restore_command = 'cp /mnt/wal_archive/%f %p'
recovery_target_time = '2026-09-30 09:30:00+08'
# 关键:目标点在时间线 1 上,显式指定,避免跟随 latest 落到时间线 2
recovery_target_timeline = '1'
recovery_target_action = 'promote'pg_ctl start -D /var/lib/postgresql/18/mainLOG: starting point-in-time recovery to 2026-09-30 09:30:00+08
LOG: restored log file "0000000100000000000000A4" from archive
LOG: redo starts at 0/0A400060
LOG: recovery stopping before commit of transaction 743, time 2026-09-30 09:30:12+08
LOG: selected new timeline ID: 3
LOG: archive recovery complete注意恢复日志:虽然目标点在时间线 1 上,提升后产生的是时间线 3(编号在历史最大值之上继续递增),而不是复用时间线 2——时间线编号永不复用,这保证任何历史都能被唯一标识。
示例五:核对归档中的时间线链完整性
# 列出归档中所有时间线编号及其历史文件
for tli in $(ls /mnt/wal_archive/ | cut -c1-8 | sort -u); do
echo "timeline: $tli"
ls /mnt/wal_archive/${tli}.history 2>/dev/null || echo " (history file: not present)"
donetimeline: 00000001
(history file: not present)
timeline: 00000002
(history file: present)
timeline: 00000003
(history file: present)健康状态的特征:初始时间线(1)可以没有历史文件,其余每条时间线都必须有。任何一个非初始时间线缺 .history,都要立即从数据目录 pg_wal 中补一份进归档(提升后的实例 pg_wal 里保留着原始副本)。
示例六:用时间线判断旧主库是否接受过写入
故障转移善后时,最关键的问题之一是「旧主库分叉之后写没写过数据」。时间线视角的验证方法:
# 在旧主库(停机状态)上列出 pg_wal 中时间线 1 的最后几个段
ls /var/lib/postgresql/18/main/pg_wal/ | grep ^00000001 | sort | tail -30000000100000000000000A5
0000000100000000000000A6# 用 pg_waldump 检查分叉点之后是否还有业务提交
pg_waldump -p /var/lib/postgresql/18/main/pg_wal 0000000100000000000000A6 | \
grep -c "COMMIT" || echo "no commits after fork"如果分叉点之后没有任何 COMMIT 记录,说明旧主库只提交了零星事务甚至什么都没写,pg_rewind 丢弃的量可以忽略;如果有大量提交,就要评估是否需要先抢救数据。
示例七:停机实例的时间线取证
场景:一台来历不明的旧实例数据目录需要判断「它停在什么时候、哪条时间线上」,但不确定它还能不能安全启动。
# 无需启动实例,pg_controldata 直接读控制文件
pg_controldata /data/old_instance | grep -E "Timeline|checkpoint location|Database cluster state"Database cluster state: shut down
Latest checkpoint location: 0/2A000060
Latest checkpoint's TimeLineID: 3
Latest checkpoint's PrevTimeLineID: 2解读:集群状态 shut down 表示干净关闭,可以放心处理;它停在时间线 3、上一条时间线是 2——说明这条历史经历过两次提升。拿这些值与归档目录的时间线清单、运维事件记录三方核对,就能确定这台实例在历史中的位置。这是处理「历史遗留环境」时风险最低的取证方式:全程只读,不触碰数据文件。
注意事项
提升必然产生新时间线,编号只增不减、永不复用。 恢复到旧时间线再提升,产生的是「最大编号 + 1」的新时间线,而不是复用中间的编号。
.history 文件永远不会被改写。 它是历史锚点,任何工具都不应修改它;如果发现 .history 内容可疑,应从可信备份恢复而不是手工编辑。
归档脚本必须覆盖 .history 文件。 通用写法(按 %f 归档,不按文件名形态过滤)天然正确;任何按文件名长度/正则过滤的归档脚本都要重新审视。
不要清理旧时间线的 WAL 段。 它们是恢复到分叉前历史的依据,也是时间线祖先链的一部分。清理策略应与当前时间线的 WAL 完全一致。
recovery_target_timeline = 'latest' 是「编号最大」而不是「数据最完好」。 数据修复类 PITR 必须显式指定时间线编号,并核对目标点确实落在所选时间线的 WAL 覆盖范围内。
备库自动跟随主库的时间线切换,但前提是它能拿到 .history。 流复制下历史文件由备库向主库请求;归档恢复下历史文件必须已在归档目录中。两种来源都要保证。
pg_waldump 走到分叉边界报「无记录」不是损坏。 分叉点所在段文件在两条时间线上各有一份,读旧时间线副本走到边界后的区域属于另一条时间线,属正常现象。
pg_control_checkpoint() 是 PostgreSQL 10+ 的函数。 更早版本用 pg_controldata 读取 Latest checkpoint's TimeLineID,两者数值一致。
监控体系应采集时间线。 时间线突然 +1 通常意味着发生过提升(计划内或计划外),这是值得告警的事件;把时间线与 pg_postmaster_start_time()、提升日志对照,可以还原事件序列。
同一集簇的时间线编号是全局的。 所有备库、级联下游、归档共享同一条时间线历史。如果某个「备库」的时间线编号与主库不一致且更高,警惕它是不是被误提升过——这是脑裂的典型信号。
时间线取证优先用只读手段。 面对可疑实例,先 pg_controldata 再考虑启动;一次带着错误时间线的意外启动,制造的问题比它回答的问题更多。
时间线机制与 PITR 是一体的。 recovery_target_timeline 的选择、归档中多时间线的清理策略、.history 的保存,都直接影响 PITR 的可用性;变更其中任何一项前,先回顾本页与 PITR 章节的相关约束。
最佳实践
把时间线编号纳入集群交付文档与监控面板。 每次变更(提升、PITR)留痕:时间、原因、从几到几。
对时间线变更设置告警。
timeline_id发生变化即告警,由值班确认是否为计划内操作。计划外的提升是最需要立刻知道的事件。归档命令使用通用写法,并用一次真实提升验证
.history归档。 验收归档链路时,.history文件要与 WAL 段一起检查。数据修复类 PITR 显式指定
recovery_target_timeline。 不依赖latest,并在恢复前用归档目录文件名核对目标点所在的时间线。定期核对归档中时间线链的完整性。 每条非初始时间线都要有
.history,祖先链逐级可查。旧时间线 WAL 与
.history的保留策略等同于当前时间线。 若必须清理,清理决策要经过评审并留档。故障转移 Runbook 中加入「记录分叉点 LSN」一步。 提升后从
0000000N.history读出分叉点,写进事件记录,pg_rewind 与后续审计都要用它。用
pg_waldump检查旧主库分叉后的提交记录,量化善后风险。 比凭感觉判断「应该没写多少」可靠得多。多级时间线(3 级以上)的集群考虑择机「重建洗干净」。 长链条的时间线管理成本高、认知负担重,在业务低峰期用
pg_basebackup重建备库、重组归档,能显著降低后续维护风险。自动化工具集群以工具的集群状态记录为准。 Patroni / repmgr 维护的节点与时间线状态比手工推断可靠,手工操作前先查工具状态。
测试环境模拟一次完整的时间线演化。 initdb → 打底 → 提升一次 → PITR 一次 → 再提升,把整套时间线痕迹(文件名、
.history、日志行)摸熟,生产上遇到才不慌。向团队普及时间线知识。 时间线相关的误操作(清理旧 WAL、归档过滤 .history)往往出自「不知道这是正常现象」的同事,一次内部分享的收益远大于事后抢修。
下一节
继续学习:自动故障转移方案
参考链接
- PostgreSQL 官方文档:Recovery Configuration(recovery_target_timeline)
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)
- PostgreSQL 官方文档:WAL Internals(WAL 文件布局)
- PostgreSQL 官方文档:pg_controldata
- PostgreSQL 官方文档:pg_waldump
- PostgreSQL 官方文档:System Administration Functions(pg_control_checkpoint)
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:pg_rewind
- PostgreSQL 官方文档:Write Ahead Log — Configuration Parameters
- PostgreSQL 官方文档:Streaming Replication Protocol