混合负载节点上 batch job 写 50GB 后前台 P99 飙 16 倍:vm.dirty_*、ionice、cgroup v2 io.max 三层 I/O 控制到底谁压谁?线上到底怎么配?
该问题的正文、回答与讨论。
背景
我维护一个 12 节点(16C64G,Kubernetes 1.28.9,containerd 1.7.13,内核 5.15.0-91,cgroup v2)的自建集群,磁盘是 7200RPM SATA(不是 SSD,这点很关键)。节点上混跑两类工作负载:
- 前台:Java 17 无状态订单/支付服务(4 副本,每副本 4C/4Gi),P99 基线 50ms,SLA 要求 P99 < 200ms;
- 后台 batch:Go 写的 ETL 作业,每天 02:00 跑,单次写入 ~50GB 中间文件到本地
/data(ext4,data=ordered),跑完即删。
现象(我实际抓到的数据)
batch job 运行窗口(约 35 分钟)内,同节点上的前台 Java 服务:
| 指标 | 常态 | batch 窗口 |
|---|---|---|
| P99 RT | 48ms | 780ms(16 倍) |
| iowait(vmstat 1 wa) | 0–1% | 35–45% |
| iostat -x %util | 5–12% | 42–62%(没打满!) |
| iostat -x await | 1–3ms | 15–22ms |
| iostat -x wMB/s | 2–8 | 310–480 |
| /proc/pressure/io some avg10 | 0.1–0.3 | 5.2 |
| /proc/pressure/io full avg10 | 0.0 | 1.8 |
| dirty pages(/proc/meminfo) | ~80MB | 峰值 3.2GB |
关键矛盾:磁盘 %util 才 60%、await 才 20ms,但前台 P99 已经飙到 780ms——说明瓶颈不在"磁盘写不过来",而在写回节奏把前台 I/O 的完成时间拖长了(大概率是 dirty page writeback 和前台小 I/O 在同一个提交队列里排队,或者 data=ordered 的 journal 刷盘抢了前台 read 的带宽)。
我已经试过的(按时间顺序)
-
调
vm.dirty_ratio20→10、vm.dirty_background_ratio10→5- 效果:dirty 峰值从 3.2GB 降到 ~1.8GB,P99 从 780ms 降到 ~620ms。有改善但远不够。
- 困惑:
dirty_ratio是系统级参数,我搞不清它和 cgroup v2io.max之间是"取 min"还是"各管各的"。
-
给 batch 进程加
ionice -c3(idle 优先级)- 效果:几乎没有可观测差异,P99 还是 750ms+。
- 困惑:cgroup v2 下
ionice是不是已经被io.max的权重机制取代了?还是ionice只影响 CFQ/BFQ 而我的磁盘用的是mq-deadline?
-
cgroup v2
io.max限 batch 的 cgroup:wbps=104857600(100MB/s 写带宽)- 效果:P99 降到 ~400ms(有改善),但 batch 跑完时间从 35 分钟变成 72 分钟,业务方不接受。
- 困惑:
io.max限的是"带宽"还是"IOPS"?wbps是字节/s 对吧?那wbps和riops能不能同时设?
-
cpuset把 batch 钉到 4 个核(0–3),前台用 4–15- 效果:CPU 争抢确实消了(iowait 之外的 sys 时间降了 30%),但 I/O 侧没有任何改善,P99 还是 700ms+。
- 结论:I/O 瓶颈和 CPU 绑定是两回事,
cpuset管不了磁盘。
-
把 batch 的
/data挂到单独一块盘(第二块 7200RPM)- 效果:P99 回到 55ms,问题消失。
- 但:不是所有节点都有第二块盘,而且加盘的成本不低,我想搞清楚在单盘场景下到底怎么把参数调对。
我想问的(希望得到的回答形态)
-
三层控制的实际优先级和交互:
vm.dirty_ratio(系统级,管 dirty page 何时回写)→ionice(IO scheduler 级,管优先级)→ cgroup v2io.max(cgroup 级,管带宽/IOPS 上限)——这三层在 5.15 内核 + cgroup v2 下,到底是怎么叠加的?是"最严的生效"还是"各管一段"?有没有人画过一张清晰的调用链? -
data=ordered的 journal 刷盘:前台服务的小 I/O(比如读一条订单、写一条审计日志)会不会被 batch 的 journal 刷盘阻塞?data=writeback能不能缓解?如果改data=writeback,crash 后数据不一致的风险在生产上怎么兜底? -
io.max的wbpsvswbps+riops组合:如果我想"限 batch 的写带宽到 150MB/s,但不限它的读 IOPS",io.max怎么写?riops和rbps能不能同时设、是 AND 还是 OR? -
有没有比"调参数"更稳的方案:比如把 batch 的写入走
O_DIRECT绕过 page cache(但 Go 的os.File默认是 buffered,怎么改)?或者用fallocate+fdatasync替代fsync减少 journal 压力?这些方案在 5.15 内核上的实际效果有人实测过吗? -
开放问题:你们线上混跑"前台低延迟 + 后台大批量 I/O"的场景,最终到底是怎么解决的?是拆盘、拆节点、还是纯靠参数调?5 节点和 50 节点的做法一样吗?
补充:我之前在站内问过 K8s 节点压力分诊(#693),这次是更聚焦 I/O 层面的问题。另外站内有篇讲 PSI 内存压力预警的文章(#904),它的思路——"在 OOM 之前用 PSI 做早期信号"——我其实也想套到 I/O 上:
/proc/pressure/io的some/full能不能做成 Prometheus 告警,在 P99 还没飙之前就触发 batch 降速?有没有人落地过?