Pod cgroup v2 临时资源调节实践
Last modified:
有时线上需要临时调节某几个 Pod 的资源使用,但又不想立即修改工作负载规格、触发控制面变更或重建实例。这种情况下,如果节点使用 cgroup v2,可以直接改对应 Pod cgroup 里的 cpu.max、cpu.weight、memory.high 或 IO 相关文件。
这不是长期配置手段,更适合临时止血、压测隔离或问题定位。长期方案仍然应该回到 Kubernetes 的资源规格里。
本文记录一次实际整理过程:怎么从 Pod 找到节点上的 cgroup 路径,怎么把「8 核」「12 核」换算成 cpu.max,哪些 cgroup 值适合临时调整,以及怎么写一个通过 SSH 操作节点的脚本。
背景
Kubernetes 工作负载最终都会落到节点上的 Pod 里运行。对 Linux 内核来说,每个 Pod 都属于某个 cgroup。
如果节点启用了 cgroup v2,CPU 配额主要通过 cpu.max 表达。Linux kernel 文档里对 cpu.max 的格式是:
|
|
含义是:在每个 $PERIOD 微秒周期里,最多使用 $MAX 微秒 CPU 时间。max 表示不限制。官方文档见 Control Group v2 - CPU Interface Files。
常见周期是 100000 微秒,也就是 100ms。因此:
|
|
表示每 100ms 最多使用 800ms CPU 时间,即 8 核。
|
|
表示 12 核。
Kubernetes CPU 资源单位也按 CPU core 计量,1 表示 1 个 CPU,500m 表示 0.5 个 CPU。这个定义可以看 Kubernetes 官方文档 Resource Management for Pods and Containers。
路径怎么找
目标不是容器内部的 cgroup,而是宿主机上 Pod 对应的 cgroup。先从 Kubernetes API 拿 Pod 的 UID 和所在节点:
|
|
在 systemd 风格的 cgroup v2 层级里,Burstable Pod 的路径通常长这样:
|
|
这里有一个容易踩的坑:只替换 UID 里的 -,不要把整条路径里的 - 都替换掉。
错误做法类似这样:
|
|
它会把 kubepods-burstable.slice 也改坏。
更稳的是只处理 UID:
|
|
如果不想假设路径,也可以在节点上查:
|
|
查看当前限制
拿到路径后,可以看两个文件:
|
|
cpu.max 是当前限制。cpu.stat 里能看到使用和 throttling 相关统计,例如 nr_periods、nr_throttled、throttled_usec。
如果只是临时确认某个 Pod 当前值,最小命令就是:
|
|
临时限制为 N 核
假设周期固定用 100000 微秒,N 核对应:
|
|
限制为 8 核:
|
|
限制为 12 核:
|
|
批量处理时,不建议手写半截路径再复制。可以先用 kubectl 输出 Pod 名和 UID,再只对 UID 做替换:
|
|
再在对应节点写 cpu.max。
恢复应该从哪里来
一开始很容易想到:限制前把原始 cpu.max 记到本地文件,恢复时写回。
但这种方式有两个问题:
- 本地状态文件容易丢。
- 多人操作时,本地记录不一定是当前 Pod 规格对应的值。
更合适的恢复来源是 Kubernetes API:从 Pod 的 resources.limits.cpu 读取声明值,再转换为 cpu.max 写回节点。
例如 Pod limit 是:
|
|
恢复值就是:
|
|
如果是:
|
|
恢复值就是:
|
|
这仍然不是「长期状态修复」。它只是让节点上的 cgroup 回到 Kubernetes 当前声明的 CPU limit。
脚本化
最后整理成一个脚本放在文章附件里:pod-cgroup-tune.sh。入口保持简单:
|
|
也可以直接下载后执行:
|
|
它做几件事:
- 本地通过
kubectl精确查询传入的 Pod。--pod必须是完整 Pod 名;如果 Pod 不存在,就直接报错。 - 从 Pod 的
spec.nodeName找到节点,再从 Node 的InternalIP拼出 SSH 目标root@<node-ip>。 - 通过一次 SSH 到目标节点,在
/sys/fs/cgroup下找到对应 Pod cgroup 并执行目标操作。 show先展示 Pod 声明的resources.limits.cpu和换算出的cpu.max,再读取当前 cgroup 的 CPU、memory、IO、pids 关键状态。set修改相对适合临时调节的 cgroup 值,例如cpu.max、cpu.weight、memory.high、io.max。其中--cores会转换成cpu.max。restore默认只恢复有 Kubernetes API 来源的cpu.max。如果显式加--all,才会把其它可调项重置到 cgroup v2 默认/不限值。
如果节点 SSH 用户不是 root,可以显式指定:
|
|
脚本里保留了完整参数校验和 SSH 写入逻辑,下面只摘两段关键转换代码。
--cores 转 cpu.max 的逻辑:
|
|
Kubernetes CPU quantity 需要额外支持 m:
|
|
路径定位可以不依赖固定 QoS 目录,直接在远端按 Pod UID 搜:
|
|
这样对 kubepods-burstable.slice、kubepods-guaranteed.slice 这类路径差异更宽容。
改 cpu.max 会改变容器里看到的 CPU 数量吗
不会。
cpu.max 控制的是 CPU 时间配额。它限制进程在一个周期内最多能消耗多少 CPU 时间,但不会改变进程可见的 CPU 拓扑。
所以把:
|
|
改成:
|
|
只是把 CPU 时间上限从 17.62 核调到 8 核。容器或进程里看到的这些值通常不会跟着变:
|
|
如果想改变进程能在哪些 CPU 上运行,那是 cpuset.cpus 的范围。但这比改 cpu.max 更敏感,可能影响线程、NUMA 和调度亲和性。临时压 CPU 使用量时,优先改 cpu.max,不要轻易碰 cpuset。
还有哪些值可以看或临时改
不能把 cgroup 目录里所有可写文件都当成「安全旋钮」。判断一个值能不能临时改,至少看三点:
- 它是 hard limit 还是 soft control。
- 触发后是降速、回收,还是直接失败 / OOM / 阻塞。
- kubelet、container runtime 或业务进程是否会依赖它保持某种拓扑或资源假设。
下面按风险分组。
优先只看,不写
这些文件适合排查时读取,通常不需要写:
| 文件 | 用途 | 说明 |
|---|---|---|
cpu.stat |
看 CPU throttling | 关注 nr_throttled、throttled_usec。 |
cpu.pressure |
看 CPU PSI 压力 | 判断任务是否因为 CPU 争用产生 stall。 |
memory.current |
看当前内存使用 | 只读。 |
memory.peak |
看历史峰值 | 可用于判断是否接近限制。 |
memory.events |
看 OOM / high / max 事件 | 判断是否触发过回收或 OOM。 |
memory.pressure |
看内存 PSI 压力 | 判断回收或内存争用影响。 |
io.stat |
看块设备 IO 统计 | 可用于判断读写热点。 |
io.pressure |
看 IO PSI 压力 | 判断 IO stall。 |
pids.current |
看当前进程数 | 只读。 |
pids.events |
看进程数限制事件 | 判断是否触发过 PID 限制。 |
这些值不会改变 Pod 行为,适合先建立基线。
相对适合临时调节
这些值可以作为临时调节入口,但仍然要先看当前值,再小步改。
| 文件 | 作用 | 相对安全的用法 | 风险 |
|---|---|---|---|
cpu.max |
CPU hard quota | 限制最多能用多少 CPU 时间,或写回声明值恢复。 | 限太低会明显变慢、throttling 升高,但通常不会直接杀进程。 |
cpu.weight |
CPU 权重 | 在 CPU 竞争时降低或提高相对份额。默认通常是 100,范围是 1..10000。 |
只有在 CPU 竞争时才明显生效;不会限制绝对上限。 |
cpu.weight.nice |
用 nice 语义设置 CPU 权重 | 用 -20..19 表达相对优先级。 |
粒度比 cpu.weight 粗。 |
memory.high |
内存 soft throttle | 设置一个高水位,超过后触发回收和节流。 | 会导致延迟升高;过低可能让业务严重抖动,但不是直接 OOM。 |
io.weight |
IO 权重 | 在同一设备竞争时调整相对 IO 份额。 | 只影响竞争下的相对分配,不是绝对限速。 |
io.max |
IO hard limit | 对指定块设备限制 BPS / IOPS。 | 设备号和限速值必须确认;限太低可能造成业务超时。 |
这几类里,cpu.max 最适合做「临时压 CPU 使用量」;cpu.weight 更像调优相对优先级;memory.high 可以给内存压力一个软边界;io.max 适合明确知道是哪块磁盘、要压读还是写时使用。
Linux cgroup v2 官方文档对这些接口的语义有明确说明,尤其是 cpu.max、cpu.weight、memory.high、io.max 这些文件,建议操作前对照 Control Group v2 文档。
脚本里对应的修改入口是 set:
|
|
这些值也可以组合在一次 set 里写入:
|
|
默认恢复只恢复 cpu.max,因为它可以从 Pod resources.limits.cpu 计算出来:
|
|
其它值没有 Kubernetes API 里的“原始值”来源。脚本可以用 --all 把它们重置到 cgroup v2 的默认/不限语义,但这不是还原历史原值:
|
|
--all 的重置规则是:
| 文件 | 恢复值 |
|---|---|
cpu.max |
从 Pod resources.limits.cpu 换算。 |
cpu.weight |
100 |
cpu.weight.nice |
0 |
memory.high |
max |
io.weight |
default 100 |
io.max |
对当前已有设备行写回 rbps=max wbps=max riops=max wiops=max。 |
这些默认值来自 Linux cgroup v2 接口语义:cpu.weight 默认 100,cpu.weight.nice 默认 0,memory.high 默认 max,io.weight 默认权重是 100,io.max 里的 max 表示移除对应 BPS / IOPS 限制。注意它们是“默认/不限值”,不是“之前的业务原值”。如果之前已经有人手动设过非默认值,--all 会覆盖掉它。
show 会一起展示这些关注项:
|
|
实际会按分组展示下面这些文件,存在就输出,不存在就跳过:
| 分组 | 文件 | 说明 |
|---|---|---|
| CPU | cpu.max |
CPU hard quota,格式是 max period,max 表示不限制。 |
| CPU | cpu.weight |
CPU 竞争时的相对权重,范围 1..10000。 |
| CPU | cpu.weight.nice |
nice 风格的 CPU 权重,范围 -20..19。 |
| CPU | cpu.stat |
CPU 使用和 throttling 计数。 |
| CPU | cpu.pressure |
CPU PSI 压力。 |
| Memory | memory.current |
当前内存使用量,单位字节。 |
| Memory | memory.peak |
历史内存峰值,单位字节。 |
| Memory | memory.high |
memory soft throttle 水位。 |
| Memory | memory.max |
memory hard limit,过低可能触发 OOM。 |
| Memory | memory.events |
high / max / OOM 等事件计数。 |
| Memory | memory.pressure |
Memory PSI 压力。 |
| IO | io.weight |
块设备 IO 相对权重。 |
| IO | io.max |
按块设备设置的 BPS / IOPS hard limit。 |
| IO | io.stat |
块设备 IO 使用计数。 |
| IO | io.pressure |
IO PSI 压力。 |
| PIDs | pids.current |
当前进程 / 线程数量。 |
| PIDs | pids.max |
进程 / 线程数量上限。 |
| PIDs | pids.events |
PID 限制事件计数。 |
不建议手动改
这些值不是不能改,而是不适合在 Pod 临时限速场景里随手改:
| 文件 | 不建议原因 |
|---|---|
cpuset.cpus / cpuset.mems |
会改变 CPU / NUMA 亲和性,可能影响 QEMU vCPU 线程、NUMA 拓扑和调度预期。 |
memory.max |
hard memory limit。设低了可能直接 OOM,Pod 里的进程可能被杀。 |
memory.swap.max |
可能改变 swap 行为,引入延迟尖刺或内存压力连锁反应。 |
pids.max |
设低后 fork/thread 创建可能失败,业务不一定能正确处理。 |
cgroup.freeze |
会冻结整个 cgroup,Pod 看起来像挂住。 |
cgroup.kill |
会杀掉 cgroup 内进程,不是限速工具。 |
如果目标是「不影响 Pod 存活,只是让它少吃资源」,优先顺序通常是:
- 先观察
cpu.stat、memory.events、*.pressure。 - CPU 用
cpu.max控绝对上限,或用cpu.weight调相对优先级。 - 内存只考虑
memory.high,不要轻易改memory.max。 - IO 只有在确认块设备和影响面后,再考虑
io.max或io.weight。 - 不碰
cpuset、pids.max、cgroup.freeze、cgroup.kill。
风险和边界
直接写 cgroup 文件有几个边界要清楚:
- 这是节点本地状态,不是 Kubernetes 声明式状态。
- Pod 重建、迁移或节点重启后,手动写入可能丢失。
- kubelet 或运行时后续如果重新同步资源,也可能覆盖这个值。
- 批量操作前要确认 Pod 所在节点,不能跨节点拿错路径。
- 写的是 Pod 级 cgroup 时,会影响这个 Pod 内所有容器,而不只是某个进程。
因此这类操作适合短期控制,不适合长期容量管理。长期配置应该修改工作负载或 Pod 的 CPU request/limit,并通过正常发布流程生效。
小结
这次处理里最关键的不是 echo 一行命令,而是三件事:
- 明确操作对象是节点上的 Pod cgroup。
- 明确
cpu.max是 CPU 时间配额,不是 CPU 可见数量。 - 恢复时从 Kubernetes 当前声明的 CPU limit 计算,而不是依赖本地临时记录。
按这个边界使用,cpu.max 是一个很直接的临时限速工具;越过这个边界,它就会变成绕过控制面的隐性状态。