pgBackRest备份解析

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_serverpgbackrest.conf: 配置了 repo1-host=repo_server, repo1-host-user=pgbackrest, pg1-path=/path/to/pgdata 等。
    • repo_serverpgbackrest.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)

  1. 启动命令 (在 repo_server):

    • repo_server 上以 pgbackrest 用户执行 pgbackrest --stanza=mystanza --type=full backup
    • pgBackRest 进程启动,读取 repo_server 上的 /etc/pgbackrest.conf (或指定配置文件)。
    • 解析命令行参数 (--stanza, --type) 和配置文件,确定 Stanza mystanza 的相关信息,包括数据库服务器地址 (pg1-host=pg_server)、用户 (pg1-user=postgres) 和仓库路径 (repo1-path=/path/to/repo)。
  2. 获取仓库锁 (在 repo_server):

    • pgBackRest 尝试在备份仓库中为 mystanza 创建并锁定一个文件(例如 /path/to/repo/backup/mystanza/backup.lock)。
    • 目的: 防止同一时间对同一个 Stanza 执行另一个备份或恢复操作,保证操作的原子性和一致性。如果锁定失败(文件已存在并被锁定),命令会报错退出。
  3. 连接数据库服务器 & 准备备份 (SSH 交互):

    • pgBackRest 在 repo_server 上的主进程需要与 pg_server 上的 PostgreSQL 实例交互以开始备份。

    • SSH 命令 (概念性): 它会执行类似如下的 SSH 命令来在 pg_server 上启动一个 pgBackRest remote 进程来处理数据库端的操作:

      Bash 复制代码
      ssh 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 上的主进程。
  4. 传输数据文件 (并行的 SSH 交互):

    • repo_server 上的主进程了解到备份已在 pg_server 开始。

    • 它会确定需要复制的文件列表(对于 full 备份,是 $PGDATA 下的所有文件,除了 pg_wal 等排除项)。

    • 主进程会启动多个工作 (worker) 进程 (数量由 process-max 控制) 在 repo_server 上并行处理文件传输。

    • 每个 worker 进程 (在 repo_server) 会与 pg_server 建立 SSH 连接来获取文件数据:

      • SSH 命令 (概念性): 每个 worker 可能执行类似命令,请求特定的文件或文件块:

        Bash 复制代码
        ssh 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 进程成功传输和验证。

  5. 结束数据库服务器上的备份 (SSH 交互):

    • 当所有 worker 进程在 repo_server 上完成文件传输后,主进程再次通过 SSH 连接到 pg_server 来结束备份模式。

    • SSH 命令 (概念性):

      Bash 复制代码
      ssh 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 上的主进程。
  6. 验证 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 是否已经在那里。
  7. 写入备份元数据 (在 repo_server):

    • pgBackRest 在备份仓库中为这次备份创建一个重要的清单文件 (manifest file) ,通常是 backup.manifest

    • 此文件包含:

      • 备份的详细信息 (类型、时间戳、LSN 范围、PostgreSQL 版本、系统标识符等)。
      • 备份中包含的所有文件的列表,及其大小、修改时间、校验和等。
      • 关于依赖的 WAL 段的信息。
      • 表空间信息(如果有)。
    • 这个文件对于后续的恢复操作至关重要。

  8. 更新备份信息和清理 (在 repo_server):

    • 更新 Stanza 的全局信息文件 (例如 backup.info),记录这次新备份的状态。
    • 根据配置的保留策略 (repo1-retention-full, repo1-retention-diff 等),检查并删除过期的旧备份集及其相关的 WAL 文件。
    • 释放仓库锁: 删除之前创建的锁文件 (/path/to/repo/backup/mystanza/backup.lock)。
    • 备份命令执行结束,向用户报告成功或失败。

四、pgBackRest 保证备份的一致性

  1. 利用 PostgreSQL 的备份 API: pgBackRest 在备份开始时会调用 PostgreSQL 的 pg_start_backup() 函数。这个函数会强制执行一个检查点(checkpoint),并将数据库集群的状态标记为正在进行基础备份。同时,PostgreSQL 会记录当前的 WAL 位置(LSN),并将这些信息写入数据目录下的 backup_label 文件。在备份期间,所有对数据库的修改都会被记录在 WAL 文件中。备份结束时,pgBackRest 调用 pg_stop_backup() 函数,PostgreSQL 会再执行一个检查点,并记录备份结束时的 WAL 位置,将这些信息写入 backup_history 文件。
  2. 文件复制:pg_start_backup()pg_stop_backup() 之间,pgBackRest 会复制 PostgreSQL 数据目录中的文件。由于数据库在运行,文件内容可能会在复制过程中发生变化。然而,这种变化并不会导致备份最终不一致,因为所有在备份期间发生的修改都已经被完整地记录在了 WAL 文件中。
  3. WAL 归档的完整性: pgBackRest 的关键在于确保在备份开始和结束的 WAL 位置之间的所有 WAL 文件都被成功地、连续地归档到 pgBackRest 的仓库中。通常,这是通过将 PostgreSQL 的 archive_command 参数配置为调用 pgbackrest archive-push 命令来实现的。
  4. 恢复时的 WAL 重放: 在执行恢复时,pgBackRest 首先恢复的是文件复制完成时的物理文件状态(即备份结束时的物理快照)。然后,PostgreSQL 在启动时会进入恢复模式,并利用 pgBackRest 仓库中归档的 WAL 文件进行重放。通过按顺序应用 WAL 文件中记录的修改,PostgreSQL 可以将数据库恢复到备份结束时的一致状态,或者进一步重放到更晚的、指定的时间点(即 PITR)。

五、WAL 文件存储的数据格式

WAL 文件是 PostgreSQL 用于记录所有数据库修改的二进制日志文件。它们不是简单的文本文件,而是结构化的二进制数据流。WAL 文件存储的数据被称为 WAL 记录。

WAL 文件由一个文件头和一系列连续的 WAL 记录组成。WAL 记录的格式是二进制的,并且其具体结构取决于记录所描述的操作类型。

每条 WAL 记录通常包含以下几个主要部分(这些是概念上的组成部分,实际在二进制文件中是紧凑排列的):

  1. 记录头部 (Record Header):

    • 总长度: 记录的总字节数。
    • LSN: 这条 WAL 记录自身的 LSN(一个不断增长的逻辑地址)。
    • 前一个记录的 LSN: 指向前一条 WAL 记录的 LSN,用于构建 WAL 的逻辑链。
    • 事务 ID (XID): 与该记录关联的事务 ID(如果记录与某个事务相关)。
    • 信息标志 (Info Flags): 描述记录的特性,例如是否是 Full Page Write。
    • 记录类型标识: 标识这条 WAL 记录描述的是哪种类型的操作(例如,插入、更新、删除、检查点、事务提交等)。PostgreSQL 有很多内置的 WAL 记录类型,每个子系统(堆表、索引、锁、事务管理器等)都有自己特有的记录类型。
  2. 数据块引用 (Block References):

    • 对于修改数据块的 WAL 记录,头部后面会跟着对受影响数据块的引用信息。这包括:

      • 关系文件节点 (Relation File Node): 唯一标识哪个表、索引或序列文件被修改。
      • 分叉编号 (Fork Number): 标识文件中的哪个"分叉"(例如,主数据分叉 main,空闲空间映射 fsm,可见性映射 vm)。
      • 块号 (Block Number): 标识文件中的具体哪个数据块(通常是 8KB 的页面)。
    • 这些引用信息告诉恢复机制应该加载哪个数据块到内存进行修改。

  3. 重做信息 (Redo Information):

    • 这是 WAL 记录中包含实际修改数据的部分。其具体内容完全取决于记录的类型。

    • 例如:

      • INSERT 记录: 可能包含新插入元组的二进制数据。
      • UPDATE 记录: 可能包含旧元组的清理信息和新元组的数据。
      • DELETE 记录: 可能包含标记元组为已删除所需的信息。
      • 索引记录: 包含修改索引结构所需的信息(例如,在 B-tree 索引中插入或删除一个条目)。
      • 提交/中止记录: 包含事务提交或中止的时间戳、LSN 等信息。
      • 检查点记录: 包含检查点相关的系统状态信息。
    • 对于 Full Page Write 记录,这里的重做信息就是受影响的整个数据页面的完整二进制镜像。

  4. 校验和 (Checksum) (可选):

    • 如果在 data_checksums = on 初始化了数据库集群,或者在 WAL 级别启用了校验,WAL 记录会包含一个校验和,用于验证记录的完整性,防止 WAL 文件本身的损坏。

六、WAL 恢复过程

中具体是如何恢复的?

WAL 恢复过程(Recovery / WAL Replay)是在 PostgreSQL 启动时,发现数据文件处于不一致状态(通常是由于非正常关机)或者需要从一个基础备份恢复时自动触发的。这个过程由 PostgreSQL 的后端进程负责执行。 具体步骤和细节如下:

  1. 进入恢复状态: 当 PostgreSQL 启动检测到需要恢复时,它不会像正常启动那样直接开放连接。而是进入恢复模式,通常由一个或多个后端的恢复进程负责。

  2. 定位恢复起点 (REDO Point): PostgreSQL 读取控制文件 (pg_control) 和最近的检查点(checkpoint)记录。这些信息包含了恢复必须从哪个 WAL 位置(LSN - Log Sequence Number)开始前滚,这个位置就是 REDO Point。这个 REDO Point 之前的所有修改都已经通过检查点被写入数据文件了,所以不需要重复应用。

  3. 获取并读取 WAL 文件: 从 REDO Point 所在的 WAL 段文件开始,PostgreSQL 需要按顺序读取后续的 WAL 段文件。正如之前提到的,这依赖于 restore_command 的配置。PostgreSQL 会计算出下一个需要的 WAL 文件名,然后调用 restore_command 将该文件从归档位置获取到 PGDATA/pg_wal 目录下。如果文件已在 pg_wal 目录中(例如,是 crash 时未归档的最新 WAL 段),则直接读取。

  4. 解析 WAL 记录: 获取到 WAL 段文件后,PostgreSQL 会顺序读取文件中的每一条 WAL 记录。每条 WAL 记录都代表着一个数据库操作(例如,插入一行、更新一个索引条目、提交一个事务等)所导致的底层数据块的修改。

  5. 应用 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),表示它与磁盘上的版本不同。这些脏页面会在后续的检查点过程中被后台的写入进程刷写到磁盘。
  6. 处理事务状态: WAL 记录中也包含事务的提交(COMMIT)或中止(ABORT)信息。在重放过程中,PostgreSQL 会追踪事务的状态。只有标记为已提交的事务的修改才会被视为最终有效的。如果遇到中止的事务的修改记录,这些修改最终会被忽略或回滚(尽管物理修改可能已经被重放,但在逻辑上不会保留)。

  7. 顺序重放直到恢复目标: PostgreSQL 会持续地、按 LSN 的严格顺序读取和重放 WAL 记录,直到达到配置的恢复目标(例如,某个时间点、LSN 或归档的末尾)。

  8. 完成恢复: 一旦达到恢复目标,PostgreSQL 会停止 WAL 重放,执行一些清理工作,然后切换到正常运行模式,并开始接受客户端连接。


七、WAL 恢复机制的重放幂等性

假设场景:

  • 备份开始 (pg_start_backup()) 在 LSN LSN_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 检查

  1. 数据页面头部的 LSN (pd_lsn): PostgreSQL 的每一个数据页面(Page,通常是 8KB)的头部都包含一个 LSN (pd_lsn)。这个 pd_lsn 记录了这个页面的最后一次修改所对应的 WAL 记录的 LSN 。当一个事务修改了某个数据页面并将相关的 WAL 记录写入 WAL 文件并刷盘后,这个页面的 pd_lsn 也会被更新为该 WAL 记录的 LSN。

  2. 恢复时的 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。
相关推荐
iuyou️15 分钟前
Spring Boot知识点详解
java·spring boot·后端
一弓虽27 分钟前
SpringBoot 学习
java·spring boot·后端·学习
姑苏洛言36 分钟前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
光而不耀@lgy1 小时前
C++初登门槛
linux·开发语言·网络·c++·后端
方圆想当图灵1 小时前
由 Mybatis 源码畅谈软件设计(七):SQL “染色” 拦截器实战
后端·mybatis·代码规范
毅航2 小时前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
我的golang之路果然有问题2 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
柏油2 小时前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql
写bug写bug4 小时前
Java Streams 中的7个常见错误
java·后端
Luck小吕4 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议