简短回答: 磁盘和 CPU 看起来都很闲,pgBackRest 备份照样能拖慢 PostgreSQL,因为把整个库读一遍会把 page cache 里 Postgres 的热数据挤出去。让备份跑在单独的 cgroup 里并设置
MemoryHigh(再加读带宽上限),它就只能回收自己的缓存。在我的库上,kswapd 扫描量降到原来的 1/3 到 1/9,statement timeout 归零,最慢的页面从 28.8 秒降到 16.1 秒。变慢并没有完全消失,下一步要动的是delta。
这是我给产品背后那台 Postgres 调了一周备份的记录。下面的内容都来自我的运维笔记,数字都是工具实际输出的。
环境是什么样的?
- 一台 PostgreSQL 主库,用 Pigsty 管理,数据约 46 GB。没有备库,所以备份只能从主库做。
- pgBackRest stanza 叫
pg-meta,用 cron 调度(时间为北京时间):
| 时间 | 任务 | 仓库 |
|---|---|---|
| 周一到周六 02:30 | 差异备份 | 独立运维机上的 repo2 |
| 周日 02:30 | 全量 | repo2 |
| 周日 03:30 | 全量,加密 | Cloudflare R2,只在这次运行时挂成 repo3 |
为什么周日要做两次全量?pgBackRest 每次运行只写一个仓库,也不能在仓库之间复制,所以异地那份必须自己再读一遍数据。每天的差异备份有 21–44 GB,而整个库才 46 GB,异地改成差异备份几乎省不了什么。
症状是什么?
2026-10-18 周日的备份窗口里,Vaneform(我做的网站流量查询工具,读的就是这个库)的报告查询从平时约 12 毫秒变成好几秒:783 次慢页面,最慢 28.8 秒,还有 2 次批处理 statement timeout。
为什么问题不在磁盘?
第一反应是 I/O。但数字指向别处:
- 磁盘利用率 13–23%,await 约 1 毫秒。
- CPU 75–80% 空闲。
- kswapd 每秒扫描约 6,000 页。
备份把 46 GB 数据流过大约 6 GB 的 page cache。它每读一个文件,就挤掉一些 Postgres 缓存着的页,原本命中内存的查询只好去读盘,还要排在内存回收后面。磁盘远没饱和,是缓存被反复冲刷。
内存上限为什么有用?
在 cgroup v2 下,page cache 记在读文件的那个进程所在的 cgroup 名下。备份跑在自己的 cgroup 里并设了 MemoryHigh,到达上限后内核会先回收备份自己的缓存,它就没法把别人的缓存挤掉。
我让每个备份任务都通过一个小脚本启动一个临时 systemd unit:
#!/bin/bash
# Run one scheduled pgBackRest job gently on the database host.
set -euo pipefail
job=$1 # ops-full | ops-diff | r2-full
command=(/usr/bin/pgbackrest --stanza=pg-meta --repo=2 --type=diff backup) # per job
read_cap=${BAY_PGBACKREST_READ_MBPS:-20}M
read_iops=${BAY_PGBACKREST_READ_IOPS:-600}
memory_high=${BAY_PGBACKREST_MEMORY_HIGH:-1G}
# A later job waits for the running one (Sunday R2 full waits for the ops full).
exec /usr/bin/flock --wait 21600 /run/lock/bay-pgbackrest.lock \
/usr/bin/systemd-run --unit="bay-pgbackrest-$job-$(date +%Y%m%d%H%M)" \
--wait --collect --quiet \
-p User=postgres -p Group=postgres \
-p Nice=19 -p IOSchedulingClass=idle -p CPUWeight=20 -p IOWeight=10 \
-p MemoryHigh="$memory_high" \
-p "IOReadBandwidthMax=/pg/data $read_cap" -p "IOReadIOPSMax=/pg/data $read_iops" \
-p RuntimeMaxSec=6h \
-- "${command[@]}"
flock 保证同一时间只跑一个任务。限制值都来自环境变量,调参不用改代码。这个脚本放在 Bay 里(我自己做的管服务器的小运维控制台),cron 调用它,不再直接调用 pgBackRest。
效果如何?
换上脚本后的第一晚,2026-10-19 周一,差异备份:
- 备份: 成功完成,30.8 GB,02:30:01 到 03:24:51,共 55 分钟;限速前差异备份约 36 分钟。
- 限制确实生效: 用一个
IOReadBandwidthMax=5M的探测 unit 读一个没缓存的文件,速度正好 5.0 MB/s,io.max显示rbps=5000000。
02:30 到 03:30 的 sar,限速前的周日全量对比限速后的周一差异:
| 指标 | 之前 | 之后 |
|---|---|---|
| 磁盘读(整盘) | 16.7–28.6 MB/s | 19.7–24.7 MB/s |
| kswapd 扫描 pgscank/s | 4,070–6,885 | 670–2,014 |
| 回收页数 pgsteal/s | 8,200–13,900 | 5,800–8,100 |
应用这边:超过 1.5 秒的页面 167 次(偏少,因为一个 pod 重启丢了日志),最慢 16.1 秒,0 次 statement timeout,没有 5xx。前一晚是 783 次慢页面,最慢 28.8 秒。
结论:明显好转,但没解决。回收量仍远高于平时约每秒 300 页的水平;而且限速后备份跑得更久,慢的时段挪到了窗口后半段。
哪些 systemd 限制没起作用?
IOSchedulingClass=idle 和 IOWeight 在这台 VM 上完全没用。磁盘调度器是 none,这两个设置需要 BFQ。指望它们之前先看 /sys/block/<dev>/queue/scheduler。真正起作用的只有带宽/IOPS 上限和内存上限。
另外,unit 停止时 systemd 打印的 "memory peak" 在这里没参考价值,无关的 unit 也报很小的值。要在 unit 运行时读它 cgroup 里的 memory.peak。
下一步动什么?
Pigsty 生成的 pgbackrest.conf 在 [global] 里写了 delta=y。对备份来说,delta 表示 pgBackRest 用校验和判断文件是否变化、不信任时间戳,于是每次差异备份都要把整个库读一遍。
我找到的改动最小的办法,是一个只影响备份的覆盖配置:
# /etc/pgbackrest/conf.d/bay-ops.conf
[global:backup]
delta=n
恢复时仍保留 delta=y,那才是它真正有用的地方。我的预期(还没实测)是差异备份只读约 31 GB 变化的文件,不再读全部 46 GB,窗口从约 55 分钟缩到 30 分钟左右。另一个办法是给 patroni.service 加 MemoryLow,让全局回收优先回收备份的缓存。
怎么确认备份可用?
对指定的备份集跑 verify,同样套上限制:
sudo -u postgres pgbackrest --stanza=pg-meta --repo=2 --set=<label> verify
2026-10-18 的全量(46.2 GB,实际存储 8.1 GB)77 秒验证通过。比起恢复时才发现备份坏了,验证的成本很低。
常见问题
磁盘使用率很低,为什么 pgBackRest 还会拖慢 PostgreSQL?
把整个库读一遍会让数据流过 page cache,挤掉 PostgreSQL 原本从内存里读的页。我这次磁盘只有 13–23% 繁忙,kswapd 却每秒扫描约 6,000 页。
IOSchedulingClass=idle 对备份有用吗?
只在 BFQ 调度器下有用。磁盘调度器是 none 的 VM 上它没有效果。带宽和 IOPS 上限(IOReadBandwidthMax、IOReadIOPSMax)不需要 BFQ。
pgBackRest 备份时 delta=y 是什么作用?
让 pgBackRest 对每个文件算校验和来判断变化,不依赖时间戳和大小。这样更稳妥,但每次差异备份都要读完整个库。在 [global:backup] 下设 delta=n,就只在恢复时使用 delta。
限速后备份会慢多少?
读带宽限制在 20 MB/s 后,我的差异备份从约 36 分钟变成 55 分钟。限速主要削掉峰值:改之前备份平均约 19 MB/s,峰值 38 MB/s。
jqz