日期: 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 IPCPOSIX 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)            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

设计哲学总结:

  1. 防御性编程: 每个可能的故障点都有对应的检测和处理机制

  2. 优雅降级: 从 Smart Shutdown → Fast Shutdown → Immediate Shutdown 的多级策略

  3. 自动化恢复: Crash Recovery 无需人工干预,确保数据一致性

  4. 最小权限: 信号处理器只做最少的工作,复杂逻辑延迟到安全上下文

  5. 资源隔离: 共享内存、锁、文件句柄等资源都有明确的归属和清理责任

这套机制使得 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 源码进行分析,部分内部实现细节可能随版本变化而调整。建议读者结合具体版本的源码进行对照学习。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐