简短回答: 磁盘和 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。