10. 资源超卖与QoS
request 和 limit 不是一对简单的上下限
你写过一个 Pod,设了 resources.requests.cpu=500m, resources.limits.cpu=1000m。看起来很简单,但这背后藏着 K8S 最容易被低估的设计。
request:调度器的依据。你 request 500m,Scheduler 找一台至少还剩 500m 的节点。 limit:内核的限制。你 limit 1000m,内核保证你最多用 1 核,多了 throttle。
关键点:request 决定调度时的分配,limit 是运行时的上限——这中间是有差值的。 一个 Node 有 4 核,你 request 了 10 个 500m 的 Pod,调度进去之后总 limit 可能是 10 核。平时大家都规矩用 200m,无事发生。突然四个 Pod 同时飙到了 1000m——CPU 开始争抢。
这个现象就叫超卖(overcommit)。你卖的 limit 总和大于物理资源,赌的是大家不会同时用满。
QoS:谁先被"牺牲"
当 Node 内存不够的时候,kubelet 必须有选择地杀掉 Pod。不能随机杀——数据库应该比批处理任务活得久。K8S 用 QoS 来做这个区分:
| QoS 等级 | 条件 | 被杀优先级 |
|---|---|---|
| Guaranteed | request == limit(且设了 CPU 和内存) | 最低——除非 Node 彻底 OOM |
| Burstable | request 有机会 < limit | 中等 |
| BestEffort | 没设 request 和 limit | 最高——内存压力一来第一个杀 |
// k8s.io/kubernetes/pkg/apis/core/v1/helper/qos/qos.go
func GetPodQOS(pod *v1.Pod) v1.PodQOSClass {
// Guaranteed: 所有容器的 request==limit(CPU 和 memory 都设了)
// Burstable: 至少一个容器设了 request(但不满足 Guaranteed)
// BestEffort: 一个容器都没设 request
}
QoS 不是 YAML 里的一个标签——你设 resources 的方式就决定了 QoS 等级。这个隐含的机制导致了大量"为什么 Pod 被杀了"的问题,根源往往在这。
CPU 超卖 vs 内存超卖
超卖在 CPU 和内存上有不同的风险。
CPU 超卖相对安全。CPU 是可压缩资源——你用不了的时候别人能用。Node 的 CFS(Completely Fair Scheduler)会根据 cgroup 的 cpu.shares 分配时间片。你设了 limit=500m 但只用 100m——多出来的 400m 会被其他 Burstable Pod 分走。唯一的代价是限流(throttle)——你突然需要 500m 但 CFS 说"你配额用完了,等下一个周期"。
cgroup v1 下的 CPU 限制在 cpu.cfs_quota_us 和 cpu.cfs_period_us:
# container runtime(比如 containerd)根据 Pod 的 limits.cpu 来写这些文件
echo 50000 > /sys/fs/cgroup/cpu/kubepods/burstable/pod-xxx/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/kubepods/burstable/pod-xxx/cpu.cfs_period_us
# 每 100ms 周期内最多用 50ms = 0.5 核
内存超卖很危险。内存不可压缩——分配了就是分配了,OOM Killer 会直接杀进程。你的 Pod limit=2Gi,Node 只有 8G,10 个这样的 Pod——只要同时用超过 8G,最早触发的就是 OOM Killer 按 QoS 逆序杀人。
所以在生产上,很多人只超卖 CPU,不超卖内存。内存 request=limit,保证不会同时争抢。
源码视角:kubelet 驱逐逻辑
kubelet 不光管起 Pod,还管杀 Pod。内存压力来了,它在 eviction_manager.go 里做决策:
// k8s.io/kubernetes/pkg/kubelet/eviction/eviction_manager.go 简化
func (m *managerImpl) evictPods(evictionReclaimResources map[v1.ResourceName]nodeReclaimResources) {
// 1. 按优先级排序:BestEffort 排最前(先杀)
// 2. 同 QoS 内,按用量超过 request 的比例排(超用更多的先杀)
// 3. 按 eviction signal 阈值逐步执行
// - memory.available < 100Mi → 开始驱逐 BestEffort
// - memory.available < 50Mi → 开始驱逐 Burstable
for _, threshold := range thresholds {
pods := getPodsByQOSClass(BestEffort)
for _, pod := range pods {
killPod(pod)
if !isUnderPressure() { break }
}
}
}
驱逐信号(eviction signal)是 kubelet 自己监测的——比如 memory.available 低于 evictionHard 阈值。
kubelet 的驱逐有两种阈值配置:
硬驱逐(evictionHard)——触及即杀,不回旋:
kubelet --eviction-hard=memory.available<200Mi,nodefs.available<10%,nodefs.inodesFree<5%
一旦 Node 可用内存掉到 200Mi 以下,kubelet 立刻开始按 QoS 逆序杀 Pod,没有任何缓冲。
软驱逐(evictionSoft + evictionSoftGracePeriod)——给你一个宽限期:
kubelet --eviction-soft=memory.available<500Mi \
--eviction-soft-grace-period=memory.available=2m
内存掉到 500Mi 以下持续 2 分钟之后才触发驱逐。这 2 分钟给了你宝贵的缓冲——HPA 可以扩 Pod 分散压力、你的应用有机会自行释放内存(GC)、运维可以手动介入。
软 + 硬结合是有用的配置:
kubelet --eviction-hard=memory.available<100Mi \ # 最后防线
--eviction-soft=memory.available<500Mi \ # 预警线
--eviction-soft-grace-period=memory.available=2m
- 500Mi 以下持续 2 分钟 → 开始软驱逐(给宽限期)
- 100Mi 以下 → 立刻硬驱逐(不能再等了)
所有驱逐信号:
| 信号 | 含义 | 典型阈值 |
|---|---|---|
memory.available | Node 可用内存 | 100Mi–500Mi |
nodefs.available | Node 主文件系统可用空间 | 10% |
nodefs.inodesFree | Node 主文件系统 inode | 5% |
imagefs.available | 镜像文件系统可用空间 | 15% |
pid.available | Node 可用 PID 数 | 100 |
不设驱逐阈值等于告诉 kubelet"内存满了也别杀 Pod"——Linux OOM Killer 会直接出手,而且它不按 QoS 排序——随机杀。驱逐是 K8S 在 OOM Killer 之前的一道可控防线。
实际怎么做超卖
生产环境的超卖不是"把 limit 写大一点"就完事。
第一步:真实用量画像——你的应用平时用多少、峰值多少、什么时候峰值。Prometheus + Grafana 盯一周,别拍脑袋。
第二步:设置合理的 request 和弹性 limit:
resources:
requests:
cpu: 500m # 正常用到的 CPU——保证调度时有这个量
memory: 512Mi # 建议 request == limit 不超卖内存
limits:
cpu: 2000m # 峰值允许冲到 2 核
memory: 512Mi
第三步:配 HPA 当安全网——CPU 开始接近 limit 就让 HPA 扩 Pod,别等着 throttle。上一章讲的 HPA 在这起作用了。
思考题:上面这些"设 request/limit"靠的是人脑估算——你打个
kubectl describe pod看看实际用量,拍一个数。但如果你的 Pod 跑了三个月,这三个月里的 CPU 用量峰值是 600m、平时是 200m、request 你写了 2 核——明显在浪费。有没有工具能分析历史用量,自动算出更合理的 resource 建议值?
K3S 的超卖
K3S 底层用的是同样的 kubelet 和 containerd,所以 QoS、cgroup 限制、驱逐逻辑跟 K8S 完全一致。超卖机制上没有区别——都是 Linux cgroup + kubelet eviction。
区别在于 K3S 常跑在资源紧张的边缘设备上(比如树莓派 4B 只有 8G 内存),超卖的风险更高。你在一台 8G 的机器上跑 API Server、Controller Manager、Scheduler 全嵌在 k3s server 里,再加上你的业务 Pod,留给超卖的余量很少。所以 K3S 场景下更建议保守策略——内存不超卖,CPU 适度超。
思考题:QoS 不只是"谁先被杀"的顺序——它还在底层影响了 OOM score 和
/proc/<pid>/oom_score_adj的值。Guaranteed 的 Pod 的 oom_score_adj 是多少?Burstable 呢?这个值是怎么跟request/limit公式算出来的?
小结
- request 是调度依据,limit 是运行时上限,超卖发生在差值上
- QoS 分 Guaranteed / Burstable / BestEffort 三级,内存不足时逐级驱逐
- CPU 超卖相对安全(可压缩),内存超卖风险高(OOM 直接杀)
- kubelet 在
eviction_manager里执行逐级驱逐 - K3S 超卖机制跟 K8S 一样,但边缘设备资源紧,策略应保守
解答
有没有工具能分析历史用量,给出更合理的 resource 建议?
有。不要手动盯 kubectl describe 去估——让工具跑一遍历史数据,给你一个带数据的建议值。
KRR(Kubernetes Resource Recommender)——Robusta 开源,用 Prometheus 的历史数据算最优的 request/limit:
# 跑一次分析
krr simple --namespace default
输出:
Namespace | Name | CPU Req (cur → rec) | CPU Lim (cur → rec) | Mem Req (cur → rec) | Mem Lim (cur → rec)
default | nginx | 500m → 100m | 2000m → 300m | 512Mi → 128Mi | 512Mi → 256Mi
KRR 只出建议,不会自动改你的 YAML——它跑一次,给你一张表,"这些 Pod 现在配置不合理,建议改成这些值"。然后你自己去改 Deployment。
VPA(Vertical Pod Autoscaler)——上一章讲的,但它有 Recommender 模式可以只出建议:
updatePolicy:
updateMode: "Off" # 不自动改,只给建议
VPA 的 Recommender 会持续监控 Pod 的用量,生成 recommendation 对象:
kubectl describe vpa nginx-vpa
# Recommendation:
# Container: nginx
# Lower Bound: 100m cpu, 64Mi mem
# Target: 200m cpu, 128Mi mem
# Upper Bound: 500m cpu, 256Mi mem
Target 就是 VPA 建议你用的 request 值。跟 KRR 的区别是:VPA 是持续监控的动态建议,KRR 是一次性扫描的历史分析。
Goldilocks——Fairwinds 开源,跟 KRR 方向类似。kubectl goldilocks 也是读历史用量给建议。
这些工具的底层逻辑都一样:
- 从 Prometheus/Metrics API 拉 Pod 过去 N 天的 CPU/内存用量
- 算 P95(95% 分位值 + 一定 buffer)→ 这是建议的 request
- 算 P99 或 max + buffer → 这是建议的 limit
- 输出对比表——当前配置 vs 建议配置——告诉你哪些 Pod 在浪费
2. QoS 在底层怎么影响 OOM score?
QoS 不只是 kubelet 驱逐时的顺序表——它是通过 Linux 内核的 oom_score_adj 机制实现的。
当你给 Pod 配置了 request 和 limit 之后,kubelet(通过 CRI 和 cgroup manager)会为 Pod 的进程设置 /proc/<pid>/oom_score_adj。值越大,OOM Killer 越优先杀:
// kubelet/pkg/kubelet/cm/qos_container_manager_linux.go 简化
func GetOOMScoreAdjust(pod *v1.Pod, container *v1.Container) int {
// Guaranteed: request == limit → OOM score 极低
if isGuaranteedPod(pod) {
return -997 // 几乎不会被 OOM Killer 选中
}
}
| QoS | oom_score_adj | OOM Killer 视图 |
|---|---|---|
| Guaranteed | -997 | 最低优先——几乎不会被 OOM kill |
| Burstable | min(max(1000 - (memory.request / machine_memory * 1000), 2), 999) | 中等——内存 request 越小分越高 |
| BestEffort | 1000 | 最高优先——OOM Killer 最先杀 |
公式的含义:Burstable Pod 的 request 相对于 Node 总内存越小,被 OOM kill 的概率越高。你给 Pod 设 request=100Mi 但 Node 有 64G——那就很接近 BestEffort 的优先级了。
这个映射是 Linux OOM Killer 的标准接口——oom_score_adj 范围 -1000 到 1000,值越大越先死。kubelet 在起 Pod 时,根据 QoS 写入对应的值。这就是 QoS 的三级保护从 YAML 落到内核进程的完整链路。
反过来也是一种超卖——不是"让更多 Pod 挤在同样的 Node 上",而是"识别浪费的资源,回收给别的 Pod 用"。你的 Node 还是那么多台,但如果每个 Pod 都多要了一倍的 CPU request,等于集群容量被虚高了 50%——Scheduler 以为没容量了,其实还能放。降低虚高的 request = 释放被锁死的空闲资源。
下一章,资源紧张到不想加物理机——虚拟节点。