尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

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.available100Mi也就是说节点内存可用量一旦低于 100Mikubelet 就会开始逐 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 实际吃到多少内存、多少磁盘、多少 inodekubelet 按这个做减法判断节点是否撑得住。这两套口径的差异就是一切调度和驱逐问题的根源。调度器说装得下不代表运行时不爆运行时没爆也不代表requests声明合理。Eviction 就是第二套口径的执行者——它依据的资源模型本质上是节点层面的实时用量模型而不是调度器手里的请求声明模型。1.3 这个机制存在的意义很多人恨 Eviction觉得它老是没事找事。但从集群治理的角度看Eviction 是资源模型的最后一道兜底保证节点不会因为个别 Pod 失控而整体崩溃。没有 Eviction一个内存泄漏的 Pod 会直接把节点打 OOM进而拖累同节点所有业务。有了 Evictionkubelet 能在崩溃前把部分 Pod 迁走牺牲局部保全局。理解了这一点后面的所有设计决策都顺了——我们要做的不是关掉 Eviction而是让它在正确的时机、按正确的顺序、驱逐正确的对象。2. 资源模型的原点requests 与 limits 的分工2.1 买票和实际占座是两回事用一个生活化的类比requests相当于你买票时申报的我大概会占几个座位调度器根据这个决定放不放你上车limits相当于硬性规定的最多占几个座超过就会被按超载处理。requests只用于调度不限制运行时。limits不参与调度拟合但会限制 CGroup 资源上限CPU 是 throttle内存是 OOM 或驱逐。这就产生了一个很常见的坑一个 Pod 声明requests: 100mlimits: 4Gi调度器看到它只占 100m 的 CPU 额度就把它塞进一个已经很满的节点。运行起来之后它实际吃到 3Gi 内存别的 Pod 就遭殃了。等到 kubelet 触发 Eviction你会发现最先被驱逐的往往是那些实际用量远超 requests的 Pod——因为它超用了自己申报的份额。2.2 QoS 等级Eviction 的优先队列Pod 的requests和limits是否相等、是否设置决定了 Pod 的 QoS 等级。Eviction 在选择驱逐对象时QoS 等级是核心排序依据。QoS 等级配置特征Eviction 中的地位典型风险Guaranteedrequests limits且每个容器都设置最后动只有超用或极端压力才会被驱逐数量多时挤占其他 PodBurstablerequests 存在但至少有一个容器 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.available500MiMemoryPressurenodefs.availablekubelet 根目录所在文件系统可用空间nodefs.available10%DiskPressurenodefs.inodesFree根文件系统可用 inode 数nodefs.inodesFree5%DiskPressureimagefs.available容器镜像所在文件系统可用空间imagefs.available15%DiskPressureimagefs.inodesFree镜像文件系统可用 inode 数imagefs.inodesFree5%DiskPressurepid.available可用 PID 数量pid.available10%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 低到高排队。实际规则有三层先看 Pod 的实际用量是否超过它的requests超用的排在最前面再用 QoS 等级排序BestEffort 排最前Burstable 其次Guaranteed 最后最后在同等级内看 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 四种驱逐容易混先分清日常工作中驱逐这个词被用滥了我梳理一下四种完全不同的机制机制触发方依据是否尊重 PDBKubelet 节点压力驱逐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 -50describe node里的 Conditions 会显示MemoryPressureTrue的起始时间以及 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-bymemory | grep worker-035.2 那次事故的根因链排查到这一步根因基本清晰了节点上有一个日志采集的 DaemonSet没配 requests/limits实际内存吃到 2.8GiQoS 是 BestEffort被 kubelet 第一个点名有两个批量训练任务 Pod声明了 requests 但声明值极低300Mi实际各吃了 6Gi属于典型的声明与用量严重脱节被排在驱逐名单第二梯队我们核心的 Redis 从节点配置的是 Burstablerequests 给的 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.inodesFree5%inode 耗尽比空间耗尽更难处理提醒一定要监控pid.available5%~10%过低会导致系统无法 fork 新进程比内存还致命evictionMinimumReclaim内存至少回收 200Mi~500Mi磁盘至少 5%。这个组合的核心思想是给 kubelet 留出从预警到行动的时间同时避免它反复横跳。阈值不是越小越好也不是越大越好它要和节点的实际规格、业务特性匹配。比如 64G 内存的节点你设 100Mi 的硬阈值基本等于没有设 2Gi又可能过度敏感。6.2 从资源模型源头减少驱逐概率与其事后调 Eviction 参数不如从源头把资源模型做健康。我的强制要求有三条所有 Pod 必须有 requests核心业务 requests 要贴近实际用量的 70%~80%不能拍脑袋所有核心业务配 Guaranteed QoS也就是 requests limits非核心批处理任务单独用 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 的资源声明又在撒谎。
返回列表