K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

本轮操作记录:资源模型、QoS、OOM 与 CPU 节流实验

这一轮我要把 Kubernetes 资源模型里最核心、也最容易误解的几件事做成事实: 1. `requests` 为什么决定调度,而不是 `limits` 2. `BestEffort / Burstable / Guaranteed` 到底怎么来的 3. CPU 超限为什么只是变慢 4. Memory 超限为什么会 `OOMKilled` 5. `OOMK

Markdown11-操作记录-资源模型-QoS-OOM与CPU节流实验.md2026年4月10日 04:56

本轮操作记录:资源模型、QoS、OOM 与 CPU 节流实验

本轮目标

这一轮我要把 Kubernetes 资源模型里最核心、也最容易误解的几件事做成事实:

  1. requests 为什么决定调度,而不是 limits
  2. BestEffort / Burstable / Guaranteed 到底怎么来的
  3. CPU 超限为什么只是变慢
  4. Memory 超限为什么会 OOMKilled
  5. 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 左右
  • hk6526993821214 CPU / 8Gi

实时使用里:

  • cp-3 大约 397m CPU / 4142Mi
  • us590068728056 大约 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 BestEffort
  • 40 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: systemd
  • memorySwap: {}
  • 没有显式的 eviction 阈值覆盖

节点条件看到:

  • MemoryPressure = False
  • DiskPressure = False
  • PIDPressure = False
  • Ready = True

原理解释

这说明当前实验发生时:

  • 节点没有进入整体压力状态

所以后面看到的 OOMKilled 不是:

  • 节点在驱逐 Pod

而是:

  • 容器自己撞上了内存 limit

Step 6:设计本轮实验对象

我创建了哪些文件

路径:

  • manifests/11-resources/00-namespace-resource-lab.yaml
  • manifests/11-resources/10-besteffort-pod.yaml
  • manifests/11-resources/11-burstable-pod.yaml
  • manifests/11-resources/12-guaranteed-pod.yaml
  • manifests/11-resources/20-too-large-request-pod.yaml
  • manifests/11-resources/30-oom-pod.yaml
  • manifests/11-resources/40-cpu-burst-pod.yaml
  • manifests/11-resources/41-cpu-limit-pod.yaml

为什么这样拆

因为这课里混着三种不同层面的现象:

  1. 调度层
  2. 运行时 cgroup 层
  3. QoS 推导层

如果不拆开,会学成一锅粥。

三类样本分别干什么

QoS 样本

  • besteffort-demo
  • burstable-demo
  • guaranteed-demo

用来证明:

  • QoS 是怎么由 requests / limits 自动推导出来的

调度样本

  • too-large-request

用来证明:

  • requests 太大时,哪怕节点实时 usage 不高,Pod 也会 Pending

运行时样本

  • oom-demo
  • cpu-burst-demo
  • cpu-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-demo
  • burstable-demo
  • guaranteed-demo
  • cpu-burst-demo
  • cpu-limit-demo
  • oom-demo(起初)

Pending

  • too-large-request

QoS 分类

  • besteffort-demo -> BestEffort
  • burstable-demo -> Burstable
  • guaranteed-demo -> Guaranteed
  • cpu-burst-demo -> Burstable
  • cpu-limit-demo -> Guaranteed
  • oom-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: 20
  • memory: 20Gi

事件里写得很明确:

  • 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }
  • 4 Insufficient cpu
  • 4 Insufficient memory

原理解释

这一步非常关键。

它说明:

  1. Pod 必须完整放进某一个节点,不会跨节点拆分
  2. scheduler 看的是 requests,不是 limits
  3. 实时 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: OOMKilled
  • Exit Code: 137
  • Restart Count: 2
  • memory limit = 64Mi
  • memory request = 32Mi

为什么日志只到 58Mi 左右

因为应用打印的“我已经分配了多少”只是它自己那部分视角。

真实内存使用还包括:

  • shell 进程本身
  • 变量管理开销
  • 其他运行时开销

所以很可能在你“应用自认为还没到 64Mi”时,整个 cgroup 其实已经逼近 limit。

原理解释

这一步给出了最标准的内存超限证据链:

  • 资源配置里有 memory limit
  • 应用持续吃内存
  • 容器被杀
  • Reason = OOMKilled
  • Exit 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 = 999m
  • cpu-limit-demo = 101m

两者都还在:

  • Running

并没有谁因为 CPU 打满而被杀掉。

原理解释

这一步先说明了现象层:

  • 没 CPU limit 的 Pod 可以接近一整核
  • 配了 100m CPU 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 100000
  • nr_throttled = 0
  • throttled_usec = 0

cpu-limit-demo

  • cpu.max = 10000 100000
  • 5 秒前后:
    • nr_throttled: 785 -> 835
    • throttled_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 = False
  • DiskPressure = False
  • PIDPressure = False
  • Ready = 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>

重点找:

  • OOMKilled
  • Exit Code 137
  • CrashLoopBackOff

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 个核心结论做成了现场证据:

  1. too-large-request 证明调度器看的是 requests 账本,而不是 top 的瞬时空闲。
  2. besteffort-demo / burstable-demo / guaranteed-demo 证明 QoS 是由资源声明自动推导的。
  3. cpu-burst-demo / cpu-limit-demo 证明 CPU limit 的本质是 cgroup 节流,不是杀进程。
  4. oom-demo 证明 memory limit 的典型后果是 OOMKilled + Exit Code 137 + CrashLoopBackOff

你现在已经不只是知道“resources 里能写 requests 和 limits”。

你已经开始真正理解:

  • scheduler 的资源账本
  • kubelet / cgroup 的运行时约束
  • QoS 的保护等级
  • OOM 与 Eviction 的边界

这条主线打通以后,下一课再进入:

  • HPA
  • 伸缩策略
  • requests 作为 autoscaling 分母

你就不会只会背公式了。