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

资讯详情

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

K8S驱逐机制详解:从资源模型到Eviction配置与实操

K8S驱逐机制详解:从资源模型到Eviction配置与实操 做 K8S 运维久了几乎每隔一段时间就会收到类似告警节点内存使用率超过 90%、磁盘用量持续攀升、某个 Pod 被标记为Evicted。第一次看到Evicted状态的时候很多刚接触 K8S 的同学会以为 Pod 是不是写崩了其实不是这是 kubelet 在依据资源模型做回收也就是我们今天要聊的 Eviction。Eviction 是 K8S 调度体系里非常关键的一环它负责在节点资源不足时把 Pod 腾走让集群重新恢复稳定。这篇文章我会从资源模型说起把驱逐机制的底层逻辑、配置项、实操方法以及踩坑经历一次讲清楚。1. 资源模型与驱逐机制的底层逻辑1.1 Requests 和 Limits一切资源判断的起点在理解 Eviction 之前必须先把 K8S 的资源模型搞明白。K8S 认为一个 Pod 使用资源的方式由两个维度来描述requests和limits。requests是调度器用来做资源预留的依据表示这个 Pod 至少需要多少资源limits是运行时对资源上限的约束表示这个 Pod 最多能用多少资源。这两个值可以相等也可以不相等组合出来的效果差异非常大。举个例子一个 Pod 的配置是requests: memory512Milimits: memory1Gi。这意味着调度器在给这个 Pod 找节点时会认为它在目标节点上占用了 512Mi 的内存而实际运行时它最多只能用到 1Gi超过之后就会被内核干掉。很多刚上手的朋友会觉得limits越大越安全但实际上这会在节点压力变大的时候给自己埋下雷requests写小了调度器容易把太多 Pod 塞到一个节点上limits写大了Pod 突发资源使用时会和邻居抢内存甚至触发 OOM。在节点资源视角下K8S 把一台机器抽象成三层节点总容量capacity物理机或虚拟机的资源总量。已分配资源allocated所有 Pod 的requests之和再加上 kubelet、容器运行时等系统组件的预留。实际使用量usage当前真实在用的资源数。Eviction 判断的核心不是「已分配」超了没有而是「实际使用量」接近或超过节点容量时的响应。这就要引出 QOS 等级了。1.2 QoS 等级决定了驱逐时的优先级顺序K8S 根据 Pod 的requests和limits是否设置、是否相等将所有 Pod 划分为三个 QoS 等级Guaranteedrequests和limits都设置了而且数值相等。这类 Pod 的资源是完整预留的在节点资源紧张时优先级最高一般最后才被驱逐。Burstable设置了requests但limits未设置或者大于requests。这类 Pod 是最常见的允许突发使用资源驱逐顺序居中。BestEffortrequests和limits都没设置表示能用多少用多少。这类 Pod 在节点资源不足时会被最先清理掉。你可能会问既然已经预留了资源为什么还要驱逐因为实际使用才是节点压力的直接来源。一个Burstable的 Pod 虽然只申请了 100Mi 内存但如果业务突发膨胀把 1Gi 内存全部吃满整个节点的可用内存就会骤降kubelet 就要站出来清理。换句话说驱逐是保障节点整体可用性的最后防线它优先牺牲低优先级的突发型 Pod保住高优级的核心业务。这是很多人第一次接触 Eviction 时最容易忽略的点驱逐不是靠“谁用得多就杀谁”而是严格按照 QoS 等级从低到高选择牺牲对象的。所以想让业务更抗揍第一件事就是把核心组件做成Guaranteed至少requests和limits要相等。1.3 调度与回收的关系为什么说 Eviction 也是调度的一环从题目里的K8S调度-依据资源模型实行资源回收来看Eviction 并不是孤立动作它和调度器是双向联动的。节点资源一旦触发驱逐节点状态会立刻被标记为MemoryPressure或DiskPressure调度器之后就不会再把新的 Pod 调度到这台节点上。也就是说驱逐负责把老 Pod 赶走调度器负责把新 Pod 挡在门外两者配合才能恢复节点健康。我见过不少团队只关注requests怎么设完全不关注节点压力标记结果某台节点被压到内存几乎耗尽调度器还一个劲往里塞新的 Pod最终整台机器 OOM 甚至挂掉。理解 Eviction 的调度联动才能把资源模型从静态配置变成动态平衡。2. Eviction 机制的核心细节与执行流程2.1 驱逐信号源内存、磁盘、Inode、PID 都在监控范围内kubelet 里的驱逐管理器eviction manager会持续监控多个信号源常见的有四类memory.available节点可用内存计算方式是节点内存总量减去 cgroup 限制值里的当前使用量。nodefs.available和nodefs.inodesFree节点根文件系统的可用空间与可用 inode 数量主要影响 Pod 写入本地临时文件、EmptyDir 数据。imagefs.available和imagefs.inodesFree容器镜像所在文件系统的可用情况通常与节点文件系统分离。pid.available节点剩余可用的 PID 数量防止进程数耗尽导致系统异常。每个信号源都有自己的阈值一旦实际数值低于阈值kubelet 就会触发对应的驱逐动作。你可以在节点上执行kubectl describe node在Conditions一栏看到类似MemoryPressure、DiskPressure、PIDPressure的状态标记。这些标记是调度器判断节点是否可用的依据也是排查问题的第一入口。2.2 软驱逐与硬驱逐生存和死亡的边界K8S 的驱逐阈值分为两类软阈值soft eviction threshold和硬阈值hard eviction threshold。软驱逐可以理解为一个“警告线”。比如内存可用量低于 1Gi 时kubelet 并不立刻删除 Pod而是等待一个宽限期eviction-soft-grace-period比如 1 分钟。如果一分钟之后节点资源仍然没有恢复再开始驱逐 Pod。这样设计的初衷是给临时抖动留出缓冲时间比如一个 Pod 正在做大规模计算内存陡增但很快就会释放如果立刻驱逐就太冤枉了。硬驱逐则没有商量的余地一旦低于阈值立即触发。比如memory.available小于 200Mikubelet 会立刻开始杀 Pod甚至整个节点都可能面临内核 OOM。硬阈值是兜底逻辑通常配置得比软阈值低得多。此外还有一个参数eviction-max-pod-grace-period它控制的是 Pod 被驱逐时最多能有多少秒优雅终止时间。如果 Pod 自身设置了terminationGracePeriodSecondskubelet 会取两者中较小的值如果没有设置则使用这个默认值。这一步非常关键因为优雅终止意味着业务有清理连接、刷盘、注销服务的机会。2.3 驱逐执行顺序从 QoS 最低的开始淘汰当超过了软驱逐或硬驱逐阈值之后kubelet 按照下面的优先级顺序选择要驱逐的 Pod首先驱逐已经不可用的 Pod比如健康检查失败多时的容器然后驱逐BestEffort类 Pod接着驱逐Burstable类 Pod优先选择实际使用量超过requests最多、以及用量增长速率最高的最后才考虑Guaranteed类 Pod而且只有在其实际使用量超过limits的情况下才会被驱逐。这个逻辑非常合理GuaranteedPod 对资源的“承诺”最高业务一般也最重要只有它违反了自己的资源承诺时kubelet 才有理由动它。而BestEffort本身没有资源承诺自然成了首当其冲的淘汰对象。整个驱逐过程在 kubelet 内是异步的不是一次性把所有 Pod 全杀掉而是一批一批地清理每次清理之后重新评估节点压力。这样做既能让业务有时间完成优雅终止又能防止雪崩式全量删除导致的服务大面积中断。2.4 手动驱逐Node Drain 与主动维护除了资源压力引发的自动驱逐之外运维场景里还有一种更常见的主动回收叫kubectl drain。当我们需要对节点做内核升级、硬件维修或扩容时会先把节点标记为不可调度cordon然后把上面的 Pod 安全地驱逐到其他节点。drain命令本质上就是手工触发的 Eviction区别在于它不看资源阈值而是按你的操作指令来执行。kubectl drain node-name --ignore-daemonsets --delete-emptydir-data--ignore-daemonsets是必须加的因为 DaemonSet 的 Pod 本来就应该在每台节点上运行drain 之后它们还会被重新创建如果不忽略会造成无限循环。--delete-emptydir-data表示允许删除使用 EmptyDir 的 Pod因为这些数据只存在于本机排空节点时如果不清理其他节点接管后也无法恢复。3. 实操过程配置驱逐策略与调优3.1 查看当前节点的驱逐阈值与压力状态开始调优之前先检查集群当前的驱逐配置和节点状态。先看节点资源情况kubectl describe node node-1重点关注Conditions和Allocated resources。如果节点显示MemoryPressureTrue说明之前发生过驱逐而且现在可能还在持续。Allocated resources一栏列出了所有 Pod 的requests总和可以判断节点剩余可调度资源是不是已经很少了。再看 kubelet 的启动参数和配置ps aux | grep kubelet如果 kubelet 是通过 systemd 管理的通常配置文件在/var/lib/kubelet/config.yaml启动参数在/etc/systemd/system/kubelet.service.d/目录下的文件中。不同发行版路径略有差异但整体思路一致。3.2 修改 kubelet 驱逐阈值配置kubelet 的驱逐配置可以在启动参数里用--eviction-hard、--eviction-soft指定也可以写在config.yaml里我个人更推荐后者管理起来更清晰。常见的配置如下apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration evictionHard: memory.available: 500Mi nodefs.available: 10% nodefs.inodesFree: 5% imagefs.available: 15% evictionSoft: memory.available: 1Gi nodefs.available: 15% evictionSoftGracePeriod: memory.available: 1m30s nodefs.available: 1m30s evictionMaxPodGracePeriod: 60 systemReserved: memory: 1Gi cpu: 500m kubeReserved: memory: 1Gi cpu: 500m这里有几个关键点要注意。首先systemReserved和kubeReserved是给系统进程和 kubelet 自身预留的资源如果不设置kubelet 可能会把节点的最后一点资源全部分配给业务 Pod导致系统组件不稳定。其次evictionHard中的memory.available建议不要设置太小比如低于 200Mi因为内核 OOM 触发往往发生在可用内存见底的时候kubelet 需要在系统级 OOM 之前提前介入否则可能出现整机故障。修改完配置后重启 kubelet 才能生效systemctl daemon-reload systemctl restart kubelet需要注意如果你使用的是云厂商托管集群或者 Kubeadm 之外的管理工具kubelet 配置可能会在节点重建时被覆盖建议通过集群级别的配置管理方式来维护而不是直接改节点上的文件。3.3 验证驱逐配置是否生效重启 kubelet 后可以用下面的命令验证配置是否加载成功kubectl get --raw /api/v1/nodes/node-1/proxy/configz返回结果里会包含evictionHard和evictionSoft的当前值。如果返回结果与你的配置一致说明 kubelet 已成功加载。想模拟驱逐场景验证可以在一台测试节点上人为降低硬驱逐阈值再用工具压满内存比如下面这条命令会分配一个约 2G 的驻留内存stress-ng --vm 1 --vm-bytes 2G --vm-hang 0 等 kubelet 触发驱逐之后查看被驱逐 Pod 的事件kubectl get events --field-selector involvedObject.kindPod -n namespace --sort-by.lastTimestamp事件里会出现一条类似The node was low on resource: memory, requested 1Gi, used 2Gi的记录。这条记录非常有用能直观看到触发驱逐的原因、资源类型、请求值和使用值。3.4 在完整调度链路中配合 PDB 使用驱逐动作对业务来说毕竟是破坏性的为了减少影响建议在关键应用上设置 PodDisruptionBudgetPDB。PDB 定义了一个应用在主动驱逐比如drain时可以容忍的不可用副本数。apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: web-pdb spec: minAvailable: 2 selector: matchLabels: app: web这段配置表示appweb的 Pod 在主动驱逐时至少需要保持 2 个副本可用。如果当前只有 2 个副本drain命令会阻塞直到其他副本正常启动或者手动取消。但不是所有驱逐都会受 PDB 约束节点资源压力引发的驱逐node.kubernetes.io/unschedulable等污点容忍之外的情况不会等待 PDB这是自动驱逐与主动维护的重要区别。在实际生产中我一般会为无状态服务设置minAvailable: 50%或maxUnavailable: 1既保证可用性又不会阻塞节点维护。4. 常见问题与排查技巧实录4.1 Pod 频繁被驱逐但节点资源明明还有剩余这种情况最让人难受节点内存看着还剩不少但 Pod 就是被反复Evicted。原因通常不在内存总量而在 cgroup 层的限制。K8S 计算memory.available时依据的是节点的 cgroup 内存控制不是free -m看到的数值。也就是说如果节点本身被某个 cgroup 限制住了可用内存即使系统层面还有内存kubelet 依然会认为资源不足。另外还有一种常见原因就是requests设置得非常大但实际使用量却很低。比如你给一个 128Mi 内存的 Java 服务配了requests: 4Gi节点资源被大量“虚占”调度器不断往其他节点塞 Pod结果某台节点反而被实际使用量打爆。排查思路先用kubectl top nodes看实际使用量再用kubectl describe node看分配量两者一对比就能判断是“真满”还是“假满”。如果是假的缩小requests或者给 Pod 打上高优先级调度即可。4.2 大内存 Pod 回收时报错数据清理不过来搜索热词里有一条“数据量大的时候回收资源会报错”这个在真实场景非常常见。当一个 Pod 占用几十 GB 内存、还在往外写大量临时文件时驱逐虽然触发了但 Pod 的优雅终止过程可能非常慢。如果evictionMaxPodGracePeriod设置得太短Pod 会直接被强杀导致数据来不及落盘甚至业务出现脏数据。我踩过的一个坑是某次跑了几个小时的批量计算任务因为节点内存告警触发驱逐强制 kill 之后临时计算结果全部丢失重新跑了一次成本极高。后面我们调整了方案批量任务不直接跑在被动驱逐风险较高的节点上而是配合priorityClassName提升优先级同时把terminationGracePeriodSeconds加大到 300 秒保证核心结果能快速落盘。对于确实需要大量资源且无法容忍驱逐中断的任务最好的办法是在业务侧做 checkpoint每隔一段时间把计算状态写到持久化存储里这样即使被驱逐新调度的 Pod 也能从最近一次检查点恢复。4.3 节点磁盘满导致驱逐不彻底磁盘压力是另一个大坑。当一个节点的/var/lib/docker或者/var/lib/containerd所在分区写满的时候kubelet 会触发DiskPressure开始驱逐节点上的 Pod。但问题在于被驱逐的 Pod 如果挂载了 EmptyDirVolume 里的数据仍然留在节点上清理不掉磁盘占用一直降不下来。更麻烦的是容器镜像也会占大量磁盘空间。当你设置imagefs.available: 15%时如果镜像缓存过多即使 Pod 已经全部清掉磁盘压力依然存在。这种情况需要同时做好镜像回收和日志清理配置 dockerd 或 containerd 的镜像回收策略定期清理无用的悬空镜像给 Pod 里的日志目录设置合理的emptyDir大小限制防止日志无限增长在节点上使用journalctl --vacuum-size200M这类命令压缩系统日志。4.4 驱逐后 Pod 一直显示 Evicted但不消失这是最容易被忽略的细节之一。Evicted状态其实只是 Pod 的一个终态标记它不会马上被删除会留存一段时间方便运维排查。如果你用的是 Deployment 管理驱逐发生后控制器会自动创建新的 Pod所以你会看到同名 Pod 的新实例在跑旧的EvictedPod 还挂在列表里。清理这些历史EvictedPod 可以用命令批量删除kubectl get pods -A | grep Evicted | awk {print $1, $2} | xargs -n2 kubectl delete pod -n但要注意如果节点本身资源已经恢复这些残留 Pod 不清理也没有实质影响。如果资源持续紧张说明requests配置不合理或者节点规格不够需要从容量规划层面解决光删 Pod 是治标不治本。4.5 初始化报错 the api server is not healthy 与资源回收的关联有些团队的 K8S 集群是手工搭建的经常在kubeadm init阶段看到the api server is not healthy after 4m这样的报错。这个报错虽然通常是 apiserver 启动或 etcd 连接出了问题但和资源模型也有一定的关联如果控制平面节点本身内存不足apiserver 容器会被 OOM 或者被 kubelet 驱逐导致初始化时健康检查失败。排查时可以先看控制平面节点的资源占用kubectl get node -A systemctl status kubelet确认节点不是MemoryPressure或者DiskPressure状态再检查 apiserver 和 etcd 容器是否处于Running。控制平面节点的kubeReserved一定要预留足够的内存否则业务负载一高控制面组件很容易被波及继而出现各种“诡异”的集群问题。5. 驱逐之后调度恢复与集群自愈5.1 驱逐与调度器的联动恢复当 kubelet 触发驱逐时会同时将节点标记为对应的压力状态调度器就不再往这个节点调度新 Pod。但当资源通过驱逐回收之后节点压力标记并不会立刻消失而是有一个冷却过程。你可以手动删除节点上的压力条件让调度器尽快恢复也可以等待 kubelet 的下一次状态更新自然恢复。实际生产中我一般不手动干预让 kubelet 自己更新状态更安全因为如果资源没有真正恢复就强行让调度器介入很容易再次引发驱逐循环。更好的做法是设置合理的软驱逐阈值留足宽限期让节点在压力可控的情况下自然恢复。5.2 善用 descheduler 做二次平衡驱逐解决的是“节点资源不够用”的问题但现实里还有一种情况集群整体资源充足资源分布却不均匀。有的节点上 Pod 数量爆炸CPU 使用率 90%有的节点却空转。这时候可以引入 descheduler 项目按策略把高负载节点上的 Pod 重新调度到空闲节点。常见策略是LowNodeUtilization它会找出使用率过低的节点然后把其他节点上的 Pod 迁移过去。迁移动作本质上也是驱逐只不过它依据的是集群整体资源模型而不是单节点压力。对于多租户、大规模集群来说descheduler 是 Eviction 机制的重要补充。5.3 多租户场景下的资源配额与驱逐防线在多团队共用集群的场景下如果每个团队随意设置requests和limits集群的资源模型很容易失控。建议为每个 Namespace 配置 ResourceQuota同时为每个 Pod 的默认资源限制配置 LimitRange强制约束apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota spec: hard: requests.cpu: 8 requests.memory: 16Gi limits.cpu: 16 limits.memory: 32Gi有了配额之后即使某个团队把requests设得很夸张也只能用掉配额内的资源不会侵占其他团队的可用容量。驱逐机制在这套体系里的角色就是兜底防线配额限制分配驱逐限制实际使用两者结合才能让集群的资源模型正常运转。回到开头那句话Eviction 不是故障而是集群在做资源回收的正常动作。想让自己负责的集群少出问题核心还是把资源模型设计好requests别虚高limits别乱设QoS 等级尽可能向Guaranteed靠拢关键服务配上 PDB节点预留好系统资源。这几件事做扎实了驱逐发生的频率会低很多而且即使发生影响面也可控。最后再分享一个小技巧。我排查驱逐相关事故时习惯先拉出节点上的驱逐事件时间线再结合 Pod 的调度时间对比。你会发现很多所谓的“偶发驱逐”其实都是requests长期虚高导致节点实际承载能力被高估某次流量峰值一来整条链路就崩了。与其不停调大集群规格不如先把资源模型找准。
返回列表