概述
PostgreSQL 的物理备份(Physical Backup)是数据库高可用与灾难恢复的基石。与逻辑备份(pg_dump)不同,物理备份直接复制数据库集群的数据目录文件,保留了事务日志(WAL)的完整上下文,是实现 时间点恢复(PITR, Point-In-Time Recovery) 和搭建流复制备库的前提。
本文将从 PostgreSQL 官方提供的底层备份函数出发,深入剖析 pg_backup_start / pg_backup_stop 的工作原理、排他/非排他备份模式的历史演进与陷阱,进而介绍自动化工具 pg_basebackup 的核心参数,最后盘点 PG 生态中主流的第三方备份工具(pgBackRest、Barman、pg_probackup)并进行横向对比,为生产环境的备份选型提供参考。
纲要
pg_backup_start/pg_backup_stop:底层备份函数的工作原理full_page_writes与块折断预防- 检查点(Checkpoint)与
backup_label文件
- 排他备份模式(Exclusive Backup Mode)vs 非排他备份模式(Non-Exclusive Backup Mode)
- 排他模式的致命缺陷:崩溃恢复时的“时光倒流”陷阱
- PostgreSQL 15 的重大变更:移除排他模式与函数重命名
pg_basebackup:官方自动化物理备份工具- 核心用法与
--max-rate限速参数 - PostgreSQL 17 新特性:增量备份(Incremental Backup)
- 核心用法与
- 第三方备份工具生态对比
- pgBackRest、Barman、pg_probackup 功能矩阵
- 各工具的限制与适用场景
- 生产环境备份选型建议
底层备份 API:pg_backup_start 与 pg_backup_stop
PostgreSQL 提供了一组底层函数,用于将数据库实例置入“备份模式”(Backup Mode),以便外部工具(如 cp、rsync、存储快照)能够安全地复制数据目录。
核心工作流程
一个标准的低级别物理备份流程如下:
- 进入备份模式:执行
SELECT pg_backup_start('label', false);。 - 复制数据目录:使用
cp、rsync或存储快照技术复制整个数据目录。 - 退出备份模式:执行
SELECT pg_backup_stop();。
在执行 pg_backup_start 时,PostgreSQL 内部会依次完成以下动作:
- 强制开启
full_page_writes:在备份期间,每个数据页第一次被修改时会将完整页面写入 WAL。这是为了预防“块折断”(Block Splitting)问题——操作系统通常以 4KB 为单位写入,而 PostgreSQL 数据页默认为 8KB,若在写入过程中发生崩溃,可能导致数据页损坏。full_page_writes确保了恢复时能从 WAL 中获取完整的页面副本进行修复。 - 切换 WAL 日志段:强制切换当前 WAL 日志文件,确保备份开始后的所有变更都记录在新的 WAL 段中。
- 执行检查点(Checkpoint):将内存中的脏数据刷写到磁盘,并将检查点位置记录到
backup_label文件中。 - 创建
backup_label文件:该文件位于数据目录根目录,记录了备份的起始 LSN(Log Sequence Number)、检查点位置、备份标签等信息。
pg_backup_stop 则执行对称的清理工作:退出备份模式、写入一条备份结束的 WAL 记录、创建 backup_history 文件(记录备份的时间范围和 WAL 起止位置),并删除 backup_label 文件(非排他模式下返回其内容而非写入磁盘)。
整个备份流程的时序关系如下图所示:
排他备份模式的陷阱与 PostgreSQL 15 的变革
在 PostgreSQL 15 之前,pg_start_backup 和 pg_stop_backup 支持两种模式:排他备份(Exclusive Backup) 和非排他备份(Non-Exclusive Backup)。
排他模式(Exclusive Backup)的致命缺陷
排他备份模式存在一个严重的安全隐患:如果在备份期间数据库崩溃,重启时可能导致恢复时间异常延长,甚至启动失败。
其根本原因在于 backup_label 文件。正常情况下,PostgreSQL 崩溃恢复时从控制文件 pg_control 中读取最新的检查点位置开始回放 WAL。但若数据目录中存在 backup_label 文件,PostgreSQL 会优先从中读取检查点位置。
考虑以下场景:
- 管理员执行
pg_start_backup,生成了一个backup_label文件(记录了两天前的检查点 LSN)。 - 由于某种原因(如备份脚本中断),
pg_stop_backup未被调用,backup_label残留。 - 两天后数据库崩溃。
- 重启时,PostgreSQL 检测到
backup_label文件,从两天前的检查点开始回放 WAL——而非从最近的检查点。
这意味着数据库需要重放过去两天产生的所有 WAL 日志,恢复时间被无限拉长。更糟糕的是,如果这两天的 WAL 日志已被归档或回收,数据库将直接启动失败,报错 could not locate required checkpoint record。
注意:排他模式自 PostgreSQL 9.6 引入非排他模式后便被标记为弃用(Deprecated),历经 6 个主版本的过渡期后,于 PostgreSQL 15 中正式移除。
非排他模式(Non-Exclusive Backup)的优势
非排他备份模式解决了上述问题。其核心差异在于:
- 不会在数据目录中创建
backup_label文件,而是将备份标签信息保存在内存中,并在pg_stop_backup执行时以结果集形式返回。 - 允许同时运行多个非排他备份(排他模式同一时间只允许一个备份会话)。
- 即使备份会话异常中断,也不会留下干扰崩溃恢复的残留文件。
因此,所有现代备份工具(pg_basebackup、pgBackRest、Barman 等)均默认使用非排他模式。
PostgreSQL 15 的函数重命名
为避免混淆并明确标识非排他模式为唯一选项,PostgreSQL 15 将相关函数进行了重命名:
| PostgreSQL 14 及更早 | PostgreSQL 15 及之后 |
|---|---|
pg_start_backup(label, exclusive, ...) | pg_backup_start(label, false) |
pg_stop_backup() | pg_backup_stop() |
pg_is_in_backup() | 已移除 |
pg_backup_start_time() | 已移除 |
版本提示:若你的备份脚本仍在使用 pg_start_backup / pg_stop_backup,请尽快迁移至 PostgreSQL 15+ 的新 API。
pg_basebackup:官方自动化备份工具
pg_basebackup 是 PostgreSQL 官方提供的物理备份工具,底层自动调用 pg_backup_start / pg_backup_stop 并完成数据目录的复制。
基本用法
# 全量备份到指定目录
pg_basebackup -D /backup/base -U replicator -h 192.168.1.100 -p 5432 -Fp -P -v
| 参数 | 说明 |
|---|---|
-D | 备份目标目录 |
-U | 连接用户名(需具备 REPLICATION 权限) |
-h / -p | 数据库主机和端口 |
-Fp | 输出格式为 plain(目录结构),-Ft 为 tar 格式 |
-P | 显示进度信息 |
-v | 详细输出 |
-X stream | 备份期间流式传输 WAL(推荐) |
限速参数 --max-rate:生产环境的必备利器
在生产环境中,备份操作可能占用大量 I/O 带宽,影响业务响应。pg_basebackup 提供了 --max-rate 参数来限制传输速率:
pg_basebackup -D /backup/base --max-rate=100M -U replicator -h 192.168.1.100
上述命令将备份传输速率限制为 100MB/s。该参数在以下场景尤为实用:
- 网络带宽有限的环境
- 对 I/O 延迟敏感的生产系统
- 需要在业务高峰期执行备份时
PostgreSQL 17 新特性:增量备份(Incremental Backup)
从 PostgreSQL 17 开始,pg_basebackup 原生支持块级增量备份(Block-Level Incremental Backup)。
增量备份的原理是:基于上一次全量备份的 backup_manifest 清单文件,仅备份自参考备份以来发生变化的数据块(而非整个文件)。
# 1. 创建全量备份
pg_basebackup -D /backup/full -c fast -P -v
# 2. 基于全量备份创建增量备份(PostgreSQL 17+)
pg_basebackup -D /backup/inc1 --incremental=/backup/full/backup_manifest -c fast -P -v
重要限制:
- 增量备份不能直接用于恢复,必须使用
pg_combinebackup将增量备份与依赖的全量备份合并。 --incremental参数仅适用于 PostgreSQL 17 及更高版本的服务器。- 使用增量备份前,需将
summarize_wal参数设置为on。
第三方备份工具生态对比
PostgreSQL 社区拥有丰富的第三方备份解决方案,各有侧重。以下对主流的四个工具进行横向对比。
| 特性 | pgBackRest | Barman | pg_probackup | WAL-G |
|---|---|---|---|---|
| 开发者 | Crunchy Data | EnterpriseDB (EDB) | Postgres Professional | 社区 |
| 全量备份 | ✅ | ✅ | ✅ | ✅ |
| 增量备份 | ✅(块级) | ✅(rsync 差分) | ✅(PTRACK 页级) | ✅(块级) |
| 对象存储(S3) | ✅ | ✅(需插件) | ✅ | ✅ |
| 加密 | ✅ | ✅ | ✅ | ✅ |
| 数据校验 | ✅ | ✅ | ✅ | ✅ |
| 并行备份/恢复 | ✅ | ✅ | ✅ | ✅ |
| 压缩 | ✅ | ✅ | ✅ | ✅ |
| 备库备份 | ❌(依赖 SSH 免密) | ✅ | ✅ | ✅ |
| 限制 | 仅支持 8KB 数据块大小 | 需配置 SSH 免密 | 不支持 2GB 以上大文件分段 | 配置较复杂 |
pgBackRest:功能最全面的首选
pgBackRest 是目前 PG 生态中功能最强大、生态最完善的备份工具。它支持:
- 并行备份与恢复
- 块级增量备份
- 加密与压缩
- S3、Azure、GCS 等云存储
- 备份完整性校验
主要限制:
- 仅支持 8KB 数据块大小:若数据库编译时指定了
--with-blocksize=16或--with-blocksize=32,pgBackRest 将无法使用,报错page size is 32768 but must be 8192。 - 备库备份依赖 SSH 免密:在通过跳板机访问的生产环境中,配置 SSH 免密存在安全风险。
尽管存在上述限制,pgBackRest 凭借其卓越的健壮性和功能完整性,仍是绝大多数生产环境的首选。
pg_probackup:PTRACK 页级增量的独特优势
pg_probackup 由 Postgres Professional 开发,其最突出的特点是支持 PTRACK(Page Tracking)页级增量备份——仅备份自上次备份以来实际发生变化的页面,增量效率极高。
主要限制:
- 不支持大于 2GB 的单个文件分段(segment)
- 社区活跃度相对较低
Barman:EDB 官方支持的成熟方案
Barman 是 EnterpriseDB 推出的备份工具,与 EDB 的商业支持体系紧密结合。适合已经深度使用 EDB 产品线的企业用户。
选型建议
- 通用生产环境:优先选择 pgBackRest,功能全面且生态成熟。
- 超大规模数据库(PB 级):可评估 pg_probackup 的 PTRACK 增量效率。
- EDB 生态用户:选择 Barman 可获得最佳兼容性。
- 轻量级/简单场景:直接使用
pg_basebackup即可满足需求(PostgreSQL 17+ 已支持增量备份)。
API 速览
pg_backup_start(label text [, fast boolean]) → text
| 属性 | 描述 |
|---|---|
| 所属 | PostgreSQL 系统函数 |
| 适用版本 | PostgreSQL 15+ |
| 参数 | label:备份标签(任意文本标识);fast:是否快速启动(强制立即检查点) |
| 返回值 | 备份起始的 WAL 位置(LSN) |
| 说明 | 将数据库置入备份模式。在 PostgreSQL 15 中,exclusive 参数已被移除 |
-- PostgreSQL 15+ 用法
SELECT pg_backup_start('my_backup_20260814', false);
pg_backup_stop([wait_for_archive boolean]) → record
| 属性 | 描述 |
|---|---|
| 所属 | PostgreSQL 系统函数 |
| 适用版本 | PostgreSQL 15+ |
| 参数 | wait_for_archive:是否等待 WAL 归档完成(默认 true) |
| 返回值 | 包含 lsn、labelfile、spcmapfile 等字段的记录 |
| 说明 | 退出备份模式,返回备份标签和表空间映射内容(非排他模式下需手动保存) |
SELECT * FROM pg_backup_stop();
pg_basebackup 命令行工具
| 属性 | 描述 |
|---|---|
| 所属 | PostgreSQL 客户端工具 |
| 适用版本 | PostgreSQL 9.1+(增量备份需 17+) |
| 关键参数 | -D、-U、-h、-p、--max-rate、--incremental(17+) |
| 权限要求 | REPLICATION 权限或超级用户 |
# 全量备份 + 限速
pg_basebackup -D /backup/full -U replicator -h 192.168.1.100 -p 5432 --max-rate=100M -Fp -P -v
# 增量备份(PostgreSQL 17+)
pg_basebackup -D /backup/inc --incremental=/backup/full/backup_manifest -U replicator -h 192.168.1.100 -P -v
Demo:基于 Node.js 的完整备份示例
以下示例使用 Node.js 和 pg 库(原生 PostgreSQL 驱动)演示完整的物理备份流程,包含底层 API 调用和文件复制。
运行说明
- 安装依赖:
npm init -y npm install pg - 修改数据库连接配置(
host、port、user、password、database)。 - 确保备份目录(
/tmp/pg_backup)存在且有写入权限。 - 执行脚本:
node backup.js
完整代码
const { Client } = require('pg');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
// ===== 配置 =====
const PG_CONFIG = {
host: '127.0.0.1',
port: 5432,
user: 'postgres',
password: 'your_password',
database: 'postgres',
};
const BACKUP_LABEL = 'node_backup_20260814';
const BACKUP_DIR = '/tmp/pg_backup';
const DATA_DIR = '/var/lib/postgresql/18/main'; // 根据实际 PG 数据目录修改
// ===== 核心备份函数 =====
async function performPhysicalBackup() {
const client = new Client(PG_CONFIG);
await client.connect();
try {
console.log('[1/5] 进入备份模式...');
// PostgreSQL 15+ 使用 pg_backup_start,不再有 exclusive 参数
const startResult = await client.query(
'SELECT pg_backup_start($1, false) as lsn',
[BACKUP_LABEL]
);
const startLsn = startResult.rows[0].lsn;
console.log(` -> 备份起始 LSN: ${startLsn}`);
console.log('[2/5] 复制数据目录...');
// 使用 rsync 复制数据目录(排除不需要的文件)
const rsyncCmd = `rsync -av --exclude='postmaster.pid' --exclude='pg_wal/*' ${DATA_DIR}/ ${BACKUP_DIR}/`;
await execPromise(rsyncCmd);
console.log(` -> 数据已复制到: ${BACKUP_DIR}`);
console.log('[3/5] 复制 WAL 日志...');
// 复制 WAL 日志(pg_wal 目录)
const walDir = path.join(DATA_DIR, 'pg_wal');
const walBackupDir = path.join(BACKUP_DIR, 'pg_wal');
if (!fs.existsSync(walBackupDir)) {
fs.mkdirSync(walBackupDir, { recursive: true });
}
const walRsyncCmd = `rsync -av ${walDir}/ ${walBackupDir}/`;
await execPromise(walRsyncCmd);
console.log(` -> WAL 已复制到: ${walBackupDir}`);
console.log('[4/5] 退出备份模式...');
const stopResult = await client.query(
'SELECT * FROM pg_backup_stop()'
);
const stopData = stopResult.rows[0];
console.log(` -> 备份结束 LSN: ${stopData.lsn}`);
// 非排他模式下,备份标签内容由 pg_backup_stop 返回,需手动保存
if (stopData.labelfile) {
const labelPath = path.join(BACKUP_DIR, 'backup_label');
fs.writeFileSync(labelPath, stopData.labelfile);
console.log(` -> backup_label 已保存到: ${labelPath}`);
}
if (stopData.spcmapfile) {
const spcPath = path.join(BACKUP_DIR, 'tablespace_map');
fs.writeFileSync(spcPath, stopData.spcmapfile);
console.log(` -> tablespace_map 已保存到: ${spcPath}`);
}
console.log('[5/5] 备份完成!');
console.log(` -> 备份位置: ${BACKUP_DIR}`);
console.log(` -> 恢复时需结合 WAL 归档进行 PITR`);
} catch (error) {
console.error('备份失败:', error);
// 尝试退出备份模式(清理残留状态)
try {
await client.query('SELECT pg_backup_stop()');
} catch (_) { /* ignore */ }
throw error;
} finally {
await client.end();
}
}
// ===== 辅助函数 =====
function execPromise(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, { maxBuffer: 1024 * 1024 * 100 }, (error, stdout, stderr) => {
if (error) {
reject(new Error(`命令执行失败: ${error.message}\n${stderr}`));
return;
}
resolve(stdout);
});
});
}
// ===== 执行 =====
performPhysicalBackup()
.then(() => {
console.log('\n✅ 物理备份成功完成!');
process.exit(0);
})
.catch((err) => {
console.error('\n❌ 物理备份失败:', err.message);
process.exit(1);
});
技术点总结
| 技术点 | 说明 |
|---|---|
| 底层 API 调用 | 使用 pg_backup_start / pg_backup_stop 手动控制备份模式 |
| 非排他模式 | pg_backup_start 不再包含 exclusive 参数,默认非排他 |
| 备份标签保存 | 非排他模式下 backup_label 由 pg_backup_stop 返回,需手动写入文件 |
| 数据复制 | 使用 rsync 实现增量式文件复制,支持断点续传 |
| WAL 复制 | 单独复制 pg_wal 目录,确保恢复时有完整的 WAL 日志 |
| 错误处理 | 备份失败时自动调用 pg_backup_stop 清理残留状态 |
PostgreSQL 原生指令对照:
-- 进入备份模式
SELECT pg_backup_start('node_backup_20260814', false);
-- 退出备份模式
SELECT * FROM pg_backup_stop();
多语言示例
Go 语言示例:基于 pgx 驱动实现物理备份
本示例使用 Go 语言和 pgx 驱动(PostgreSQL 原生驱动)调用底层备份 API,并通过 rsync 命令复制数据目录。
运行说明
- 初始化 Go 模块并安装依赖:
go mod init backup_demo go get github.com/jackc/pgx/v5 - 修改代码中的数据库连接配置(
host、port、user、password、database)以及备份目录和数据目录路径。 - 确保系统已安装
rsync命令。 - 执行:
go run backup.go
完整代码
package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/jackc/pgx/v5"
)
const (
backupLabel = "go_backup_20260814"
backupDir = "/tmp/pg_backup_go"
dataDir = "/var/lib/postgresql/18/main" // 根据实际 PG 数据目录修改
)
func main() {
ctx := context.Background()
// 数据库连接配置
connStr := "postgres://postgres:your_password@127.0.0.1:5432/postgres"
conn, err := pgx.Connect(ctx, connStr)
if err != nil {
panic(fmt.Errorf("连接失败: %w", err))
}
defer conn.Close(ctx)
fmt.Println("[1/5] 进入备份模式...")
var startLsn string
err = conn.QueryRow(ctx, "SELECT pg_backup_start($1, false)", backupLabel).Scan(&startLsn)
if err != nil {
panic(fmt.Errorf("pg_backup_start 失败: %w", err))
}
fmt.Printf(" -> 备份起始 LSN: %s\n", startLsn)
fmt.Println("[2/5] 复制数据目录...")
rsyncCmd := exec.Command("rsync", "-av", "--exclude=postmaster.pid", "--exclude=pg_wal/*",
dataDir+"/", backupDir+"/")
if out, err := rsyncCmd.CombinedOutput(); err != nil {
panic(fmt.Errorf("rsync 数据目录失败: %w, 输出: %s", err, out))
}
fmt.Printf(" -> 数据已复制到: %s\n", backupDir)
fmt.Println("[3/5] 复制 WAL 日志...")
walSrc := filepath.Join(dataDir, "pg_wal")
walDst := filepath.Join(backupDir, "pg_wal")
if err := os.MkdirAll(walDst, 0755); err != nil {
panic(fmt.Errorf("创建 WAL 备份目录失败: %w", err))
}
rsyncWalCmd := exec.Command("rsync", "-av", walSrc+"/", walDst+"/")
if out, err := rsyncWalCmd.CombinedOutput(); err != nil {
panic(fmt.Errorf("rsync WAL 失败: %w, 输出: %s", err, out))
}
fmt.Printf(" -> WAL 已复制到: %s\n", walDst)
fmt.Println("[4/5] 退出备份模式...")
var stopLsn, labelfile, spcmapfile string
err = conn.QueryRow(ctx, "SELECT lsn, labelfile, spcmapfile FROM pg_backup_stop()").
Scan(&stopLsn, &labelfile, &spcmapfile)
if err != nil {
panic(fmt.Errorf("pg_backup_stop 失败: %w", err))
}
fmt.Printf(" -> 备份结束 LSN: %s\n", stopLsn)
if labelfile != "" {
labelPath := filepath.Join(backupDir, "backup_label")
if err := os.WriteFile(labelPath, []byte(labelfile), 0644); err != nil {
panic(fmt.Errorf("写入 backup_label 失败: %w", err))
}
fmt.Printf(" -> backup_label 已保存到: %s\n", labelPath)
}
if spcmapfile != "" {
spcPath := filepath.Join(backupDir, "tablespace_map")
if err := os.WriteFile(spcPath, []byte(spcmapfile), 0644); err != nil {
panic(fmt.Errorf("写入 tablespace_map 失败: %w", err))
}
fmt.Printf(" -> tablespace_map 已保存到: %s\n", spcPath)
}
fmt.Println("[5/5] 备份完成!")
fmt.Printf(" -> 备份位置: %s\n", backupDir)
fmt.Println(" -> 恢复时需结合 WAL 归档进行 PITR")
}
技术点总结
- 使用
pgx驱动的QueryRow执行pg_backup_start/pg_backup_stop - 通过
os/exec调用rsync实现数据复制 - 正确处理非排他模式下返回的
labelfile和spcmapfile - 错误处理和资源清理(defer 关闭连接)
Python 语言示例:基于 psycopg2 实现物理备份
本示例使用 Python 和 psycopg2 驱动调用备份 API,并通过 subprocess 执行 rsync。
运行说明
- 安装依赖:
pip install psycopg2-binary - 修改代码中的数据库连接参数(
host、port、user、password、database)以及备份目录和数据目录。 - 确保系统已安装
rsync。 - 执行:
python backup.py
完整代码
import os
import subprocess
import psycopg2
from psycopg2 import sql
BACKUP_LABEL = "py_backup_20260814"
BACKUP_DIR = "/tmp/pg_backup_py"
DATA_DIR = "/var/lib/postgresql/18/main" # 根据实际 PG 数据目录修改
def run_cmd(cmd):
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"命令执行失败: {cmd}\n{result.stderr}")
return result.stdout
def main():
# 数据库连接
conn = psycopg2.connect(
host="127.0.0.1",
port=5432,
user="postgres",
password="your_password",
database="postgres"
)
conn.autocommit = True
cur = conn.cursor()
try:
print("[1/5] 进入备份模式...")
cur.execute("SELECT pg_backup_start(%s, false)", (BACKUP_LABEL,))
start_lsn = cur.fetchone()[0]
print(f" -> 备份起始 LSN: {start_lsn}")
print("[2/5] 复制数据目录...")
rsync_cmd = f"rsync -av --exclude='postmaster.pid' --exclude='pg_wal/*' {DATA_DIR}/ {BACKUP_DIR}/"
run_cmd(rsync_cmd)
print(f" -> 数据已复制到: {BACKUP_DIR}")
print("[3/5] 复制 WAL 日志...")
wal_src = os.path.join(DATA_DIR, "pg_wal")
wal_dst = os.path.join(BACKUP_DIR, "pg_wal")
os.makedirs(wal_dst, exist_ok=True)
wal_rsync = f"rsync -av {wal_src}/ {wal_dst}/"
run_cmd(wal_rsync)
print(f" -> WAL 已复制到: {wal_dst}")
print("[4/5] 退出备份模式...")
cur.execute("SELECT lsn, labelfile, spcmapfile FROM pg_backup_stop()")
stop_lsn, labelfile, spcmapfile = cur.fetchone()
print(f" -> 备份结束 LSN: {stop_lsn}")
if labelfile:
label_path = os.path.join(BACKUP_DIR, "backup_label")
with open(label_path, "w") as f:
f.write(labelfile)
print(f" -> backup_label 已保存到: {label_path}")
if spcmapfile:
spc_path = os.path.join(BACKUP_DIR, "tablespace_map")
with open(spc_path, "w") as f:
f.write(spcmapfile)
print(f" -> tablespace_map 已保存到: {spc_path}")
print("[5/5] 备份完成!")
print(f" -> 备份位置: {BACKUP_DIR}")
print(" -> 恢复时需结合 WAL 归档进行 PITR")
except Exception as e:
print(f"备份失败: {e}")
# 尝试清理
try:
cur.execute("SELECT pg_backup_stop()")
except:
pass
raise
finally:
cur.close()
conn.close()
if __name__ == "__main__":
main()
技术点总结
- 使用
psycopg2的自动提交模式(autocommit=True)以避免显式事务干扰 - 通过
subprocess.run执行rsync并捕获异常 - 使用
os.makedirs递归创建目录 - 在异常处理中尝试调用
pg_backup_stop()清理残留状态
Java 语言示例:基于 JDBC 实现物理备份
本示例使用 Java 和标准 JDBC 驱动(org.postgresql)调用备份 API,并通过 ProcessBuilder 执行 rsync。
运行说明
- 创建 Maven 项目并添加依赖(或直接下载 PostgreSQL JDBC 驱动):
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.3</version> </dependency> - 修改代码中的数据库连接 URL、用户名、密码以及备份目录和数据目录。
- 确保系统已安装
rsync。 - 编译并运行:
javac Backup.java java Backup
完整代码
import java.io.*;
import java.nio.file.*;
import java.sql.*;
public class Backup {
private static final String BACKUP_LABEL = "java_backup_20260814";
private static final String BACKUP_DIR = "/tmp/pg_backup_java";
private static final String DATA_DIR = "/var/lib/postgresql/18/main"; // 根据实际修改
private static final String DB_URL = "jdbc:postgresql://127.0.0.1:5432/postgres";
private static final String DB_USER = "postgres";
private static final String DB_PASS = "your_password";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS)) {
conn.setAutoCommit(true);
System.out.println("[1/5] 进入备份模式...");
String startLsn;
try (PreparedStatement stmt = conn.prepareStatement("SELECT pg_backup_start(?, false)")) {
stmt.setString(1, BACKUP_LABEL);
ResultSet rs = stmt.executeQuery();
rs.next();
startLsn = rs.getString(1);
}
System.out.println(" -> 备份起始 LSN: " + startLsn);
System.out.println("[2/5] 复制数据目录...");
String rsyncCmd = String.format("rsync -av --exclude='postmaster.pid' --exclude='pg_wal/*' %s/ %s/",
DATA_DIR, BACKUP_DIR);
runCommand(rsyncCmd);
System.out.println(" -> 数据已复制到: " + BACKUP_DIR);
System.out.println("[3/5] 复制 WAL 日志...");
String walSrc = DATA_DIR + File.separator + "pg_wal";
String walDst = BACKUP_DIR + File.separator + "pg_wal";
Files.createDirectories(Paths.get(walDst));
String walRsync = String.format("rsync -av %s/ %s/", walSrc, walDst);
runCommand(walRsync);
System.out.println(" -> WAL 已复制到: " + walDst);
System.out.println("[4/5] 退出备份模式...");
String stopLsn, labelfile, spcmapfile;
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT lsn, labelfile, spcmapfile FROM pg_backup_stop()")) {
rs.next();
stopLsn = rs.getString("lsn");
labelfile = rs.getString("labelfile");
spcmapfile = rs.getString("spcmapfile");
}
System.out.println(" -> 备份结束 LSN: " + stopLsn);
if (labelfile != null && !labelfile.isEmpty()) {
String labelPath = BACKUP_DIR + File.separator + "backup_label";
Files.write(Paths.get(labelPath), labelfile.getBytes());
System.out.println(" -> backup_label 已保存到: " + labelPath);
}
if (spcmapfile != null && !spcmapfile.isEmpty()) {
String spcPath = BACKUP_DIR + File.separator + "tablespace_map";
Files.write(Paths.get(spcPath), spcmapfile.getBytes());
System.out.println(" -> tablespace_map 已保存到: " + spcPath);
}
System.out.println("[5/5] 备份完成!");
System.out.println(" -> 备份位置: " + BACKUP_DIR);
System.out.println(" -> 恢复时需结合 WAL 归档进行 PITR");
} catch (Exception e) {
System.err.println("备份失败: " + e.getMessage());
// 尝试清理
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS)) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("SELECT pg_backup_stop()");
}
} catch (Exception ignored) {}
e.printStackTrace();
System.exit(1);
}
}
private static void runCommand(String cmd) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder("bash", "-c", cmd);
pb.redirectErrorStream(true);
Process p = pb.start();
int exitCode = p.waitFor();
if (exitCode != 0) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}
}
throw new IOException("命令执行失败,退出码: " + exitCode);
}
}
}
技术点总结
- 使用 JDBC 的
PreparedStatement执行pg_backup_start,使用Statement执行pg_backup_stop - 通过
ProcessBuilder调用rsync,捕获输出和错误流 - 使用
Files和Paths进行文件和目录操作 - 在
finally或异常处理中尝试调用pg_backup_stop()释放备份模式
三种语言实现对比
| 语言 | 驱动 | 外部命令执行 | 文件操作 | 错误清理 |
|---|---|---|---|---|
| Go | pgx/v5 | os/exec | os.WriteFile, os.MkdirAll | defer 关闭连接,异常时调用 pg_backup_stop |
| Python | psycopg2 | subprocess.run | open, os.makedirs | try/finally 关闭连接,异常时尝试清理 |
| Java | JDBC | ProcessBuilder | Files.write, Files.createDirectories | catch 中重试 pg_backup_stop |
三种实现均遵循 PostgreSQL 15+ 的非排他备份模式,正确处理 pg_backup_stop 返回的 labelfile 和 spcmapfile,并包含完整的错误处理和资源释放逻辑。
阶段性总结
本附录提供了 Go、Python、Java 三种语言的物理备份示例,均基于 Node.js 原版示例的流程进行移植。核心步骤完全一致:调用 pg_backup_start → 复制数据目录和 WAL → 调用 pg_backup_stop → 保存备份标签文件。各语言实现均使用其生态中最主流的 PostgreSQL 驱动,并通过系统命令调用 rsync 完成文件复制,确保了备份的原子性和一致性。开发者可根据自身技术栈直接选用对应版本,并调整数据目录、备份目录及数据库连接参数即可投入生产使用。
项目难点与解决方案
核心难点
排他备份模式的历史遗留问题:在 PostgreSQL 15 之前,pg_start_backup 默认使用排他模式,许多存量备份脚本未显式指定非排他模式。升级到 PostgreSQL 15 后,这些脚本将直接报错或产生不可预期的行为。
解决方案
- 脚本迁移:将所有
pg_start_backup(label)调用改为pg_backup_start(label, false),并移除对pg_stop_backup()返回值中backup_label文件的依赖——改为捕获函数返回的labelfile字段并手动保存。 - 工具升级:使用
pg_basebackup或 pgBackRest 等已适配 PostgreSQL 15 的工具替代手工脚本。
广度
涉及 PostgreSQL 内核函数、备份协议、WAL 机制、文件系统复制、第三方工具生态等多个技术层面。
深度
深入剖析了 backup_label 文件导致崩溃恢复异常的根本原因,以及 PostgreSQL 15 移除排他模式的设计考量。
复杂度
排他模式的移除涉及备份工具链的全面适配,从底层 API 到上层备份脚本均需调整。同时,PG 生态中多种备份工具各有优劣,选型需综合考虑数据规模、基础设施、运维能力等因素。
官方文档
- PostgreSQL 官方文档:连续归档与 PITR
- PostgreSQL 官方文档:pg_basebackup
- PostgreSQL 官方文档:pg_backup_start
- PostgreSQL 官方文档:Release 15 — 移除排他备份模式
- PostgreSQL 官方文档:Release 17 — 增量备份
参考链接
- pgBackRest 官方文档
- Barman 官方文档
- pg_probackup 官方文档
- EDB 博客:Exclusive Backup Mode Finally Removed in Postgres 15
总结
PostgreSQL 的物理备份体系从底层的 pg_backup_start / pg_backup_stop 函数到上层的 pg_basebackup 工具,再到功能丰富的第三方备份方案(pgBackRest、Barman、pg_probackup),形成了完整的分层架构。
排他备份模式在 PostgreSQL 15 中被正式移除,统一使用非排他模式,消除了 backup_label 文件残留导致崩溃恢复异常的重大隐患。pg_basebackup 在 PostgreSQL 17 中新增了块级增量备份能力,进一步缩小了官方工具与第三方工具的功能差距。
在生产环境选型时,pgBackRest 凭借其功能全面性和生态成熟度成为通用首选,但需注意其仅支持 8KB 数据块大小的限制。
1658



被折叠的 条评论
为什么被折叠?



