2. 流复制原理与WAL机制

PostgreSQL 的流复制并不是把数据文件直接“拷贝”到备库,而是围绕 WAL(Write-Ahead Logging,预写式日志)建立的一条持续传输与重放链路。理解这条链路,是理解高可用、同步语义、故障切换和复制延迟的前提。本节从 WAL 的生成与归档讲起,再引入 LSN 与时间线这两个贯穿复制全程的坐标概念,最后对比物理复制与逻辑复制,并厘清同步与异步复制的语义差异。

WAL 的生成:先写日志,再写数据

PostgreSQL 采用预写式日志机制,核心规则是:任何数据页的修改在写入磁盘之前,描述该修改的 WAL 记录必须先被持久化。这样即使数据页尚未落盘就发生崩溃,也能通过重放 WAL 恢复出一致状态。

WAL 记录在内存中先进入 WAL 缓冲区(wal_buffers),随后由后台进程写入 WAL 段文件。WAL 段文件默认大小为 16MB,命名形如 000000010000000000000001,由时间线号、逻辑日志号和段号共同决定。当事务提交时,如果 synchronous_commit 为 on,提交动作会等待本地 WAL 刷盘(或等待同步备库确认)后才向客户端返回成功。

WAL 的持久化位置由 pg_wal 目录承载。归档则是把已经写满或即将被回收的 WAL 段复制到 archive_status 标记之外的长期存储中,用于时间点恢复(PITR)或搭建新的备库。归档由 archive_mode 和 archive_command 控制,例如:

archive_mode = on
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'

其中 %p 是 WAL 段在 pg_wal 中的路径,%f 是文件名。归档命令返回 0 表示成功,PostgreSQL 才会把该段标记为已归档。归档与流复制是两条并行的 WAL 消费路径:流复制负责“实时传输”,归档负责“长期留存与补建”。

LSN:复制链路上的全局坐标

LSN(Log Sequence Number)是 WAL 中的字节偏移量,用两个十六进制数表示,例如 0/1A2B3C4D。它单调递增,是复制链路上最关键的坐标:主库当前写入位置、备库已接收位置、备库已重放位置,都用 LSN 描述。

在复制连接中,备库会向主库报告自己已接收并持久化的 LSN,主库据此决定从哪个位置继续发送 WAL。常用观察点包括:

  • pg_current_wal_lsn():主库当前写入的 LSN。
  • pg_current_wal_insert_lsn():主库当前插入位置的 LSN,通常略大于写入 LSN。
  • pg_last_wal_receive_lsn():备库已接收并写入本地 WAL 的 LSN。
  • pg_last_wal_replay_lsn():备库已重放完成的 LSN。

复制延迟可以用 LSN 差值近似衡量:主库 pg_current_wal_lsn() 与备库 pg_last_wal_replay_lsn() 之间的字节差,除以主库的 WAL 生成速率,就是大致的时间延迟。注意这是近似值,因为 WAL 生成速率并非恒定。

时间线:切换后的分叉标识

时间线(Timeline)用于标识数据库集群在恢复或切换后的历史分叉。初始时间线为 1。当备库被提升为主库时,它会创建一条新的时间线,例如从 1 提升后进入时间线 2。这样做的目的是避免旧主库和新主库各自继续写入同一段 WAL 历史,造成无法区分的数据分叉。

时间线号体现在 WAL 文件名中,也记录在 pg_control 文件里。搭建备库或做 PITR 时,必须选择正确的时间线,否则会重放错误的历史分支。切换后,旧主库若重新加入集群,通常需要以新时间线为基础重新搭建或执行 rewinding,而不能直接继续旧时间线。

物理复制与逻辑复制的区别

物理复制传输的是 WAL 记录的原始字节流,备库按块级别重放,因此要求主备库的 PostgreSQL 大版本、页大小、架构等底层格式一致。它复制的是整个集群,包括所有数据库、系统表和索引。流复制就是典型的物理复制。

逻辑复制则基于逻辑解码,把 WAL 解析成行级变更(INSERT/UPDATE/DELETE),再在订阅端以 SQL 方式应用。它可以选择性地复制部分表,允许主备库版本不同,也允许订阅端有额外的索引或触发器。逻辑复制不复制 DDL,也不复制序列状态,需要额外处理。

两者最直观的差异是:物理复制“复制集群”,逻辑复制“复制表的数据变更”。物理复制延迟通常更低,逻辑复制灵活性更高但开销更大。

同步与异步复制的语义

异步复制下,主库提交事务时只等待本地 WAL 刷盘,不等待备库确认。因此主库故障时,尚未传输到备库的事务会丢失,备库提升后可能缺少最后一段数据。它的优点是提交延迟低、对备库故障不敏感。

同步复制下,主库提交事务时会等待至少一个同步备库确认已接收并持久化 WAL,才向客户端返回成功。这保证了已提交事务在备库上不丢失,但代价是提交延迟受备库影响,且备库故障可能导致主库写入阻塞。同步复制的控制参数包括:

  • synchronous_commit:控制提交等待级别,如 on、remote_write、local、off。
  • synchronous_standby_names:指定哪些备库参与同步确认,支持 FIRST、ANY 等语法。

例如 synchronous_standby_names = 'FIRST 1 (standby1, standby2)' 表示 standby1 和 standby2 中按顺序第一个可用的备库作为同步备库。若两个都不可用,主库提交会等待,直到有同步备库恢复或配置被调整。

同步复制保证的是“已提交事务不丢失”,而不是“零延迟”。备库重放可能滞后于接收,因此同步确认通常只要求 WAL 已接收并持久化,而不要求已重放完成。

WAL 生成、传输与重放的整体链路

把上述概念串起来,可以看到一条完整的链路:主库产生 WAL 记录并写入 pg_wal;WAL 发送进程(walsender)通过复制连接把 WAL 流发送给备库;备库的 WAL 接收进程(walreceiver)接收并写入本地 pg_wal;随后备库的启动进程(startup)按 LSN 顺序重放 WAL,使数据页逐步追上主库。归档则在主库侧把 WAL 段复制到归档目录,作为长期留存和补建备库的来源。

PostgreSQL 流复制 WAL 链路 主库 Primary 事务提交 synchronous_commit WAL 缓冲区 → pg_wal 16MB 段文件 walsender 进程 发送 WAL 流 archive_command 归档 WAL 段 备库 Standby walreceiver 进程 接收 WAL 流 写入本地 pg_wal pg_last_wal_receive_lsn() startup 进程重放 pg_last_wal_replay_lsn() 数据页逐步追上主库 只读查询可见 WAL 流(复制连接) 归档路径(长期留存) 备库反馈已接收 LSN LSN 坐标: 主库写入 LSN → 备库接收 LSN → 备库重放 LSN 时间线 Timeline 标识分叉

从图中可以看到,WAL 是贯穿主备的唯一数据载体,LSN 是它的位置坐标,时间线是它的历史分支标识。物理复制沿这条链路传输原始 WAL,逻辑复制则是在 WAL 之上再做一层逻辑解码。同步与异步的区别,本质上就是主库提交时是否等待这条链路上备库的确认。掌握这条链路,后续配置流复制、监控延迟、执行切换时,就能清楚每一步在操作什么、观察什么、判断什么。