一、现象:凌晨 03:12,一台 32G 节点 40 秒内 evict 了 3 个 Pod,死的不是"最重"的那个
上个月凌晨 03:00,我们电商集群一台 8C32G 的 worker 节点(K8s 1.28.9,containerd 1.7.7,cgroup v2)触发了内存驱逐。节点上跑着 4 个 Pod:
| Pod | 类型 | requests | limits | 实际 used | |---|---|---|---|---| | A-订单服务(Java 17) | Burstable | 4G | 16G | 14G | | B-库存服务(Java 17) | Burstable | 12G | 16G | 13G | | C-搜索服务(Go) | Burstable | 2G | 8G | 3G | | D-日志采集(Fluent Bit) | BestEffort | — | — | 500Mi |
一个跑在 node-2 上的离线批处理任务(不属于这 4 个 Pod,是另一个 namespace 的 Job)峰值吃了 6G,把节点可用内存从 12G 压到 800Mi,命中 --eviction-hard=memory.available<1Gi。
kubelet 在 40 秒内按以下顺序 evict 了 3 个 Pod:
03:12:01 Evicted D-日志采集 (BestEffort) ← 第一个死,只用了 500Mi
03:12:14 Evicted A-订单服务 (Burstable, Δ=10G) ← 第二个死,requests 最小
03:12:28 Evicted B-库存服务 (Burstable, Δ=1G) ← 第三个死
C-搜索服务活了下来。
反直觉点:
- D(BestEffort,只用了 500Mi)最先死——"最没用"的反而最先被 evict;
- A(requests=4G,看起来"很轻")排第二——因为它的
used − requests = 10G,是"超量使用"最多的; - B(requests=12G,看起来"很重")排第三——因为它的
used − requests = 1G,用得很"规矩"; - 真正该做的是把 A 的 requests 从 4G 提到 14G(接近 used),让它不再排 evict 队首。
二、根因:kubelet 的 evict 排序算法不是 LRU,是 QoS class + delta 双维度
很多人以为 kubelet 驱逐 Pod 是"按内存用量从大到小",错了。实际算法(kubelet 源码 pkg/kubelet/eviction/generic_resource_manager.go)是:
第一优先级:QoS class 排序
- BestEffort(无 requests、无 limits)→ 最先被 evict
- Burstable(有 requests 或 limits,但不相等)→ 其次
- Guaranteed(requests == limits,且 CPU/内存都设了)→ 最后
第二优先级:同 QoS class 内,按 used − requests 降序(delta 大的先)
这个 delta 就是"超量使用量"。kubelet 的逻辑是:你 requests 写 4G 但用了 14G,说明你"超额"了 10G,在内存紧张时你"欠"节点 10G,所以先还(先 evict)。
为什么 requests 不是"预留"而是"调度记账"?
requests 在 K8s 里有两个完全不同的作用:
- 调度器:决定 Pod 能不能被调度到某个节点(节点 Allocatable 要 ≥ requests 之和);
- kubelet evict:决定内存紧张时谁先死(delta 大的先)。
requests 写小,不会让 Pod 拿到更多内存(limits 才控制上限),只会让它在 evict 队列里排更前。这是最反直觉的一点。
三、具体做法:把 requests 写到"真实 used"附近,让 delta 趋近 0
1. 先看节点当前的请求/用量分布
# 看节点 Allocatable 和已分配
kubectl describe node node-2 | grep -A 10 "Allocated resources"
# 输出示例:
# Allocated resources:
# (Total limits may be over 100 percent, i.e., overcommitted.)
# Resource Requests Limits
# -------- -------- ------
# cpu 18 (225%) 40 (500%)
# memory 18Gi (56%) 40Gi (125%)
# ephemeral-storage 0 (0%) 0 (0%)
# 看节点实际内存用量
kubectl top node node-2
# NAME CPU(cores) MEMORY(bytes)
# node-2 6200m 28Gi
# 看各 Pod 的 used
kubectl top pod -n prod --sort-by=memory
2. 调整 Pod 的 requests 到"真实 used"附近
# A-订单服务:把 requests 从 4G 提到 14G(接近 used 14G)
# 这样 delta = 14G - 14G = 0,evict 顺序里排到队尾
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
spec:
containers:
- name: order
resources:
requests:
memory: 14Gi # 从 4Gi 提到
limits:
memory: 16Gi
3. 验证调整效果
# 等待一段时间后再次查看 Pod 的内存使用情况
kubectl top pod -n prod --sort-by=memory
# 确认 A-订单服务的 requests 已调整,且 delta 接近 0
四、踩过的坑或反直觉之处
- requests 写小不会让 Pod 拿到更多内存:requests 是调度器的记账,不是资源预留。要让 Pod 有更多内存,需要调整 limits。
- Burstable Pod 在内存紧张时优先被驱逐:因为它们的 QoS class 比 Guaranteed 和 BestEffort 低,即使它们的内存使用量不高。
- delta 大的 Pod 优先被驱逐:即使它们的内存使用量不高,但它们的 delta 大,所以会被优先驱逐。
五、结论与可复用清单
- 理解 kubelet 的 evict 排序算法:不是按内存用量从大到小,而是按 QoS class 和 delta 双维度。
- 调整 requests 到真实 used 附近:让 delta 接近 0,避免被优先驱逐。
- 监控节点和 Pod 的内存使用情况:定期查看节点和 Pod 的内存使用情况,及时调整 requests 和 limits。
- 合理设置 eviction 阈值:根据节点的内存情况,合理设置 eviction 阈值,避免频繁驱逐。
可复用清单:
- 使用
kubectl describe node查看节点的 Allocatable 和已分配资源。 - 使用
kubectl top node查看节点的实际内存使用情况。 - 使用
kubectl top pod查看各 Pod 的内存使用情况。 - 调整 Pod 的 requests 到真实 used 附近。
- 监控节点和 Pod 的内存使用情况,及时调整 requests 和 limits。
- 合理设置 eviction 阈值,避免频繁驱逐。
注意事项:
- requests 是调度器的记账,不是资源预留。
- Burstable Pod 在内存紧张时优先被驱逐。
- delta 大的 Pod 优先被驱逐。
- 调整 requests 和 limits 需要谨慎,避免影响其他 Pod 的运行。
通过以上步骤,可以有效避免 Pod 被频繁驱逐,提高集群的稳定性和可靠性。