2762 字
约 9 分钟
18
S15-A06 · Linux 资源治理:CPU、内存与 I/O 的隔离

一个批处理任务把整机拖垮,复盘通常只写"资源不足"。真正发生的是三件不同的事:抢走 CPU 时间片、吃光页缓存逼别人读盘、把磁盘队列堵成长龙。三者的信号、手段、代价完全不同,用同一种"加机器"应对只是把问题推后。

这篇按争抢形态拆判断链:什么信号 → 上哪一层手段 → 这层能保证什么、保证不了什么。


1. 三种争抢形态与各自的信号

形态 机制 直接信号 常见误判
CPU 抢占 时间片按权重分配 %steal 高、run queue 长 把 load 高当成 IO 问题
内存压力 页缓存回收、swap、OOM memory.current 逼近上限、memory.events 计数 只看 free 认为"内存够"
IO 排队 请求在块层排队 块设备 await 与队列深度 用 iowait 判断 IO 饱和

load average 包含 D 状态任务,会被 IO 与内存回收污染——load 高推不出 CPU 忙。反过来 IO 打满而 CPU 有活干时 iowait 反而不高,它统计的是"CPU 空闲且有未完成 IO"。


2. PSI:比 load 更早的信号

PSI 直接度量"任务因资源不足无法推进"的时间占比,是判断要不要隔离的第一手依据:

$ cat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io
some avg10=12.40 avg60=8.10 avg300=5.32 total=...
full avg10=2.10  avg60=1.05 avg300=0.84 total=...   # cpu 无 full 行
文件 some full 读法
cpu 至少一个任务在等 CPU 不存在 持续个位数百分比即份额不够
memory 有任务在等回收 全部任务卡住 full 出现即全局停顿
io 有任务在等 IO 全部任务在等 IO 与 await 一起看

cgroup v2 下每个 cgroup 也有 cpu.pressure 等文件:按 cgroup 看 pressure 才知道谁在挨饿,全局文件只说明整机有人在等。


3. 四层手段与升级判据

层 手段 保证 保证不了 升级判据
1 优先级 nice / ionice CPU 权重、IO 类 内存、页缓存、峰值 只影响"分多少"
2 cgroup cpu.max、memory.max、io.max 硬上限与份额 内核开销、缓存争用 需按服务限额
3 容器 limit 编排层 limits 单容器边界 节点级干扰、超卖 多服务混布
4 物理隔离 cpuset、NUMA、独立盘 缓存与带宽 成本、弹性 延迟敏感

判据是代价递增:nice 零成本不保证,cgroup 有观测成本但可执行,容器 limit 需编排配合,NUMA 与独占盘直接花钱。常见错误是跳过 2、3 直接买机器。


4. nice 与 cpunice 的实际效果边界

项 事实 结论
作用对象 仅 CFS 的 CPU 分配 对 IO、内存、页缓存无效
权重关系 每 +1 nice 权重约乘 0.8,5 级约差 4 倍(按内核权重表核对) 差 5 级才明显
空闲时 低优先级仍能跑满 nice 不能限速
叠加 cgroup v2 里与 cpu.weight 同级换算 别两处都调
# 只在"有多余 CPU"时让出,不构成任何上限
$ nice -n 19 ./batch.sh
$ ionice -c 3 -p $(pgrep -f batch.sh)   # idle 类,仅在无其他 IO 时执行
$ chrt -b 0 ./batch.sh                  # SCHED_IDLE,比 nice 19 更彻底

SCHED_IDLE 是更强的"只捡空闲"语义,适合纯后台任务,同样不限 IO 与内存。用 nice 治 IO 干扰,是把油门当刹车。


5. cgroup v2 的三个关键文件与语义

文件 语义 需竞争才生效 典型误用
cpu.max $QUOTA $PERIOD,200000 100000 即 2 核 否,硬顶 只设 cpu.weight 以为有上限
cpu.weight 1–10000,默认 100 是 独占机器时设了没用
memory.max 硬上限,触顶组内 OOM 否 设成峰值,波动即 OOM
memory.high 软限,超后节流回收 否 与 max 同值,失去缓冲
io.max MAJ:MIN rbps/wbps/… 否 以为能管住所有 buffered 写
# 直接走 cgroupfs(v2 统一层级)
$ echo "200000 100000" > /sys/fs/cgroup/batch/cpu.max
$ echo 2G               > /sys/fs/cgroup/batch/memory.max
$ echo 1500M            > /sys/fs/cgroup/batch/memory.high
$ echo "8:16 rbps=104857600 wbps=52428800" > /sys/fs/cgroup/batch/io.max

memory.high 常被忽略:它让超限的组先被回收节流(变慢)而非直接被杀。给突发留缓冲的姿势是 high 略低于 max。


6. 设了 limit 不等于设了保障

想法 实际 需要的配置
设了 cpu.max 就有保障 只给上限,竞争时仍可能拿不到 配 cpu.weight 提份额
设了 memory.max 就不被饿 保证不吃超,不保证不被回收 memory.min/low 护工作集
设了容器 limit 就隔离 节点缓存、内存带宽仍共享 cpuset 或物理隔离
$ echo 512M > /sys/fs/cgroup/web/memory.min    # 不可回收的保护量
$ echo 1G   > /sys/fs/cgroup/web/memory.low    # 低水位,压力下优先保留

limit 是天花板,reservation 是地板。 只有天花板时,压力下被牺牲的总是没有地板的一方——这就是"没超 limit 却被拖慢"的来源。


7. 内存压力的连锁反应与 OOM 选谁

顺序几乎固定:页缓存回收 → direct reclaim(延迟陡增)→ 匿名页换出 → 分配失败 → OOM。

阶段 信号 处理
页缓存回收 file 下降、workingset_refault 上升 降 memory.high
direct reclaim memory.pressure 的 full 抬头 给关键组 memory.low
swap 抖动 memory.swap.current、pswpout 上升 限 memory.swap.max
OOM memory.events 的 oom_kill 调 oom_score_adj 或 oom.group

OOM killer 按 oom_score 选择:以进程内存占用(RSS + swap + 页表)归一化打分,再由 oom_score_adj(-1000…1000)调整。容器里的差别是选择范围被限制在 cgroup 内,且整组可能一起被杀:

$ cat /proc/<pid>/oom_score          # 当前分数,越高越先被杀
$ echo -500 > /proc/<pid>/oom_score_adj   # 保护关键进程(不能到 -1000)
$ echo 1 > /sys/fs/cgroup/batch/memory.oom.group   # 整组一起杀,避免半死状态

容器运行时通常给 1 号进程偏高 oom_score_adj,让整体先死而非留下孤儿进程——"为什么死的是 init"由此而来,偏移按 runtime 版本核对。


8. IO 隔离的难点

手段 约束对象 覆盖 buffered 写? 备注
io.max(blk-throttle) 已提交的 bio 部分:只约束回写阶段 脏页生成阶段不限速
io.weight 相对份额 是(按 blkcg) 竞争时才生效
io.latency 按延迟目标分配 是 需内核与调度器支持
$ echo "8:16 wbps=52428800" > /sys/fs/cgroup/batch/io.max
# 关键:buffered write 先堆脏页、后由内核回写线程刷出
# 想真正控住写入节奏,要么 O_DIRECT,要么控制 fsync 频率与批大小

这就是"限了写带宽磁盘仍被拖垮"的原因:buffered 写只是往页缓存塞数据,io.max 管的是后来回写的 bio,脏页堆到阈值才回写,冲击仍在。该做的是限制脏页生成速率(批量与 fsync 节奏),而非只靠块层限速。


9. CPU 亲和性、NUMA 与 slice 组织

$ taskset -c 2-5 ./worker                 # 绑核:减少迁移、提高缓存命中
$ numactl --cpunodebind=0 --membind=0 ./worker   # NUMA 绑定:避免远端内存
$ systemctl set-property batch.slice CPUWeight=20 MemoryMax=4G IOWeight=50
手段 收益 代价
taskset 绑核 缓存局部性、抖动下降 绑太窄被自己峰值卡住
cpuset.cpus 与批处理物理分离 核数刚性、利用率降
NUMA 绑定 远端内存访问减少(本地与远端典型差一到两倍,按平台核对) 内存不均时难调度

systemd slice 把这些参数变成声明式配置:CPUWeight、MemoryMax、MemoryHigh、IOWeight、TasksMax 落到 cgroup,重启仍生效,比手写 cgroupfs 可审计。

# /etc/systemd/system/batch.slice.d/limits.conf
[Slice]
CPUWeight=20
MemoryHigh=3G
MemoryMax=4G
IOWeight=50
TasksMax=512

10. 混部取舍与监控口径

维度 混部 分池
利用率 高,峰值互补 低,按峰值预留
干扰 缓存与带宽争用难隔离 物理隔离,可预测
适用 批处理与在线错峰 延迟敏感核心链路

监控口径必须按 cgroup 聚合:进程会重启、fork、短命,按进程看永远对不上账。

$ cat /sys/fs/cgroup/batch/cpu.stat     # usage_usec / throttled_usec
$ cat /sys/fs/cgroup/batch/memory.stat  # anon / file / workingset
$ cat /sys/fs/cgroup/batch/io.stat      # rbytes / wios

throttled_usec 增长说明 cpu.max 真在卡人;workingset_refault 增长说明工作集被反复换出。这两个数比"使用率 80%"有用得多。


11. 排错

现象 原因 处理
批处理一跑,在线接口 P99 飙升 CPU 份额与 IO 均未限 cpu.max + io.max,或 cpuset 分离
没超 limit 却被 OOM 上限按峰值设,回收跟不上 降 memory.high 留缓冲
load 高但 CPU 空闲 D 状态任务在等 IO 看块设备 await 与 PSI io
容器反复重启无日志 cgroup OOM 查 memory.events 与 dmesg
限了写带宽仍被拖垮 buffered 写先堆脏页 控批量与 fsync,或 O_DIRECT
绑核后吞吐下降 绑太窄,单核峰值为瓶颈 放宽 cpuset

12. 边界:什么时候不值得做隔离

场景 更合适 理由
单服务独占整机 不用 cgroup 细分 没有竞争者,限额只增加配置面
负载稳定且峰值可预测 容量规划 + 简单监控 隔离的成本高于波动损失
突发完全不可控(如外部爬取) 限流 + 排队 隔离挡不住流量,限流才挡得住
延迟要求不高、离线任务 错峰调度 时间维度的隔离比资源维度便宜
只有一台机器且预算固定 优先级 + 观测 物理隔离买不起,先保可观测

收益随"干扰频率 × 干扰代价"上升:偶尔一次拖慢,加监控与错峰就够;每天多次且影响收入,才值得上 cpuset 与独立盘。


13. 自测

  1. load 高但 CPU 空闲说明什么?为何不等于 IO 饱和?
  2. %steal 高指向哪一类问题?
  3. nice 19 与 SCHED_IDLE 差在哪?共同盲区是什么?
  4. cpu.max 与 cpu.weight 各在什么条件下生效?
  5. 设了 memory.max 为何还会"没超限却变慢"?
  6. io.max 为何管不住 buffered 写的瞬时冲击?
S15-A06 · Linux 资源治理:CPU、内存与 I/O 的隔离
https://www.clxhxhhr.top/posts/6170/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。