
有时线上需要临时调节某几个 Pod 的资源使用，但又不想立即修改工作负载规格、触发控制面变更或重建实例。这种情况下，如果节点使用 cgroup v2，可以直接改对应 Pod cgroup 里的 `cpu.max`、`cpu.weight`、`memory.high` 或 IO 相关文件。

这不是长期配置手段，更适合临时止血、压测隔离或问题定位。长期方案仍然应该回到 Kubernetes 的资源规格里。

本文记录一次实际整理过程：怎么从 Pod 找到节点上的 cgroup 路径，怎么把「8 核」「12 核」换算成 `cpu.max`，哪些 cgroup 值适合临时调整，以及怎么写一个通过 SSH 操作节点的脚本。

<!--more-->

## 背景

Kubernetes 工作负载最终都会落到节点上的 Pod 里运行。对 Linux 内核来说，每个 Pod 都属于某个 cgroup。

如果节点启用了 cgroup v2，CPU 配额主要通过 `cpu.max` 表达。Linux kernel 文档里对 `cpu.max` 的格式是：

```text
$MAX $PERIOD
```

含义是：在每个 `$PERIOD` 微秒周期里，最多使用 `$MAX` 微秒 CPU 时间。`max` 表示不限制。官方文档见 [Control Group v2 - CPU Interface Files](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html)。

常见周期是 `100000` 微秒，也就是 100ms。因此：

```text
800000 100000
```

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

```text
1200000 100000
```

表示 12 核。

Kubernetes CPU 资源单位也按 CPU core 计量，`1` 表示 1 个 CPU，`500m` 表示 0.5 个 CPU。这个定义可以看 Kubernetes 官方文档 [Resource Management for Pods and Containers](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)。

## 路径怎么找

目标不是容器内部的 cgroup，而是宿主机上 Pod 对应的 cgroup。先从 Kubernetes API 拿 Pod 的 UID 和所在节点：

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

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

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

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

错误做法类似这样：

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

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

更稳的是只处理 UID：

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

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

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

## 查看当前限制

拿到路径后，可以看两个文件：

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

`cpu.max` 是当前限制。`cpu.stat` 里能看到使用和 throttling 相关统计，例如 `nr_periods`、`nr_throttled`、`throttled_usec`。

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

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

## 临时限制为 N 核

假设周期固定用 `100000` 微秒，N 核对应：

```text
N * 100000 100000
```

限制为 8 核：

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

限制为 12 核：

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

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

```bash
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 是：

```text
8
```

恢复值就是：

```text
800000 100000
```

如果是：

```text
500m
```

恢复值就是：

```text
50000 100000
```

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

## 脚本化

最后整理成一个脚本放在文章附件里：[pod-cgroup-tune.sh](https://jimyag.com/posts/pod-cgroup-v2-tuning/files/pod-cgroup-tune.sh)。入口保持简单：

```bash
./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>
```

也可以直接下载后执行：

```bash
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.max`、`cpu.weight`、`memory.high`、`io.max`。其中 `--cores` 会转换成 `cpu.max`。
6. `restore` 默认只恢复有 Kubernetes API 来源的 `cpu.max`。如果显式加 `--all`，才会把其它可调项重置到 cgroup v2 默认/不限值。

如果节点 SSH 用户不是 `root`，可以显式指定：

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

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

`--cores` 转 `cpu.max` 的逻辑：

```bash
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`：

```bash
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 搜：

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

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

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

不会。

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

所以把：

```text
1762000 100000
```

改成：

```text
800000 100000
```

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

```bash
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_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 文档](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html)。

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

```bash
# 设置 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` 里写入：

```bash
./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` 计算出来：

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

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

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

`show` 会一起展示这些关注项：

```text
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 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 存活，只是让它少吃资源」，优先顺序通常是：

1. 先观察 `cpu.stat`、`memory.events`、`*.pressure`。
2. CPU 用 `cpu.max` 控绝对上限，或用 `cpu.weight` 调相对优先级。
3. 内存只考虑 `memory.high`，不要轻易改 `memory.max`。
4. IO 只有在确认块设备和影响面后，再考虑 `io.max` 或 `io.weight`。
5. 不碰 `cpuset`、`pids.max`、`cgroup.freeze`、`cgroup.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` 是一个很直接的临时限速工具；越过这个边界，它就会变成绕过控制面的隐性状态。

