备份一跑网站就卡:nice、ionice 与 cgroups v2 权重给后台任务降级的完整实战

备份一跑,网站就卡

很多个人站长都遇到过这个现象:设了个凌晨的 cron 备份任务,结果任务一启动,网站就明显变慢,SSH 敲命令要等好几秒才回显;白天手动跑个 tar 打包或 mysqldump,服务器更是像被人掐住脖子。资源监控一看,CPU 和内存都没满,iowait 却冲到了 40% 以上,load average 飙到十几。

这不是"配置不够"的问题,而是进程之间在抢资源,谁先抢到谁先用。Linux 默认对所有进程一视同仁,备份任务和你的 Web 服务平起平坐,它一旦开始疯狂读写磁盘,PHP-FPM 就得排队等 IO,网站自然卡。解决办法不是加钱升级配置,而是给进程分层——让后台任务自觉让路,把 CPU 和磁盘 IO 的优先权留给访客请求。本文讲清楚 nice、renice、ionice 以及 cgroups v2 这套工具怎么用。

先认识两个优先级维度:CPU 与 IO 是分开的

最常见的误解是"调低优先级就等于让出一切"。实际上 Linux 有两套独立的优先级机制:

  • CPU 调度优先级:由 nice 值控制,范围 -20(最高)到 19(最低),默认 0。它只影响进程"轮到 CPU 时谁先跑",不影响磁盘。
  • 磁盘 IO 优先级:由 ionice 控制,独立于 nice。一个 nice 值很低的备份进程,如果 ionice 没设,照样能把磁盘 IO 吃满。

所以正确的做法是两个维度一起放低,而不是只设其中一个。这也是很多人"用 nice -n 19 跑了备份还是卡"的原因——nice 管不了磁盘队列。

nice 与 renice:给 CPU 让路

启动时设定优先级用 nice:

# 以最低 CPU 优先级跑备份
nice -n 19 tar czf /backup/site-$(date +%F).tar.gz /var/www/example

# 查看进程的 nice 值
ps -eo pid,ni,comm | grep tar
# 或
ps -o pid,ni,cmd -p $(pgrep -f 'tar czf')

nice -n 19 意味着这个进程只有在没有其他进程需要 CPU 时才运行。对备份这种"慢一点无所谓、但不能拖累线上"的任务,19 是最合适的值。

已经是运行中的进程怎么办?用 renice:

# 把 PID 12345 的 CPU 优先级降到最低
renice -n 19 -p 12345

# 把某个用户的所有进程降级(区分 daemon 与值班运维)
renice -n 10 -u backup

有个重要限制:只有 root 或具备 CAP_SYS_NICE 的进程才能提高优先级(设负值);降低优先级(设正值)普通用户对自己的进程也能做。普通用户无法把自己的进程调回高优先级——降下去容易,升回来要么重启,要么用 root renice 调回。所以生产上别随手给自己用的进程降级。

ionice:让磁盘 IO 排队时靠后

ionice 有三个调度类别(class):

  • class 0(none):不指定,使用内核默认(通常按 nice 值推断)。
  • class 1(realtime):实时,抢占式,会无视其他进程直接抢 IO。除非你非常清楚在做什么,永远别用——它可能饿死日志写入甚至导致系统卡死。
  • class 2(best-effort):默认类别,配合 0-7 的优先级等级(越小越优先)。备份任务用 7(最低)。
  • class 3(idle):只在没有其他进程请求 IO 时才使用磁盘,是最温和的一档。
# 备份任务:最好用 idle,彻底不和线上抢
ionice -c 3 tar czf /backup/site.tar.gz /var/www/example

# 或 best-effort 最低档
ionice -c 2 -n 7 tar czf /backup/site.tar.gz /var/www/example

# 两个维度一起设:CPU 最低 + IO 最低
nice -n 19 ionice -c 3 tar czf /backup/site-$(date +%F).tar.gz /var/www/example

# 对运行中的进程调整(PID 已知)
ionice -c 3 -p 12345

注意 ionice 的生效前提:它依赖块设备的调度器支持。cfq 调度器完整支持 ionice;较新的 bfq 也支持;而 none/mq-deadline 调度器对 ionice 的处理较弱,可能表现为"设了没效果"。用 NVMe SSD 的机器很多默认是 none,这时 ionice 效果有限,得靠 cgroups 的 io.weight(见下节)。检查当前调度器:

cat /sys/block/sda/queue/scheduler
# 输出示例: [none] mq-deadline     ← 方括号内是当前生效的

cgroups v2:比 nice/ionice 更硬的分层

如果你的内核是 cgroups v2(现代 Debian 11+/Ubuntu 22+ 默认),可以用 io.weight 和 cpu.weight 做更精细、更"硬"的资源分区——它对不支持的调度器也有效,且能保护关键服务的下限。确认版本:

stat -fc %T /sys/fs/cgroup
# cgroup2fs  → v2
# tmpfs      → v1

v2 下给一个备份服务限速的典型流程(以 systemd 服务为例,最省事):

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly site backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

# CPU 权重:默认是 100,调到 20 表示只占约 1/5 的 CPU 份额
CPUWeight=20
# IO 权重:默认 100,调到 10 表示对磁盘的争用能力很低
IOWeight=10
# 内存上限,防止备份进程吃光内存触发 OOM
MemoryMax=512M

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl start backup.service

CPUWeight / IOWeight 是相对权重,不是绝对配额。它决定当资源被争夺时,各 cgroup 能分到的比例。把备份设为 10,把 Web 服务(比如 PHP-FPM 的 slice)设为 300,则磁盘繁忙时 PHP 大致能拿到 300/310 的份额,几乎不受影响。

给关键服务设下限保护

反过来,除了"压低备份",还可以"抬高线上"。在 v2 里可以给 PHP-FPM 设置 CPU 权重上限保护:

# 查看 PHP-FPM 当前所属 cgroup
systemctl status php8.2-fpm | grep CGroup

# 临时调整该 slice 的权重
echo 500 > /sys/fs/cgroup/system.slice/php8.2-fpm.service/cpu.weight
echo 400 > /sys/fs/cgroup/system.slice/php8.2-fpm.service/io.weight

永久化则通过 systemctl set-property 或 drop-in 文件:

systemctl set-property php8.2-fpm.service CPUWeight=500 IOWeight=400
# 会写入 /etc/systemd/system/php8.2-fpm.service.d/ 下的 drop-in

让 MySQL 备份也不拖慢线上:一条完整命令

把上面所有手段加起来,这就是我目前在用的夜间备份命令——CPU 最低、IO 空闲级、限速输出,三层保护:

nice -n 19 ionice -c 3 \
  mysqldump --single-transaction --quick \
            --default-character-set=utf8mb4 \
            zz1984 \
  | gzip -1 > /backup/db-$(date +%F).sql.gz

几点说明:

  • --single-transaction 让 InnoDB 备份不加全局锁,线上照常读写。
  • --quick 强制逐行读取,避免把整张表塞进内存。
  • gzip -1 用最低压缩级别。压缩是 CPU 密集操作,-9 要多花几倍 CPU 才多压 10%,对凌晨备份毫无必要——-1 反而更快、对线上扰动更小。
  • 最外层 nice -n 19 ionice -c 3 同时压低 CPU 和 IO。注意:管道里前面进程的优先级需要这样写在整个管道前,或者逐段用 nice 包裹。

另外强烈建议在备份脚本里加 flock,防止两次备份重叠导致 IO 翻倍:

# /usr/local/bin/backup.sh
exec 9>/var/lock/backup.lock
flock -n 9 || { echo "上一次备份还在跑,退出"; exit 0; }
# ... 备份命令 ...

排查:怎么确认优先级真的生效了

# 查看进程的 nice 与 IO 优先级(ionice 值显示为 cls/nice)
ps -o pid,ni,cls,rtprio,cmd -p 12345
ionice -p 12345

# 实时看系统 IO 争用(需要 sysstat)
iostat -x 2
# 关注 %util(设备使用率)与 await(平均等待毫秒)

# 看哪些进程在大量读写
iotop -o -P   # -o 只显示有 IO 的,-P 只显示进程

# 看各 cgroup 的 IO/CPU 统计
systemd-cgtop

验收标准很简单:跑一次备份,同时用 iostat 观察 await。如果备份期间 Web 请求对应的 await 不再明显升高、网站响应时间保持平稳,说明分层生效了。反之,如果 await 依旧飙升,多半是磁盘调度器不支持 ionice(换成 cgroups IOWeight),或者 IO 瓶颈根本不在调度上(比如机械盘随机读太多,那是另一个量级的问题)。

systemd 服务里怎么设:更可靠的写法

如果备份不是靠 cron 触发,而是写成了 systemd service(推荐,因为能自然用上 cgroups 的权重),那就别在 ExecStart 里套 nice 了,直接用 unit 的原生字段更清晰:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup (low priority)

[Service]
Type=oneshot
# systemd 原生支持 Nice 和 IOSchedulingClass,等价于 nice/ionice
Nice=19
IOSchedulingClass=idle        # 对应 ionice -c 3
IOSchedulingPriority=7
CPUWeight=20
IOWeight=10
# 输出限速:可选,限制设备带宽(v2)
# IOReadBandwidthMax=/dev/sda 50M
ExecStart=/usr/local/bin/backup.sh

Nice=19 直接对应 CPU 优先级,IOSchedulingClass=idle 对应 ionice -c 3。用 systemd 字段而不是在命令里套 nice/ionice 的好处:unit 里设置的是这个服务 cgroup 的属性,会作用于服务启动的所有子进程(包括 shell 里 fork 出来的 tar、gzip),不用逐个包。而命令行 nice -n 19 tar ... | gzip ... 里,管道右边的 gzip 其实没被 nice 覆盖到——这是个很隐蔽的坑,很多人以为整条管道都降级了,其实只有左边一半。

要验证子进程是否真的都被降级:

# 备份运行中,查看服务的所有进程及 nice 值
systemd-cgls /system.slice/backup.service
ps -eo pid,ni,cls,cmd --sort=ni | grep -E 'tar|gzip'

如果看到 gzip 的 nice 还是 0,那就证实了管道右半段没被覆盖——这时候 systemd 字段写法的优势就体现出来了。

别忘了数据库与日志:还有两件事会拖慢线上

进程优先级解决的是"抢资源"的问题,但有两类后台动作本身就会直接冲击磁盘,值得单独提一句。一是 MySQL 的后台线程,比如 innodb_page_cleaners 脏页刷盘、redo log 落盘,它们的优先级由 MySQL 自己管,不要试图用 ionice 去压——压了反而可能造成刷盘跟不上、提交变慢。遇到这类问题的正确做法是调参数(比如 innodb_io_capacity),而不是改进程优先级。

二是 logrotate 的压缩。日志轮转时的 compress 步骤会产生可观的 CPU 和 IO 开销,尤其是日志多、单文件大的站点。可以在 logrotate 配置里给压缩过程降级:

# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    rotate 14
    compress
    compresscmd /usr/bin/nice
    compressext .gz
    compressoptions -n 19
    delaycompress
    missingok
    notifempty
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

compresscmd /usr/bin/nice 配合 compressoptions -n 19,让轮转时的压缩进程以最低优先级运行。delaycompress 则把压缩推迟到下一轮,避免在当前轮转(可能正好是访问高峰)时就去压缩。这两个选项加起来,日志轮转对线上的扰动几乎归零。个人站往往日志量不大而忽略了这点,但一旦日志涨到几百 MB,这条配置能救你一次。

结语:好钢用在刀刃上

个人站的资源永远有限,但访客体验不该被后台任务拖累。记住三件事:CPU 优先级用 nice,磁盘优先级用 ionice,两者要一起设;modern 内核上更进一步,用 cgroups v2 的 CPUWeight/IOWeight 做硬分层;备份压缩级别别贪高,-1 比 -9 更适合夜间任务。

这些调整都是"零成本"的——不用多花一分钱,只是把资源分配的规则定清楚。做完之后你会发现,凌晨备份和网站卡顿这两件事,从此可以彻底脱钩。

Last modification:September 27th, 2026 at 08:27 pm

Leave a Comment