十三、PostgreSQL 物理备份 - pg_basebackup

1. 冷备

冷备份,也称为一致性备份。使用操作系统支持的各种拷贝命令,把整个 PGDATA 备份。冷备份前,建议干净地关闭数据库(pg_ctl stop -m fast)。在同架构同平台同数据库版本的迁移需求中,使用该方式,且极大节省迁移时间,且操作简单,安全性高。

备份示例:

tar -jcv -f /home/postgres/bak/dbbak0817.tar.bz2 $PGDATA

恢复示例:

tar -jxv -f /home/postgres/bak/dbbak0817.tar.bz2 -C /

image

 

2. 热备

热备份,也称为不一致性备份

  1. 使用前必须开启日志归档 - 为了实现 PITR
  2. 可以在数据库打开状态下备份

在线热备的优势

  1. 业务连续性: 在线备份可以在数据库服务不停止的情况下进行,这意味着在备份过程中,应用程序仍然可以正常进行读写操作,不会影响正常的业务处理。

  2. 无数据丢失风险: 因为备份是在数据库运行状态下进行,因此备份出来的数据是实时的,可以确保在发生故障时,损失的数据量最小。

  3. 降低停机时间: 在进行备份时无需停止数据库服务,所以不需要专门安排维护窗口,减少了计划内停机时间。

  4. 灵活恢复点目标(RPO): 利用 PostgreSQL 的流复制(Streaming Replication)或 WAL 归档功能,可以实现几乎实时的备份,从而实现非常短的恢复点目标。

  5. 支持滚动升级和迁移: 在线备份可以让您在升级或迁移数据库系统时,无需停止业务就能进行数据同步,有利于平滑过渡。

  6. 增强灾难恢复能力: 在线备份结合 WAL 日志归档,可以实现到任意时间点的恢复(Point-In-Time Recovery, PITR),提高了灾难恢复的灵活性和准确性。

在线热备的三种方式

  1. pg_basebackup
  2. pg_backup_start() 和 pg_stop_backup()
  3. 文件系统快照的备份

pg_basebackup

从 PostgreSQL 9.1 版本开始提供的一个方便基础备份的工具。它会把整个数据库实例的数据都拷贝出来,而不只是把实例中部分(如某个数据库或表)单独备份。

注意:归档日志需要单独备份。

pg_basebackup 工作原理

  1. 创建检查点,打开全页写,创建备份标签(存储检查点位置、时间等信息)
  2. 通过流复制协议与数据库建立连接,WAL Sender 进程向 pg_basebackup 发送数据库物理文件
  3. pg_basebackup 接收到文件后写入目标位置(压缩或不压缩)

pg_basebackup 工作流程:

  1. 执行 pg_start_backup 命令
    1. 强制进入全页写入模式。
    2. 切换到当前的 WAL 段文件(8.4 或更高版本)。
    3. 执行检查点。
    4. 创建 backup_label 文件 —— 该文件创建于基本目录顶层中,包含有关该基本备份本身的关键信息,如检查点位置。
  2. 使用 tar/cp 命令对 $PGDATA 目录进行备份 
  3. 执行 pg_stop_backup 命令 
    1. 如果 pg_start_backup 强制更改了全页写模式,则将其重置为非全页写模式。
    2. 写一个备份结束的 XLOG 记录。
    3. 切换 WAL 日志。
    4. 创建备份历史文件 —— 此文件包含备份标签文件的内容和执行 pg_stop_backup 的时间戳。
    5. 删除备份标签文件。

image

使用pg_start_backup和pg_stop_backup手动热备进行全量恢复

  1. 执行 pg_start_backup 函数:该函数执行 checkpoint,将 checkpoint 信息写入数据目录下的 backup_label 文件,该文件很重要,否则启动实例的时候会提示找不到检查点。同时在归档目录下会对正在使用的归档日志进行标记。
  2. 拷贝数据目录到指定位置
  3. 执行 pg_stop_backup 函数:该命令删除 backup_label 文件,写 WAL_BACKUP_END 日志,并在 pg_wal 目录中写入 backup 文件,该文件记录了热备开始和结束的 LSN 信息。backup 文件格式为:热备开始的日志文件名.开始LSN的块内偏移.backup
# 开启wal日志归档 - 已开启

# 调用备份函数
# select pg_backup_start('baseline');

# 归档数据文件
postgres@k8s-master01:~/test$ tar -cf data.tar /data/pgdata/
tar: Removing leading `/' from member names

# 调用关闭归档函数
select pg_backup_stop();
NOTICE:  all required WAL segments have been archived
                               pg_backup_stop                                
-----------------------------------------------------------------------------
 (A/C7000190,"START WAL LOCATION: A/C5000060 (file 000000040000000A000000C5)+
 CHECKPOINT LOCATION: A/C50000B8                                            +
 BACKUP METHOD: streamed                                                    +
 BACKUP FROM: primary                                                       +
 START TIME: 2026-09-07 09:16:01 CST                                        +
 LABEL: baseline                                                            +
 START TIMELINE: 4                                                          +
 ","")
(1 row)

Time: 1562.704 ms (00:01.563)

# 插入测试数据
mydb=# insert into test_3 values (3,'b');
INSERT 0 1
mydb=# insert into test_3 values (4,'b');
INSERT 0 1
mydb=# insert into test_3 values (5,'b');
INSERT 0 1

# 切换wal日志,确保归档
mydb=# select pg_switch_wal();
 pg_switch_wal 
---------------
 A/C8000230
(1 row)

# kill -p pg 主进程
# 删除data目录 
# 解压备份文件到 data目录

# 修改postgresql.conf,添加如下:
restore_command = 'cp /postgres/arch/%f %p'
recovery_target_timeline = 'latest'

# 创建 recovery.signa 文件,告诉pg需要recovery
touch recovery.signal

# 启动pg,并查询test_3表
mydb=# select * from test_3;
 id | name 
----+------
  1 | a
  2 | b
  3 | b
  4 | b
  5 | b
(5 rows)

这个备份恢复基于wal日志归档,假如备份之后有大量DML,服务器磁盘cresh,且最后一个不完整的wal日志没有归档,那么最后未归档的wal日志里面的数据会丢失。

 

使用pg_basebackup热备进行全量恢复

# 备份时压缩
pg_basebackup -D /data/pgdata/backups/ -Ft -z -P

# 备份时不压缩
pg_basebackup -D /data/pgdata/backups/ -Ft -P

# 使用pg_basebackup命令生成了backup_label文件
postgres@k8s-master01:/data/pgdata/data$ cat backup_label.old 
START WAL LOCATION: A/CE000060 (file 000000050000000A000000CE)
CHECKPOINT LOCATION: A/CE0000B8
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2026-09-07 10:13:02 CST
LABEL: pg_basebackup base backup
START TIMELINE: 5


# 直接停止数据库
pg_ctl stop -m immediate

# 删除pg数据库的所有数据
rm -fr $PGDATA/*

# 使用压缩包进行恢复
tar -xvf base.tar.gz -C /data/pgdata/data/
# 如果创建了独立表空间,那么独立表空间也要恢复恢复
tar -xvf 33207.tar.gz -C /data/pgdata/test_tsp/

# 修改postgresql.conf,添加如下:
restore_command = 'cp /postgres/arch/%f %p'
recovery_target_timeline = 'latest'

# 创建恢复文件
touch recovery.signal

# 启动数据库
pg_ctl start 

 

3. 不完全恢复的场景

  • 由于归档日志丢失,完全恢复失败。
  • 所有未归档的 WAL 日志文件都丢失。
  • 用户错误
    • 一张重要的表被删除。
    • 表中无效的数据被提交。

基于时间点恢复

定点恢复,又称基于时间点的数据恢复(Point-In-Time Recovery),根据给定的时间点,将数据库快速恢复至该点,是数据库误操作后进行救援的常规手段。

  1. 原理是依据之前的物理备份文件加上 WAL 的预写日志模式备份做的恢复;
  2. 当我们进行了基于时间点的还原(PITR)后,数据库会启用新的时间线并继续进行操作

 

 image

PITR 过程几乎与正常恢复(实例恢复)过程相同;它们之间的唯一区别是以下两点:

  1. 从哪里读取 WAL 段/归档日志?
    1. 正常恢复模式 —— 从 $PGDATA 目录下的 pg_wal 子目录恢复。
    2. PITR 模式 —— 来自配置参数 archive_command 中设置的存档目录。
  2. 从哪里读取检查点位置?
    1. 正常恢复模式 —— 从 pg_control 文件。
    2. PITR 模式 —— 来自备份标签文件(backup_label)。

 

PITR 恢复流程概述如下:

  1. 为了找到重做点,PostgreSQL 使用内部函数 read_backup_label 从备份标签文件中读取"检查点位置"的值。
  2. PostgreSQL 从 recovery.conf(12 及以后版本为 postgresql.conf)中读取一些参数值;主要读取 restore_command 和 recovery_target_time。
  3. PostgreSQL 开始从重做点重放 WAL 数据,可以很容易地从"检查点位置"的值中获取。
  4. 恢复过程完成后,将在 pg_wal 子目录中创建时间线历史文件。

默认情况下,恢复将会一直恢复到 WAL 日志的末尾。在 recovery_target、recovery_target_lsn、recovery_target_name、recovery_target_time 和 recovery_target_xid 中,最多只能使用一个,如果在配置文件中使用了多个,将使用最后一个。

  1. recovery_target = 'immediate':这个参数指定恢复应该在达到一个一致状态后尽快结束,即尽早结束。在从一个在线备份中恢复时,这意味着备份结束的那个点。
  2. recovery_target_name (string):指定(pg_create_restore_point() 所创建)的已命名的恢复点,进行恢复。
  3. recovery_target_time (timestamp):这个参数指定按时间戳恢复。
  4. recovery_target_xid (string):这个参数指定按事务 ID 进行恢复。
  5. recovery_target_lsn (pg_lsn):这个参数指定按预写日志位置的 LSN 进行恢复。
  6. recovery_target_inclusive (boolean):指定我们是否仅在指定的恢复目标之后停止(true),或者仅在恢复目标之前停止(false)。适用于 recovery_target_lsn、recovery_target_time 或者 recovery_target_xid 被指定的情况。这个设置分别控制事务是否有准确的目标 WAL 位置(LSN)、提交时间或事务 ID 将被包括在该恢复中。默认值为 true。
  7. recovery_target_action (enum):指定在达到恢复目标时服务器应该立刻采取的动作,包括 pause(暂停)、promote(接受连接)、shutdown(停止服务器),其中 pause 为默认动作。需要执行select pg_wal_replay_resume()

recovery_target_timeline 指的是选择在哪条时间线上恢复,而以上参数则指的是恢复到哪个位置。

无论还原到哪个位置,最后都需要将还原参数删除

 

时间线

 

 

image

image

基于recovery_target_name恢复

recovery_target_name = ""  # e.g. 'daily backup 2018-01-14'

指 pg_create_restore_point(text) 创建的还原点。如果数据库中有多个重复命名的还原点,遇到第一个则停止。因为它不需要从 abort 或 commit 判断结束点,不需要判断参数 recovery_target_inclusive 的值。

# 备份数据库
pg_basebackup -D /data/pgdata/backups/`date +%F` -Ft -P 

# 备份目录
ll 2026-09-07/
total 6088552
drwxr-x--- 2 postgres postgres       4096 Sep  7 13:37 ./
drwxr-x--- 3 postgres postgres       4096 Sep  7 13:35 ../
-rw-r----- 1 postgres postgres     394680 Sep  7 13:37 backup_manifest
-rw-r----- 1 postgres postgres 6217483264 Sep  7 13:37 base.tar
-rw-r----- 1 postgres postgres   16780288 Sep  7 13:37 pg_wal.tar

# 创建还原点
select pg_create_restore_point('first_pt');
 pg_create_restore_point 
-------------------------
 A/D90001D0
(1 row)

# 删除表test
mydb=# drop table test;
DROP TABLE

# 切换wal日志
mydb=# select pg_walfile_name(pg_switch_wal());
     pg_walfile_name      
--------------------------
 000000080000000A000000D9
(1 row)

# stop pg
pg_ctl stop 
waiting for server to shut down..... done
server stopped

# 删除数据库
rm -fr /data/pgdata/data/* 

# 用备份恢复数据库
tar -xf base.tar -C /data/pgdata/data/

# 创建恢复文件
touch recovery.signal

# test 表已恢复

# 数据库还是pause状态
mydb=# show recovery_target_action;
 recovery_target_action 
------------------------
 pause
(1 row)

# 无法插入数据
mydb=# insert into test values (1,'q');
ERROR:  cannot execute INSERT in a read-only transaction

# pg_controldata显示数据库页处于:in archive recovery
postgres@k8s-master01:/data/pgdata/data$ pg_controldata 
pg_control version number:            1700
Catalog version number:               202406281
Database system identifier:           7677313638838763418
Database cluster state:               in archive recovery
pg_control last modified:             Mon 07 Sep 2026 01:53:02 PM CST
Latest checkpoint location:           A/D90000B8
Latest checkpoint's REDO location:    A/D9000060
Latest checkpoint's REDO WAL file:    000000080000000A000000D9
Latest checkpoint's TimeLineID:       8
Latest checkpoint's PrevTimeLineID:   8

# 执行elect pg_wal_replay_resume()后数据可以插入
mydb=# select pg_wal_replay_resume();
 pg_wal_replay_resume 
----------------------
 
(1 row)
mydb=# insert into test values (1,'q');
INSERT 0 1

 

基于recovery_target_time恢复

ecovery_target_time = ""
# e.g. '2018-01-14 22:39:00 EST'

配合 recovery_target_inclusive 使用;

如果在同一个时间点有多个事务回滚或提交:

  • 其值为 false 则恢复到这个时间点第一个回滚或提交的事务(含)
  • 其值为 true 则恢复到这个时间点最后一个回滚或提交的事务(含)

如果时间点上刚好只有 1 个事务回滚或提交:

  • 那么其值为 true 和 false 一样,恢复将处理到这个事务包含的 xlog 信息(含)

如果时间点没有匹配的事务提交或回滚信息:

  • 那么其值 true 和 false 一样,恢复将处理到这个时间后的下一个事务回滚或提交的 xlog 信息(含)
# 备份数据库
pg_basebackup -D /data/pgdata/backups/`date +%F` -Ft -P 

# 查看当前时间
mydb=# select now();
              now              
-------------------------------
 2026-09-07 14:17:33.217803+08
(1 row)

# 删除test表
mydb=# drop table test_2;
DROP TABLE

# 切换wal日志
mydb=# select pg_walfile_name(pg_switch_wal());
     pg_walfile_name      
--------------------------
 0000000A0000000A000000DD
(1 row)


# stop pg
pg_ctl stop 

# 删除数据库
rm -fr /data/pgdata/data/* 

# 恢复数据库
tar -xf 2026-09-07/base.tar -C /data/pgdata/data/

# 创建恢复文件
touch recovery.signal

# 将恢复时间
recovery_target_time = '2026-09-07 14:17:33'

# start pg

# test_2 表已恢复

# 执行elect pg_wal_replay_resume()后数据可以插入
mydb=# select pg_wal_replay_resume();
 pg_wal_replay_resume 

 

基于recovery_target_lsn恢复

recovery_target_lsn = '0/1604CB88'
# 查询当前LSN
postgres=# select pg_current_wal_lsn();
 pg_current_wal_lsn 
--------------------
 A/E30001E0
(1 row)

# 删除test_2表
mydb=# drop table test_2;
DROP TABLE

# 切换wal日志
mydb=# select pg_walfile_name(pg_switch_wal());
     pg_walfile_name      
--------------------------
 0000000D0000000A000000E3
(1 row)

# stop pg

# 删除数据库
rm -fr /data/pgdata/data/* 

# 备份恢复
tar -xf 2026-09-07/base.tar -C /data/pgdata/data/

# 创建恢复文件
touch recovery.signal

# 修改postgresql.conf,设置recovery_target_lsn参数
recovery_target_lsn = 'A/E30001E0'

# 启动数据库
pg_ctl start

# test_2 表已恢复

# 执行elect pg_wal_replay_resume()后数据可以插入
mydb=# select pg_wal_replay_resume();
 pg_wal_replay_resume 

 

基于recovery_target_xid恢复

recovery_target_xid,可以配合 recovery_target_inclusive 使用,recovery_target_inclusive 只影响日志的输出,并不影响恢复进程截止点的选择,截止都截止于这个 xid 的 xlog 位置。也就是说无论如何都包含了这个事务的 xlog 信息的 recovery。

这里需要特别注意 xid 的信息体现在结束时,而不是分配 xid 时。所以恢复到 xid=100 提交/回滚点,可能 xid=102 已经先提交了。那么包含 xid=102 的 xlog 信息会被 recovery。

mydb=# select txid_current();
 txid_current 
--------------
         2853
(1 row)

mydb=# drop table test_2;
DROP TABLE

# 切换wal日志
select pg_walfile_name(pg_switch_wal());
     pg_walfile_name      
--------------------------
 0000000F0000000A000000E3
(1 row)

# stop pg

# 删除数据库
rm -fr /data/pgdata/data/* 

# 备份恢复
tar -xf 2026-09-07/base.tar -C /data/pgdata/data/

# 创建恢复文件
touch recovery.signal

# 修改postgresql.conf,设置recovery_target_lsn参数
recovery_target_xid = '2852'

# 启动数据库
pg_ctl start

# test_2 表已恢复

# 执行elect pg_wal_replay_resume()后数据可以插入
mydb=# select pg_wal_replay_resume();
 pg_wal_replay_resume 

 

 

posted @ 2026-09-07 09:13  BinBin-HF  阅读(6)  评论(0)    收藏  举报