凌晨两点,告警炸了:订单服务(核心)挂了,几个无关的离线批处理任务却还活着。你一脸懵——节点明明只是内存压力,为什么"最该活的核心服务先死"?
这不是玄学,是 QoS(Quality of Service)在背后决定谁生谁死。
一句话点透原理
Kubernetes 给每个 Pod 打一个服务质量等级:Guaranteed > Burstable > BestEffort。节点资源紧张时,等级越低越先被干掉。你没给核心服务设 request/limit ,它就落到了 BestEffort ,于是在内存风暴里第一个被驱逐——看似随机,实则必然。
下面用面试题把这套机制彻底串透。
一、QoS 三类判定规则(对照表)
判定只看 CPU 和内存的 requests / limits ,与镜像、端口、别的服务质量都无关。
| QoS 等级 |
判定规则 |
典型场景 |
驱逐优先级 |
| Guaranteed |
每个容器都设了 CPU 和内存的 request,且 request == limit |
数据库、核心网关 |
最后死 |
| Burstable |
至少一个容器设了 request 或 limit ,但不满足 Guaranteed |
普通业务服务 |
中间死 |
| BestEffort |
所有容器都没设 request 和 limit |
离线任务、临时脚本 |
最先死 |
关键坑位:只设 limit 不设 request 时,K8s 会自动把 request 补成与 limit 同值,因此它也算 Guaranteed。很多人以为"设了 limit 没设 request 是 Burstable",错了。
补充一点:判定是逐容器的,Pod 里只要有任意一个容器不满足 Guaranteed 全规则,整个 Pod 就降级。比如主容器 request==limit 很标准,但 sidecar 容器啥都没设,这个 Pod 就会掉到 BestEffort 。所以"保核心"必须把 Pod 内所有容器的 request/limit 都对齐,漏一个 sidecar 就前功尽弃。
二、三组可抄的 YAML 示例
1. Guaranteed(request == limit)
apiVersion: v1
kind: Pod
metadata:
name: qos-guaranteed
spec:
containers:
- name: app
image: nginx
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "200m"
memory: "256Mi"
2. Burstable(设了 request 但 limit 不同或只设其一)
apiVersion: v1
kind: Pod
metadata:
name: qos-burstable
spec:
containers:
- name: app
image: nginx
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
memory: "512Mi"
# 只设内存 limit,CPU 没设
3. BestEffort(什么都不设)
apiVersion: v1
kind: Pod
metadata:
name: qos-besteffort
spec:
containers:
- name: app
image: busybox
command: ["sh", "-c", "sleep 3600"]
验证 QoS 等级
kubectl get pod qos-guaranteed -o jsonpath='{.status.qosClass}'
# 输出:Guaranteed
kubectl get pod qos-burstable -o jsonpath='{.status.qosClass}'
# 输出:Burstable
kubectl get pod qos-besteffort -o jsonpath='{.status.qosClass}'
# 输出:BestEffort
批量看全节点 Pod 的 QoS:
kubectl get pods -A -o custom-columns=NS:.metadata.namespace,POD:.metadata.name,QOS:.status.qosClass,NODE:.spec.nodeName
三、驱逐顺序到底怎么排
节点触发驱逐阈值后,kubelet 按以下顺序选受害者:
- BestEffort 先死——它们没有任何资源保障。
- Burstable 其次——但内部还有排序:实际用量超出 request 越多的越先死。一个 request 内存 128Mi 却用了 500Mi 的,比只用了 150Mi 的先走。
- Guaranteed 最后——只有把所有 Burstable/BestEffort 都清完还不够,才会动它。
同 QoS 内部,统一按"超出 request 的量"从大到小排。注意:排序依据不是 limit ,而是实际用量相对 request 的超出值。
四、kubelet 驱逐机制:软驱逐 vs 硬驱逐
kubelet 在节点层面做驱逐,目标是把节点从压力中拉回来。两种触发方式:
硬驱逐(eviction-hard):条件一满足立刻驱逐,无宽限期。默认值:
--eviction-hard=memory.available<100Mi,nodefs.available<10%,imagefs.available<15%,nodefs.inodesFree<5%
软驱逐(eviction-soft):条件满足后,还要持续超过 --eviction-soft-grace-period 才动手,给业务一个缓冲。例如:
--eviction-soft=memory.available<500Mi
--eviction-soft-grace-period=memory.available=1m30s
--eviction-max-pod-grace-period=30
此外还有压力类型区分:
- nodefs 压力:容器日志、emptyDir 把节点根盘写满,触发驱逐。
- imagefs 压力:镜像和容器可写层占满磁盘,触发镜像回收+驱逐。
- pid 压力:进程数打满,kubelet 也会驱逐。
查看节点当前驱逐配置(kubelet 配置多在 /var/lib/kubelet/config.yaml):
cat /var/lib/kubelet/config.yaml | grep -i eviction
五、核心区分:kubelet 驱逐 vs 内核 OOM Killer
面试必问,两套机制完全不同,别混:
| 维度 |
kubelet 驱逐(Evicted) |
cgroup OOM Killer(OOMKilled) |
| 触发依据 |
节点层面可用资源 |
容器内存 limit |
| 触发者 |
kubelet 进程 |
内核内存管理子系统 |
| Pod 状态 |
Evicted |
OOMKilled |
| 后续行为 |
被删,可重新调度到别的节点 |
容器内进程被杀,Pod 原地重启 |
| 典型原因 |
节点内存/磁盘整体紧张 |
单个容器突破自己的 limit |
排查命令:
kubectl get pod -o jsonpath='{.status.lastState.terminated.reason}'
# 输出 OOMKilled 表示被内核杀;输出空且 status.phase=Failed 且 reason=Evicted 表示被 kubelet 赶走
kubectl describe pod <pod> | grep -A5 -i "last state\|reason"
oom_score_adj 映射(OOM 打分)
内核决定杀谁看 oom_score_adj ,分数越高越先被杀:
- Guaranteed:
-997(几乎不会被 OOM)
- BestEffort:
1000(最先被杀)
- Burstable:
1000 - (1000 * memoryRequest / memoryCapacity),约落在 2 ~ 999
查看某容器进程的 OOM 权重:
# 找到容器主进程 PID
PID=$(docker inspect -f '{{.State.Pid}}' <container_id>)
cat /proc/$PID/oom_score_adj
# 例如 Burstable 可能输出 899
oom_score = oom_score_adj + 进程自身内存占用得分,内核挑总分最高的杀。
六、PriorityClass 与抢占
PriorityClass 用来给 Pod 排调度和抢占的优先级:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
preemptionPolicy: PreemptLowerPriority # 或 Never
globalDefault: false
description: "核心服务专用"
value 越大越优先调度、越容易抢占低优先级 Pod。
preemptionPolicy: PreemptLowerPriority:调度时把低优先级 Pod 挤走。
globalDefault: true:没声明 priorityClassName 的 Pod 自动套用。
面试高频陷阱:PriorityClass 只影响调度和抢占,完全不影响 kubelet 驱逐顺序!一个 PriorityClass 拉满的 BestEffort Pod,节点内存吃紧时照样第一个被驱逐。想保命,QoS 才是正解。
七、生产保核心服务的组合拳
单靠 QoS 还不够,生产上这样组合:
- QoS 打底:核心服务一律
request == limit ,锁 Guaranteed 。
- PriorityClass 兜底:配高 value +
preemptionPolicy: PreemptLowerPriority ,调度阶段就占住好位置。
- PDB 防雪崩:保证驱逐/升级时至少有多少个副本活着。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: core-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: order-service
- 独占节点池:核心服务打污点容忍 + nodeSelector ,和 BestEffort 离线任务物理隔离。
- 系统预留:kubelet 配
kube-reserved、system-reserved,并让 eviction-hard 阈值高于预留值,给系统进程留缓冲,避免把节点自己拖死。
# /var/lib/kubelet/config.yaml 片段
kubeReserved:
cpu: "500m"
memory: "1Gi"
systemReserved:
cpu: "500m"
memory: "1Gi"
evictionHard:
memory.available: "1.5Gi"
nodefs.available: "10%"
八、踩坑实录
坑 1:某次线上核心服务用了 HPA ,副本数靠 request 算,但当时没设 limit 。结果 QoS 是 Burstable ,节点抖动时被驱逐,HPA 疯狂扩容反而加剧压力。修复:补上 limit == request 升 Guaranteed。
坑 2:以为 PriorityClass 高就高枕无忧,结果 OOM 时被内核杀。原因:Pod 是 Burstable ,oom_score_adj 高达 900,节点内存告急时第一个进内核黑名单。教训:priorityClassName 救不了 OOM。
坑 3:eviction-hard 阈值设成了 memory.available<50Mi,但 kube-reserved 只留了 200Mi ,节点频繁把 kubelet 自己逼到不稳。正确做法:硬阈值必须明显大于系统预留。
九、面试追问清单(Q&A)
- Q:request 和 limit 都不设会怎样?
A:Pod 落入 BestEffort ,节点任何资源压力下都最先被驱逐,且 OOM 时 oom_score_adj=1000 最先死。
- Q:只设 limit 不设 request ,属于哪类?
A:Guaranteed 。K8s 自动把 request 补成等于 limit。
- Q:只设 request 不设 limit 呢?
A:Burstable 。它不满足"所有容器 request==limit",但至少有一个容器设了 request。
- Q:QoS 会影响调度吗?
A:不会。调度只认资源 request 和节点可分配量;QoS 只决定"出事时谁先死"。
- Q:kubelet 驱逐和 OOM Killer 区别?
A:前者看节点整体可用量,Pod 标 Evicted 可重调度;后者看容器 limit ,进程被杀,Pod 标 OOMKilled 原地重启。
- Q:PriorityClass 很高的 Pod 会被 OOM 杀吗?
A:会。PriorityClass 不参与 kubelet 驱逐和 OOM 打分,只有 Guaranteed 才能把 oom_score_adj 压到 -997。
- Q:同是 Burstable ,谁先被驱逐?
A:实际内存用量超出 request 越多的越先走。
- Q:Guaranteed 会被驱逐吗?
A:极端情况下会。当所有 BestEffort/Burstable 清完仍不达标,kubelet 才会动 Guaranteed ;OOM 时同理,它只是最后才轮到。
- Q:怎么验证 Pod 是哪种死法?
A:kubectl get pod -o jsonpath='{.status.lastState.terminated.reason}' 看 OOMKilled;kubectl describe 看 Evicted 原因。
- Q:软驱逐的好处是什么?
A:给短暂尖峰留 grace-period 缓冲,避免瞬时抖动就误杀业务。
十、生产排查 Checklist(照着走)
节点告警后,按这个编号顺序定位,十分钟厘清是哪种死法:
- 看 Pod 状态:
kubectl get pod -A -o wide | grep -i "evict\|oom" 先分出 Evicted 还是 OOMKilled 。
- 看死因字段:
kubectl get pod <pod> -o jsonpath='{.status.lastState.terminated.reason}' ,输出 OOMKilled 是内核杀,空且 phase=Failed 多半是 kubelet 驱逐。
- 看 QoS:
kubectl get pod <pod> -o jsonpath='{.status.qosClass}' ,确认是不是 BestEffort 在裸奔。
- 看节点资源:
kubectl top node 与 kubectl describe node <node> | grep -i "allocatable\|pressure" ,确认是否 MemoryPressure / DiskPressure。
- 看驱逐事件:
kubectl describe node <node> 底部 Events 会写明 Evicting pod 及原因阈值。
- 进节点看 OOM 权重:
cat /proc/$(docker inspect -f '{{.State.Pid}}' <cid>)/oom_score_adj ,对照三类映射核实。
- 查 kubelet 配置:
cat /var/lib/kubelet/config.yaml | grep -i eviction 确认硬/软阈值是否过紧。
- 定方案:若为核心服务,补
request==limit 升 Guaranteed ,加 PriorityClass、PDB、独占池。
十一、真实案例拆解
案例一:大促当晚核心网关集体失联
现象:大促流量上来,某节点内存到 92%,订单网关(当时 Burstable ,没设 limit )全部 Evicted ,而旁边的日志采集 BestEffort 反而还在。根因:网关只设了 request 没设 limit ,一旦超用就成了"超 request 最多"的那批,被 kubelet 优先选中。修复:把 gw 的 limit 设成与 request 相等,升级为 Guaranteed ,并配 high-priority PriorityClass 与 PDB(minAvailable=2)。此后同类压力只杀离线任务。
案例二:Pod 反复重启但节点没事
现象:Pod 状态 CrashLoopBackOff ,describe 显示 OOMKilled,但节点内存还很充足。这是典型 cgroup OOM——容器自身 limit 太小,单进程突破 limit 就被内核干掉,与节点整体无关。处理:调大该容器 memory.limit ,同时用上面 Checklist 第 6 步确认 oom_score_adj 与 QoS 匹配。
案例三:PriorityClass 拉满仍被驱逐
这是面试最常挖的坑。一个 Pod 配了 value=1000000 的 PriorityClass ,却没设 request/limit ,QoS 是 BestEffort 。节点抖动时被第一个驱逐,运维以为"高优先级怎么会先死"。真相:PriorityClass 只管调度抢占,完全不参与 kubelet 驱逐排序,BestEffort 在任何节点压力下都排第一。结论再次印证——保命靠 QoS ,不靠 PriorityClass。
十二、常见 FAQ 速查
- Q:emptyDir 算 nodefs 压力吗? 算。emptyDir 默认用节点根盘,写爆触发 nodefs 驱逐。
- Q:镜像过多会触发哪种? imagefs 压力,kubelet 先回收无用镜像,不够才驱逐。
- Q:Guaranteed 一定不被 OOM 吗? 几乎不会,但节点整体 OOM 且别无选择时理论上仍可能,只是
oom_score_adj=-997 把概率压到极低。
- Q:OOM 后 Pod 会去别的节点吗? 不会,OOMKilled 是原地重启;只有 Evicted 才被删并重调度。
- Q:怎么让核心服务永不进驱逐名单? 没有绝对方案,但 Guaranteed + 独占池 + 系统预留 + 高 PriorityClass + PDB 已是工程上最稳的组合。
十三、节点可分配量与驱逐阈值的换算
理解"为什么阈值要高于系统预留",得会算节点可分配资源。公式:
allocatable = capacity - kube-reserved - system-reserved - eviction-hard 阈值
举例:节点 32Gi 内存,kube-reserved=1Gi ,system-reserved=1Gi ,eviction-hard=memory.available<1.5Gi 。则用户 Pod 最多能用约 32 - 1 - 1 - 1.5 = 28.5Gi 。当实际可用跌破 1.5Gi ,kubelet 立即驱逐。注意:驱逐看的是 available(capacity 减去已用),不是 allocatable ,所以预留和阈值都必须留出余量,否则 kubelet 自身和 ssh 等系统进程会在内存告急时一起被内核盯上。
一个实用技巧:把 eviction-hard 设成 kube-reserved + system-reserved 之和再往上加一点,例如预留共 2Gi ,则硬阈值设 2.5Gi ,保证系统层永远有缓冲。
十四、一次完整实战演练
假设你是值班,收到 MemoryPressure 告警,按下面流程跑一遍:
kubectl get nodes 看到 node-a 状态 MemoryPressure 。
kubectl describe node node-a 底部 Events 出现 Evicting pod qos-besteffort/clean-job because node is under memory pressure ,确认是 kubelet 驱逐。
kubectl get pod -n default -o custom-columns=POD:.metadata.name,QOS:.status.qosClass,NODE:.spec.nodeName | grep node-a ,发现被赶走的正是两个 BestEffort 和一个超用严重的 Burstable 。
- 核心订单 Pod 是 Guaranteed ,侥幸存活,但
oom_score_adj 排查显示它是 -997 ,确认内核层也被保护。
- 你意识到根因是离线
clean-job 没设任何 limit ,平时占用忽高忽低。于是给离线任务补 requests 并单独调度到 batch 节点池,与核心服务彻底隔离。
- 同时把核心服务
eviction-hard 之外的系统预留上调,避免下次抖动连 kubelet 都受影响。
跑完这套,你不仅平息了告警,还把"随机驱逐"变成了可预期的行为。
十五、六大误区对照表
| 误区 |
真相 |
| 设了 limit 没设 request 是 Burstable |
错,K8s 自动补 request=limit ,属 Guaranteed |
| PriorityClass 高就不会被驱逐 |
错,它只管调度抢占,不参与驱逐与 OOM 排序 |
| QoS 影响调度 |
错,调度只看 request 与 allocatable |
| OOMKilled 之后 Pod 会去别的节点 |
错,OOM 是原地重启;只有 Evicted 才重调度 |
| 节点内存够就不会有问题 |
错,容器自身突破 limit 照样被内核 OOM |
| 驱逐和 OOM 是一回事 |
错,触发依据、触发者、后续行为全不同 |
把这张表背下来,面试官基本挖不出坑。
十六、监控与告警建议
光懂原理不够,得让"驱逐"可被观测:
- 在 Prometheus 里监控
kube_node_status_condition{condition="MemoryPressure"} 与 DiskPressure ,一置位就告警。
- 用
kube_pod_status_reason{reason="Evicted"} 统计被驱逐 Pod 数量,突增即异常。
- 容器 OOM 用
kube_pod_container_status_restarts_total 突增结合 reason=OOMKilled 定位。
- 给核心服务加 SLO 告警,QoS 出问题时业务指标会先抖,比节点阈值更早预警。
把这些接进 Grafana 面板,节点一有压力你比 kubelet 还先知道。
案例四:磁盘满比内存更隐蔽
某次节点 DiskPressure 告警,但内存很闲。排查发现是某应用疯狂写日志到 emptyDir ,把 nodefs 撑到 95%,kubelet 按 nodefs.available<10% 硬阈值驱逐了该节点上所有 BestEffort 。教训:磁盘压力同样走 QoS 排序,且日志型 Pod 极易中招。应对:给日志类容器设 ephemeral-storage 的 request/limit ,并配日志轮转,别让 emptyDir 无上限膨胀。
Q:ephemeral-storage 也参与 QoS 吗?
A:不参与 QoS 判定,QoS 只看 CPU 和内存。但它有独立的驱逐阈值,磁盘超用照样被赶。
Q:同节点上 Guaranteed 之间还会互相杀吗?
A:通常不会。只有整体 OOM 且别无选择才可能,而 Guaranteed 的 oom_score_adj=-997 已让其几乎免疫。
十七、总结
节点压力下的"随机驱逐"其实有铁律:QoS 决定生死顺序——BestEffort 先死,Burstable 按超 request 量排,Guaranteed 最后。再记住两条生命线:kubelet 驱逐(Evicted,看节点)和内核 OOM(OOMKilled,看 limit )是两码事;PriorityClass 管调度抢占、管不了驱逐和 OOM 。生产保核心,记住组合拳:Guaranteed 打底 + 高 PriorityClass + PDB + 独占池 + 系统预留。把这套讲清楚,面试基本稳了。最后送一句口诀:QoS 定生死,PriorityClass 管排队,PDB 防雪崩,预留保系统——四个维度各管一段,组合起来才叫生产级稳定性。下次再遇到"核心服务先死",你就能一眼定位是 QoS 没配好,而不是甩锅给玄学。