PostgreSQL 内核级故障处理全解:从信号机制到 WAL 恢复的完整链路
日期: 2026-06-12 适用数据库: PostgreSQL 17.x 阅读时间: 约 60 分钟 难度等级: ⭐⭐⭐⭐⭐ (专家级)
目录
背景与动机
PostgreSQL 作为企业级关系型数据库,其高可用性和数据一致性依赖于一套精心设计的故障检测与恢复机制:
典型故障场景:
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ 进程被 kill │ │ Backend异常 │ │ 网络连接断开 │ │
│ │ kill -9 PID │ │ Segmentation│ │ 客户端超时 │ │
│ │ │ │ Fault │ │ 网络分区 │ │
│ └──────┬──────┘ └──────┬──────┘ └───────┬─────────┘ │
│ │ │ │ │
│ └─────────────────┼───────────────────┘ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ PostgreSQL 故障检测 │ │
│ │ 与恢复系统 │ │
│ └───────────┬────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 运行时检测 → 信号处理 + 状态监控 │ │
│ │ 2. 异常处理 → 死锁检测 + 锁超时 + 资源清理 │ │
│ │ 3. 启动检测 → 冲突检测 + 一致性验证 │ │
│ │ 4. 恢复机制 → WAL 重放 + Checkpoint 恢复 │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
核心设计目标:
✅ 数据一致性: ACID 特性的严格保证
✅ 快速检测: 毫秒级故障发现能力
✅ 优雅降级: 从 Smart Shutdown 到 Immediate Shutdown 的多级策略
✅ 自动恢复: 无需人工干预的 Crash Recovery
文章亮点:
✅ 源码级深度: 直接分析 postmaster.c, deadlock.c, proc.c, startup.c 核心文件
✅ 完整链路: 从信号产生到资源释放的全流程追踪
✅ 理论结合: Wait-for-Graph 算法、ARIES 恢复理论的实际应用
✅ 实战图示: 详细的流程图和状态转换图
✅ 生产指导: 针对不同场景的配置优化建议
一、运行时故障检测机制
1.1 信号驱动架构
PostgreSQL 采用事件驱动的信号架构作为整个故障检测系统的基石:
// src/backend/postmaster/postmaster.c (第 550-558 行)
// Postmaster 主进程的信号处理器注册
pqsignal(SIGHUP, handle_pm_reload_request_signal); // 配置重载
pqsignal(SIGINT, handle_pm_shutdown_request_signal); // 快速关闭 (Ctrl+C)
pqsignal(SIGQUIT, handle_pm_shutdown_request_signal); // 立即关闭
pqsignal(SIGTERM, handle_pm_shutdown_request_signal); // 优雅关闭
pqsignal(SIGALRM, SIG_IGN); // 忽略
pqsignal(SIGPIPE, SIG_IGN); // 忽略 (客户端断开)
pqsignal(SIGUSR1, handle_pm_pmsignal_signal); // 子进程通信
pqsignal(SIGUSR2, dummy_handler); // 保留给子进程
pqsignal(SIGCHLD, handle_pm_child_exit_signal); // ⭐ 子进程退出检测
信号处理的层次结构:
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL 信号体系 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: 操作系统内核 │
│ ├── 进程终止 → SIGCHLD (发送给父进程) │
│ ├── Socket 断开 → SIGPIPE (被忽略) │
│ ├── 定时器到期 → SIGALRM (锁超时) │
│ └── 用户操作 → SIGTERM/SIGINT/SIGQUIT │
│ │
│ Layer 2: Postmaster 主进程 │
│ ├── handle_pm_child_exit_signal() → 处理子进程退出 │
│ ├── handle_pm_shutdown_request_signal() → 关闭请求处理 │
│ └── handle_pm_reload_request_signal() → 配置热重载 │
│ │
│ Layer 3: Backend/辅助进程 │
│ ├── die() handler → SIGQUIT 快速死亡 │
│ ├── quickdie() handler → SIGABRT 紧急退出 │
│ ├── StatementCancelHandler() → 查询取消 │
│ └── DeadLockTimeoutHandler() → 死锁检测触发 │
│ │
│ Layer 4: 应用层响应 │
│ ├── process_pm_child_exit() → 清理子进程状态 │
│ ├── process_pm_shutdown_request() → 执行关闭逻辑 │
│ └── HandleChildCrash() → 崩溃后的紧急处理 │
│ │
└─────────────────────────────────────────────────────────────────┘
信号安全原则(POSIX 标准):
// 信号处理器的关键约束:
// 1. 只能调用 async-signal-safe 函数
// 2. 不能使用 malloc/free (可能持有锁)
// 3. 不能访问非 volatile 全局变量 (编译器优化问题)
// PostgreSQL 的解决方案: 使用 volatile flag + Latch 通知
static volatile sig_atomic_t pending_pm_child_exit = false;
static void
handle_pm_child_exit_signal(SIGNAL_ARGS)
{
int save_errno = errno;
// 只设置标志位,不执行任何复杂逻辑!
pending_pm_child_exit = true;
// 唤醒主循环
SetLatch(MyLatch);
errno = save_errno;
}
1.2 子进程崩溃检测(SIGCHLD)
这是 PostgreSQL 最核心的运行时故障检测机制,实现在 [postmaster.c]中。
完整的子进程生命周期管理:
// postmaster.c 第 2510-2556 行
static void
process_pm_child_exit(void)
{
int pid; /* process id of dead child process */
int exitstatus; /* its exit status */
/*
* 循环处理所有已退出的子进程 (使用 WNOHANG 非阻塞方式)
* 这确保了我们不会遗漏任何子进程的退出事件
*/
while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)
{
/*
* 根据子进程类型进行不同的处理:
* - Startup Process: 数据库启动/恢复的核心进程
* - BgWriter/Checkpointer/WalWriter: 后台写入进程
* - Backend: 客户端连接进程
* - Auxiliary: 辅助进程 (autovacuum, walreceiver 等)
*/
// ... 子进程识别逻辑 (详见下文)
/*
* 判断退出是否为"崩溃":
* EXIT_STATUS_0: 正常退出 (exit code 0)
* EXIT_STATUS_1: FATAL 级别错误退出 (预期内的严重错误)
* 其他: 未预期的崩溃 (segmentation fault, abort 等)
*/
if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
{
HandleChildCrash(pid, exitstatus, _("untracked child process"));
}
}
/* 触发状态机转换 */
PostmasterStateMachine();
}
子进程分类处理策略:
process_pm_child_exit() 处理流程:
waitpid(-1, &status, WNOHANG)
│
▼
┌───────────────────────────────────────────────────┐
│ 识别子进程类型 │
├───────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Startup Process? │──Yes→ 检查恢复状态 │
│ └────────┬────────┘ ├─ 正常完成 → 进入 PM_RUN │
│ │No └─ 异常退出 → CRASHED! │
│ ▼ │
│ ┌─────────────────┐ │
│ │ BgWriter? │──Yes→ 非0退出 → CRASHED! │
│ └────────┬────────┘ │
│ │No │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Checkpointer? │──Yes→ 关键进程! │
│ └────────┬────────┘ ├─ 正常 → OK │
│ │No └─ 异常 → CRASHED! │
│ ▼ │
│ ┌─────────────────┐ │
│ │ WalWriter? │──Yes→ 非0退出 → CRASHED! │
│ └────────┬────────┘ │
│ │No │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Backend? │──Yes→ CleanupBackend() │
│ └────────┬────────┘ ├─ 崩溃 → HandleChildCrash│
│ │No └─ 正常 → 记录日志 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ BG Worker? │──Yes→ 类似 Backend 处理 │
│ └────────┘
│ │
└───────────────────────────────────────────────────┘
崩溃处理核心函数 HandleChildCrash():
// postmaster.c 第 2790-2811 行
static void
HandleChildCrash(int pid, int exitstatus, const char *procname)
{
/*
* 避免重复处理:
* - 如果已经处于 FatalError 状态,说明之前已经处理过崩溃
* - 如果正在进行 ImmediateShutdown,直接返回
*/
if (FatalError || Shutdown == ImmediateShutdown)
return;
/* 记录崩溃日志 */
LogChildExit(LOG, procname, pid, exitstatus);
ereport(LOG,
(errmsg("terminating any other active server processes")));
/*
* ⚠️ 关键操作: 进入错误处理模式
* 这将触发后续的紧急关闭流程
*/
HandleFatalError(PMQUIT_FOR_CRASH, true);
}
HandleFatalError 的紧急响应机制:
// postmaster.c 内部实现 (简化版)
static void
HandleFatalError(PMQuitMode quitMode, bool crash_recovery_needed)
{
/*
* 设置全局错误标志,阻止新的连接接受
* 所有后续的 ServerLoop() 调用都会检查这个标志
*/
FatalError = true;
connsAllowed = false;
/*
* 向所有子进程发送 SIGQUIT (quickdie handler)
* 这会导致所有 backend 执行快速退出路径:
* 1. 释放持有的 LWLock
* 2. 回滚当前事务
* 3. 清理 PGPROC 结构
* 4. 调用 _exit(1) 立即终止
*/
SignalChildren(SIGQUIT);
/*
* 更新 PMState 为 PM_WAIT_DEAD_END
* 等待所有子进程退出后进行恢复或关闭
*/
UpdatePMState(PM_WAIT_DEAD_END);
/* 如果需要崩溃恢复,标记该需求 */
if (crash_recovery_needed && pmState < PM_WAIT_BACKENDS)
{
StartupStatus = STARTUP_CRASHED;
}
}
1.3 网络连接断开检测
PostgreSQL 在多个层面实现了网络断开的检测:
层次 1: 操作系统层面(SIGPIPE 忽略)
// postmaster.c 第 555 行
pqsignal(SIGPIPE, SIG_IGN);
/*
* 为什么忽略 SIGPIPE?
*
* 当客户端突然断开连接时:
* - 如果正在向 socket 写入数据,OS 会发送 SIGPIPE
* - 默认行为是终止进程 (这太激进了!)
* - PostgreSQL 选择忽略它,而是通过 write() 返回值检测
*
* 这是 Unix 网络编程的标准最佳实践
*/
层次 2: libpq/pqcomm 层面(I/O 错误检测)
// src/backend/libpq/be-secure.c 或 pqcomm.c (概念示例)
/*
* 每次 socket I/O 操作后都检查返回值
*/
static int
secure_write(Port *port, const void *ptr, size_t len)
{
ssize_t n;
n = send(port->sock, ptr, len, 0);
if (n < 0)
{
int saved_errno = errno;
switch (saved_errno)
{
case ECONNRESET: /* 连接被对方重置 */
case EPIPE: /* 断开的管道 */
/*
* 客户端已经断开连接
* 设置中断标志,让主循环处理清理工作
*/
InterruptedByClientDisconnect = true;
return -1;
case EAGAIN: /* 非阻塞模式下的暂时不可用 */
#if EAGAIN != EWOULDBLOCK
case EWOULDBLOCK:
#endif
return 0; /* 稍后重试 */
default:
return -1; /* 其他错误 */
}
}
return n;
}
层次 3: Keepalive 和超时机制
-- postgresql.conf 中的相关配置
tcp_keepalives_idle = 60 -- 发送 keepalive 探测前空闲秒数
tcp_keepalives_interval = 30 -- 探测间隔
tcp_keepalives_count = 8 -- 最大探测次数 (总超时 ≈ 4分钟)
-- 连接级别设置
SET tcp_keepalives_idle = 30; -- 更积极的检测
SET tcp_keepalives_count = 5; -- 更快的失败判定
网络断开检测在查询处理中的嵌入位置:
Backend Main Loop (postgres.c):
┌─────────────────────────────────────────────────────────────┐
│ │
│ for (;;) { │
│ /* │
│ * Step 1: 读取查询 │
│ */ │
│ if (socket_read(query) == ERROR) { │
│ if (errno == ECONNRESET) { │
│ goto client_disconnect_cleanup; │
│ } │
│ } │
│ │
│ /* │
│ * Step 2: 解析和执行 │
│ */ │
│ execute_query(query); │
│ │
│ /* │
│ * Step 3: 发送结果 │
│ */ │
│ if (send_result(result) == EPIPE) { │
│ goto client_disconnect_cleanup; │
│ } │
│ } │
│ │
│client_disconnect_cleanup: │
│ 1. 中断当前事务 (TransactionAbort) │
│ 2. 释放所有持有的锁 (ProcReleaseLocks) │
│ 3. 清理 PGPROC 入口 (ProcKill) │
│ 4. 关闭 socket │
│ 5. 退出进程 (_exit(0)) │
│ │
└─────────────────────────────────────────────────────────────┘
1.4 Postmaster 状态机
Postmaster 通过一个有限状态机来管理系统生命周期中的各种状态转换:
Postmaster State Machine (PMState):
┌──────────────┐
│ PM_INIT │ ← 初始化阶段
└──────┬───────┘
│ CreateSharedMemoryAndSemaphores()
▼
┌──────────────┐
│ PM_STARTUP │ ← 启动 Startup Process
└──────┬───────┘
│ Startup 完成 / 恢复完成
▼
┌────────────────────────┐
│ PM_RECOVERY │ ← 归档恢复 (PITR)
│ PM_HOT_STANDBY │ ← 流复制备库
└───────────┬────────────┘
│ 恢复完成 / promote
▼
┌────────────────────────┐
│ PM_RUN │ ← 正常运行状态 ★
└───────────┬────────────┘
│ 收到 Shutdown 请求
▼
┌────────────────────────┐
│ PM_STOP_BACKENDS │ ← 停止接受新连接
└───────────┬────────────┘
│ 所有 Backend 已退出
▼
┌────────────────────────┐
│ PM_WAIT_BACKENDS │ ← 等待辅助进程退出
└───────────┬────────────┘
│ 辅助进程退出
▼
┌────────────────────────┐
│ PM_WAIT_DEAD_END │ ← 最后清理
└───────────┬────────────┘
│ 清理完成
▼
┌──────────────┐
│ PM_NO_CHILDREN │ ← 准备退出
└──────┬───────┘
│ ExitPostmaster()
▼
┌──────────────┐
│ (退出) │
└──────────────┘
特殊状态转换 (故障场景):
PM_RUN ──[子进程崩溃]──→ PM_WAIT_DEAD_END (跳过中间步骤!)
│ │
│ ├── FatalError = true
│ ├── SignalChildren(SIGQUIT)
│ └── StartupStatus = CRASHED (如需恢复)
│
└──[Smart Shutdown]──→ PM_STOP_BACKENDS → PM_WAIT_BACKENDS → ...
状态机的实现细节:
// postmaster.c 第 4534-4620 行 (简化版)
static void
PostmasterStateMachine(void)
{
if (pmState == PM_INIT)
{
/*
* 初始化完成后:
* 1. 创建共享内存
* 2. 创建 PID 文件
* 3. 启动 Startup Process
*/
pg_usleep(100000L); /* 100ms delay */
/* 创建共享内存段 */
CreateSharedMemoryAndSemaphores();
/* 写入 PID 文件 */
AddToDataDirLockFile(LOCK_FILE_LINE_PM_PID, getpid());
UpdatePMState(PM_STARTUP);
}
if (pmState == PM_STARTUP)
{
/*
* 等待 Startup Process 完成:
* - 正常启动: 进入 PM_RUN
* - PITR 恢复: 进入 PM_RECOVERY / PM_HOT_STANDBY
* - 崩溃恢复: StartupStatus = STARTUP_CRASHED
*/
if (StartupStatus == STARTUP_NOT_RUNNING && !FatalError)
{
UpdatePMState(PM_RUN);
/* 启动 bgwriter, checkpointer 等 */
}
}
if (pmState == PM_RUN)
{
/*
* 正常运行时的状态转换:
* - 检查 shutdown 请求
* - 检查 FatalError 标志
* - 重启失败的辅助进程
*/
if (FatalError)
{
/* 紧急转移到等待状态 */
UpdatePMState(PM_WAIT_DEAD_END);
}
else if (Shutdown >= SmartShutdown)
{
UpdatePMState(PM_STOP_BACKENDS);
}
}
/* ... 其他状态处理 ... */
}
二、异常处理机制
2.1 死锁检测算法
PostgreSQL 实现了基于 Wait-for-Graph (等待图) 的死锁检测算法,这是经典的操作系统理论在实际数据库系统中的应用。
理论基础:Wait-for-Graph
Wait-for-Graph 定义:
G = (V, E)
V = {P₁, P₂, ..., Pₙ} (进程集合)
E = {(Pᵢ, Pⱼ)} (边集合: Pᵢ 等待 Pⱼ 持有的锁)
死锁条件 (充要条件):
Wait-for-Graph 中存在环 (Cycle) ⇔ 系统存在死锁
示例:
Process A ──holds──→ Lock X ──waits──→ Process B
│ │
│ ┌────holds────┐ │
│ ↓ ↑ │
│ Lock Y ←──waits──┘ │
│ │
└───────────────────────────────────────────┘
存在环: A → B → A ⇒ 死锁!
核心数据结构 ([deadlock.c]:
// deadlock.c 第 47-54 行
/*
* One edge in the waits-for graph.
* 等待图中的一条边
*/
typedef struct
{
PGPROC *waiter; /* 等待方的锁组领导者 */
PGPROC *blocker; /* 阻塞方的锁组领导者 */
LOCK *lock; /* 被等待的锁对象 */
int pred; /* TopoSort 工作空间 */
int link; /* TopoSort 工作空间 */
} EDGE;
// deadlock.c 第 72-77 行
/*
* 死锁详情信息 (用于输出诊断信息)
* 注意: 这些信息在释放锁管理器的分区锁之后仍然可以安全访问
*/
typedef struct
{
LOCKTAG locktag; /* 被等待锁的 ID */
LOCKMODE lockmode; /* 请求的锁模式 */
int pid; /* 阻塞方进程的 PID */
} DEADLOCK_INFO;
死锁检测主函数 DeadLockCheck():
// deadlock.c 第 219-282 行
DeadLockState
DeadLockCheck(PGPROC *proc)
{
/* 初始化: 无约束条件 */
nCurConstraints = 0;
nPossibleConstraints = 0;
nWaitOrders = 0;
blocking_autovacuum_proc = NULL;
/*
* Step 1: 递归搜索死锁环
* 使用 DFS (深度优先搜索) 遍历 Wait-for-Graph
*/
if (DeadLockCheckRecurse(proc))
{
/*
* 发现无法解决的硬死锁 (Hard Deadlock)!
*
* 再次调用 FindLockCycle 记录详细信息
* 用于生成用户可读的错误报告
*/
int nSoftEdges;
TRACE_POSTGRESQL_DEADLOCK_FOUND();
nWaitOrders = 0;
if (!FindLockCycle(proc, possibleConstraints, &nSoftEdges))
elog(FATAL, "deadlock seems to have disappeared");
return DS_HARD_DEADLOCK; /* 通知调用者回滚事务 */
}
/*
* Step 2: 应用软死锁解决方案
* 通过重新排序等待队列来解决潜在的死锁
*/
for (int i = 0; i < nWaitOrders; i++)
{
LOCK *lock = waitOrders[i].lock;
PGPROC **procs = waitOrders[i].procs;
int nProcs = waitOrders[i].nProcs;
dclist_head *waitQueue = &lock->waitProcs;
Assert(nProcs == dclist_count(waitQueue));
/*
* 重置队列并按新顺序添加进程
* 这可能使某些等待者获得锁
*/
dclist_init(waitQueue);
for (int j = 0; j < nProcs; j++)
dclist_push_tail(waitQueue, &procs[j]->links);
/* 唤醒可能获得锁的等待者 */
ProcLockWakeup(GetLocksMethodTable(lock), lock);
}
/* 返回检测结果 */
if (nWaitOrders > 0)
return DS_SOFT_DEADLOCK; /* 解决了软死锁 */
else if (blocking_autovacuum_proc != NULL)
return DS_BLOCKED_BY_AUTOVACUUM; /* 被 autovacuum 阻塞 */
else
return DS_NO_DEADLOCK; /* 无死锁 */
}
死锁检测的递归搜索算法:
// deadlock.c (FindLockCycleRecurse 伪代码)
static bool
FindLockCycleRecurse(PGPROC *checkProc, int depth,
EDGE *softEdges, int *nSoftEdges)
{
/*
* DFS 遍历 Wait-for-Graph:
*
* Base Case:
* - 如果 checkProc 已经在 visitedProcs 中 → 发现环!
*
* Recursive Step:
* - 对于 checkProc 等待的每个锁:
* - 找到该锁的持有者 (blocker)
* - 递归检查 blocker 是否在等待其他锁
*/
/* 检查是否已经访问过 (环检测) */
for (int i = 0; i < nVisitedProcs; i++)
{
if (visitedProcs[i] == checkProc)
{
/* 找到环! 记录环上的边 */
return true;
}
}
/* 标记为已访问 */
visitedProcs[nVisitedProcs++] = checkProc;
/* 遍历当前进程等待的所有锁 */
for each lock that checkProc is waiting for:
{
/* 找到该锁的所有持有者 */
for each holder of lock:
{
EDGE newEdge = {checkProc, holder, lock};
/* 递归搜索 */
if (FindLockCycleRecurse(holder, depth+1, softEdges, nSoftEdges))
{
/*
* 尝试通过"软边"解决:
* 如果这条边是软边的候选 (可以改变等待顺序),
* 记录它以便后续尝试重新排序
*/
if (is_soft_edge_candidate(newEdge))
{
softEdges[*nSoftEdges++] = newEdge;
continue; /* 尝试其他路径 */
}
return true; /* 硬死锁,无法避免 */
}
}
}
return false; /* 无环 */
}
死锁检测的触发时机:
锁获取流程 (ProcSleep in proc.c):
ProcSleep(locallock, lockMethodTable, lock):
│
├── 将自己加入锁的等待队列
│
├── 设置锁超时定时器
│ ├── enable_timeout(DEADLOCK_TIMEOUT, DeadlockTimeout)
│ └── 默认值: 1 秒 (postgresql.conf: deadlock_timeout)
│
├── 进入睡眠状态
│ ├── PGSemaphoreLock(MyProc->sem)
│ └── 等待被唤醒...
│
├── [被唤醒后]
│ ├── 检查唤醒原因:
│ │ ├── 正常获得锁 → 返回成功
│ │ ├── 死锁超时 → 触发死锁检测!
│ │ │ └── CheckDeadLock()
│ │ │ ├── DeadLockCheck(MyProc)
│ │ │ ├── if DS_HARD_DEADLOCK:
│ │ │ │ └── 报错: ERROR: deadlock detected
│ │ │ │ → 用户事务回滚
│ │ │ └── if DS_SOFT_DEADLOCK:
│ │ │ └── 重新排队, 继续等待
│ │ └── 取消操作 → 退出等待
│
└── 返回状态码
典型的死锁错误报告:
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 789;
blocked by process 12346.
Process 12346 waits for ShareLock on transaction 790;
blocked by process 12345.
HINT: See server log for query details.
CONTEXT: while updating row in table "accounts"
STATEMENT: UPDATE accounts SET balance = balance - 100 WHERE id = 1;
2.2 锁超时机制
除了死锁检测,PostgreSQL 还提供了基于超时的锁等待控制:
-- postgresql.conf 配置
lock_timeout = 0 -- 单个锁的最大等待时间 (0=无限)
statement_timeout = 0 -- 整个语句的最大执行时间
idle_in_transaction_session_timeout = 0 -- 空闲事务超时
-- 会话级别设置
SET LOCAL lock_timeout = '5s'; -- 当前会话锁等待最多5秒
超时实现的底层机制:
// src/backend/utils/misc/timeout.c (概念实现)
/*
* PostgreSQL 的超时系统基于 SIGALRM 信号
*
* 架构:
* 1. 每种超时类型有独立的启用/禁用状态
* 2. 使用堆 (min-heap) 管理多个活跃超时
* 3. 最先到期的超时决定 alarm() 的设置
*/
typedef enum TimeoutId
{
TIMEOUT_STARTUP_PACKET,
TIMEOUT_BOOT_AUTO_ERROR,
TIMEOUT_LOCK_TIMEOUT, /* ⭐ 锁等待超时 */
TIMEOUT_STATEMENT_TIMEOUT, /* ⭐ 语句执行超时 */
STANDBY_DEADLOCK_TIMEOUT,
STANDBY_LOCK_TIMEOUT,
STANDBY_STREAMING_TIMEOUT,
IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
/* ... 更多类型 ... */
} TimeoutId;
/*
* 超时触发时的处理:
*/
static void
handle_sig_alarm(SIGNAL_ARGS)
{
int save_errno = errno;
/* 检查哪种超时触发了 */
if (is_timeout_active(TIMEOUT_LOCK_TIMEOUT))
{
got_deadlock_timeout = true; /* 标记死锁检测应触发 */
}
if (is_timeout_active(TIMEOUT_STATEMENT_TIMEOUT))
{
got_statement_timeout = true; /* 标记语句应取消 */
}
/* 唤醒等待中的进程 */
SetLatch(MyLatch);
errno = save_errno;
}
锁超时 vs 死锁超时的区别:
| 特性 | lock_timeout |
deadlock_timeout |
|---|---|---|
| 用途 | 控制最大等待时间 | 触发死锁检测的时间点 |
| 默认值 | 0 (禁用) | 1s |
| 行为 | 超时报错并取消 | 超时开始检测,不一定会报错 |
| 开销 | 低 (只需计时) | 高 (需要构建 Wait-for-Graph) |
| 粒度 | 单个锁 | 全局 |
2.3 PG_TRY/PG_CATCH 异常捕获
PostgreSQL 实现了一套类似 C++ try/catch 的异常处理机制,用于保证资源的正确清理:
// src/include/utils/elog.h (第 385-416 行)
#define PG_TRY(...) \
do { \
sigjmp_buf *_save_exception_stack##__VA_ARGS__ = PG_exception_stack; \
ErrorContextCallback *_save_context_stack##__VA_ARGS__ = error_context_stack; \
sigjmp_buf _local_sigjmp_buf##__VA_ARGS__; \
bool _do_rethrow##__VA_ARGS__ = false; \
if (sigsetjmp(_local_sigjmp_buf##__VA_ARGS__, 0) == 0) \
{ \
PG_exception_stack = &_local_sigjmp_buf##__VA_ARGS__
#define PG_CATCH(...) \
} \
else \
{ \
PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
error_context_stack = _save_context_stack##__VA_ARGS__
#define PG_FINALLY(...) \
} \
else \
_do_rethrow##__VA_ARGS__ = true; \
{ \
PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
error_context_stack = _save_context_stack##__VA_ARGS__
#define PG_END_TRY(...) \
} \
if (_do_rethrow##__VA_ARGS__) \
PG_RE_THROW(); \
PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
error_context_stack = _save_context_stack##__VA_ARGS__; \
} while (0)
使用示例:
/* 实际代码示例 (来自 executor) */
void
ExecInsert(Estate, slot, ...)
{
PG_TRY();
{
/*
* 正常执行路径:
* 可能在这里触发 ERROR (违反约束、唯一性冲突等)
*/
heap_insert(relation, tuple, ...);
index_insert(indexDesc, values, ...);
/* 如果到这里,说明没有异常 */
}
PG_CATCH();
{
/*
* 异常处理路径:
* 这里可以进行资源清理
* 但注意: 大部分清理应该通过回调函数自动完成
*/
ErrorData *edata = CopyErrorData();
/* 记录错误信息 */
ereport(LOG, (errmsg("insert failed: %s", edata->message)));
FreeErrorData(edata);
/* 重新抛出异常 (让上层处理) */
PG_RE_THROW();
}
PG_END_TRY();
}
底层实现原理:
PG_TRY/PG_CATCH 实现原理:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 编译期展开: │
│ ───────────── │
│ │
│ PG_TRY(); │
│ { │
│ risky_operation(); │
│ } │
│ PG_CATCH(); │
│ { │
│ cleanup(); │
│ } │
│ PG_END_TRY(); │
│ │
│ 展开后等价于: │
│ ────────────────── │
│ do { │
│ sigjmp_buf *save = PG_exception_stack; │
│ sigjmp_buf local_buf; │
│ if (sigsetjmp(local_buf, 0) == 0) │
│ { │
│ PG_exception_stack = &local_buf; /* TRY block */ │
│ { │
│ risky_operation(); │
│ } │
│ } │
│ else │
│ { │
│ PG_exception_stack = save; /* CATCH block */ │
│ { │
│ cleanup(); │
│ } │
│ } │
│ PG_exception_stack = save; │
│ } while (0); │
│ │
│ 执行流程: │
│ ────────── │
│ │
│ sigsetjmp() 保存上下文到 local_buf │
│ │ │
│ ├── 正常执行 → risky_operation() 成功 │
│ │ │ │
│ │ ▼ │
│ │ 跳过 CATCH 块, 恢复 PG_exception_stack │
│ │ │
│ └── risky_operation() 调用 ereport(ERROR, ...) │
│ │ │
│ ▼ │
│ Longjmp 到 local_buf (siglongjmp) │
│ │ │
│ ▼ │
│ sigsetjmp 返回非0 → 进入 CATCH 块 │
│ │ │
│ ▼ │
│ cleanup() 执行 │
│ │
└─────────────────────────────────────────────────────────────┘
2.4 资源清理流程
当检测到异常(崩溃、死锁、客户端断开等)后,PostgreSQL 需要系统地清理各类资源:
Backend 进程的资源清理顺序:
// src/backend/storage/lmgr/proc.c (ProcKill 函数)
static void
ProcKill(int code, Datum arg)
{
PGPROC *proc = MyProc;
ASSERT(proc != NULL);
/* ============================================================
* Phase 1: 释放持有的所有锁
* ============================================================ */
ProcReleaseLocks(true); /* allLocks = true */
/*
* ProcReleaseLocks() 实现:
* 1. 遍历本地锁表 (LocalLockTable)
* 2. 对于每个持有的锁:
* a. 从 LOCK 对象的 holders 列表中移除
* b. 减少 LOCK 的 grant count
* c. 如果 grant count 变为 0, 唤醒等待队列中的下一个
* 3. 清空本地锁表
*/
/* ============================================================
* Phase 2: 从共享内存中移除 PGPROC 条目
* ============================================================ */
SpinLockAcquire(ProcStructLock);
/* 标记 PGPROC 为 unused */
proc->pid = 0;
proc->backendId = InvalidBackendId;
proc->databaseId = InvalidOid;
proc->roleId = InvalidOid;
proc->isBackgroundWorker = false;
memset(&proc->myProcLocks, 0, sizeof(proc->myProcLocks));
/* 加入空闲列表供复用 */
proc->links.next = ProcGlobal->freeProcs;
ProcGlobal->freeProcs = &proc->links;
ProcGlobal->numInUseBackends--;
SpinLockRelease(ProcStructLock);
/* ============================================================
* Phase 3: 释放信号量
* ============================================================ */
PGSemaphoreUnlock(proc->sem);
/* ============================================================
* Phase 4: 事务级清理 (如果还在事务中)
* ============================================================ */
if (IsTransactionState())
{
AbortCurrentTransaction();
}
/* ============================================================
* Phase 5: 清理会话级资源
* ============================================================ */
/* 关闭打开的 cursors */
PortalHashTableDeleteAll();
/* 释放临时表 */
AtEOXact_Commit(false); /* isCommit = false */
/* 清理 GUC 上下文 */
AtExit_LocalGUC();
}
/* on_shmem_exit 回调注册 */
static void
RemoveProcFromArray(int code, Datum arg)
{
Assert(MyProc != NULL);
/*
* 从 ProcArray 中移除 (影响快照可见性判断)
* 这必须在 ProcReleaseLocks 之后执行!
*/
ProcArrayRemove(MyProc, InvalidTransactionId);
}
需要清理的资源清单:
Backend 进程退出时必须清理的资源:
┌──────────────────────────────────────────────────────────────┐
│ 类别 │ 资源名称 │ 清理函数 │
├──────────────────────────────────────────────────────────────┤
│ 锁资源 │ 表级锁 (RowShare, │ ProcReleaseLocks │
│ │ RowExclusive, etc.) │ │
│ │ 页级锁 (BufferLock) │ UnlockBuffer │
│ │ 信号量 (LWLock) │ LWLockRelease │
├──────────────────────────────────────────────────────────────┤
│ 事务资源 │ 活动事务 │ AbortTransaction │
│ │ 子事务状态 │ CleanupSubtransaction│
│ │ XID 分配 │ ReleaseCurrentXid │
├──────────────────────────────────────────────────────────────┤
│ 内存资源 │ 内存上下文 (MemoryCtx)│ MemoryContextDelete│
│ │ 临时文件 │ CloseTempFiles │
│ │ 排序/Hash 临时空间 │ ExecEndNode │
├──────────────────────────────────────────────────────────────┤
│ 共享内存条目 │ PGPROC 结构 │ ProcKill │
│ │ ProcArray 条目 │ ProcArrayRemove │
│ │ PredicateLocks │ ReleasePredicateLocks│
├──────────────────────────────────────────────────────────────┤
│ I/O 资源 │ 打开的文件描述符 │ close(fd) │
│ │ Socket 连接 │ closesocket() │
│ │ 缓冲区引用 │ ReleaseBuffer │
├──────────────────────────────────────────────────────────────┤
│ 客户端状态 │ Portal (游标) │ PortalDrop │
│ │ Prepared Statements │ DropCachedPlan │
│ │ 协议状态 │ pq_comm_reset │
└──────────────────────────────────────────────────────────────┘
崩溃时的紧急清理流程对比:
正常退出 vs 异常崩溃的清理差异:
Normal Exit (exit(0) or proc_exit(0)):
┌─────────────────────────────────────────┐
│ 1. 调用 atexit_callback / on_exit_cb │
│ 2. 优雅地释放所有资源 │
│ 3. flush I/O buffers │
│ 4. 通知 postmaster 正常退出 │
│ 5. _exit(0) │
└─────────────────────────────────────────┘
Crash Exit (SIGSEGV, SIGABRT, assert failure):
┌─────────────────────────────────────────┐
│ 1. 信号处理器立即激活 │
│ 2. quickdie() handler: │
│ ├── 阻止新的信号处理 (防止嵌套) │
│ ├── 尝试写入崩溃日志到 stderr │
│ └── _exit(1) 立即终止 │
│ │
⚠️ 问题: 资源可能未完全清理! │
│ │
✅ 解决方案: │
│ Postmaster 通过以下机制检测并补救: │
│ 1. 共享内存中 PGPROC.pid == 0 检查 │
│ 2. ProcArray 检测到缺失的后端 │
│ 3. 后续的 Checkpoint/VACUUM 清理残留 │
└─────────────────────────────────────────┘
三、启动时的冲突检测
当 PostgreSQL 因故障退出后再次启动时,必须检测并处理多种潜在冲突。
3.1 PID 文件检查
PID 文件 (postmaster.pid) 是 PostgreSQL 启动时的第一道防线:
// src/backend/storage/ipc/pidfile.c (CreateLockFile 函数)
/*
* PID 文件格式 ($DATA_DIR/postmaster.pid):
* Line 1: postmaster PID (e.g., 12345)
* Line 2: data directory path
* Line 3: start time (Unix timestamp)
* Line 4: port number (e.g., 5432)
* Line 5: socket directory
* Line 6: listen addresses
* Line 7: shared memory key (System V IPC)
* Line 8: postmaster status (starting/stopping/running/etc.)
*/
bool
CreateLockFile(const char *filename, const char *refName,
bool amPostmaster, bool isDDLock,
const char *refFullName)
{
int fd;
char buffer[MAXPGPATH];
int ntries;
int len;
int encoded_pid;
pid_t other_pid;
/*
* Step 1: 尝试创建锁文件 (O_CREAT | O_EXCL)
* 如果文件已存在, O_EXCL 会导致创建失败
*/
fd = open(filename, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd < 0)
{
/*
* 文件已存在! 需要进一步检查:
*/
if ((fd = open(filename, O_RDONLY, 0)) < 0)
{
/* 无法读取已有文件, 但可能是权限问题 */
return false;
}
/*
* Step 2: 读取现有 PID 文件的内容
*/
if ((len = read(fd, buffer, sizeof(buffer) - 1)) < 0)
{
close(fd);
return false;
}
close(fd);
buffer[len] = '\0';
/* 解析出已有的 PID */
other_pid = atoi(buffer);
/*
* Step 3: 检查该 PID 的进程是否仍在运行
* 使用 kill(pid, 0) 进行无损检测:
* - 返回 0: 进程存在
* - 返回 -1 且 errno == ESRCH: 进程不存在
*/
if (other_pid <= 0)
{
/* 无效 PID, 可以删除旧文件并重试 */
unlink(filename);
return CreateLockFile(filename, refName, amPostmaster,
isDDLock, refFullName);
}
if (kill(other_pid, 0) == 0)
{
/*
* ⚠️ 冲突! 该 PID 的进程仍存在
*
* 对于 postmaster.pid:
* 报错: "another postmaster is running (PID %d)"
*
* 对于 DataDirLockFile:
* 说明另一个实例正在使用相同的数据目录!
*/
ereport(FATAL,
(errcode(ERRCODE_LOCK_FILE_EXISTS),
errmsg("pre-existing shared memory block (key %lu, ID %lu) is still in use; "
"terminate any old server processes first",
(unsigned long) shmKey, (unsigned long) shmId)));
return false;
}
else if (errno == ESRCH)
{
/*
* 进程不存在, 但 PID 文件残留
* 可能是上次异常退出留下的
* 安全删除并重试
*/
unlink(filename);
return CreateLockFile(filename, refName, amPostmaster,
isDDLock, refFullName);
}
}
/*
* Step 4: 成功创建锁文件
* 写入当前进程信息和时间戳
*/
snprintf(buffer, sizeof(buffer), "%d\n%s\n%d\n",
amPostmaster ? getpid() : 0,
refName ? refName : "",
(int) time(NULL));
write(fd, buffer, strlen(buffer));
return true;
}
PID 文件检查流程图:
pg_ctl start 执行流程:
┌─────────────────────────────────────────────────────────────┐
│ │
│ pg_ctl start │
│ │ │
│ ▼ │
│ 读取 postgresql.conf │
│ │ │
│ ▼ │
│ fork() → postmaster 进程 │
│ │ │
│ ▼ │
│ CreateLockFile("postmaster.pid") │
│ │ │
│ ├── [成功] → 继续 │
│ │ │
│ └── [文件已存在] │
│ │ │
│ ▼ │
│ 读取已有 PID │
│ │ │
│ ▼ │
│ kill(pid, 0) 检测进程存活 │
│ │ │
│ ├── [进程存在] ❌ FATAL │
│ │ │ │
│ │ ▼ │
│ │ "another postmaster is running" │
│ │ → pg_ctl 退出 (rc=1) │
│ │ │
│ └── [进程不存在] │
│ │ │
│ ▼ │
│ 删除残留的 .pid 文件 │
│ │ │
│ ▼ │
│ 重新尝试创建 │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 共享内存冲突检测
PostgreSQL 支持两种共享内存实现:System V IPC 和 POSIX Shared Memory (shm_open)。
System V IPC 共享内存检测:
// src/backend/storage/ipc/shmem.c (InternalIpcMemoryCreate)
static void
InternalIpcMemoryCreate(IpcMemoryKey memKey, Size size)
{
IpcMemoryId shmid;
/*
* 尝试创建共享内存段
* IPC_CREAT | IPC_EXCL: 如果已存在则报错
* 0666: 权限 (实际受 umask 影响)
*/
shmid = shmget(memKey, size, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
if (errno == EEXIST)
{
/*
* ⚠️ 共享内存段已存在!
*
* 可能原因:
* 1. 上一次的 postmaster 进程异常退出, 未清理
* 2. 另一个 PostgreSQL 实例正在使用相同的 key
* 3. 恶意程序占用了该 key
*/
/*
* 尝试获取已存在的段的 ID
*/
shmid = shmget(memKey, 0, 0);
if (shmid >= 0)
{
struct shmid_ds shminfo;
/* 获取共享内存段的信息 */
if (shmctl(shmid, IPC_STAT, &shminfo) >= 0)
{
/*
* 检查附加进程数 (shm_nattch)
* 如果为 0, 说明是孤儿段 (可以安全删除)
* 如果 > 0, 说明仍有进程在使用
*/
if (shminfo.shm_nattch == 0)
{
/*
* 孤儿共享内存段
* 先删除旧段, 再创建新段
*/
if (shmctl(shmid, IPC_RMID, NULL) >= 0)
{
ereport(WARNING,
(errmsg("removing stale shared memory segment (%lu)",
(unsigned long) shmid)));
/* 重新尝试创建 */
shmid = shmget(memKey, size,
IPC_CREAT | IPC_EXCL | 0666);
}
}
else
{
/*
* 仍有进程附加, 真正的冲突!
*/
ereport(FATAL,
(errcode(ERRCODE_UNDEFINED_FILE),
errmsg("pre-existing shared memory block "
"(key %lu, ID %lu) is still in use; "
"terminate any old server processes first",
(unsigned long) memKey,
(unsigned long) shmid))));
}
}
}
}
else if (errno == EINVAL)
{
/* 请求的大小超过系统限制 */
ereport(FATAL,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("shared memory request size exceeds system limit"),
errhint("Increase kernel.shmax or reduce shared_buffers.")));
}
else if (errno == ENOMEM)
{
/* 系统内存不足 */
ereport(FATAL,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("not enough shared memory available")));
}
}
/*
* 附加到共享内存段
* 并初始化 PGShmemHeader
*/
SharedMemAddress = shmat(shmid, NULL, 0);
// ...
}
共享内存冲突的场景矩阵:
┌──────────────────────────────────────────────────────────────────┐
│ 场景 │ 检测方法 │ 处理策略 │
├──────────────────────────────────────────────────────────────────┤
│ 正常关闭后重启 │ PID文件不存在 │ 正常启动 │
│ │ 共享内存不存在 │ │
├──────────────────────────────────────────────────────────────────┤
│ 崩溃后重启 │ PID文件存在但 │ 删除PID文件 │
│ (postmaster已退出) │ 进程不存在 │ 检查共享内存 │
│ │ │ 如无附加则删除重建 │
├──────────────────────────────────────────────────────────────────┤
│ postmaster仍运行 │ PID文件存在 │ FATAL错误退出 │
│ (误操作) │ kill检测存活 │ 提示用户先停止 │
├──────────────────────────────────────────────────────────────────┤
│ 共享内存孤儿段 │ shm_nattch==0 │ WARNING提示 │
│ (罕见) │ │ 自动删除并重建 │
├──────────────────────────────────────────────────────────────────┤
│ 多实例冲突 │ 相同数据目录 │ FATAL错误 │
│ (DataDirLockFile) │ DataDirLockFile │ 拒绝启动 │
└──────────────────────────────────────────────────────────────────┘
3.3 Socket 端口占用检测
// src/backend/libpq/pqcomm.c (StreamServerPort)
int
StreamServerPort(int family, const char *hostName, unsigned short portNumber,
const char *unixSocketDir, ListenSocket *ListenSockets,
int MaxListen)
{
int fd, err;
int one = 1;
struct addrinfo hints;
struct addrinfo *addr, *addr_list;
char portStr[32];
int listen_index = 0;
/* 准备地址信息 */
snprintf(portStr, sizeof(portStr), "%d", portNumber);
memset(&hints, 0, sizeof(hints));
hints.ai_family = family;
hints.ai_flags = AI_PASSIVE;
hints.ai_socktype = SOCK_STREAM;
/* 解析地址 */
err = getaddrinfo(hostName, portStr, &hints, &addr_list);
if (err || !addr_list)
{
ereport(LOG, (errmsg("could not resolve host \"%s\": %s",
hostName ? hostName : "*", gai_strerror(err))));
return STATUS_ERROR;
}
for (addr = addr_list; addr; addr = addr->ai_next)
{
/* 创建 socket */
fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
if (fd < 0)
{
ereport(LOG, (errmsg("socket failed: %m")));
continue;
}
/*
* ⚠️ 关键: 设置 SO_REUSEADDR
*
* 为什么需要这个选项?
* - TIME_WAIT 状态的 socket 占用端口
* - 允许快速重启而不必等待 2MSL 超时
* - 但不能解决真正的"端口被占用"问题
*/
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
{
ereport(LOG, (errmsg("setsockopt(SO_REUSEADDR) failed: %m")));
closesocket(fd);
continue;
}
#ifdef IPV6_V6ONLY
if (addr->ai_family == AF_INET6)
{
if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)) < 0)
{
ereport(LOG, (errmsg("setsockopt(IPV6_V6ONLY) failed: %m")));
closesocket(fd);
continue;
}
}
#endif
/*
* 绑定地址和端口
* 如果端口已被占用, bind() 会失败
*/
if (bind(fd, addr->ai_addr, addr->ai_addrlen) < 0)
{
int saved_errno = errno;
ereport(LOG,
(errcode_for_socket_access(),
(errmsg("could not bind%s address: %m",
(addr->ai_family == AF_UNIX) ? " to Unix-domain socket" : ""),
saved_errno == EADDRINUSE ?
errhint("Is another postmaster already running on port %d? "
"If not, wait a few seconds and retry.",
(int) portNumber) : 0)));
closesocket(fd);
/*
* EADDRINUSE: 地址已被使用
* - 可能是另一个 postmaster 实例
* - 可能是 socket 处于 TIME_WAIT 状态
*/
if (saved_errno != EADDRINUSE)
return STATUS_ERROR;
continue;
}
/* 开始监听连接请求 */
if (listen(fd, MaxBacklog) < 0)
{
ereport(LOG, (errmsg("listen failed: %m")));
closesocket(fd);
return STATUS_ERROR;
}
ListenSockets[listen_index] = fd;
listen_index++;
}
freeaddrinfo(addr_list);
NumListenSockets = listen_index;
return STATUS_OK;
}
端口占用问题的排查指南:
# 检查端口占用情况
$ netstat -tlnp | grep 5432
# 或者
$ ss -tlnp | grep 5432
# 输出示例:
# tcp 0 0 0.0.0.0:5432 0.0.0.0:* LISTEN 12345/postgres
# 如果确认是残留进程, 可以:
$ kill 12345 # 先尝试优雅关闭
$ kill -9 12345 # 强制终止 (最后手段)
# 检查 TIME_WAIT 状态的连接
$ netstat -tan | grep :5432 | grep TIME_WAIT
# Linux 特定: 调整 TIME_WAIT 参数 (谨慎使用!)
# sysctl -w net.ipv4.tcp_tw_reuse=1
# sysctl -w net.ipv4.tcp_fin_timeout=15
3.4 数据目录完整性检查
PostgreSQL 启动时会进行多项数据目录完整性验证:
// postmaster.c (main 函数中的检查序列)
int
main(int argc, char *argv[])
{
/* ... */
/*
* Check 1: 验证数据目录存在且可访问
*/
if (!DataDir)
{
write_stderr("%s: no data directory specified\n", progname);
exit(1);
}
if (chdir(DataDir) < 0)
{
write_stderr("%s: could not change directory to \"%s\": %s\n",
progname, DataDir, strerror(errno));
exit(1);
}
/*
* Check 2: 验证数据目录的有效性
* 检查 PG_VERSION 文件是否存在
*/
{
char path[MAXPGPATH];
FILE *fp;
join_path_components(path, DataDir, "PG_VERSION");
fp = fopen(path, "r");
if (!fp)
{
write_stderr("%s: data directory \"%s\" does not exist\n",
progname, DataDir);
exit(1);
}
/* 读取版本号并验证兼容性 */
char version_str[10];
if (fscanf(fp, "%9s", version_str) != 1)
{
write_stderr("%s: could not read PG_VERSION from \"%s\"\n",
progname, path);
exit(1);
}
fclose(fp);
/* 检查版本匹配 */
if (strcmp(version_str, PG_MAJORVERSION) != 0)
{
write_stderr(
"%s: database files are incompatible with server\n"
"The data directory was initialized by PostgreSQL version %s, "
"which is not compatible with this version %s.\n",
progname, version_str, PG_MAJORVERSION);
exit(1);
}
}
/*
* Check 3: 检查 control file (pg_control)
* 包含数据库的状态、checkpoint 位置等关键信息
*/
{
ControlFileData *controlFile;
bool crcOk;
/* 读取并校验 pg_control */
controlFile = ReadControlFile(&crcOk);
if (!crcOk)
{
/*
* pg_control CRC 校验失败!
*
* 可能原因:
* 1. 磁盘损坏
* 2. 上次写入未完成 (崩溃)
* 3. 文件系统损坏
*/
write_stderr(
"%s: pg_control CRC is incorrect\n"
"The file might be corrupt. You might need to restore from backup.\n",
progname);
exit(1);
}
/* 检查数据库状态 */
if (controlFile->state != DB_SHUTDOWNED &&
controlFile->state != DB_IN_PRODUCTION &&
controlFile->state != DB_IN_CRASH_RECOVERY)
{
write_stderr(
"%s: database system was not properly shut down; "
"automatic recovery required\n",
progname);
/* 这不是致命错误, 会进入 Crash Recovery */
}
}
/* ... 继续启动流程 ... */
}
pg_control 文件的关键字段:
pg_control 结构体 (ControlFileData):
┌─────────────────────────────────────────────────────────────┐
│ 字段名 │ 类型 │ 含义 │
├─────────────────────────────────────────────────────────────┤
│ system_identifier │ uint64 │ 唯一的系统标识符 │
│ state │ DBState │ 数据库状态 ★ │
│ time │ pg_time_t │ 最后更新时间戳 │
│ checkPointCopy │ CheckPoint │ 最新 checkpoint 信息 │
│ minRecoveryPoint │ XLogRecPtr │ 最小恢复位置 (PITR) │
│ unloggedLSN │ XLogRecPtr │ unlogged 表的 LSN │
│ NextXid │ FullTransactionId 下一个分配的 XID │
│ nextOid │ Oid │ 下一个 OID │
│ wal_level │ int │ WAL 级别 │
│ MaxConnections │ int │ 最大连接数 │
│ max_worker_processes│ int │ 最大 worker 数量 │
│ data_checksum_version│ uint32 │ 校验和版本 │
│ crc │ pg_crc32 │ CRC 校验码 ★ │
└─────────────────────────────────────────────────────────────┘
DBState 枚举值:
┌─────────────────────────────────────────────────────────────┐
│ DB_SHUTDOWNED │ 正常关闭 (无需恢复) │
│ DB_SHUTDOWNING │ 正在关闭中 │
│ DB_IN_CRASH_RECOVERY │ 崩溃恢复中 (需要 WAL 重放) ★ │
│ DB_IN_ARCHIVE_RECOVERY│ 归档恢复中 (PITR) │
│ DB_IN_PRODUCTION │ 正常运行中 │
└─────────────────────────────────────────────────────────────┘
四、WAL 恢复机制
4.1 恢复原理:CRASH-RECOVERY vs POINT-IN-TIME RECOVERY
PostgreSQL 基于 ARIES (Algorithms for Recovery and Isolation Exploiting Semantics) 理论实现了 WAL 恢复机制。
ARIES 恢复理论的三大核心原则:
ARIES Recovery Principles:
1. Write-Ahead Logging (WAL):
┌─────────────────────────────────────────────────┐
│ "Before modifying a page, log the change first" │
│ │
│ 顺序: │
│ 1. 生成 WAL Record │
│ 2. 写入 WAL Buffer │
│ 3. fsync() WAL 到磁盘 │
│ 4. 修改数据页 │
│ 5. (可选) fsync() 数据页 │
└─────────────────────────────────────────────────┘
2. Repeating History During Redo:
┌─────────────────────────────────────────────────┐
│ "Redo repeats history exactly as it happened" │
│ │
│ 恢复时重放从 checkpoint 后的所有 WAL 记录 │
│ 确保数据达到一致状态 │
└─────────────────────────────────────────────────┘
3. Logging Changes During Undo:
┌─────────────────────────────────────────────────┐
│ "Undo operations are also logged (CLR)" │
│ │
│ Compensation Log Records (CLR): │
│ - 记录回滚操作本身 │
│ - 支持崩溃后的恢复 (repeatable undo) │
└─────────────────────────────────────────────────┘
两种恢复模式的区别:
┌──────────────────────────────────────────────────────────────────┐
│ 恢复模式比较 │
├────────────────────────────┬──────────────────────────────────────┤
│ Crash Recovery │ Point-In-Time Recovery (PITR) │
├────────────────────────────┼──────────────────────────────────────┤
│ 触发条件: │ 触发条件: │
│ - 非正常关闭 (kill, crash) │ - recovery_target_time 设置 │
│ - pg_control.state != │ - recovery_target_xid 设置 │
│ DB_SHUTDOWNED │ - recovery_target_name 设置 │
│ │ - standby_mode = on │
├────────────────────────────┼──────────────────────────────────────┤
│ 起点: │ 起点: │
│ 最近的一致性 Checkpoint │ 基础备份 + 归档 WAL │
│ │ (或 pg_start_backup()) │
├────────────────────────────┼──────────────────────────────────────┤
│ 终点: │ 终点: │
│ WAL 末尾 (最新位置) │ 指定的目标时间/XID/name │
├────────────────────────────┼──────────────────────────────────────┤
│ 结果: │ 结果: │
│ 恢复到崩溃前的最新状态 │ 恢复到指定时间点的状态 │
│ (可能有少量未提交事务) │ (用于回滚人为错误) │
├────────────────────────────┼──────────────────────────────────────┤
│ 典型场景: │ 典型场景: │
│ - 断电恢复 │ - 误删数据恢复 │
│ - OS 崩溃恢复 │ - 数据库迁移 │
│ - 进程崩溃恢复 │ - 搭建只读副本 │
└────────────────────────────┴──────────────────────────────────────┘
4.2 Startup 进程初始化
Startup 进程是负责执行恢复的核心进程,定义在 [startup.c]中。
Startup 进程的生命周期:
// startup.c 第 150-250 行 (概念整合)
void
StartupProcessMain(void)
{
/*
* Phase 1: 初始化信号处理器
*/
pqsignal(SIGHUP, StartupProcSigHupHandler); // 配置重载
pqsignal(SIGINT, SIG_IGN); // 忽略
pqsignal(SIGTERM, StartupProcShutdownHandler); // 关闭请求
pqsignal(SIGUSR1, StartupProcTriggerHandler); // promote 请求
InitProcessLocalLatch();
/*
* Phase 2: 附加到共享内存
* 作为辅助进程加入 ProcArray
*/
InitAuxiliaryProcess();
/*
* Phase 3: 设置恢复状态
* 根据 pg_control 的 state 字段确定恢复模式
*/
switch (ControlFile->state)
{
case DB_IN_CRASH_RECOVERY:
/* 崩溃恢复模式 */
recoveryTargetTime = 0;
recoveryTargetXid = InvalidTransactionId;
break;
case DB_IN_ARCHIVE_RECOVERY:
case DB_SHUTDOWNED:
/* 归档恢复 (PITR) 或正常启动 */
readRecoveryConfigFile(); /* 读取 recovery.signal 等 */
break;
default:
elog(FATAL, "unexpected state in pg_control: %u", ControlFile->state);
}
/*
* Phase 4: 执行恢复
* 这是核心的重放循环
*/
PerformRecovery();
/*
* Phase 5: 恢复完成, 转换角色
*/
if (ArchiveRecoveryRequested && IsUnderPostmaster)
{
/*
* 如果是流复制备库:
* 保持为 Startup Process, 继续接收和应用 WAL
* 进入 PM_HOT_STANDBY 或 PM_RECOVERY 状态
*/
StandbyMode = true;
WaitForWALToBeArchived(); /* 等待新的 WAL */
}
else
{
/*
* 如果是主库:
* Startup Process 任务完成, 退出
* Postmaster 将转换为 PM_RUN 状态
*/
proc_exit(0);
}
}
Startup 进程在整体架构中的位置:
PostgreSQL 启动过程中的进程创建顺序:
Postmaster (PID=1)
│
├── [Step 1] 创建共享内存和信号量
│
├── [Step 2] 创建 Startup Process (PID=2)
│ │
│ ├── 读取 pg_control
│ ├── 确定 Checkpoint 位置
│ ├── 开始 WAL 重放循环
│ │ ├── ReadRecord() 读取 WAL
│ │ ├── RmgrTable[rmid].rm_redo() 应用记录
│ │ └── 检查恢复目标 (如果是 PITR)
│ │
│ ├── [恢复完成]
│ │ ├── 更新 pg_control.state = DB_SHUTDOWNED
│ │ ├── 创建新的 REDO Checkpoint
│ │ └── 退出 (proc_exit(0))
│ │
│ └── [如果是备库] 进入持续恢复模式
│
├── [Step 3] Startup 完成 → PM_RUN
│
├── [Step 4] 创建后台辅助进程
│ ├── Background Writer (bgwriter)
│ ├── Checkpointer
│ ├── WAL Writer (walwriter)
│ ├── WAL Receiver (walreceiver, 如果是备库)
│ ├── AutoVacuum Launcher
│ ├── Stats Collector
│ └── SysLogger
│
└── [Step 5] 开始接受客户端连接
│
└── fork() → Backend Processes (on demand)
4.3 WAL 重放流程
WAL 重放是恢复过程的核心步骤,涉及从 WAL 日志中读取记录并重新应用到数据页面。
WAL 记录的结构:
// src/include/access/xlogrecord.h
typedef struct XLogRecord
{
uint32 xl_tot_len; /* 记录总长度 (包括头部) */
TransactionId xl_xid; /* 生成此记录的事务 ID */
uint32 xl_prev; /* 前一条记录的位置 */
uint8 xl_info; /* 标志位和 RMID */
RmgrId xl_rmid; /* 资源管理器 ID (见下方) */
/* 2 bytes of padding here, initialize to zero */
pg_crc32c xl_crc; /* CRC 校验码 */
} XLogRecord;
/*
* Resource Manager IDs (xl_rmid):
*
* RM_XLOG_ID (0) → 通用事务日志 (commit, abort, checkpoint)
* RM_XACT_ID (1) → 事务操作 (xact commit/abort records)
* RM_SMGR_ID (2) → Storage Manager (create/drop/truncate)
* RM_HEAP_ID (3) → Heap 操作 (insert/update/delete)
* RM_HEAP2_ID (4) → Heap 辅助操作 (cleanup, freeze)
* RM_BTREE_ID (5) → B-Tree 索引操作
* RM_HASH_ID (6) → Hash 索引操作
* RTREE_ID (7) → R-Tree 索引操作 (已废弃)
* GIN_ID (8) → GIN 索引操作
* GIST_ID (9) → GiST 索引操作
* SEQUENCE_ID (10) → Sequence 操作
* SPOCKE_ID (11) → Replication Origin
* LOGICALMSG_ID (12) → Logical Decoding Message
* HEAP3_ID (13) → New Heap operations
* DEFAULT_ID (14) → Default Resource Manager
*/
WAL 重放的主循环:
// src/backend/access/transam/xlogrecovery.c (概念实现)
void
PerformRecovery(void)
{
XLogRecPtr ReadRecPtr; /* 当前读取位置 */
XLogRecPtr EndRecPtr; /* 当前记录结束位置 */
XLogRecord *record;
/*
* Step 1: 定位起始恢复点
* 通常是从最近的 Checkpoint 开始
*/
XLogRecPtr checkpointLoc = ControlFile->checkPoint;
ReadCheckpointRecord(checkpointLoc);
/*
* Step 2: 主重放循环
*/
for (;;)
{
/*
* 读取下一条 WAL 记录
* 可能从本地 pg_wal 目录或归档存储中读取
*/
record = ReadRecord(&ReadRecPtr, PANIC);
if (record == NULL)
{
/*
* 到达 WAL 末尾
* 对于 Crash Recovery: 恢复完成
* 对于 PITR: 检查是否到达恢复目标
*/
break;
}
/*
* Step 3: 检查恢复目标 (仅 PITR 模式)
*/
if (recoveryTargetSet)
{
if (reachedRecoveryTarget(record))
{
/*
* 到达恢复目标点
* 停止重放
*/
break;
}
}
/*
* Step 4: 应用 WAL 记录 (Redo)
*
* 根据 rmid 分派到不同的资源管理器
* 每个 RM 负责自己的 redo 逻辑
*/
RmgrTable[record->xl_rmid].rm_redo(EndRecPtr, record);
/*
* Step 5: 更新恢复进度
* 用于进度报告和统计
*/
lastReplayedEndRecPtr = EndRecPtr;
}
/*
* Step 6: 恢复完成处理
*/
RecoveryComplete();
}
各类 WAL 记录的 Redo 示例:
/* Heap Insert 的 Redo 实现 (heapam.c) */
void
heap_xlog_insert(XLogReaderState *record)
{
XLogRecPtr lsn = record->EndRecPtr;
xl_heap_insert *xlrec = (xl_heap_insert *) XLogRecGetData(record);
Buffer buffer;
Page page;
OffsetNumber offnum;
ItemId lp = NULL;
HeapTupleHeader htup;
/*
* 恢复目标页面到缓冲区
* 如果页面不在缓冲池中, 从磁盘读取
*/
buffer = XLogReadBuffer(xlrec->target.node, xlrec->target.block, true);
page = BufferGetPage(buffer);
/*
* 检查页面的 LSN (Log Sequence Number)
*
* 重要优化: 如果页面的 LSN >= 当前记录的 LSN,
* 说明该更改已经被应用过了 (可能通过之前的部分写入),
* 可以跳过本次 redo
*/
if (PageGetLSN(page) >= lsn)
{
UnlockReleaseBuffer(buffer);
return; /* 跳过 */
}
/*
* 应用实际的插入操作
* 重建元组数据并将其添加到页面
*/
offnum = PageAddItem(page, (Item) htup, newlen,
xlrec->offnum, false, false);
/*
* 更新页面的 LSN
* 这对于后续的 "LSN check" 优化至关重要
*/
PageSetLSN(page, lsn);
MarkBufferDirty(buffer);
UnlockReleaseBuffer(buffer);
}
/* B-Tree Insert 的 Redo 实现 (nbtxlog.c) */
void
btree_xlog_insert(XLogReaderState *record)
{
XLogRecPtr lsn = record->EndRecPtr;
xl_btree_insert *xlrec = (xl_btree_insert *) XLogRecGetData(record);
Buffer buffer;
Page page;
buffer = XLogReadBuffer(xlrec->target.node, xlrec->target.block, true);
page = BufferGetPage(buffer);
if (PageGetLSN(page) >= lsn)
{
UnlockReleaseBuffer(buffer);
return;
}
/*
* 应用 B-Tree 叶子页面的修改
* 包括: 插入元组、调整页面结构、更新高键等
*/
if (xlrec->clearflag)
{
PageClearHasGarbage(page);
}
PageIndexTupleDeleteNoCompact(page, xlrec->offset);
if (!PageAddItem(page, item, newlen, xlrec->offnum, false, false))
elog(PANIC, "failed to add item");
/* 更新页面分裂相关信息 (如果有) */
if (XLogRecHasBlockRef(record, 1))
{
/* 处理右兄弟页面的更新 */
// ...
}
PageSetLSN(page, lsn);
MarkBufferDirty(buffer);
UnlockReleaseBuffer(buffer);
}
WAL 重放的完整流程图:
WAL Recovery 流程:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Start │
│ │ │
│ ▼ │
│ 读取 pg_control │
│ │ │
│ ├── state = DB_SHUTDOWNED → 无需恢复, 直接启动 │
│ │ │
│ └── state = DB_IN_CRASH_RECOVERY │
│ │ 或 DB_IN_ARCHIVE_RECOVERY (PITR) │
│ ▼ │
│ 定位最近的一致性 Checkpoint │
│ │ │
│ ├── 读取 checkpoint record │
│ ├── 获取 Redo Pointer (开始重放位置) │
│ └── 恢复快照信息 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ WAL Redo Loop │ │
│ │ │ │
│ │ for each WAL record from RedoPointer to EndOfWAL: │ │
│ │ { │ │
│ │ ① ReadRecord() 读取记录 │ │
│ │ ├── 从 pg_wal/ 目录读取 │ │
│ │ └── 或从 archive_command 获取 (PITR) │ │
│ │ │ │
│ │ ② [PITR only] 检查恢复目标 │ │
│ │ ├── reachedTimeTarget()? │ │
│ │ ├── reachedXidTarget()? │ │
│ │ └── reachedNameTarget()? │ │
│ │ │ │
│ │ ③ RmgrTable[rmid].rm_redo() 应用记录 │ │
│ │ ├── heap_redo: 重放堆操作 │ │
│ │ ├── btree_redo: 重放索引操作 │ │
│ │ ├── xact_redo: 处理事务提交/回滚 │ │
│ │ └── smgr_redo: 处理文件操作 │ │
│ │ │ │
│ │ ④ 更新 LastReplayedEndRecPtr │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Recovery Complete: │
│ ├── 更新 pg_control.state = DB_SHUTDOWNED │
│ ├── 创建新的 REDO Checkpoint (强制) │
│ ├── 清理临时关系 │
│ ├── 重置 unlogged 表 │
│ └── [PITR only] 结束恢复模式 │
│ │ │
│ ▼ │
│ Transition to Normal Operation (PM_RUN) │
│ │
└─────────────────────────────────────────────────────────────────┘
4.4 Checkpoint 与一致性点
Checkpoint 是确保数据库一致性的关键机制,它定义了一个已知一致的数据状态点。
Checkpoint 的作用:
Checkpoint 的核心目的:
┌─────────────────────────────────────────────────────────────┐
│ │
│ Before Checkpoint: │
│ ─────────────────── │
│ │
│ Memory (Dirty Pages): │
│ ┌──────────────────────────────────────────────┐ │
│ │ Page A (modified, not flushed) │ │
│ │ Page B (modified, not flushed) │ │
│ │ Page C (modified, not flushed) │ │
│ │ ... │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ WAL: │
│ ┌──────────────────────────────────────────────┐ │
│ │ [CP] [rec1][rec2][rec3]...[recN] │ │
│ │ ↑ │ │
│ │ Last Checkpoint Position │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ After Checkpoint: │
│ ────────────────── │
│ │
│ Disk (Flushed Pages): │
│ ┌──────────────────────────────────────────────┐ │
│ │ Page A ✓ (flushed to disk) │ │
│ │ Page B ✓ (flushed to disk) │ │
│ │ Page C ✓ (flushed to disk) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ WAL: │
│ ┌──────────────────────────────────────────────┐ │
│ │ [old CP]...[rec1][rec2][rec3]...[New CP] │ │
│ │ ↑ │ │
│ │ New Checkpoint Record │ │
│ │ - All dirty pages flushed │ │
│ │ - WAL synced to disk │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ Recovery Only Needs: │
│ - Replay WAL after New Checkpoint Position │
│ - No need to replay earlier records │
│ - Significantly faster recovery! │
│ │
└─────────────────────────────────────────────────────────────┘
Checkpoint 的实现 ([checkpointer.c]:
// checkpointer.c (CreateCheckpoint 函数, 简化版)
void
CreateCheckpoint(int flags)
{
Checkpoint checkpoint;
XLogRecPtr recptr;
XLogSegNo _logSegNo;
/*
* Phase 1: 准备阶段
*/
ckpt_start_time = GetCurrentTimestamp();
/*
* Phase 2: 刷写所有脏页面
*
* BufferSync() 会遍历共享缓冲区:
* - 找出所有 dirty pages
* - 按 LSN 顺序排序 (确保写入顺序!)
* - 逐个刷写到磁盘
*
* 这是一个 I/O 密集的操作
*/
BufferSync(flags, checkpoint);
/*
* Phase 3: 同步 WAL 到磁盘
*
* 确保 Checkpoint 记录之前的所有 WAL 都已持久化
* 这样恢复时才能找到完整的起点
*/
XLogFlush(recptr);
/*
* Phase 4: 写入 Checkpoint 记录到 WAL
*/
checkpoint.time = (time_t) time(NULL);
checkpoint.nextFullXid = ShmemVariableCache->nextFullXid;
checkpoint.nextOid = ShmemVariableCache->nextOid;
checkpoint.nextMulti = MultiXactGenNextMXact();
/* ... 更多状态信息 ... */
recptr = XLogInsert(RM_XLOG_ID,
XLOG_CHECKPOINT_ONLINE, // or SHUTDOWN
&checkpoint, sizeof(checkpoint));
/*
* Phase 5: 更新 pg_control
*
* 将 Checkpoint 位置写入控制文件
* 这是恢复的起始点!
*/
ControlFile->checkPoint = recptr;
ControlFile->checkPointCopy = checkpoint;
ControlFile->time = (pg_time_t) time(NULL);
ControlFile->state = DB_IN_PRODUCTION;
UpdateControlFile();
/*
* Phase 6: 清理旧的 WAL 文件
*
* 删除不再需要的 WAL 段 (回收磁盘空间)
* 保留最近的几个段以备恢复之需
*/
if (flags & CHECKPOINT_IS_SHUTDOWN)
{
/* 关闭时: 保留更多 WAL 以防万一 */
KeepLogSeg(recptr, &_logSegNo);
}
else
{
/* 运行时: 根据 wal_keep_segments 配置保留 */
RemoveOldXlogFiles(_logSegNo, recptr);
}
}
Checkpoint 类型及其触发条件:
Checkpoint Types:
┌──────────────────────────────────────────────────────────────┐
│ Type │ Trigger Condition │ Characteristics │
├──────────────────────────────────────────────────────────────┤
│ │ │ │
│ Shutdown │ pg_ctl stop / smart │ Flushes ALL │
│ Checkpoint │ shutdown │ dirty pages │
│ │ │ Updates │
│ │ │ pg_control │
│ │ │ state = │
│ │ │ SHUTDOWNED │
├──────────────────┼──────────────────────────────┼─────────────┤
│ │ │ │
│ Time-based │ checkpoint_timeout (default │ Flushes │
│ (Periodic) │ = 5 min) elapsed since last │ dirty pages │
│ │ checkpoint │ Can be │
│ │ │ spread out │
│ │ │ over time │
├──────────────────┼──────────────────────────────┼─────────────┤
│ │ │ │
│ Request-based │ CHECKPOINT command issued │ Immediate │
│ │ by user/admin │ execution │
├──────────────────┼──────────────────────────────┼─────────────┤
│ │ │ │
│ Superuser │ START REPLICATION command │ Ensures │
│ Requested │ or base backup starts │ consistent │
│ │ │ backup point │
├──────────────────┼──────────────────────────────┼─────────────┤
│ │ │ │
│ End-of-Recovery │ After crash/PITR recovery │ REQUIRED! │
│ │ completes │ First thing │
│ │ │ after │
│ │ │ recovery │
└──────────────────┴──────────────────────────────┴─────────────┘
恢复过程中 Checkpoint 的特殊处理:
// xlogrecovery.c (RecoveryComplete)
static void
RecoveryComplete(void)
{
/*
* 恢复完成后必须立即执行 Checkpoint!
*
* 原因:
* 1. 此时所有脏页面都已刷新 (redo 过程中)
* 2. 需要记录一个新的恢复后的一致性点
* 3. 后续的运行从这个新 checkpoint 开始
* 4. 可以截断不需要的旧 WAL 记录
*/
/*
* 创建一个特殊的 "end-of-recovery" checkpoint
* 这个 checkpoint 是 REDO-FORCED 类型的
* 即使没有脏页面也会创建 (为了标记恢复完成)
*/
CheckPointGuts(GetLastImportantRecPtr(), CHECKPOINT_END_OF_RECOVERY);
/*
* 关键: 更新 pg_control
*
* 将状态从 DB_IN_CRASH_RECOVERY 改为 DB_SHUTDOWNED
* 这样下次启动就不需要再恢复了!
*/
ControlFile->state = DB_SHUTDOWNED;
ControlFile->time = (pg_time_t) time(NULL);
ControlFile->checkPointCopy.redo = GetLastImportantRecPtr();
UpdateControlFile();
ereport(LOG,
(errmsg("recovery has completed"),
errdetail("Last completed transaction was at log sequence time %s.",
XLogFileName2(ControlFile->checkPoint))));
}
五、完整故障处理流程图
5.1 运行时故障检测与处理全景图
┌─────────────────────────────────────────────────────────────────────────┐
│ PostgreSQL 故障处理全景图 │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 故障来源 (Fault Sources) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ 进程级故障 │ │ 系统级故障 │ │ 网络级故障 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • Kill -9 │ │ • OOM Killer │ │ • 客户端断开 │ │ │
│ │ │ • Segfault │ │ • 磁盘满 │ │ • 网络分区 │ │ │
│ │ │ • Assert失败 │ │ • IO 错误 │ │ • 超时 │ │ │
│ │ │ • Stack溢出 │ │ • 断电 │ │ • 防火墙中断 │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │ │
│ │ │ │ │ │ │
│ └─────────┼─────────────────┼─────────────────────┼──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 检测层 (Detection Layer) │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Signal Handling │ │ Poll/Select │ │ I/O Error Check │ │ │
│ │ │ │ │ Event Loop │ │ │ │ │
│ │ │ • SIGCHLD │ │ • latch based │ │ • send() retval │ │ │
│ │ │ • SIGTERM │ │ • epoll/kqueue │ │ • recv() retval │ │ │
│ │ │ • SIGSEGV │ │ • timeout check │ │ • errno codes │ │ │
│ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │
│ │ │ │ │ │ │
│ └───────────┼────────────────────┼─────────────────────┼──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 处理层 (Handling Layer) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ Child Crash Handler │ │ Network Disconnect Handler │ │ │
│ │ │ │ │ │ │ │
│ │ │ HandleChildCrash() │ │ • TransactionAbort() │ │ │
│ │ │ ├─ LogChildExit() │ │ • ProcReleaseLocks() │ │ │
│ │ │ ├─ FatalError=true │ │ • PortalDropAll() │ │ │
│ │ │ ├─ SignalChildren() │ │ • ResetCommPorts() │ │ │
│ │ │ └─ PM_WAIT_DEAD_END │ │ • _exit(0) │ │ │
│ │ └──────────┬──────────┘ └──────────────┬──────────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ Exception Handler (if needed) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │ │ │
│ │ │ │ Deadlock │ │ Lock Timeout │ │ Statement │ │ │ │
│ │ │ │ Detect │ │ Handler │ │ Cancel Handler │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │• Build WFG │ │• Report ERR │ │• Interrupt │ │ │ │
│ │ │ │• Find Cycle │ │• Abort Txn │ │• Rollback │ │ │ │
│ │ │ │• Choose Vict │ │• Clean locks │ │• Notify client│ │ │ │
│ │ │ └─────────────┘ └──────────────┘ └────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 清理层 (Cleanup Layer) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Resource Cleanup Checklist │ │ │
│ │ │ │ │ │
│ │ │ ☑ 1. Release All Locks (table/row/page/lwlock) │ │ │
│ │ │ ☑ 2. Abort Active Transaction (rollback changes) │ │ │
│ │ │ ☑ 3. Clear PGPROC Entry (shared memory) │ │ │
│ │ │ ☑ 4. Remove From ProcArray (visibility) │ │ │
│ │ │ ☑ 5. Release Predicate Locks (SSI) │ │ │
│ │ │ ☑ 6. Free Local Memory Contexts │ │ │
│ │ │ ☑ 7. Close Open Files/FDs │ │ │
│ │ │ ☑ 8. Release Buffer Pins │ │ │
│ │ │ ☑ 9. Clean Up Temp Tables/Files │ │ │
│ │ │ ☑ 10. Destroy Portals/Cursors │ │ │
│ │ │ ☑ 11. Unlink Prepared Statements │ │ │
│ │ │ ☑ 12. Close Client Socket Connection │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
5.2 启动恢复流程图
┌─────────────────────────────────────────────────────────────────────────┐
│ PostgreSQL 启动恢复流程 │
└─────────────────────────────────────────────────────────────────────────┘
User executes: pg_ctl start -D /path/to/data
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Phase 1: Pre-Startup Checks (启动前检查) │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Check 1: PID File (postmaster.pid) │ │
│ │ │ │
│ │ Does $DATADIR/postmaster.pid exist? │ │
│ │ ├─ NO → Continue to Check 2 │ │
│ │ └─ YES → Read PID → kill(pid, 0)? │ │
│ │ ├─ Running → ❌ FATAL: "already running" │ │
│ │ └─ Not running → Delete stale file → continue │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Check 2: Data Directory Lock File │ │
│ │ │ │
│ │ Can create exclusive lock on datadir? │ │
│ │ ├─ YES → Continue │ │
│ │ └─ NO → ❌ FATAL: "another instance using same datadir" │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Check 3: Shared Memory │ │
│ │ │ │
│ │ Can create shared memory segment? │ │
│ │ ├─ YES → Continue │ │
│ │ └─ NO → Check if orphan segment │ │
│ │ ├─ Orphan (nattach=0) → Remove + recreate │ │
│ │ └─ In-use → ❌ FATAL: "still in use" │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Check 4: Socket Port │ │
│ │ │ │
│ │ Can bind to configured port (default 5432)? │ │
│ │ ├─ YES → Continue │ │
│ │ └─ NO → ❌ FATAL: "port already in use" │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Check 5: pg_control Integrity │ │
│ │ │ │
│ │ Read pg_control → Verify CRC checksum │ │
│ │ ├─ Valid → Check database state │ │
│ │ │ ├─ DB_SHUTDOWNED → Normal start (no recovery) │ │
│ │ │ └─ Other → Need Recovery → Go to Phase 2 │ │
│ │ └─ Invalid → ❌ FATAL: "pg_control corrupt" │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Phase 2: Recovery Execution (恢复执行) │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Startup Process Created (PID=N) │ │
│ │ │ │
│ │ Tasks: │ │
│ │ 1. Attach to shared memory │ │
│ │ 2. Initialize signal handlers │ │
│ │ 3. Read recovery configuration (if PITR) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Locate Recovery Starting Point │ │
│ │ │ │
│ │ Read latest checkpoint record from pg_control: │ │
│ │ • checkpoint.redo → WAL position to start replay │ │
│ │ • checkpoint.nextXid → Next transaction ID │ │
│ │ • checkpoint.nextOid → Next OID │ │
│ │ • checkpoint.time → Timestamp of checkpoint │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ██████████████ WAL REDO LOOP ████████████████████████ │ │
│ │ │ │
│ │ While (more WAL records to replay): │ │
│ │ { │ │
│ │ Read WAL record from pg_wal/archive │ │
│ │ │ │ │
│ │ ├── Is it a checkpoint record? │ │
│ │ │ └─ Yes: Update recovery position info │ │
│ │ │ │ │
│ │ ├── Is it a transaction commit/abort? │ │
│ │ │ └─ Yes: Update CLOG (commit log) │ │
│ │ │ │ │
│ │ ├── Is it a heap operation? (insert/update/delete) │ │
│ │ │ └─ Yes: Apply to target page │ │
│ │ │ • Read page into buffer pool │ │
│ │ │ • If page.LSN >= record.LSN: SKIP (optimization) │ │
│ │ │ • Else: Apply modification │ │
│ │ │ • Update page.LSN = record.LSN │ │
│ │ │ │ │
│ │ ├── Is it a btree operation? │ │
│ │ │ └─ Yes: Apply index modification │ │
│ │ │ │ │
│ │ └── [PITR Only] Reached recovery target? │ │
│ │ └─ Yes: Stop replaying │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Recovery Completion Actions │ │
│ │ │ │
│ │ 1. Create end-of-recovery checkpoint (REQUIRED!) │ │
│ │ 2. Update pg_control: │ │
│ │ • state = DB_SHUTDOWNED │ │
│ │ • checkpoint = new checkpoint location │ │
│ │ 3. Clean up temporary objects │ │
│ │ 4. Reset unlogged relations (discard contents) │ │
│ │ 5. Sync all filesystems (fsync) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Phase 3: Normal Operations Transition (正常运行过渡) │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Postmaster State Machine Transition │ │
│ │ │ │
│ │ PM_STARTUP ──[startup complete]──→ PM_RUN │ │
│ │ (or PM_RECOVERY/HOT_STANDBY for replicas) │ │
│ │ │ │
│ │ Actions taken: │ │
│ │ • Startup Process exits (or stays for standby) │ │
│ │ • Background Writer started │ │
│ │ • Checkpointer started │ │
│ │ • WAL Writer started │ │
│ │ • (Optional) WAL Receiver started (for streaming replica) │ │
│ │ • AutoVacuum Launcher started │ │
│ │ • Stats Collector started │ │
│ │ • Logger started │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Ready for Connections! │ │
│ │ │ │
│ │ Server now accepting connections on port 5432 │ │
│ │ │ │
│ │ Log message: │ │
│ │ "database system is ready to accept connections" │ │
│ │ │ │
│ │ Recovery Statistics (if recovery occurred): │ │
│ │ • WAL files replayed: N │ │
│ │ • Recovery time: X seconds │ │
│ │ • Transactions recovered: M committed, N aborted │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
六、生产环境最佳实践
6.1 监控与告警配置
-- 推荐的监控指标:
-- 1. 进程存活检查
SELECT pid, usename, application_name, state, query_start, waiting_reason
FROM pg_stat_activity
WHERE state != 'idle';
-- 2. 锁等待监控 (检测潜在死锁)
SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity
ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
-- 3. 恢复状态检查
SELECT pg_is_in_recovery(),
pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn(),
pg_last_xact_replay_timestamp();
-- 4. 复制延迟 (对于备库)
SELECT client_addr, state,
sent_lsn, write_lsn, flush_lsn, replay_lsn,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;
6.2 关键参数调优建议
# postgresql.conf - 故障恢复相关参数优化
# ---------------------------------------------------
# WAL 和 Checkpoint 配置
# ---------------------------------------------------
wal_level = replica # 支持备份和流复制
wal_compression = zstd # 压缩 WAL (节省空间)
max_wal_size = 4GB # WAL 之间 checkpoint 的最大尺寸
min_wal_size = 1GB # 保留的最小 WAL 尺寸
checkpoint_completion_target = 0.9 # 平滑 checkpoint I/O (90% 时间)
checkpoint_timeout = 10min # checkpoint 时间间隔
# ---------------------------------------------------
# 故障检测参数
# ---------------------------------------------------
deadlock_timeout = 1s # 死锁检测触发时间 (默认即可)
lock_timeout = 0 # 单锁等待超时 (0=不限制)
statement_timeout = 0 # 语句超时 (根据业务设置)
idle_in_transaction_session_timeout = 10min # 空闲事务超时
# ---------------------------------------------------
# 连接和资源管理
# ---------------------------------------------------
tcp_keepalives_idle = 60 # TCP keepalive 空闲时间
tcp_keepalives_interval = 30 # keepalive 探测间隔
tcp_keepalives_count = 8 # 最大丢失探测数
listen_addresses = '*' # 监听地址
# ---------------------------------------------------
# 日志和诊断
# ---------------------------------------------------
logging_collector = on # 开启日志收集
log_min_messages = warning # 记录警告及以上
log_line_prefix = '%t [%p]: [%l-1] %q%ux@%d '
log_checkpoints = on # 记录 checkpoint
log_connections = on # 记录连接
log_disconnections = on # 记录断开
log_lock_waits = on # 记录锁等待超时
log_error_verbosity = verbose # 详细错误信息 (包含 SQLSTATE, 等)
6.3 应急处理脚本
#!/bin/bash
# pg_emergency_diagnosis.sh
# PostgreSQL 故障诊断脚本
set -euo pipefail
PGDATA="${1:-/var/lib/postgresql/data}"
PGPORT="${2:-5432}"
echo "=== PostgreSQL Emergency Diagnosis ==="
echo "Timestamp: $(date)"
echo ""
# 1. 检查 postmaster 进程
echo "[1] Checking postmaster process..."
if pgrep -f "postgres.*-D.*${PGDATA}" > /dev/null 2>&1; then
echo " Status: RUNNING (PID: $(pgrep -f "postgres.*-D.*${PGDATA}"))"
else
echo " Status: NOT RUNNING"
fi
# 2. 检查 PID 文件
echo ""
echo "[2] Checking PID file..."
PIDFILE="${PGDATA}/postmaster.pid"
if [[ -f "$PIDFILE" ]]; then
echo " PID file exists"
OLD_PID=$(head -1 "$PIDFILE")
echo " Recorded PID: ${OLD_PID}"
if kill -0 "$OLD_PID" 2>/dev/null; then
echo " Process alive: YES"
else
echo " Process alive: NO (stale PID file!)"
fi
else
echo " PID file: NOT FOUND"
fi
# 3. 检查端口占用
echo ""
echo "[3] Checking port ${PGPORT}..."
if netstat -tlnp 2>/dev/null | grep -q ":${PGPORT} "; then
echo " Port ${PGPORT}: IN USE"
netstat -tlnp 2>/dev/null | grep ":${PGPORT} "
else
echo " Port ${PGPORT}: AVAILABLE"
fi
# 4. 检查共享内存
echo ""
echo "[4] Checking shared memory..."
if ipcs -m 2>/dev/null | grep -q "postgres"; then
echo " Shared memory segments found:"
ipcs -m 2>/dev/null | grep "postgres"
else
echo " No PostgreSQL shared memory segments"
fi
# 5. 检查 pg_control
echo ""
echo "[5] Checking pg_control..."
if [[ -f "${PGDATA}/global/pg_control" ]]; then
echo " pg_control exists"
# 使用 pg_controldata 检查状态
if command -v pg_controldata &>/dev/null; then
pg_controldata -D "${PGDATA}" | grep -E "(Database cluster state|Latest checkpoint|Prior checkpoint)"
fi
else
echo " pg_control: MISSING (corrupt datadir?)"
fi
# 6. 检查最近日志
echo ""
echo "[6] Recent log entries..."
if [[ -f "${PGDATA}/log/postgresql.log" ]]; then
tail -20 "${PGDATA}/log/postgresql.log"
fi
echo ""
echo "=== Diagnosis Complete ==="
七、总结
本文深入分析了 PostgreSQL 的故障检测、异常处理和恢复机制的完整链路:
核心要点回顾:
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL 可靠性保障体系 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 运行时故障检测 │
│ • 信号驱动架构 (SIGCHLD/SIGTERM/SIGPIPE) │
│ • Postmaster 状态机 (PM_* states) │
│ • 子进程生命周期管理 (fork/exec/waitpid) │
│ • 网络断开多层级检测 (SIGPIPE/I/O errors/keepalive) │
│ │
│ 2️⃣ 异常处理机制 │
│ • 死锁检测 (Wait-for-Graph + DFS cycle detection) │
│ • 锁超时 (SIGALRM based timeout system) │
│ • PG_TRY/PG_CATCH (setjmp/longjmp exception handling) │
│ • 系统化资源清理 (locks, transactions, memory, I/O) │
│ │
│ 3️⃣ 启动冲突检测 │
│ • PID 文件互斥 (postmaster.pid + kill check) │
│ • 共享内存冲突检测 (IPC_CREAT | IPC_EXCL) │
│ • Socket 端口绑定检查 (bind() + EADDRINUSE) │
│ • pg_control 完整性验证 (CRC checksum) │
│ │
│ 4️⃣ WAL 恢复机制 │
│ • ARIES 恢复理论 (Write-Ahead, Repeating History, CLR) │
│ • Startup Process 负责 (startup.c) │
│ • WAL Redo 循环 (ReadRecord → rm_redo → apply) │
│ • Checkpoint 一致性点 (dirty page flush + sync) │
│ │
└─────────────────────────────────────────────────────────────────┘
设计哲学总结:
-
防御性编程: 每个可能的故障点都有对应的检测和处理机制
-
优雅降级: 从 Smart Shutdown → Fast Shutdown → Immediate Shutdown 的多级策略
-
自动化恢复: Crash Recovery 无需人工干预,确保数据一致性
-
最小权限: 信号处理器只做最少的工作,复杂逻辑延迟到安全上下文
-
资源隔离: 共享内存、锁、文件句柄等资源都有明确的归属和清理责任
这套机制使得 PostgreSQL 能够在各种故障场景下保持数据的持久性 (Durability) 和一致性 (Consistency),真正实现了 ACID 事务特性的承诺。
附录:关键源码索引
| 功能模块 | 主要源码文件 | 关键函数/数据结构 |
|---|---|---|
| 信号处理 | [postmaster.c] |
handle_pm_child_exit_signal(), HandleChildCrash() |
| 状态机 | [postmaster.c] |
PostmasterStateMachine(), UpdatePMState() |
| 死锁检测 | [deadlock.c] |
DeadLockCheck(), FindLockCycleRecurse(), EDGE |
| 进程管理 | [proc.c] |
ProcKill(), ProcReleaseLocks(), PGPROC |
| 异常处理 | [elog.h] |
PG_TRY(), PG_CATCH(), PG_END_TRY() |
| PID 文件 | [pidfile.c] |
CreateLockFile() |
| 共享内存 | [shmem.c] |
InternalIpcMemoryCreate() |
| Socket 管理 | [pqcomm.c] |
StreamServerPort() |
| Startup 进程 | [startup.c] |
StartupProcessMain(), PerformRecovery() |
| WAL 恢复 | [xlogrecovery.c] |
ReadRecord(), RmgrTable[] |
| Checkpoint | [checkpointer.c] |
CreateCheckpoint(), BufferSync() |
文档版本: v1.0
适用版本: PostgreSQL 13-17
最后更新: 2026-06-12
声明: 本文基于 PostgreSQL 17.x 源码进行分析,部分内部实现细节可能随版本变化而调整。建议读者结合具体版本的源码进行对照学习。
更多推荐

所有评论(0)