pgBackRest 是一个功能强大、可靠且易于使用的 PostgreSQL 备份和恢复工具。它的设计目标是处理大规模数据库和高并发负载。
一、pgBackRest 核心特性
pgBackRest 的备份原理结合了 PostgreSQL 自身的物理备份机制和 WAL (Write-Ahead Log) 归档,并在此基础上增加了许多优化和管理功能。
- 并行处理 (Parallelism): 备份和恢复过程可以使用多个进程并行执行,大大缩短了大型数据库的操作时间。
- 压缩 (Compression): 支持多种压缩算法(如 gzip, lz4, zstd)来减少备份文件的大小和网络传输量。
- 校验和 (Checksums): 在备份、恢复和 WAL 归档的各个阶段都进行数据校验,确保数据完整性。默认情况下,即使 PostgreSQL 本身的 checksums 关闭,pgBackRest 也会进行校验。
- 断点续传 (Resumable Backups): 如果备份过程中断(例如网络问题),可以从中断点继续,而不是重新开始。
- 备份轮转和保留 (Retention): 可以配置策略自动删除过期的备份,以节省存储空间。支持基于数量或时间的保留策略。
- 加密 (Encryption): 支持对备份仓库中的数据进行加密。
- Delta Restore: 在恢复时,如果目标目录已存在部分未损坏的数据文件,pgBackRest 可以只恢复不同的或缺失的文件/块,加速恢复过程。
二、pgBackRest 备份前提条件
-
所有机器: 都安装了
pgBackRest
-
数据库服务器:
pg_server
(运行 PostgreSQL) -
备份仓库服务器:
repo_server
(存储备份文件) -
PostgreSQL 用户:
postgres
(拥有$PGDATA
权限) -
备份仓库用户:
pgbackrest
(拥有备份仓库目录权限) -
Stanza 名称:
mystanza
-
pgBackRest 配置:
- 在
pg_server
的pgbackrest.conf
: 配置了repo1-host=repo_server
,repo1-host-user=pgbackrest
,pg1-path=/path/to/pgdata
等。 - 在
repo_server
的pgbackrest.conf
: 配置了pg1-host=pg_server
,pg1-user=postgres
,repo1-path=/path/to/repo
等。
- 在
-
SSH 免密登录: 已配置好
pgbackrest@repo_server
可以免密登录到postgres@pg_server
,并且postgres@pg_server
可以免密登录到pgbackrest@repo_server
。 -
命令执行: 我们将在
repo_server
上以pgbackrest
用户身份执行备份命令。
三、全量备份流程 (pgbackrest --stanza=mystanza --type=full backup
)
-
启动命令 (在
repo_server
):- 在
repo_server
上以pgbackrest
用户执行pgbackrest --stanza=mystanza --type=full backup
。 - pgBackRest 进程启动,读取
repo_server
上的/etc/pgbackrest.conf
(或指定配置文件)。 - 解析命令行参数 (
--stanza
,--type
) 和配置文件,确定 Stanzamystanza
的相关信息,包括数据库服务器地址 (pg1-host=pg_server
)、用户 (pg1-user=postgres
) 和仓库路径 (repo1-path=/path/to/repo
)。
- 在
-
获取仓库锁 (在
repo_server
):- pgBackRest 尝试在备份仓库中为
mystanza
创建并锁定一个文件(例如/path/to/repo/backup/mystanza/backup.lock
)。 - 目的: 防止同一时间对同一个 Stanza 执行另一个备份或恢复操作,保证操作的原子性和一致性。如果锁定失败(文件已存在并被锁定),命令会报错退出。
- pgBackRest 尝试在备份仓库中为
-
连接数据库服务器 & 准备备份 (SSH 交互):
-
pgBackRest 在
repo_server
上的主进程需要与pg_server
上的 PostgreSQL 实例交互以开始备份。 -
SSH 命令 (概念性): 它会执行类似如下的 SSH 命令来在
pg_server
上启动一个 pgBackRestremote
进程来处理数据库端的操作:Bashssh postgres@pg_server "pgbackrest --stanza=mystanza --log-level-file=<level> --command=backup --type=db --process=1 remote"
(注意: 这只是概念性表示,实际命令和参数可能更复杂,pgBackRest 使用其内部协议进行通信)
-
pg_server
上的remote
进程:- 这个远程进程启动,读取
pg_server
上的pgbackrest.conf
以获取pg1-path
等本地信息。 - 连接到本地 PostgreSQL 实例 (使用
pg1-path
找到 socket 或通过pg1-host
/pg1-port
连接)。 - 执行
pg_start_backup('pgBackRest backup start ...')
SQL 命令(或使用流复制协议的等效命令)。 - PostgreSQL 进入备份模式,创建一个
backup_label
文件在$PGDATA
中,并返回备份的起始 LSN (Log Sequence Number)。 - 如果
start-fast=n
(默认为y
),会等待检查点完成。通常会快速继续。 remote
进程将起始 LSN 和backup_label
内容等信息通过 SSH 连接返回给repo_server
上的主进程。
- 这个远程进程启动,读取
-
-
传输数据文件 (并行的 SSH 交互):
-
repo_server
上的主进程了解到备份已在pg_server
开始。 -
它会确定需要复制的文件列表(对于
full
备份,是$PGDATA
下的所有文件,除了pg_wal
等排除项)。 -
主进程会启动多个工作 (worker) 进程 (数量由
process-max
控制) 在repo_server
上并行处理文件传输。 -
每个 worker 进程 (在
repo_server
) 会与pg_server
建立 SSH 连接来获取文件数据:-
SSH 命令 (概念性): 每个 worker 可能执行类似命令,请求特定的文件或文件块:
Bashssh postgres@pg_server "pgbackrest --stanza=mystanza --command=backup --type=file --process=<worker_id> remote --file-path=/path/to/pgdata/base/12345/67890 --compress=<type> --checksum ..."
(同样,这是概念表示。pgBackRest 可能使用更优化的流式传输协议通过 SSH 隧道)
-
pg_server
上的remote
进程 (或由主 remote 进程协调的文件服务): 读取请求的文件,进行压缩(如果配置在源端压缩)、计算校验和,然后将数据流通过 SSH 连接发送回repo_server
上的对应 worker 进程。 -
repo_server
上的 worker 进程: 接收数据流,进行解压(如果压缩在源端)、校验和验证,然后将文件写入备份仓库 (repo1-path
) 中对应的目录结构下。
-
-
这个过程会持续进行,直到所有需要备份的文件都被 worker 进程成功传输和验证。
-
-
结束数据库服务器上的备份 (SSH 交互):
-
当所有 worker 进程在
repo_server
上完成文件传输后,主进程再次通过 SSH 连接到pg_server
来结束备份模式。 -
SSH 命令 (概念性):
Bashssh postgres@pg_server "pgbackrest --stanza=mystanza --log-level-file=<level> --command=backup --type=db --action=stop --process=1 remote"
-
pg_server
上的remote
进程:- 连接到 PostgreSQL 实例。
- 执行
pg_stop_backup()
SQL 命令(或流复制协议等效命令)。 - PostgreSQL 退出备份模式,将包含结束 LSN 和其他元信息的
backup_label
文件重命名为backup_history
文件(旧机制)或完成相关记录,并返回结束 LSN、所需的最后一个 WAL 文件名等信息。 remote
进程将这些结束信息通过 SSH 返回给repo_server
上的主进程。
-
-
验证 WAL 归档 (在
repo_server
):repo_server
上的主进程现在知道了备份覆盖的 LSN 范围(从pg_start_backup
的 LSN 到pg_stop_backup
的 LSN)。- 它会检查备份仓库中的 WAL 归档目录 (
/path/to/repo/archive/mystanza
),确认从上一个备份结束到本次备份结束之间的所有必需的 WAL 文件是否都已存在。 - 重要:
backup
命令本身通常不负责 触发 WAL 的推送 (archive-push
)。它依赖于 PostgreSQL 配置的archive_command
(指向pgbackrest archive-push
) 持续不断地将 WAL 文件推送到仓库。backup
命令只是在结束时检查 这些必需的 WAL 是否已经在那里。
-
写入备份元数据 (在
repo_server
):-
pgBackRest 在备份仓库中为这次备份创建一个重要的清单文件 (manifest file) ,通常是
backup.manifest
。 -
此文件包含:
- 备份的详细信息 (类型、时间戳、LSN 范围、PostgreSQL 版本、系统标识符等)。
- 备份中包含的所有文件的列表,及其大小、修改时间、校验和等。
- 关于依赖的 WAL 段的信息。
- 表空间信息(如果有)。
-
这个文件对于后续的恢复操作至关重要。
-
-
更新备份信息和清理 (在
repo_server
):- 更新 Stanza 的全局信息文件 (例如
backup.info
),记录这次新备份的状态。 - 根据配置的保留策略 (
repo1-retention-full
,repo1-retention-diff
等),检查并删除过期的旧备份集及其相关的 WAL 文件。 - 释放仓库锁: 删除之前创建的锁文件 (
/path/to/repo/backup/mystanza/backup.lock
)。 - 备份命令执行结束,向用户报告成功或失败。
- 更新 Stanza 的全局信息文件 (例如
四、pgBackRest 保证备份的一致性
- 利用 PostgreSQL 的备份 API: pgBackRest 在备份开始时会调用 PostgreSQL 的
pg_start_backup()
函数。这个函数会强制执行一个检查点(checkpoint),并将数据库集群的状态标记为正在进行基础备份。同时,PostgreSQL 会记录当前的 WAL 位置(LSN),并将这些信息写入数据目录下的backup_label
文件。在备份期间,所有对数据库的修改都会被记录在 WAL 文件中。备份结束时,pgBackRest 调用pg_stop_backup()
函数,PostgreSQL 会再执行一个检查点,并记录备份结束时的 WAL 位置,将这些信息写入backup_history
文件。 - 文件复制: 在
pg_start_backup()
和pg_stop_backup()
之间,pgBackRest 会复制 PostgreSQL 数据目录中的文件。由于数据库在运行,文件内容可能会在复制过程中发生变化。然而,这种变化并不会导致备份最终不一致,因为所有在备份期间发生的修改都已经被完整地记录在了 WAL 文件中。 - WAL 归档的完整性: pgBackRest 的关键在于确保在备份开始和结束的 WAL 位置之间的所有 WAL 文件都被成功地、连续地归档到 pgBackRest 的仓库中。通常,这是通过将 PostgreSQL 的
archive_command
参数配置为调用pgbackrest archive-push
命令来实现的。 - 恢复时的 WAL 重放: 在执行恢复时,pgBackRest 首先恢复的是文件复制完成时的物理文件状态(即备份结束时的物理快照)。然后,PostgreSQL 在启动时会进入恢复模式,并利用 pgBackRest 仓库中归档的 WAL 文件进行重放。通过按顺序应用 WAL 文件中记录的修改,PostgreSQL 可以将数据库恢复到备份结束时的一致状态,或者进一步重放到更晚的、指定的时间点(即 PITR)。
五、WAL 文件存储的数据格式
WAL 文件是 PostgreSQL 用于记录所有数据库修改的二进制日志文件。它们不是简单的文本文件,而是结构化的二进制数据流。WAL 文件存储的数据被称为 WAL 记录。
WAL 文件由一个文件头和一系列连续的 WAL 记录组成。WAL 记录的格式是二进制的,并且其具体结构取决于记录所描述的操作类型。
每条 WAL 记录通常包含以下几个主要部分(这些是概念上的组成部分,实际在二进制文件中是紧凑排列的):
-
记录头部 (Record Header):
- 总长度: 记录的总字节数。
- LSN: 这条 WAL 记录自身的 LSN(一个不断增长的逻辑地址)。
- 前一个记录的 LSN: 指向前一条 WAL 记录的 LSN,用于构建 WAL 的逻辑链。
- 事务 ID (XID): 与该记录关联的事务 ID(如果记录与某个事务相关)。
- 信息标志 (Info Flags): 描述记录的特性,例如是否是 Full Page Write。
- 记录类型标识: 标识这条 WAL 记录描述的是哪种类型的操作(例如,插入、更新、删除、检查点、事务提交等)。PostgreSQL 有很多内置的 WAL 记录类型,每个子系统(堆表、索引、锁、事务管理器等)都有自己特有的记录类型。
-
数据块引用 (Block References):
-
对于修改数据块的 WAL 记录,头部后面会跟着对受影响数据块的引用信息。这包括:
- 关系文件节点 (Relation File Node): 唯一标识哪个表、索引或序列文件被修改。
- 分叉编号 (Fork Number): 标识文件中的哪个"分叉"(例如,主数据分叉
main
,空闲空间映射fsm
,可见性映射vm
)。 - 块号 (Block Number): 标识文件中的具体哪个数据块(通常是 8KB 的页面)。
-
这些引用信息告诉恢复机制应该加载哪个数据块到内存进行修改。
-
-
重做信息 (Redo Information):
-
这是 WAL 记录中包含实际修改数据的部分。其具体内容完全取决于记录的类型。
-
例如:
- INSERT 记录: 可能包含新插入元组的二进制数据。
- UPDATE 记录: 可能包含旧元组的清理信息和新元组的数据。
- DELETE 记录: 可能包含标记元组为已删除所需的信息。
- 索引记录: 包含修改索引结构所需的信息(例如,在 B-tree 索引中插入或删除一个条目)。
- 提交/中止记录: 包含事务提交或中止的时间戳、LSN 等信息。
- 检查点记录: 包含检查点相关的系统状态信息。
-
对于 Full Page Write 记录,这里的重做信息就是受影响的整个数据页面的完整二进制镜像。
-
-
校验和 (Checksum) (可选):
- 如果在
data_checksums = on
初始化了数据库集群,或者在 WAL 级别启用了校验,WAL 记录会包含一个校验和,用于验证记录的完整性,防止 WAL 文件本身的损坏。
- 如果在
六、WAL 恢复过程
中具体是如何恢复的?
WAL 恢复过程(Recovery / WAL Replay)是在 PostgreSQL 启动时,发现数据文件处于不一致状态(通常是由于非正常关机)或者需要从一个基础备份恢复时自动触发的。这个过程由 PostgreSQL 的后端进程负责执行。 具体步骤和细节如下:
-
进入恢复状态: 当 PostgreSQL 启动检测到需要恢复时,它不会像正常启动那样直接开放连接。而是进入恢复模式,通常由一个或多个后端的恢复进程负责。
-
定位恢复起点 (REDO Point): PostgreSQL 读取控制文件 (
pg_control
) 和最近的检查点(checkpoint)记录。这些信息包含了恢复必须从哪个 WAL 位置(LSN - Log Sequence Number)开始前滚,这个位置就是 REDO Point。这个 REDO Point 之前的所有修改都已经通过检查点被写入数据文件了,所以不需要重复应用。 -
获取并读取 WAL 文件: 从 REDO Point 所在的 WAL 段文件开始,PostgreSQL 需要按顺序读取后续的 WAL 段文件。正如之前提到的,这依赖于
restore_command
的配置。PostgreSQL 会计算出下一个需要的 WAL 文件名,然后调用restore_command
将该文件从归档位置获取到 PGDATA/pg_wal 目录下。如果文件已在pg_wal
目录中(例如,是 crash 时未归档的最新 WAL 段),则直接读取。 -
解析 WAL 记录: 获取到 WAL 段文件后,PostgreSQL 会顺序读取文件中的每一条 WAL 记录。每条 WAL 记录都代表着一个数据库操作(例如,插入一行、更新一个索引条目、提交一个事务等)所导致的底层数据块的修改。
-
应用 WAL 记录 (Redo Logic): 对于读取到的每一条 WAL 记录,PostgreSQL 的恢复机制会执行相应的"重做"逻辑(redo logic)。这个逻辑是硬编码在 PostgreSQL 源代码中的,用于根据 WAL 记录中描述的变化来修改对应的数据页面。
- 定位受影响的数据块: 每条 WAL 记录都会包含受影响的数据块的信息,例如关系文件节点 (relfilenode)、fork 编号 (main, fsm, vm 等) 和块号 (block number)。PostgreSQL 会找到并加载内存中对应的共享缓冲区中的数据页面。如果页面不在内存中,则从磁盘加载到共享缓冲区。
- 应用修改: 根据 WAL 记录的类型和内容,PostgreSQL 会在加载到内存的数据页面上应用记录的修改。例如,对于一个 INSERT 记录,它可能包含新元组的数据,PostgreSQL 会将元组添加到页面中。对于一个 UPDATE 记录,它可能包含旧元组的标记和新元组的数据,PostgreSQL 会在页面中进行相应的修改。
- 处理 Full Page Writes (FPW): 在检查点之后,对某个数据页面的第一次修改会触发一个 Full Page Write,即将整个页面的镜像写入 WAL。在恢复时,如果遇到 FPW 记录,PostgreSQL 会直接用 WAL 中的页面镜像替换内存中的页面内容,而不是应用更细粒度的修改。这是为了应对操作系统在写入数据页面时可能发生的"部分写"问题,确保页面不会处于一个损坏的状态。
- 标记页面为"脏": 当一个页面被 WAL 重放修改后,它在共享缓冲区中会被标记为"脏"(dirty),表示它与磁盘上的版本不同。这些脏页面会在后续的检查点过程中被后台的写入进程刷写到磁盘。
-
处理事务状态: WAL 记录中也包含事务的提交(COMMIT)或中止(ABORT)信息。在重放过程中,PostgreSQL 会追踪事务的状态。只有标记为已提交的事务的修改才会被视为最终有效的。如果遇到中止的事务的修改记录,这些修改最终会被忽略或回滚(尽管物理修改可能已经被重放,但在逻辑上不会保留)。
-
顺序重放直到恢复目标: PostgreSQL 会持续地、按 LSN 的严格顺序读取和重放 WAL 记录,直到达到配置的恢复目标(例如,某个时间点、LSN 或归档的末尾)。
-
完成恢复: 一旦达到恢复目标,PostgreSQL 会停止 WAL 重放,执行一些清理工作,然后切换到正常运行模式,并开始接受客户端连接。
七、WAL 恢复机制的重放幂等性
假设场景:
-
备份开始 (
pg_start_backup()
) 在 LSNLSN_start
。 -
文件 A 原本是 A1 版本。
-
在 pgBackRest 复制文件 A 之前,PostgreSQL 执行了一个更新操作,将 A1 变成了 A2。这个更新操作被记录在了 WAL 中,对应的 LSN 是
LSN_A1_A2
(LSN_start < LSN_A1_A2
). -
pgBackRest 复制文件 A 时,读取并复制到了 A2 版本,即存储到备份机中,数据已经是最新的数据A2。
-
备份结束在 LSN
LSN_end
。需要归档并重放的 WAL 范围是[LSN_start, LSN_end]
。
那么,PostgreSQL 是如何避免将导致 A1 变成 A2 的那个 WAL 记录 (LSN_A1_A2
) 再次应用到已经包含 A2 内容的文件上的呢?
核心机制在于 数据页面头部的 LSN 和 PostgreSQL 在重放 WAL 时进行的 LSN 检查:
-
数据页面头部的 LSN (
pd_lsn
): PostgreSQL 的每一个数据页面(Page,通常是 8KB)的头部都包含一个 LSN (pd_lsn
)。这个pd_lsn
记录了这个页面的最后一次修改所对应的 WAL 记录的 LSN 。当一个事务修改了某个数据页面并将相关的 WAL 记录写入 WAL 文件并刷盘后,这个页面的pd_lsn
也会被更新为该 WAL 记录的 LSN。 -
恢复时的 LSN 检查: 在 WAL 重放过程中,当 PostgreSQL 读取到一条 WAL 记录(假设其 LSN 为
LSN_record
),并准备将其应用到某个数据页面时,它会首先将LSN_record
与内存中(或从磁盘加载到内存中)该数据页面的当前pd_lsn
进行比较。- 如果页面的
pd_lsn
小于LSN_record
,这意味着这个页面当前的物理状态还没有反映LSN_record
所描述的修改。PostgreSQL 就会执行该 WAL 记录对应的重做逻辑,将修改应用到这个页面,并将页面的pd_lsn
更新为LSN_record
。 - 如果页面的
pd_lsn
大于或等于LSN_record
,这意味着这个页面当前的物理状态已经包含了LSN_record
所描述的修改,甚至包含了更晚的修改。在这种情况下,PostgreSQL 会跳过对这个页面应用LSN_record
所描述的修改,直接处理下一条 WAL 记录或下一个页面。
- 如果页面的
回到 A1 -> A2 的例子:
- 导致 A1 变成 A2 的更新操作发生在 LSN
LSN_A1_A2
。这个操作修改了文件 A 中的某些数据页面。这些页面的pd_lsn
会被更新为LSN_A1_A2
。 - pgBackRest 复制的文件 A 是 A2 版本,这意味着复制的数据页面已经包含了 LSN
LSN_A1_A2
的修改,并且这些页面的pd_lsn
已经被设置为LSN_A1_A2
。 - 在恢复时,PostgreSQL 恢复了包含 A2 版本的这些页面。当 WAL 重放进行到 LSN
LSN_A1_A2
这条 WAL 记录时,PostgreSQL 会尝试将其应用到文件 A 中对应的页面。 - 在尝试应用之前,PostgreSQL 检查这些页面的当前
pd_lsn
。由于恢复的文件已经是 A2 版本,页面的pd_lsn
已经是LSN_A1_A2
。 - 因为页面的
pd_lsn
(LSN_A1_A2
) 大于或等于 WAL 记录的 LSN (LSN_A1_A2
),PostgreSQL 会判断这条记录的修改已经反映在页面中了,从而跳过对这个页面的重做操作。 - WAL 重放会继续按顺序进行,处理 LSN 大于
LSN_A1_A2
的 WAL 记录,直到达到恢复目标 LSN。