news 2026/10/8 8:48:17

Kubernetes资源模型与kubelet驱逐机制:从调度到回收的闭环设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kubernetes资源模型与kubelet驱逐机制:从调度到回收的闭环设计

凌晨两点半,值班手机把我吵醒。监控面板上一台 32C64G 的 worker 节点 MemoryPressure 亮红,十六个 Pod 在三分钟内被驱逐,其中两个是我们核心的 Redis 从节点。当时第一个念头是"内存不够了要扩容",可查完之后发现,真正的原因根本不是容量不够,而是 kubelet 的 Eviction 机制在按照它自己那套"资源模型"做了一次过于激进的回收。

从那次之后,我才开始把"调度"和"驱逐"放在同一个闭环里看。K8S 的调度不是把 Pod 放到节点上就结束了,放上去之后,节点上的资源模型会通过 Eviction 做二次审计——超用、抖动、缓存堆积,都会被这种机制找回来。这篇文章打算把这条链路完整讲清楚:资源模型怎么定义、Eviction 怎么依据这个模型执行回收,以及我们该怎么设计阈值和 QoS,让回收动作变得可预测、可干预、不误伤。

1. 先从一次"明明离墙还有距离却被赶下车"说起

1.1 当时的现场还原

那台节点的内存总量是 64Gi,驱逐前监控显示可用内存还有不到 300Mi,但注意,当时节点上并没有出现 OOM Killer 的痕迹,系统负载也不高。问题出在 kubelet 的 Eviction Manager 判定memory.available低于了硬阈值。

这里有个新手容易误解的点:Eviction 并不是等内存彻底耗尽才动手。kubelet 有一条默认的硬阈值,比如memory.available<100Mi,也就是说,节点内存可用量一旦低于 100Mi,kubelet 就会开始逐 Pod。这个memory.available也不是简单的"剩余内存",它是 kubelet 通过 cgroup 统计计算出来的"可回收视角下的可用内存",Page Cache、闲置的 file-backed 内存都会被算成可回收资源。

我那次事故的现场实际上是有大量 Page Cache 堆积的。我们的一个训练任务疯狂读文件,把节点的缓存区撑得很大,free -h看起来内存几乎耗尽,但那些缓存其实是可以被回收的。kubelet 在计算memory.available时对这部分有自己的处理逻辑,某些版本、某些 cgroup 配置下,它并不会像内核那样优先把可回收缓存让出来,而是直接判定内存压力成立,触发驱逐。

1.2 "距离墙还有多远"谁说了算

Kubernetes 里涉及资源判断的口径有好几套,最核心的是这两套:

  • 调度器的口径:只看 Pod 声明的requests。每个 Pod 请求了多少资源,调度器就按这个做加法,判断节点是否"装得下"。
  • kubelet 的口径:看节点侧实际用量,参考limits。Pod 实际吃到多少内存、多少磁盘、多少 inode,kubelet 按这个做减法,判断节点是否"撑得住"。

这两套口径的差异,就是一切调度和驱逐问题的根源。调度器说"装得下",不代表运行时不爆;运行时没爆,也不代表requests声明合理。Eviction 就是第二套口径的执行者——它依据的"资源模型",本质上是节点层面的实时用量模型,而不是调度器手里的请求声明模型。

1.3 这个机制存在的意义

很多人恨 Eviction,觉得它老是"没事找事"。但从集群治理的角度看,Eviction 是资源模型的最后一道兜底:保证节点不会因为个别 Pod 失控而整体崩溃。没有 Eviction,一个内存泄漏的 Pod 会直接把节点打 OOM,进而拖累同节点所有业务。有了 Eviction,kubelet 能在崩溃前把部分 Pod 迁走,牺牲局部保全局。

理解了这一点,后面的所有设计决策都顺了——我们要做的不是"关掉 Eviction",而是让它在正确的时机、按正确的顺序、驱逐正确的对象。

2. 资源模型的原点:requests 与 limits 的分工

2.1 买票和实际占座是两回事

用一个生活化的类比:requests相当于你买票时申报的"我大概会占几个座位",调度器根据这个决定放不放你上车;limits相当于硬性规定的"最多占几个座",超过就会被按超载处理。

  • requests只用于调度,不限制运行时。
  • limits不参与调度拟合,但会限制 CGroup 资源上限(CPU 是 throttle,内存是 OOM 或驱逐)。

这就产生了一个很常见的坑:一个 Pod 声明requests: 100m,limits: 4Gi,调度器看到它只占 100m 的 CPU 额度,就把它塞进一个已经很满的节点。运行起来之后它实际吃到 3Gi 内存,别的 Pod 就遭殃了。等到 kubelet 触发 Eviction,你会发现最先被驱逐的往往是那些"实际用量远超 requests"的 Pod——因为它超用了自己申报的份额。

2.2 QoS 等级:Eviction 的优先队列

Pod 的requests和limits是否相等、是否设置,决定了 Pod 的 QoS 等级。Eviction 在选择驱逐对象时,QoS 等级是核心排序依据。

QoS 等级配置特征Eviction 中的地位典型风险
Guaranteedrequests == limits,且每个容器都设置最后动,只有超用或极端压力才会被驱逐数量多时挤占其他 Pod
Burstablerequests 存在,但至少有一个容器 requests < limits中间档,优先驱逐"实际用量超过 requests"的声明不实,容易被误判
BestEffort全部容器都不设置 requests/limits最先动,压力一来基本全没运维视角的"隐形炸弹"

我见过很多团队把核心数据库 Pod 配成 Guaranteed,却把日志采集 DaemonSet 留成 BestEffort。平时没事,一旦节点有压力,日志采集先死,然后监控断流,你连驱逐现场都看不到,直接变瞎子。这是最典型的"资源模型设计失败"。

2.3 Node Allocatable 里藏着被我们忽视的三块扣减

调度器和 kubelet 都基于节点的Allocatable做判断,但很多人不知道这个值是怎么算出来的。官方公式是:

Allocatable = Node Capacity - Kube-Reserved - System-Reserved - Hard-Eviction-Threshold

也就是说,节点总容量在分配给 Pod 之前,已经被预留了三份:kubelet 自身、系统进程(sshd、systemd 等)、以及 Eviction 硬阈值对应的那部分内存。

如果没配--system-reserved和--kube-reserved,这两个值默认是 0,那公式就退化成Capacity - Eviction-Threshold。这带来的问题是:节点上系统进程本身也要吃内存,而调度器看不到这部分占用,依然按满容量塞 Pod。等到系统进程把内存吃掉一截,kubelet 再用低于阈值的memory.available触发驱逐,Pod 就成了替罪羊。

我在生产环境里见过太多莫名其妙的驱逐,最后查出来就是没预留系统进程的内存配额。给 kubelet 配置里加上systemReserved和kubeReserved,很多"灵异驱逐"直接就消失了。

3. Kubelet Eviction 的参数棋盘:信号、阈值与宽限期

3.1 六大驱逐信号表

Eviction Manager 不只看内存,它监控的信号有六类。每个信号对应一个资源维度,任何一个跌破阈值都会进入 Pressure 状态。

信号含义常见阈值写法触发后果
memory.available节点可用内存(含可回收缓存口径)memory.available<500MiMemoryPressure
nodefs.availablekubelet 根目录所在文件系统可用空间nodefs.available<10%DiskPressure
nodefs.inodesFree根文件系统可用 inode 数nodefs.inodesFree<5%DiskPressure
imagefs.available容器镜像所在文件系统可用空间imagefs.available<15%DiskPressure
imagefs.inodesFree镜像文件系统可用 inode 数imagefs.inodesFree<5%DiskPressure
pid.available可用 PID 数量pid.available<10%PIDPressure

注意nodefs和imagefs的区分:如果 /var/lib/kubelet 和容器镜像目录在同一个文件系统上,kubelet 只会用nodefs的信号来判断,imagefs的配置会失效。很多同学配置了 imagefs 的阈值却发现不生效,大概率就是这个原因。

3.2 软阈值、硬阈值与宽限期的配合

evictionHard是硬性指标,一旦突破,kubelet 立即开始驱逐,没有商量余地。evictionSoft是软性指标,突破后不会马上驱逐,而是给 Pod 一个evictionSoftGracePeriod宽限期,在这个时间内如果能恢复到阈值以上,就一切照旧。

我推荐的做法是把两者配合使用:软阈值报警、硬阈值兜底。比如:

apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration evictionSoft: memory.available: "1Gi" evictionSoftGracePeriod: memory.available: "1m30s" evictionHard: memory.available: "300Mi" nodefs.available: "10%" nodefs.inodesFree: "5%" imagefs.available: "15%" pid.available: "5%" evictionPressureTransitionPeriod: "3m" evictionMinimumReclaim: memory.available: "200Mi"

宽限期是这个机制里最有价值的一个旋钮。它让 kubelet 在真正动手前,给 GC 线程、给运维介入留了时间窗口。一个 90 秒的软宽限期,往往比多 200Mi 的硬阈值更能保护业务。

3.3 最小回收量:防止"驱逐完又立刻触发"的抖动

evictionMinimumReclaim的作用是:当 kubelet 决定执行驱逐时,至少要回收这么多资源才停手。没有这个参数,kubelet 可能驱逐一个 Pod 只回收了 100Mi,内存刚刚回到阈值以上,过几秒又被顶破,于是再次驱逐,形成一个驱逐风暴。

我给生产环境配evictionMinimumReclaim.memory.available = 200Mi之后,驱逐次数明显下降。因为它一次性把水位拉得足够高,给了后续调度和业务恢复的时间。

3.4 镜像 GC 与容器 GC 的协同

很多人不知道,DiskPressure 其实有更温和的回收路径:镜像 GC 和容器 GC。kubelet 在触发 Pod 驱逐之前,会先尝试删除不再使用的容器镜像和已经死掉的容器。默认的image-gc-high-threshold是 85%,image-gc-low-threshold是 80%。

也就是说,磁盘使用率超过 85%,kubelet 就开始删镜像,删到 80% 为止。如果删完镜像仍然低于nodefs.available的驱逐阈值,才轮到驱逐 Pod。

这也是为什么我建议把nodefs.available的硬阈值设在 10% 而不是 5%——要给镜像 GC 留出操作空间。5% 的阈值意味着磁盘几乎满了才开始处理,GC 可能来不及腾挪就直接驱逐业务。

4. QoS 与驱逐次序:kubelet 是怎么"点名"的

4.1 一个相当反直觉的排序规则

kubelet 选驱逐对象时,不是简单按 QoS 低到高排队。实际规则有三层:

  1. 先看 Pod 的实际用量是否超过它的requests,超用的排在最前面;
  2. 再用 QoS 等级排序,BestEffort 排最前,Burstable 其次,Guaranteed 最后;
  3. 最后在同等级内看 PriorityClass,优先级的先保,低优先级的先牺牲。

这就解释了为什么 Guaranteed 的 Pod 也会被驱逐:Guaranteed 要求 requests == limits,理论上不可能"超用",但如果你把 CPU 的 requests 和 limits 配成相同值,内存没有设置 limits,或者反过来,有些容器没有同时满足所有资源维度的相等条件,QoS 就会降级,或者运行时因为 cgroup 配置问题实际用量超过限制。只要"实际用量超过 requests"这个条件成立,无论什么 QoS 都会被排到前面。

4.2 PriorityClass 能救命,但不是万能的

给核心业务配上高优先级的 PriorityClass,在 kubelet 驱逐排序里确实能往后排。但要清楚,PriorityClass 真正强势的场合是调度器的抢占(Preemption),而不是 kubelet 的节点压力驱逐。

调度抢占是这么工作的:一个高优先级 Pod 调度不上,调度器会把节点上低优先级的 Pod 赶走(标记为 victim),把位置腾出来给高优先级 Pod。这个流程里,受害者 Pod 的 PDB 一般也拦不住。

而 kubelet 的节点压力驱逐更"现实":内存都爆了,谁实际吃得多先赶谁。PriorityClass 只是同等级内部的排序键,如果节点上没有其他能替代的高耗内存 Pod,你的核心业务就算优先级调到最高,也照样可能被驱逐。真正能让它活下来的,是合理设置 requests/limits,保证它不超用。

4.3 PDB 在节点压力驱逐下只是"参考意见"

先说结论:PodDisruptionBudget 主要用于保护主动驱逐场景,比如我们主动kubectl drain节点、或者通过 Eviction API 驱赶 Pod。节点压力驱逐场景下,kubelet 在无法回收足够资源时,是可以驱逐被 PDB 保护的 Pod 的。

这个点很关键,因为它决定了一个设计原则:不要把 PDB 当作节点压力下的安全网。PDB 是给运维人员自己操作时上的"纪律约束",不是给 kubelet 设置的"免死金牌"。你在做集群维护排空节点时,PDB 帮你保证业务最少存活副本;但节点内存爆炸时,kubelet 认资源不认 PDB。

4.4 四种"驱逐"容易混,先分清

日常工作中"驱逐"这个词被用滥了,我梳理一下四种完全不同的机制:

机制触发方依据是否尊重 PDB
Kubelet 节点压力驱逐kubelet Eviction Manager节点实际资源信号不保证
调度器抢占kube-scheduler高优先级 Pod 无法调度一般不尊重
Eviction API / drain运维人员主动操作尊重
Descheduler被调度的工具集群资源不平衡尊重

其中 Descheduler 是前几年开始被广泛使用的一个独立组件,不是 Kubernetes 核心自带的。它会根据集群的整体资源分布,把某些 Pod 主动驱逐掉,让调度器重新放置,从而平衡各节点负载。这算是"基于资源模型的资源回收"里最温和、最可控的一种手段,强烈建议大型集群引入。

5. 实战复盘:一次 MemoryPressure 驱逐的完整排查链路

5.1 从告警到定位,按什么顺序排查

回到开头那次事故。我在排查时走的路径,现在整理成可复现的清单:

先看节点状态和事件,确认驱逐确实发生了:

kubectl describe node worker-03 | grep -A10 Conditions kubectl get events -A --sort-by=.lastTimestamp | tail -50

describe node里的 Conditions 会显示MemoryPressure=True的起始时间,以及 Kubernetes 的LastHeartbeatTime。events里能直接看到类似Evicting container kube-proxy的驱逐动作记录。

然后拉 kubelet 日志,找驱逐决策的直接原因:

journalctl -u kubelet --since "2024-11-20 02:15:00" | grep -i evict

日志里会打印类似eviction manager: attempting to reclaim memory和具体的 threshold 信息。这一步能确认是哪个信号、哪个阈值触发的。

再看节点和 Pod 的实际资源使用:

kubectl top node worker-03 kubectl top pod -A --sort-by='memory' | grep worker-03

5.2 那次事故的根因链

排查到这一步,根因基本清晰了:

  • 节点上有一个日志采集的 DaemonSet,没配 requests/limits,实际内存吃到 2.8Gi,QoS 是 BestEffort,被 kubelet 第一个点名;
  • 有两个批量训练任务 Pod,声明了 requests 但声明值极低(300Mi),实际各吃了 6Gi,属于典型的"声明与用量严重脱节",被排在驱逐名单第二梯队;
  • 我们核心的 Redis 从节点,配置的是 Burstable,requests 给的 1Gi,实际因为流量高峰吃到 2.5Gi,超用了 requests,排在了同一梯队里。

最终 kubelet 从日志采集和训练任务开始驱逐,回收内存仍然不够,于是轮到了超用状态的 Redis 从节点。两个 Redis 从节点被驱逐后,主从复制拓扑直接重建,业务出现秒级抖动。

这个案例里没有任何一个环节是"系统不知道"的:kubelet 知道每个 Pod 的实际用量,知道它们的 QoS,知道它们是否超用requests。它只是按照规则执行了回收,而规则本身暴露了我们的资源模型设计问题——核心业务的 requests 给低了,非核心业务的资源根本没有声明。

5.3 cgroup 层面的细节验证

如果要更深入地确认内存压力来源,可以进到节点上看 cgroup 统计。cgroup v2 环境下:

cat /sys/fs/cgroup/memory.events

这个文件会记录high和max事件的累计次数。high事件频繁出现但max没触发,说明有 cgroup 的内存上限在压着,但没有直接 OOM。再配合:

cat /sys/fs/cgroup/memory.current cat /sys/fs/cgroup/memory.stat | grep -E '^(anon|file|slab) '

可以拆分出匿名页、文件页和 slab 的占比。如果 file 占比很高,说明 Page Cache 是主要压力来源,这时候驱逐 Pod 其实不是最优解,更该做的是调内核参数或者检查是否有异常的读文件行为。这也是当时我们判断"内存其实够用,但 kubelet 不这么认为"的关键证据。

6. 回收策略设计:把 Eviction 当调度闭环的一部分来布置

6.1 阈值设置的实践建议

经过多次踩坑,我在新集群上的 kubelet 配置基本稳定在这套参数:

  • memory.available硬阈值:500Mi~1Gi,软阈值可高些到 1.5Gi,宽限期 90 秒;
  • nodefs.available硬阈值:10%,软阈值 15%;
  • nodefs.inodesFree:5%,inode 耗尽比空间耗尽更难处理,提醒一定要监控;
  • pid.available:5%~10%,过低会导致系统无法 fork 新进程,比内存还致命;
  • evictionMinimumReclaim:内存至少回收 200Mi~500Mi,磁盘至少 5%。

这个组合的核心思想是:给 kubelet 留出"从预警到行动"的时间,同时避免它反复横跳。阈值不是越小越好,也不是越大越好,它要和节点的实际规格、业务特性匹配。比如 64G 内存的节点,你设 100Mi 的硬阈值,基本等于没有;设 2Gi,又可能过度敏感。

6.2 从资源模型源头减少驱逐概率

与其事后调 Eviction 参数,不如从源头把资源模型做健康。我的强制要求有三条:

  1. 所有 Pod 必须有 requests,核心业务 requests 要贴近实际用量的 70%~80%,不能拍脑袋;
  2. 所有核心业务配 Guaranteed QoS,也就是 requests == limits;
  3. 非核心批处理任务单独用 PriorityClass 降低优先级,并允许被抢占。

做到这三点,调度器的"加法"和 kubelet 的"减法"就基本对齐了。调度器不会再因为 requests 声明过低而把太多 Pod 塞进同一节点,kubelet 驱逐时的候选名单也不会把真正的核心业务排在最前面。

6.3 监控预警要跑在 Eviction 前面

Eviction 是最后一道防线,不能让它成为你的第一道告警。我现在的监控策略是:

  • 节点内存可用量低于软阈值的两倍时,就触发 Warning 告警;
  • kubelet 出现任何 eviction 事件,立即告警并保留现场日志;
  • 定期用kubectl describe node检查每个节点的资源分配率(Allocated vs Allocatable),超过 80% 就评估扩容或疏散;
  • 对 BestEffort Pod 做专项扫描,发现一个整改一个。

把告警水位线提前,你会发现真正触发 Eviction 的次数会变得极少。它应该是一个"理论上永远不该触发"的保险机制,而不是日常运维里频繁登场的角色。

6.4 主动验证回收策略的小技巧

最后分享一个我常用的小操作:在测试环境里专门挑一台节点,用一个临时 Pod 持续灌内存,模拟内存压力逐渐逼近阈值的过程。脚本里可以按一定速率递增分配内存,同时观察监控面板上MemoryPressure什么时候出现、kubelet 从哪个 Pod 开始驱逐、回收之后水位如何回落。

这个验证跑上几轮,你对 eviction manager 的行为模式会有非常直观的体感,比读十篇源码分析都有用。我第一次把evictionMinimumReclaim加上去,就是因为在测试里看到驱逐风暴的实际触发过程,才确定这个参数对生产环境不可或缺。

K8S 的资源治理,说到底是让"声明"尽量接近"现实",让"调度"和"回收"互相咬合。Eviction 不是敌人,它是这个闭环里最后一个肯说真话的角色——它把你资源模型里的所有水分都挤出来给你看。与其想着怎么绕过它,不如好好利用它帮我们暴露问题、逼我们改进声明规范。我现在的习惯是每次看到驱逐事件,第一反应不是"又要扩容了",而是"哪个 Pod 的资源声明又在撒谎"。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 8:48:16

美团大模型 Agent 实践手册:外卖场景的工程化落地与避坑指南

简介&#xff1a;这是一份系统梳理美团大模型Agent落地经验的技术手册&#xff0c;面向大模型应用开发工程师、业务技术负责人及关注Agent工程化的读者。手册从基础认知到未来展望共分八章&#xff0c;既详解龙猫大模型&#xff08;LongCat-Flash-Chat&#xff09;核心架构、模…

作者头像 李华
网站建设 2026/10/8 8:47:01

医共体AI大模型智能体规划设计方案与落地避坑指南

简介&#xff1a;一份面向医院管理者、医共体规划人员及医疗AI从业者的项目规划设计方案PPT&#xff0c;聚焦智慧医院医共体与AI大模型智能体的融合落地。方案从建设背景与需求分析切入&#xff0c;系统梳理资源分配不均、信息孤岛、基层能力断层等痛点&#xff0c;并给出架构设…

作者头像 李华
网站建设 2026/10/8 8:45:54

Java权限模型实战:从RBAC到数据权限与Spring Boot落地

最近又在技术群里看到有人问&#xff1a;“Java项目里的权限到底怎么做&#xff1f;”底下回复五花八门&#xff0c;有说直接上Spring Security的&#xff0c;有说抄一套若依的&#xff0c;也有说用Sa-Token更省事。说实话&#xff0c;权限模型这个东西我在Java后端摸爬滚打了五…

作者头像 李华
网站建设 2026/10/8 8:44:47

superpowers是什么?AI编程技能扩展包的安装与实战指南

1. superpowers 到底是什么&#xff1a;为什么有人能把 AI 编程工具越用越顺手如果你最近在用各类 AI 编程助手&#xff0c;应该会在 GitHub、技术社区或者即刻上反复刷到这个叫“superpowers”的词。评论区问得最多的不是“这是什么”&#xff0c;而是“具体怎么用”“有哪些 …

作者头像 李华
网站建设 2026/10/8 8:44:30

Agent原生存储桶设计:万亿级记忆与状态管理实战

这几年我一直在折腾Agent相关的基础设施&#xff0c;从编排框架、工具链到记忆系统&#xff0c;绕了一大圈&#xff0c;最后发现一个最不起眼、却最要命的问题&#xff1a;Agent跑起来之后&#xff0c;那些记忆、状态、工具结果到底往哪里放&#xff1f; 直接扔S3&#xff1f;…

作者头像 李华
网站建设 2026/10/8 8:44:29

Debian中文输入法安装全攻略:从框架选择到乱码排查

如果你在 Debian 上装输入法装到怀疑人生&#xff0c;这不是你的问题。我自己的经历是&#xff0c;第一次在一台全新 Debian 12 上给搜狗输入法装完&#xff0c;重启之后右下角根本没有图标&#xff0c;按 CtrlSpace 也没反应&#xff0c;折腾了一个晚上才意识到是输入法框架没…

作者头像 李华