ᕕ( ᐛ )ᕗ Jimyag's Blog

Pod cgroup v2 临时资源调节实践

Last modified:

有时线上需要临时调节某几个 Pod 的资源使用,但又不想立即修改工作负载规格、触发控制面变更或重建实例。这种情况下,如果节点使用 cgroup v2,可以直接改对应 Pod cgroup 里的 cpu.maxcpu.weightmemory.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 的格式是:

1
$MAX $PERIOD

含义是:在每个 $PERIOD 微秒周期里,最多使用 $MAX 微秒 CPU 时间。max 表示不限制。官方文档见 Control Group v2 - CPU Interface Files

常见周期是 100000 微秒,也就是 100ms。因此:

1
800000 100000

表示每 100ms 最多使用 800ms CPU 时间,即 8 核。

1
1200000 100000

表示 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 和所在节点:

1
2
kubectl get pod -n <namespace> <pod-name> \
  -o jsonpath='{.spec.nodeName}{" "}{.metadata.uid}{"\n"}'

在 systemd 风格的 cgroup v2 层级里,Burstable Pod 的路径通常长这样:

1
/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid-with-underscore>.slice

这里有一个容易踩的坑:只替换 UID 里的 -,不要把整条路径里的 - 都替换掉。

错误做法类似这样:

1
echo "$path" | sed 's/-/_/g'

它会把 kubepods-burstable.slice 也改坏。

更稳的是只处理 UID:

1
2
3
uid="fd636d02-0c2f-40e7-9015-122e79f270de"
pod_id="pod${uid//-/_}"
path="/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-${pod_id}.slice"

如果不想假设路径,也可以在节点上查:

1
ssh root@<node-ip> "find /sys/fs/cgroup -type d -name '*podfd636d02_0c2f_40e7_9015_122e79f270de*' | head -n 1"

查看当前限制

拿到路径后,可以看两个文件:

1
ssh root@<node-ip> "cat '<cgroup-path>/cpu.max'; cat '<cgroup-path>/cpu.stat'"

cpu.max 是当前限制。cpu.stat 里能看到使用和 throttling 相关统计,例如 nr_periodsnr_throttledthrottled_usec

如果只是临时确认某个 Pod 当前值,最小命令就是:

1
ssh root@<node-ip> "cat /sys/fs/cgroup/.../cpu.max"

临时限制为 N 核

假设周期固定用 100000 微秒,N 核对应:

1
N * 100000 100000

限制为 8 核:

1
ssh root@<node-ip> "echo '800000 100000' > '<cgroup-path>/cpu.max'"

限制为 12 核:

1
ssh root@<node-ip> "echo '1200000 100000' > '<cgroup-path>/cpu.max'"

批量处理时,不建议手写半截路径再复制。可以先用 kubectl 输出 Pod 名和 UID,再只对 UID 做替换:

1
2
3
4
5
kubectl get po -n <namespace> \
  -o jsonpath='{range .items[?(@.spec.nodeName=="<node-name>")]}{.metadata.name}{" "}{.metadata.uid}{"\n"}{end}' |
while read -r pod uid; do
  printf '%s %s\n' "$pod" "pod${uid//-/_}"
done

再在对应节点写 cpu.max

恢复应该从哪里来

一开始很容易想到:限制前把原始 cpu.max 记到本地文件,恢复时写回。

但这种方式有两个问题:

  1. 本地状态文件容易丢。
  2. 多人操作时,本地记录不一定是当前 Pod 规格对应的值。

更合适的恢复来源是 Kubernetes API:从 Pod 的 resources.limits.cpu 读取声明值,再转换为 cpu.max 写回节点。

例如 Pod limit 是:

1
8

恢复值就是:

1
800000 100000

如果是:

1
500m

恢复值就是:

1
50000 100000

这仍然不是「长期状态修复」。它只是让节点上的 cgroup 回到 Kubernetes 当前声明的 CPU limit。

脚本化

最后整理成一个脚本放在文章附件里:pod-cgroup-tune.sh。入口保持简单:

1
2
3
4
./pod-cgroup-tune.sh show    --namespace <ns> --pod <pod-name>
./pod-cgroup-tune.sh set     --namespace <ns> --pod <pod-name> --cores 8
./pod-cgroup-tune.sh set     --namespace <ns> --pod <pod-name> --cpu-weight 50
./pod-cgroup-tune.sh restore --namespace <ns> --pod <pod-name>

也可以直接下载后执行:

1
2
curl -fsSL https://jimyag.com/posts/pod-cgroup-v2-tuning/files/pod-cgroup-tune.sh -o pod-cgroup-tune.sh
chmod +x pod-cgroup-tune.sh

它做几件事:

  1. 本地通过 kubectl 精确查询传入的 Pod。--pod 必须是完整 Pod 名;如果 Pod 不存在,就直接报错。
  2. 从 Pod 的 spec.nodeName 找到节点,再从 Node 的 InternalIP 拼出 SSH 目标 root@<node-ip>
  3. 通过一次 SSH 到目标节点,在 /sys/fs/cgroup 下找到对应 Pod cgroup 并执行目标操作。
  4. show 先展示 Pod 声明的 resources.limits.cpu 和换算出的 cpu.max,再读取当前 cgroup 的 CPU、memory、IO、pids 关键状态。
  5. set 修改相对适合临时调节的 cgroup 值,例如 cpu.maxcpu.weightmemory.highio.max。其中 --cores 会转换成 cpu.max
  6. restore 默认只恢复有 Kubernetes API 来源的 cpu.max。如果显式加 --all,才会把其它可调项重置到 cgroup v2 默认/不限值。

如果节点 SSH 用户不是 root,可以显式指定:

1
./pod-cgroup-tune.sh show --namespace <ns> --pod <pod-name> --ssh-user <user>

脚本里保留了完整参数校验和 SSH 写入逻辑,下面只摘两段关键转换代码。

--corescpu.max 的逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
DEFAULT_PERIOD_US=100000

quota_for_cores() {
  local cores="$1"
  awk -v cores="$cores" -v period="$DEFAULT_PERIOD_US" '
    BEGIN {
      if (cores !~ /^[0-9]+([.][0-9]+)?$/ || cores <= 0) {
        exit 1
      }
      printf "%.0f %d\n", cores * period, period
    }
  '
}

Kubernetes CPU quantity 需要额外支持 m

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
quota_for_k8s_cpu() {
  local cpu="$1"
  awk -v cpu="$cpu" -v period="$DEFAULT_PERIOD_US" '
    BEGIN {
      if (cpu ~ /^[0-9]+m$/) {
        cores = substr(cpu, 1, length(cpu) - 1) / 1000
      } else if (cpu ~ /^[0-9]+([.][0-9]+)?$/) {
        cores = cpu + 0
      } else {
        exit 1
      }
      if (cores <= 0) {
        exit 1
      }
      printf "%.0f %d\n", cores * period, period
    }
  '
}

路径定位可以不依赖固定 QoS 目录,直接在远端按 Pod UID 搜:

1
2
pod_id="pod${uid//-/_}"
ssh root@<node-ip> "find /sys/fs/cgroup -type d -name '*${pod_id}*' | head -n 1"

这样对 kubepods-burstable.slicekubepods-guaranteed.slice 这类路径差异更宽容。

改 cpu.max 会改变容器里看到的 CPU 数量吗

不会。

cpu.max 控制的是 CPU 时间配额。它限制进程在一个周期内最多能消耗多少 CPU 时间,但不会改变进程可见的 CPU 拓扑。

所以把:

1
1762000 100000

改成:

1
800000 100000

只是把 CPU 时间上限从 17.62 核调到 8 核。容器或进程里看到的这些值通常不会跟着变:

1
2
3
nproc
lscpu
cat /proc/cpuinfo | grep processor | wc -l

如果想改变进程能在哪些 CPU 上运行,那是 cpuset.cpus 的范围。但这比改 cpu.max 更敏感,可能影响线程、NUMA 和调度亲和性。临时压 CPU 使用量时,优先改 cpu.max,不要轻易碰 cpuset

还有哪些值可以看或临时改

不能把 cgroup 目录里所有可写文件都当成「安全旋钮」。判断一个值能不能临时改,至少看三点:

  1. 它是 hard limit 还是 soft control。
  2. 触发后是降速、回收,还是直接失败 / OOM / 阻塞。
  3. kubelet、container runtime 或业务进程是否会依赖它保持某种拓扑或资源假设。

下面按风险分组。

优先只看,不写

这些文件适合排查时读取,通常不需要写:

文件 用途 说明
cpu.stat 看 CPU throttling 关注 nr_throttledthrottled_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.maxcpu.weightmemory.highio.max 这些文件,建议操作前对照 Control Group v2 文档

脚本里对应的修改入口是 set

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 设置 cpu.max。这里的 8 会转换为 800000 100000。
./pod-cgroup-tune.sh set --namespace <ns> --pod <pod-name> --cores 8

# 调整 CPU 相对权重,范围 1..10000,默认通常是 100。
./pod-cgroup-tune.sh set --namespace <ns> --pod <pod-name> --cpu-weight 50

# 用 nice 语义调整 CPU 权重,范围 -20..19。
./pod-cgroup-tune.sh set --namespace <ns> --pod <pod-name> --cpu-weight-nice 10

# 设置 memory.high。这里用字节数;也可以写 max 表示取消 high 水位。
./pod-cgroup-tune.sh set --namespace <ns> --pod <pod-name> --memory-high 8589934592
./pod-cgroup-tune.sh set --namespace <ns> --pod <pod-name> --memory-high max

# 设置 IO 相对权重。具体格式取决于内核接口,常见形式是 default 100 或 "<major>:<minor> <weight>"。
./pod-cgroup-tune.sh set --namespace <ns> --pod <pod-name> --io-weight 'default 100'

# 设置 IO hard limit。设备号可以从 lsblk 或 /sys/dev/block 确认。
./pod-cgroup-tune.sh set --namespace <ns> --pod <pod-name> --io-max '8:0 rbps=10485760 wbps=10485760'

这些值也可以组合在一次 set 里写入:

1
2
3
4
5
6
7
./pod-cgroup-tune.sh set \
  --namespace <ns> \
  --pod <pod-name> \
  --cores 8 \
  --cpu-weight 50 \
  --memory-high 8589934592 \
  --io-max '8:0 rbps=10485760 wbps=10485760'

默认恢复只恢复 cpu.max,因为它可以从 Pod resources.limits.cpu 计算出来:

1
./pod-cgroup-tune.sh restore --namespace <ns> --pod <pod-name>

其它值没有 Kubernetes API 里的“原始值”来源。脚本可以用 --all 把它们重置到 cgroup v2 的默认/不限语义,但这不是还原历史原值:

1
./pod-cgroup-tune.sh restore --namespace <ns> --pod <pod-name> --all

--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 默认 100cpu.weight.nice 默认 0memory.high 默认 maxio.weight 默认权重是 100io.max 里的 max 表示移除对应 BPS / IOPS 限制。注意它们是“默认/不限值”,不是“之前的业务原值”。如果之前已经有人手动设过非默认值,--all 会覆盖掉它。

show 会一起展示这些关注项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Kubernetes declared resources
FIELD                  VALUE                    DESCRIPTION
cpu limit              8                        Pod declared CPU limit from Kubernetes
cpu.max                800000 100000            cpu.max value converted from declared CPU limit

path: /sys/fs/cgroup/...

CPU
FILE                     VALUE                            DESCRIPTION
cpu.max                  800000 100000                    CPU hard quota: max period, or max for unlimited
cpu.weight               50                               Relative CPU share under contention, 1..10000
cpu.stat                 <multi-line>                     CPU usage and throttling counters
  nr_periods 10
  nr_throttled 2
  throttled_usec 30

Memory
FILE                     VALUE                            DESCRIPTION
memory.current           1048576                          Current memory usage in bytes
memory.high              max                              Memory soft throttle threshold
memory.events            <multi-line>                     Memory high/max/OOM event counters
  high 0
  max 0
  oom 0

实际会按分组展示下面这些文件,存在就输出,不存在就跳过:

分组 文件 说明
CPU cpu.max CPU hard quota,格式是 max periodmax 表示不限制。
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 存活,只是让它少吃资源」,优先顺序通常是:

  1. 先观察 cpu.statmemory.events*.pressure
  2. CPU 用 cpu.max 控绝对上限,或用 cpu.weight 调相对优先级。
  3. 内存只考虑 memory.high,不要轻易改 memory.max
  4. IO 只有在确认块设备和影响面后,再考虑 io.maxio.weight
  5. 不碰 cpusetpids.maxcgroup.freezecgroup.kill

风险和边界

直接写 cgroup 文件有几个边界要清楚:

  1. 这是节点本地状态,不是 Kubernetes 声明式状态。
  2. Pod 重建、迁移或节点重启后,手动写入可能丢失。
  3. kubelet 或运行时后续如果重新同步资源,也可能覆盖这个值。
  4. 批量操作前要确认 Pod 所在节点,不能跨节点拿错路径。
  5. 写的是 Pod 级 cgroup 时,会影响这个 Pod 内所有容器,而不只是某个进程。

因此这类操作适合短期控制,不适合长期容量管理。长期配置应该修改工作负载或 Pod 的 CPU request/limit,并通过正常发布流程生效。

小结

这次处理里最关键的不是 echo 一行命令,而是三件事:

  1. 明确操作对象是节点上的 Pod cgroup。
  2. 明确 cpu.max 是 CPU 时间配额,不是 CPU 可见数量。
  3. 恢复时从 Kubernetes 当前声明的 CPU limit 计算,而不是依赖本地临时记录。

按这个边界使用,cpu.max 是一个很直接的临时限速工具;越过这个边界,它就会变成绕过控制面的隐性状态。

#Kubernetes #Pod #Cgroup #Linux #CPU #Memory #IO