Repository Reading Site
本轮操作记录:资源模型、QoS、OOM 与 CPU 节流实验
这一轮我要把 Kubernetes 资源模型里最核心、也最容易误解的几件事做成事实: 1. `requests` 为什么决定调度,而不是 `limits` 2. `BestEffort / Burstable / Guaranteed` 到底怎么来的 3. CPU 超限为什么只是变慢 4. Memory 超限为什么会 `OOMKilled` 5. `OOMK
本轮操作记录:资源模型、QoS、OOM 与 CPU 节流实验
本轮目标
这一轮我要把 Kubernetes 资源模型里最核心、也最容易误解的几件事做成事实:
requests为什么决定调度,而不是limitsBestEffort / Burstable / Guaranteed到底怎么来的- CPU 超限为什么只是变慢
- Memory 超限为什么会
OOMKilled OOMKill和节点Eviction为什么不是一回事
Step 1:先看集群真实容量和实时使用
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl top nodes
KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes \
-o custom-columns=NAME:.metadata.name,CPU:.status.capacity.cpu,ALLOCATABLE_CPU:.status.allocatable.cpu,MEM:.status.capacity.memory,ALLOCATABLE_MEM:.status.allocatable.memory,PODS:.status.allocatable.pods
为什么先看这两组数据
因为后面所有资源现象都绕不开两类概念:
- 节点总容量 / 可分配量
- 节点实时使用量
如果这两类概念不先分开,后面你很容易把:
- 调度账本
- 实时消耗
混为一谈。
我看到的结果
节点容量里:
- 多个 worker 是
16 CPU / 16Gi左右 hk652699382121是4 CPU / 8Gi
实时使用里:
cp-3大约397m CPU / 4142Mius590068728056大约213m CPU / 2404Mi
原理解释
这一步先建立一个关键前提:
- top 看到的“现在用了多少”
- 不等于 scheduler 判断“这个 Pod 能不能放进去”的那本账
Step 2:看仓库里已有材料,确认这一课要补哪些空白
实际命令
rg -n 'requests|limits|QoS|OOM|evict|驱逐|HPA|资源' phase-* README.md ml-platform saas-platform -g '*.md'
sed -n '80,130p' phase-4/02-interview-guide.md
sed -n '180,230p' phase-4/01-troubleshooting-lab.md
为什么先读已有资料
因为仓库里并不是完全没有这些概念。
已经有一些:
- 面试答法
- 故障排查样本
- requests / limits 的入门解释
但还不够系统解释:
- 调度账本 vs 实时资源
- QoS 在真实集群里的分布
- CPU cgroup 节流证据
- OOMKill 与 Eviction 的边界
所以我决定把这课升级成:
- 真实集群资源审计
- 真实 cgroup 证据
- 真实 OOM / Pending 样本
Step 3:核对真实集群当前的 QoS 分布和样本
实际命令
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get pod -A -o 'custom-columns=NS:.metadata.namespace,NAME:.metadata.name,QOS:.status.qosClass' | sed -n '1,120p'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get pod -A -o jsonpath='{range .items[*]}{.status.qosClass}{"\n"}{end}' | sort | uniq -c
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get deploy coredns -o jsonpath='{.spec.template.spec.containers[0].resources.requests.cpu}{"\n"}{.spec.template.spec.containers[0].resources.requests.memory}{"\n"}{.spec.template.spec.containers[0].resources.limits.cpu}{"\n"}{.spec.template.spec.containers[0].resources.limits.memory}{"\n"}'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n ingress-nginx get pod -l app.kubernetes.io/component=controller -o jsonpath='{.items[0].metadata.name}{"\n"}{.items[0].status.qosClass}{"\n"}{.items[0].spec.containers[0].resources.requests.cpu}{"\n"}{.items[0].spec.containers[0].resources.requests.memory}{"\n"}{.items[0].spec.containers[0].resources.limits.cpu}{"\n"}{.items[0].spec.containers[0].resources.limits.memory}{"\n"}'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get deploy metrics-server -o jsonpath='{.spec.template.spec.containers[0].resources.requests.cpu}{"\n"}{.spec.template.spec.containers[0].resources.requests.memory}{"\n"}{.spec.template.spec.containers[0].resources.limits.cpu}{"\n"}{.spec.template.spec.containers[0].resources.limits.memory}{"\n"}'
为什么这一步很重要
我不想让你只记住教科书式的三个 QoS 名字。
我想先让你看到:
- 真实集群里大多数工作负载到底处在哪个资源等级
我看到的结果
当时集群里统计到:
70 BestEffort40 Burstable
没有现成 Guaranteed 业务样本。
而且真实组件例如:
coredns
- requests:
100m / 70Mi - memory limit:
170Mi - 没有 CPU limit
- 所以是
Burstable
ingress-nginx-controller
- requests:
100m / 90Mi - 没有限制
- 所以是
Burstable
metrics-server
- requests:
100m / 200Mi - 没有限制
- 所以是
Burstable
原理解释
这一步告诉我们:
- 真实生产里,很多系统不会一上来就配成 Guaranteed
- 最常见的是 Burstable 折中策略
- 但大量 BestEffort 说明平台资源治理还没做深
Step 4:检查节点上的 Allocated Resources
实际命令
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl describe node us590068728056 | sed -n '/Allocated resources:/,/Events:/p'
为什么必须看这一段
因为这是最接近 scheduler 视角的节点资源账本之一。
我看到的结果
节点上显示:
Requests: cpu 2880m (18%), memory 4266Mi (26%)Limits: cpu 9200m (57%), memory 7392Mi (46%)
并且提示:
Total limits may be over 100 percent, i.e., overcommitted.
原理解释
这一步非常关键。
它说明:
- requests 是调度承诺账本
- limits 可以超卖
所以你以后不能拿:
limits的总和
直接当成:
- “节点已经满了”
Step 5:补充 kubelet 与节点压力的真实证据
实际命令
ssh root@107.148.176.193 'sudo sed -n "1,260p" /var/lib/kubelet/config.yaml'
ssh root@107.148.164.118 'sudo sed -n "1,260p" /var/lib/kubelet/config.yaml'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl describe node us590068728056 | rg 'MemoryPressure|DiskPressure|PIDPressure|Ready' -n -C 1
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl describe node cp-3 | rg 'MemoryPressure|DiskPressure|PIDPressure|Ready' -n -C 1
为什么这一步重要
因为我要把:
- 容器级 OOM
- 节点级 Eviction
这两件事从底层配置和节点状态上分开。
我看到的结果
kubelet 配置里看到:
cgroupDriver: systemdmemorySwap: {}- 没有显式的 eviction 阈值覆盖
节点条件看到:
MemoryPressure = FalseDiskPressure = FalsePIDPressure = FalseReady = True
原理解释
这说明当前实验发生时:
- 节点没有进入整体压力状态
所以后面看到的 OOMKilled 不是:
- 节点在驱逐 Pod
而是:
- 容器自己撞上了内存 limit
Step 6:设计本轮实验对象
我创建了哪些文件
路径:
manifests/11-resources/00-namespace-resource-lab.yamlmanifests/11-resources/10-besteffort-pod.yamlmanifests/11-resources/11-burstable-pod.yamlmanifests/11-resources/12-guaranteed-pod.yamlmanifests/11-resources/20-too-large-request-pod.yamlmanifests/11-resources/30-oom-pod.yamlmanifests/11-resources/40-cpu-burst-pod.yamlmanifests/11-resources/41-cpu-limit-pod.yaml
为什么这样拆
因为这课里混着三种不同层面的现象:
- 调度层
- 运行时 cgroup 层
- QoS 推导层
如果不拆开,会学成一锅粥。
三类样本分别干什么
QoS 样本
besteffort-demoburstable-demoguaranteed-demo
用来证明:
- QoS 是怎么由 requests / limits 自动推导出来的
调度样本
too-large-request
用来证明:
- requests 太大时,哪怕节点实时 usage 不高,Pod 也会 Pending
运行时样本
oom-democpu-burst-democpu-limit-demo
用来证明:
- 内存超限是 OOMKill
- CPU 超限是 throttle
Step 7:先做 dry-run 校验
实际命令
find manifests/11-resources -maxdepth 1 -type f | sort
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply --dry-run=client -f manifests/11-resources
为什么要先 dry-run
因为这一轮清单比较多,而且有 shell 脚本和资源声明组合。
我想先确认:
- YAML 语法正确
- 资源类型正确
- API 可以识别
我看到的结果
所有对象都通过了 dry-run。
原理解释
这是处理多清单实验时很好的工程习惯:
- 先排除语法和对象层面的低级错误
Step 8:把所有资源实验对象真正落集群
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/11-resources
sleep 5
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n resource-lab get pod -o wide
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n resource-lab get pod -o 'custom-columns=NAME:.metadata.name,QOS:.status.qosClass,PHASE:.status.phase,NODE:.spec.nodeName,RESTARTS:.status.containerStatuses[0].restartCount'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n resource-lab get events --sort-by=.lastTimestamp | tail -n 80
我看到的结果
很快形成了三类状态:
正常 Running
besteffort-demoburstable-demoguaranteed-democpu-burst-democpu-limit-demooom-demo(起初)
Pending
too-large-request
QoS 分类
besteffort-demo -> BestEffortburstable-demo -> Burstableguaranteed-demo -> Guaranteedcpu-burst-demo -> Burstablecpu-limit-demo -> Guaranteedoom-demo -> Burstable
原理解释
这一步已经把 QoS 推导规则从“概念”变成了“可观测事实”。
而且也顺带证明:
- 同样是 CPU 燃烧 Pod
- 只要 request / limit 组合不同
- QoS 也会不同
Step 9:看 too-large-request 为什么 Pending
实际命令
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n resource-lab describe pod too-large-request | sed -n '1,200p'
我看到的结果
它的 requests 是:
cpu: 20memory: 20Gi
事件里写得很明确:
1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }4 Insufficient cpu4 Insufficient memory
原理解释
这一步非常关键。
它说明:
- Pod 必须完整放进某一个节点,不会跨节点拆分
- scheduler 看的是 requests,不是 limits
- 实时
top看起来空闲,不代表调度账本就能容纳这个请求
这正是很多“看着有资源但调度不上”的根因。
Step 10:验证内存超限会 OOMKill
实际命令
sleep 15
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n resource-lab logs oom-demo --tail=20
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n resource-lab describe pod oom-demo | sed -n '1,220p'
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n resource-lab get pod oom-demo -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n resource-lab logs oom-demo --previous --tail=30
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n resource-lab describe pod oom-demo | rg 'Reason:|Exit Code:|Restart Count:|Limits:|Requests:|QoS Class' -n -C 1
我看到的结果
日志显示容器一路分配到了:
allocated 58 Mi
随后 Pod 状态变成:
CrashLoopBackOff
describe 里明确看到:
Reason: OOMKilledExit Code: 137Restart Count: 2memory limit = 64Mimemory request = 32Mi
为什么日志只到 58Mi 左右
因为应用打印的“我已经分配了多少”只是它自己那部分视角。
真实内存使用还包括:
- shell 进程本身
- 变量管理开销
- 其他运行时开销
所以很可能在你“应用自认为还没到 64Mi”时,整个 cgroup 其实已经逼近 limit。
原理解释
这一步给出了最标准的内存超限证据链:
- 资源配置里有 memory limit
- 应用持续吃内存
- 容器被杀
Reason = OOMKilledExit Code = 137
这就是你以后排障时最应该找的组合。
Step 11:验证 CPU 超限不会杀进程,而是节流
实际命令
sleep 15
KUBECONFIG=~/.kube/config-k8s-lab kubectl top pod -n resource-lab
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n resource-lab get pod cpu-burst-demo cpu-limit-demo -o wide
我看到的结果
kubectl top pod 显示:
cpu-burst-demo = 999mcpu-limit-demo = 101m
两者都还在:
Running
并没有谁因为 CPU 打满而被杀掉。
原理解释
这一步先说明了现象层:
- 没 CPU limit 的 Pod 可以接近一整核
- 配了
100mCPU limit 的 Pod 被限制在大约 0.1 核
但要把“节流”讲透,还得看 cgroup。
Step 12:直接读取 cgroup 文件,拿到 CPU 节流铁证
实际命令
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n resource-lab exec cpu-burst-demo -- \
sh -c 'echo "cpu.max:"; cat /sys/fs/cgroup/cpu.max; echo; echo "cpu.stat(before):"; cat /sys/fs/cgroup/cpu.stat; sleep 5; echo; echo "cpu.stat(after):"; cat /sys/fs/cgroup/cpu.stat'
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n resource-lab exec cpu-limit-demo -- \
sh -c 'echo "cpu.max:"; cat /sys/fs/cgroup/cpu.max; echo; echo "cpu.stat(before):"; cat /sys/fs/cgroup/cpu.stat; sleep 5; echo; echo "cpu.stat(after):"; cat /sys/fs/cgroup/cpu.stat'
我看到的结果
cpu-burst-demo
cpu.max = max 100000nr_throttled = 0throttled_usec = 0
cpu-limit-demo
cpu.max = 10000 100000- 5 秒前后:
nr_throttled: 785 -> 835throttled_usec: 64579894 -> 68686213
原理解释
这就是 CPU limit 的底层本质。
cpu.max = 10000 100000 表示:
- 每 100000 微秒的周期里
- 最多运行 10000 微秒 CPU
也就是大约:
- 10% 一个核
100m
而 nr_throttled / throttled_usec 持续增长,说明这个进程没有死。
它只是:
- 持续被限流
这一步把“CPU 超限是 throttle,不是 kill”彻底坐实了。
Step 13:顺手对比一下容器级 OOM 与节点级压力
实际命令
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl describe node us590068728056 | rg 'MemoryPressure|DiskPressure|PIDPressure|Ready' -n -C 1
KUBECONFIG=~/.kube/config-k8s-lab \
kubectl describe node cp-3 | rg 'MemoryPressure|DiskPressure|PIDPressure|Ready' -n -C 1
我看到的结果
当前节点条件都是:
MemoryPressure = FalseDiskPressure = FalsePIDPressure = FalseReady = True
原理解释
这就把本轮实验的边界说清楚了:
- 我们确实制造了容器级 OOM
- 但没有制造节点级驱逐
所以本轮里:
OOMKilled的责任主体是 cgroup memory limit- 不是 kubelet 因节点压力主动驱逐了这个 Pod
Step 14:本轮最重要的排障链路
如果以后你遇到资源问题,我建议先按这个顺序查。
1. 看 Pod 是 Pending 还是 Running
kubectl get pod -A -o wide
kubectl describe pod <pod>
如果是 Pending,就先优先想:
- requests 太大
- 调度约束冲突
2. 看 resources 和 QoS
kubectl get pod <pod> -o yaml
kubectl get pod <pod> -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass
3. 看运行时后果
kubectl logs <pod> --previous
kubectl describe pod <pod>
重点找:
OOMKilledExit Code 137CrashLoopBackOff
4. 看节点账本
kubectl describe node <node>
kubectl top node <node>
kubectl top pod -A
重点区分:
- requests 账本
- 实时 usage
5. 必要时看 cgroup
kubectl exec <pod> -- cat /sys/fs/cgroup/cpu.max
kubectl exec <pod> -- cat /sys/fs/cgroup/cpu.stat
kubectl exec <pod> -- cat /sys/fs/cgroup/memory.max
这一步适合做“底层确认”,尤其在你要证明:
- 是 CPU throttle
- 还是别的问题
时特别有用。
本轮结论
这一轮我已经把 Kubernetes 资源模型的 4 个核心结论做成了现场证据:
too-large-request证明调度器看的是 requests 账本,而不是 top 的瞬时空闲。besteffort-demo / burstable-demo / guaranteed-demo证明 QoS 是由资源声明自动推导的。cpu-burst-demo / cpu-limit-demo证明 CPU limit 的本质是 cgroup 节流,不是杀进程。oom-demo证明 memory limit 的典型后果是OOMKilled + Exit Code 137 + CrashLoopBackOff。
你现在已经不只是知道“resources 里能写 requests 和 limits”。
你已经开始真正理解:
- scheduler 的资源账本
- kubelet / cgroup 的运行时约束
- QoS 的保护等级
- OOM 与 Eviction 的边界
这条主线打通以后,下一课再进入:
- HPA
- 伸缩策略
- requests 作为 autoscaling 分母
你就不会只会背公式了。