Linux个人心得29(深入理解K8S Pod优先级与驱逐机制:从原理到实战踩坑)
2026/5/14 14:00:12 网站建设 项目流程

"集群跑得好好的,突然一堆Pod被驱逐,上层服务雪崩——这种场景,做过生产运维的基本上都见过或听说过。今天聊聊K8S的优先级和驱逐机制,理解了这些,你才能在故障来临知道该调什么参数、该查哪个日志。"

开篇:一次真实的驱逐风暴

先说个我经历过的案例。

那天下午,监控系统突然狂响,一堆Pod状态变成Evicted。业务方反馈Web服务响应超时,排查后发现是跑在这台虚拟机上的几个核心Pod被驱逐了。

诡异的是,当时节点的内存使用率明明只有60%左右,远没到传说中的"内存不足"。后来定位到原因——运维当天做了虚拟机在线扩容,内存从16G扩到了32G,但kubelet进程没有重启,它依然按照旧的内存阈值在判断是否需要驱逐。

这个坑让我意识到,很多人以为理解了K8S的驱逐机制,其实只是知道有这个东西。真正理解它的运作原理,才能避免踩坑,也才能在故障时快速定位。

今天这篇文章,把Pod优先级和驱逐机制彻底讲透。

一、先说QoS:这是优先级和驱逐的底层基础

很多人学K8S优先级和驱逐,直接从PriorityClass开始看,这是个错误的学习路径。QoS才是底层基础,PriorityClass和驱逐策略都依赖它。

三种QoS级别到底怎么判定

K8S根据Pod的资源配置自动给Pod划分QoS等级,一共三级:

QoS级别判定条件优先级
GuaranteedPod中所有容器的requestslimits必须完全相等最高
Burstable不满足Guaranteed,但至少有一个容器设置了requestslimits中等
BestEffort没有任何容器设置requestslimits最低

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
Burstable2~999中等优先级,具体值取决于资源使用情况
BestEffort1000最容易被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的实现比直觉要复杂:

正常调度流程

  1. Pod进入activeQ等待调度

  2. Scheduler尝试为Pod选择节点

  3. 如果所有节点都资源不足,调度失败

  4. Pod进入unschedulableQ,同时触发抢占流程

抢占流程

  1. Scheduler从unschedulableQ取出Pod

  2. 在每个候选节点上模拟抢占:假设低优先级Pod被移除,重新计算是否满足调度条件

  3. 选择最优的"牺牲者"(通常是多个低优先级Pod)

  4. 选中节点后,向API Server发送删除牺牲者Pod的请求

  5. 牺牲者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为什么还要自己搞一套驱逐机制?

原因有几个:

  1. OOM Killer触发时系统已经很不健康了:内存彻底耗尽可能导致系统进入swap困境,磁盘IO暴增,连日志都写不进去

  2. OOM Killer的选择不一定符合业务需求:它只看内存使用,不看服务重要性

  3. 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杀掉,而是有一套优先级算法:

核心考量因素

  1. Priority值:PriorityClass的值越高,越不容易被驱逐(设置了PriorityClass的情况下)

  2. QoS类别:BestEffort > Burstable > Guaranteed(BestEffort最优先被驱逐)

  3. 资源使用接近limits的程度:使用量越接近limits,被驱逐优先级越高

  4. 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不感知

这正是文章开头提到的那个案例。问题根因是:

  1. 虚拟机内存在线扩容,宿主机内核已经识别到新内存

  2. kubelet进程持有的是启动时的内存信息

  3. kubelet的驱逐阈值基于旧内存计算,扩容后阈值反而"偏低了"

  4. 结果:内存使用率明明不高,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: "批处理任务,可被抢占"

监控告警方案

光配置好还不够,还需要监控:

必须监控的指标

  1. 节点驱逐事件数量(rate(kubelet_evictions[5m]))

  2. 节点资源使用率趋势

  3. Pod重启次数

  4. 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

总结

写了这么多,总结几条核心要点:

  1. QoS是基础:Guaranteed/Burstable/BestEffort不只是分类,它们直接影响Pod的生存优先级和内核OOM策略。核心服务必须Guaranteed。

  2. PriorityClass解决的是调度优先级:非抢占适合有状态服务,抢占适合无状态批量任务。抢占流程有30秒窗口的不确定性,设计架构时要考虑这点。

  3. 驱逐是主动保护机制:在资源彻底耗尽之前介入,保护系统的整体稳定性。硬驱逐必须配置,软驱逐可选。

  4. 配置要成体系:evictionHard + systemReserved + kubeReserved + PriorityClass + QoS,这些要一起考虑,不是单独配置某个就行。

  5. 监控是最后一道防线:配置再好,也需要监控来验证效果和发现异常。

最后说一句:很多故障都是"配置了但没理解"导致的。看完这篇文章,希望你对K8S优先级和驱逐机制的理解不止是"知道有这个功能",而是能真正用好它,在生产环境中从容应对各种资源压力场景。


如果你也踩过类似的坑,欢迎在评论区分享。技术这条路,踩坑不可怕,可怕的是踩完还没长记性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询