"集群跑得好好的,突然一堆Pod被驱逐,上层服务雪崩------这种场景,做过生产运维的基本上都见过或听说过。今天聊聊K8S的优先级和驱逐机制,理解了这些,你才能在故障来临知道该调什么参数、该查哪个日志。"
开篇:一次真实的驱逐风暴
先说个我经历过的案例。
那天下午,监控系统突然狂响,一堆Pod状态变成Evicted。业务方反馈Web服务响应超时,排查后发现是跑在这台虚拟机上的几个核心Pod被驱逐了。
诡异的是,当时节点的内存使用率明明只有60%左右,远没到传说中的"内存不足"。后来定位到原因------运维当天做了虚拟机在线扩容,内存从16G扩到了32G,但kubelet进程没有重启,它依然按照旧的内存阈值在判断是否需要驱逐。
这个坑让我意识到,很多人以为理解了K8S的驱逐机制,其实只是知道有这个东西。真正理解它的运作原理,才能避免踩坑,也才能在故障时快速定位。
今天这篇文章,把Pod优先级和驱逐机制彻底讲透。
一、先说QoS:这是优先级和驱逐的底层基础
很多人学K8S优先级和驱逐,直接从PriorityClass开始看,这是个错误的学习路径。QoS才是底层基础,PriorityClass和驱逐策略都依赖它。
三种QoS级别到底怎么判定
K8S根据Pod的资源配置自动给Pod划分QoS等级,一共三级:
| QoS级别 | 判定条件 | 优先级 |
|---|---|---|
| Guaranteed | Pod中所有容器的requests和limits必须完全相等 |
最高 |
| Burstable | 不满足Guaranteed,但至少有一个容器设置了requests或limits |
中等 |
| BestEffort | 没有任何容器设置requests或limits |
最低 |
Guaranteed的判定有个细节:所有容器的requests和limits都要完全相等,包括CPU和内存。如果Pod里有一个容器的requests和limits没设,或者设了但不相等,这个Pod就是Burstable而不是Guaranteed。
# Guaranteed示例:所有容器、所有资源类型都严格相等
apiVersion: v1
kind: Pod
spec:
containers:
- name: core-service
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "1Gi" # 与requests相等
cpu: "500m" # 与requests相等
- name: sidecar
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "256Mi" # 与requests相等
cpu: "100m" # 与requests相等
# Burstable示例:至少有一个容器的requests/limits没设或不相等
apiVersion: v1
kind: Pod
spec:
containers:
- name: web-app
resources:
requests:
memory: "512Mi"
cpu: "200m"
# 没有设置limits,降级为Burstable
# BestEffort示例:什么资源都不设
apiVersion: v1
kind: Pod
spec:
containers:
- name: batch-job
# 没有任何resources配置
OOM_ADJ:内核层面的优先级体现
QoS不只是K8S层面的概念,它直接映射到Linux内核的OOM Killer机制。每个容器在宿主机上对应一个cgroup,cgroup的oom_score_adj参数就体现了QoS级别:
| QoS级别 | oom_score_adj值 | 含义 |
|---|---|---|
| Guaranteed | -998 | 最不容易被OOM Kill |
| Burstable | 2~999 | 中等优先级,具体值取决于资源使用情况 |
| BestEffort | 1000 | 最容易被OOM Kill |
这里有个容易混淆的点:OOM Kill和kubelet驱逐是两套独立的机制。
OOM Killer是Linux内核在物理内存耗尽时的最后防线,而kubelet的驱逐是用户在节点资源紧张时主动采取的保护措施。两者都会删Pod,但触发条件和时机不同。很多时候你会看到Pod被驱逐而不是被OOM Kill,就是因为kubelet在内存彻底耗尽之前就已经开始行动了。
生产建议:核心应用一定要设Guaranteed
说了这么多理论,来点实在的。
核心业务Pod务必配置Guaranteed QoS。这不是过度设计,而是保障。我见过太多团队为了省事,所有Pod都不设资源限制,结果在资源紧张时BestEffort Pod被大量驱逐,业务服务也难逃一劫。
对于确实需要"尽力而为"的服务------比如一些批处理任务、监控采集Agent等------BestEffort反而是合理的选择,让它们在资源紧张时主动让步。
二、PriorityClass:给Pod排座次
理解了QoS,再来看PriorityClass。这是K8S提供的显式优先级机制,和QoS是两条独立的线,但会共同影响驱逐决策。
两种抢占策略:非抢占 vs 抢占
PriorityClass有个关键字段preemptionPolicy,控制Pod在资源不足时的行为:
# 高优先级Pod - 非抢占策略
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority-non-preemptive
value: 1000
preemptionPolicy: Never # 关键!非抢占
globalDefault: false
description: "高优先级但不抢占其他Pod"
# 最高优先级Pod - 抢占策略
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical
value: 10000
preemptionPolicy: PreemptLowerPriority # 默认值,会抢占低优先级Pod
globalDefault: false
description: "关键业务Pod,优先调度"
**非抢占(Never)**的适用场景:
-
数据库主从集群中的从节点,宕机了不应该把主节点踢走
-
有状态服务,不同节点之间有依赖关系
-
某些业务逻辑要求Pod一旦调度就必须稳定运行
**抢占(PreemptLowerPriority)**的适用场景:
-
批量任务需要尽快调度
-
无状态服务,被驱逐后可以快速在其他节点重建
-
紧急扩容时需要抢资源
很多人以为设置了PriorityClass就一定能抢到资源,这其实是误解。非抢占Pod在资源不足时只能排队等待,它不会把已经运行中的低优先级Pod赶走。这是两种完全不同的策略,选错了会出大问题。
抢占的内部机制:比你想象的复杂
抢占有意思的地方在于,它不是简单地把低优先级Pod一脚踢开。K8S的实现比直觉要复杂:
正常调度流程:
-
Pod进入activeQ等待调度
-
Scheduler尝试为Pod选择节点
-
如果所有节点都资源不足,调度失败
-
Pod进入unschedulableQ,同时触发抢占流程
抢占流程:
-
Scheduler从unschedulableQ取出Pod
-
在每个候选节点上模拟抢占:假设低优先级Pod被移除,重新计算是否满足调度条件
-
选择最优的"牺牲者"(通常是多个低优先级Pod)
-
选中节点后,向API Server发送删除牺牲者Pod的请求
-
牺牲者Pod被标记为待删除,调度器继续调度高优先级Pod
这里有个细节:牺牲者Pod的删除是异步的。删除请求发出去后,Scheduler不会等它真正被终止,而是直接继续调度高优先级Pod。这意味着你可能会看到一种诡异的状态------Pod A是"牺牲者"正在被删除,同时Pod B(高优先级)已经在同一节点上开始调度。
优雅关闭窗口的不确定性是另一个容易踩坑的点。牺牲者Pod默认有30秒的优雅关闭期(terminationGracePeriodSeconds),在这30秒内,Pod可能还占用着资源。与此同时,高优先级Pod已经被调度到同一节点,如果节点资源紧张,可能导致高优先级Pod也调度失败。这个30秒的窗口会带来调度的不确定性。
节点优先级:除了Pod优先级还有这些
前面主要说的是Pod的优先级,但实际上节点的调度优先级也很重要。Scheduler的优选阶段会计算每个节点的得分,影响因素包括:
-
静态优先级 :通过节点注解
node.kubernetes.io/node-priority配置 -
亲和性优先级:基于Pod的节点亲和性/反亲和性规则
-
QoS优先级:节点上运行的Pod的QoS分布
-
插件优先级:各类调度插件的评分结果
这些优先级主要影响调度时的节点选择,和Pod的PriorityClass是两个维度,理解这一点很重要。
三、驱逐机制:当节点撑不住时发生了什么
终于说到驱逐了。这是生产环境中最常遇到的问题。
为什么不能只依赖OOM Killer
很多人觉得,既然Linux有OOM Killer,K8S为什么还要自己搞一套驱逐机制?
原因有几个:
-
OOM Killer触发时系统已经很不健康了:内存彻底耗尽可能导致系统进入swap困境,磁盘IO暴增,连日志都写不进去
-
OOM Killer的选择不一定符合业务需求:它只看内存使用,不看服务重要性
-
kubelet有全局视角:可以协调多个Pod的驱逐,而不仅仅是单个容器
所以kubelet的驱逐机制是一个主动的、资源预保护的机制,在问题还没到不可收拾之前就介入。
软驱逐 vs 硬驱逐
kubelet支持两种驱逐策略:
硬驱逐(Hard Eviction):
-
达到阈值立即执行,不留情面
-
一旦触发,Pod立即被标记为Evicted
-
没有任何宽限期
# kubelet配置示例
evictionHard:
memory.available: "500Mi"
nodefs.available: "5%"
imagefs.available: "15%"
软驱逐(Soft Eviction):
-
达到阈值后启动观察
-
不会立即驱逐,而是给Pod一个宽限期(eviction-soft-grace-period)
-
在宽限期内如果资源使用降回阈值以下,驱逐取消
-
适合希望"给系统一个喘息机会"的场景
# 软驱逐配置
evictionSoft:
memory.available: "1Gi"
nodefs.available: "10%"
evictionSoftGracePeriod:
memory.available: "1m30s"
nodefs.available: "1m30s"
实战建议 :生产环境必须配置硬驱逐,软驱逐作为补充。软驱逐的宽限期不宜设太长,1-2分钟足够了------设得太长反而可能让系统在临界状态挣扎更久。
驱逐选择Pod的逻辑
当触发驱逐时,kubelet不是随机选一个Pod杀掉,而是有一套优先级算法:
核心考量因素:
-
Priority值:PriorityClass的值越高,越不容易被驱逐(设置了PriorityClass的情况下)
-
QoS类别:BestEffort > Burstable > Guaranteed(BestEffort最优先被驱逐)
-
资源使用接近limits的程度:使用量越接近limits,被驱逐优先级越高
-
Pod运行时长:运行时间越长的Pod越稳定,优先级越高
淘汰顺序的综合表达式大致是:
优先级 = PriorityClass值 + QoS权重 + (资源使用量/limits的比例) - 运行时长因子
这个公式不是官方明文规定的,但反映了实际的驱逐倾向。我自己的理解是:kubelet希望优先驱逐那些"最应该为自己资源占用负责"的Pod------BestEffort Pod什么都没承诺,当然最该让步;Guaranteed Pod承诺了资源保障,不应该被轻易驱逐。
关键驱逐配置参数
# kubelet驱逐相关配置详解
evictionHard:
memory.available: "500Mi" # 硬驱逐阈值
nodefs.available: "5%" # 节点根文件系统可用空间
nodefs.inodesFree: "5%" # inode数量
imagefs.available: "15%" # 镜像存储文件系统可用空间
# 还有这些可选信号:
# memory.available, nodefs.available, nodefs.inodesFree
# imagefs.available, imagefs.inodesFree
# pid.available (可用进程ID数量)
evictionSoft:
memory.available: "1Gi" # 软驱逐阈值
evictionSoftGracePeriod:
memory.available: "1m30s" # 软驱逐宽限期
evictionMinimumReclaim:
memory.available: "200Mi" # 驱逐后保留的最小余量
nodefs.available: "2Gi" # 防止反复触发驱逐
evictionPressureTransitionPeriod: 5m # 退出压力状态的冷却时间
四、实战踩坑:那些年踩过的驱逐相关故障
坑1:虚拟机在线扩容后kubelet不感知
这正是文章开头提到的那个案例。问题根因是:
-
虚拟机内存在线扩容,宿主机内核已经识别到新内存
-
kubelet进程持有的是启动时的内存信息
-
kubelet的驱逐阈值基于旧内存计算,扩容后阈值反而"偏低了"
-
结果:内存使用率明明不高,kubelet却认为资源紧张,触发驱逐
排查方法:
# 查看节点内存容量(对比Pod内看到的内存)
kubectl describe node <node-name> | grep -A 5 "Allocated resources"
# 查看kubelet日志中的驱逐相关事件
journalctl -u kubelet | grep -i evict
# 查看Pod被驱逐的原因
kubectl describe pod <pod-name> | grep -A 20 "Events"
解决方案:
-
虚拟机扩容后务必重启kubelet
-
或者使用动态资源调整机制(Kubelet动态配置)
-
自动化脚本检测到节点规格变化时自动重启kubelet
坑2:只设置了requests没设置limits
这种情况会导致Pod被归类为Burstable,驱逐优先级比Guaranteed高。
但更严重的是,requests没设limits意味着Pod可以使用任意多内存,在共享节点的场景下可能影响其他Pod。
最佳实践:requests和limits成对设置,核心服务设置Guaranteed,非核心服务根据业务特点合理配置。
坑3:驱逐阈值设置过激进
有人为了"保护"节点,把驱逐阈值设得很高,比如内存可用低于50%就驱逐。这反而是过度保护:
-
节点资源利用率低,造成浪费
-
Pod频繁驱逐影响稳定性
-
应该让系统有一定的自愈空间
建议:驱逐阈值应该基于实际业务需求和历史监控数据调优,不是一味追求"安全"。
坑4:忽略了系统预留资源
kubelet本身、操作系统日志、容器运行时都需要内存。如果不预留,kubelet驱逐Pod腾出的空间被系统进程吃掉,形成死循环。
# 系统预留配置
systemReserved:
memory: "1Gi"
cpu: "500m"
kubeReserved:
memory: "1Gi"
cpu: "500m"
五、生产环境配置建议
kubelet驱逐参数推荐配置
以下是我在生产环境中验证过的一套配置,根据业务规模和节点规格调整:
# /var/lib/kubelet/config.yaml
evictionHard:
memory.available: "500Mi"
nodefs.available: "5%"
nodefs.inodesFree: "5%"
imagefs.available: "15%"
evictionSoft:
memory.available: "1Gi"
nodefs.available: "10%"
evictionSoftGracePeriod:
memory.available: "2m"
nodefs.available: "2m"
evictionMinimumReclaim:
memory.available: "200Mi"
nodefs.available: "1Gi"
# 建议同时配置系统预留
systemReserved:
memory: "2Gi"
cpu: "1"
kubeReserved:
memory: "2Gi"
cpu: "500m"
# 驱逐压力状态转换冷却期
evictionPressureTransitionPeriod: 5m
PriorityClass推荐配置
# 最高优先级 - 核心基础设施
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: system-critical
value: 2000000
preemptionPolicy: PreemptLowerPriority
description: "系统关键组件"
---
# 高优先级 - 核心业务服务
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: business-critical
value: 1000000
preemptionPolicy: PreemptLowerPriority
description: "核心业务服务"
---
# 中优先级 - 普通业务
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: normal
value: 0
preemptionPolicy: PreemptLowerPriority
description: "普通优先级(默认值)"
---
# 低优先级 - 批处理/可抢占任务
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch
value: -10000
preemptionPolicy: PreemptLowerPriority
description: "批处理任务,可被抢占"
监控告警方案
光配置好还不够,还需要监控:
必须监控的指标:
-
节点驱逐事件数量(rate(kubelet_evictions[5m]))
-
节点资源使用率趋势
-
Pod重启次数
-
OOM Kill次数
建议的告警规则:
-
单节点驱逐Pod数量 > 5 / 分钟
-
节点内存使用率 > 85% 持续 5 分钟
-
Pod被OOM Kill(条件性告警,如果业务Pod被OOM Kill需要立即响应)
# 查看驱逐事件的命令
kubectl get events --all-namespaces --field-selector reason=Eviction
# 统计近1小时的驱逐事件
kubectl get events --all-namespaces --field-selector reason=Eviction \
--sort-by='.lastTimestamp' | tail -50
总结
写了这么多,总结几条核心要点:
-
QoS是基础:Guaranteed/Burstable/BestEffort不只是分类,它们直接影响Pod的生存优先级和内核OOM策略。核心服务必须Guaranteed。
-
PriorityClass解决的是调度优先级:非抢占适合有状态服务,抢占适合无状态批量任务。抢占流程有30秒窗口的不确定性,设计架构时要考虑这点。
-
驱逐是主动保护机制:在资源彻底耗尽之前介入,保护系统的整体稳定性。硬驱逐必须配置,软驱逐可选。
-
配置要成体系:evictionHard + systemReserved + kubeReserved + PriorityClass + QoS,这些要一起考虑,不是单独配置某个就行。
-
监控是最后一道防线:配置再好,也需要监控来验证效果和发现异常。
最后说一句:很多故障都是"配置了但没理解"导致的。看完这篇文章,希望你对K8S优先级和驱逐机制的理解不止是"知道有这个功能",而是能真正用好它,在生产环境中从容应对各种资源压力场景。
如果你也踩过类似的坑,欢迎在评论区分享。技术这条路,踩坑不可怕,可怕的是踩完还没长记性。