资讯动态

K8s节点内存驱逐实战:从Evicted到自动恢复的完整指南

发布时间:2026/9/8 2:33:22 来源:尧图企业网站定制
凌晨三点值班手机连着震了三次群里第一条消息是“线上几个核心POD全部消失了”第二条是“节点还活着但没有一个POD在上面”。我打开电脑先看了下节点状态kubectl get nodes能看到所有节点都是Ready接着看POD全部处于Evicted状态。这种“节点正常但POD全被清空”的情况十有八九是kubelet的驱逐机制被触发了而背后真正的原因基本都指向同一个问题节点内存水位失控。在K8s集群里内存是最容易出事的资源因为它不像CPU那样可以靠调度直接限流一旦节点内存被打满内核OOM Killer和kubelet驱逐会同时“抢着”杀进程结果往往是——先被杀掉的是毫无防备的业务POD而真正吃内存的进程反而活得好好的。所以今天这篇内容我把“限制节点内存使用率、内存不足时自动驱逐POD”这条链路完整讲透从K8s的资源模型、kubelet驱逐阈值配置、到如何压测验证、如何定位排查、再到如何用PriorityClass和PDB防止误杀全部按实操路径走一遍。1. 内存驱逐先从“半夜POD全部消失”说起那次事故排查到最后kubectl get events里只有一堆Evicted记录原因字段干干净净写着The node was low on resource: memory。真正有价值的信息藏在kubelet日志里journalctl -u kubelet -f --since 1 hour ago | grep -i eviction日志里会看到类似这样的输出MemoryPressure condition is true Evicting container xxx (UID: xxx)这个机制的名字叫kubelet驱逐管理器Eviction Manager。它每10秒左右做一次节点资源检查当可用内存跌破你设置的阈值时它会先尝试回收可回收资源比如未使用的容器镜像如果回收完还不够就按QoS等级从低到高逐批驱逐POD同时给节点打上node.kubernetes.io/memory-pressure污点阻止新的POD继续调度上来。1.1 为什么需要主动限制节点内存使用率很多人觉得K8s有requests和limits就够了不需要额外限制节点内存。但requests只解决“调度决策”的问题调度器根据请求值决定POD放哪个节点。一旦POD运行起来它的实际内存消耗可以远超requests甚至超过limits虽然超了limits会被内核杀掉但以下两个情况依然会让节点处于险境某些容器没有设置limits尤其是业务方图省事只设置requests或干脆两者都不设。设置了limits但limit总和超过节点总内存多个POD同时飙内存时节点可用内存会瞬间归零。这时如果没有kubelet驱逐机制兜底节点只能靠内核OOM Killer随机杀进程完全不可控。所以“限制节点内存使用率”本质上是给节点建立一道可控的应急泄洪通道当内存将要耗尽时由K8s来决定杀谁、怎么杀、以什么顺序杀。1.2 内存保护的三层结构要实现“限制节点内存使用率”至少需要明白K8s里内存防护是分层配合的层级机制作用调度层requests / LimitRange决定POD能否调度到节点限制单POD或名字空间内的用量限制层ResourceQuota限制一个namespace内所有POD的内存总和运行时层kubelet Eviction Manager节点可用内存跌到阈值时自动驱逐POD兜底层内核OOM Killer通过cgroup层级选择杀进程释放内存真正关键的是第三层。因为前两层的失误都会在第三层爆发而第三层的配置不合理则会让第四层内核OOM替你决定一切。1.3 自动驱逐只是最后手段需要先给读者一个清醒的认知驱逐不是优化手段是逃生手段。在设计上你应该让节点在“内存使用率较高但还没到危险水位”时就被监控告警发现通过HPA扩容副本、或调整业务侧内存配置来缓解而不是等到驱逐触发再亡羊补牢。自动驱逐的价值在于一旦告警没拦住、节点真的快被吃满时系统能按可预期的规则清场避免整个节点宕机或内核崩溃。但现实中的集群远没那么理想很多情况下业务方对自身内存画像一无所知限额也设置得很随意。所以有一份能兜底、可复现的节点驱逐配置是每个集群运维的必修课。2. 先算清节点内存账capacity、allocatable与预留在配置驱逐参数前先要理解K8s节点内存的“账本”怎么算。一条最核心的公式是节点可分配内存Allocatable 节点总内存Capacity - 系统预留System-Reserved - K8s预留Kube-Reserved - 驱逐阈值Eviction Threshold调度器只会看Allocatable来调度POD所以限定节点内存使用率本质上有两条路减少allocatable多预留一部分内存不给业务POD用设置严格的驱逐阈值让可用内存跌破某个值就启动驱逐。2.1 Kube-Reserved与System-Reserved的分配逻辑kube-reserved是给kubelet、容器运行时、节点上系统守护进程用的内存system-reserved是给systemd、sshd、监控agent等操作系统进程用的内存。这两块如果预留不足当业务POD把内存吃满时系统进程和kubelet本身也会陷入OOM风险严重时kubelet被系统OOM Killer干掉节点会进入NotReady状态。我的建议是大致按以下比例估算8G以内的小内存节点kube-reserved300Misystem-reserved500Mi。16G-32G的常用节点kube-reserved1Gisystem-reserved1Gi。64G以上的大内存节点kube-reserved2Gisystem-reserved2Gi但如果上面跑的是大数据类或JVM类应用建议再额外多预留2G给page cache和文件系统缓冲。2.2 查看节点当前的容量与可分配量配置完预留后用这条命令验证是否生效kubectl describe node node-name | grep -A 6 Capacity你会看到cpu、memory的capacity和allocatable两个值两者的差值就是你的预留。如果allocatable比预期高说明kubelet启动参数没有加载成功。2.3 别忘了“驱逐阈值”也是预留的一部分很多人配置了system-reserved和kube-reserved却忘了把驱逐阈值纳入内存账本。假设节点内存64G你预留了4G但驱逐阈值设的是memory.available100Mi意味着业务POD可以把内存吃到仅剩100Mi才触发驱逐。这个水位下内核可能在kubelet动手之前就先OOM了完全没有缓冲余地。所以驱逐阈值本质上也是你主动预留出来的一段“应对突发”的缓冲内存。业界比较稳妥的口径对于大内存节点把驱逐阈值设在可用的1%到5%之间同时min-reclaim给一个相对保守的数值防止反复触发。3. 驱逐阈值如何设hard、soft与min-reclaim的组合套路kubelet驱逐参数有三种很多人只配了一个--eviction-hard就以为万事大吉实际上三者的配合才是阻止内存雪崩的关键。3.1 三类驱逐参数解析参数含义类比--eviction-hard硬性驱逐线一旦可用内存低于阈值立即驱逐不等宽限时间家里冰箱空了马上叫外卖--eviction-soft软性驱逐线低于阈值后等待宽限时间期间如果恢复就不驱逐冰箱快空了先撑一两个小时再说--eviction-soft-grace-period软驱逐的宽限期超过此时间仍低于阈值才驱逐撑了多久还没补货就受不了了--eviction-min-reclaim每次驱逐至少要回收到的资源量用于控制驱逐后的反弹叫外卖至少要够吃两天的不够就不停加单一条常见的配置组合是--eviction-hardmemory.available500Mi --eviction-softmemory.available1Gi --eviction-soft-grace-periodmemory.available1m30s --eviction-min-reclaimmemory.available500Mi3.2 为什么建议先设soft再设hard只设hard的问题在于触发就是立刻驱逐没有任何回旋余地。而soft阈值更高触发后给业务方一段宽限期让POD上的程序自行释放内存很多语言运行时在内存压力下会触发GC或连接池收缩如果宽限期内内存回落到soft线以上kubelet就不会驱逐。但注意soft阈值只对内存和磁盘等可测量资源生效且那个宽限期之间必须整体满足只要低于阈值的持续时间到达宽限期就会触发驱逐。3.3 min-reclaim是防抖关键在实际生产环境里最烦人的不是驱逐本身而是“驱逐之后内存立刻又被打满再次触发驱逐”导致POD被杀了一遍又一遍。min-reclaim就是用来解决这个问题的它要求每次驱逐时不仅让节点恢复到阈值之上还要额外多腾出一部分内存。比如memory.available500Mi意味着每次驱逐至少要腾出500Mi的可用内存来避免一触发就被打满的抖动循环。3.4 对应不同节点的推荐配置参考节点总内存eviction-hardeviction-softmin-reclaim8Gmemory.available200Mimemory.available800Mimemory.available300Mi16Gmemory.available300Mimemory.available1Gimemory.available400Mi32Gmemory.available500Mimemory.available2Gimemory.available800Mi64Gmemory.available1Gimemory.available4Gimemory.available2Gi这里给的是一个经验区间不是绝对标准。真正要参考的是业务内存的波动节奏如果业务POD内存浮动很大min-reclaim要调大如果业务对延迟很敏感soft宽限期可以适当缩短让驱逐更快发生避免长时间的内存压力导致性能劣化。4. 实操用Drop-in配置kubelet并压测验证驱逐这部分我直接按一套可以在测试环境完整跑通的流程来写。假设你的节点操作系统是CentOS 7.9或Ubuntu 20.04kubelet由systemd托管CRI为containerd。4.1 制作kubelet的Drop-in配置为避免直接改/etc/systemd/system/kubelet.service主文件推荐使用systemd的Drop-in方式。创建配置文件mkdir -p /etc/systemd/system/kubelet.service.d cat /etc/systemd/system/kubelet.service.d/20-eviction.conf EOF [Service] EnvironmentKUBELET_EXTRA_ARGS--eviction-hardmemory.available500Mi --eviction-softmemory.available1Gi --eviction-soft-grace-periodmemory.available1m30s --eviction-min-reclaimmemory.available500Mi --system-reservedmemory1Gi --kube-reservedmemory1Gi EOF然后重新加载systemd并重启kubeletsystemctl daemon-reload systemctl restart kubelet systemctl status kubelet4.2 验证配置是否生效重启完成后确认节点状态里出现了内存压力相关的条件记录同时确认容量信息中allocatable的值符合预期kubectl get node node-name -o jsonpath{.status.allocatable.memory} kubectl get node node-name -o jsonpath{.status.conditions} | python3 -m json.tool正常时你会看到MemoryPressure条件不存在或为False。另外可以到kubelet的metrics端口验证kubelet_evictions和kubelet_memory_pressure指标是否暴露。4.3 用stress-ng压测触发驱逐模拟内存压力最方便的工具是stress-ng。创建一个临时POD把内存直接打满apiVersion: v1 kind: Pod metadata: name: memory-stress spec: containers: - name: stress image: polinux/stress command: [stress] args: [--vm, 1, --vm-bytes, 12G, --vm-hang, 10, --timeout, 600s] nodeSelector: kubernetes.io/hostname: target-node这里12G要结合节点总内存调整原则是让POD占用的内存超过节点可用内存的一定比例让可用内存快速跌破1Gi的soft线并持续超过90秒。然后另开一个终端观察watch -n 2 kubectl get pods -o wide大约几十秒后你会看到memory-stress这个POD先变成Evicted随后上面运行的业务POD也会逐个变成Evicted状态。4.4 查看驱逐事件和节点污点kubectl describe node target-node | grep -A 10 Events事件里会有类似这样的记录Normal Evicting pod/memory-stress The node was low on resource: memory同时查看节点Taints会看到node.kubernetes.io/memory-pressure:NoSchedule这个污点会持续存在直到节点可用内存恢复到soft阈值以上并经过--eviction-pressure-transition-period默认5分钟之后才会自动移除。很多人发现POD驱逐了但新POD不上来就是因为这个污点还挂着。5. 排查链路POD被驱逐后怎么定位怎么跟系统OOM区分POD被驱逐后新手最常犯的错是只盯着kubectl get events找原因但events信息不够详细而且可能已经滚动覆盖了。真正高效的做法是走一条完整的排查链路。5.1 第一层确认POD的驱逐原因kubectl get pod pod-name -o yaml | grep -A 30 status:在status的containerStatuses里state会有terminatedreason通常是Evictedmessage为The node was low on resource: memory。这能确认是kubelet驱逐而不是进程本身崩溃或探针失败。5.2 第二层确认节点的内存压力现状kubectl top node node-name kubectl describe node node-name | grep -A 10 Conditions查看MemoryPressure的status是否为True以及最近一次探测时间。如果当前已恢复为False但短时间内反复切换说明节点处于“内存压力振荡”状态需要关注是否频繁触发驱逐。5.3 第三层翻kubelet日志追踪驱逐决策驱逐发生时kubelet日志中会按时间顺序输出memory.available的测量值、阈值、以及是否执行驱逐的决策。使用如下命令精确定位journalctl -u kubelet --since 30 minutes ago | grep -Ei evict|memorypressure重点关注两类日志一类是Evicting container另一类是Failed to admit pod因为MemoryPressure污点导致某些POD无法被调度。5.4 第四层和内核OOM Killer区分开kubelet驱逐和内核OOM是两种完全不同的事情但表象容易混淆。查看系统日志dmesg | grep -i oom grep -i oom /var/log/messages /var/log/syslog 2/dev/null grep -i killed process /var/log/kern.log如果发现了Out of memory: Killed process ...说明已经发生了内核级别的OOM那是比kubelet驱逐更严重的状态意味着节点内存被完全耗尽内核被迫出手杀进程。排查优先级极高说明你的阈值设置得不够低、或回收速度跟不上内存增长。5.5 为什么有时“驱逐”变成了“整个节点宕机”这是很反直觉的点kubelet驱逐本身只是调API把POD杀掉不会导致节点宕机。但如果kubelet所在的系统内存没有保护system-reserved配得太小很可能在kubelet还没来得及执行驱逐之前系统OOM先把kubelet或containerd干掉了后果就是节点直接NotReady。这时候你再去看节点条件可能只剩下KubeletHasNoDiskPressure之类的假象真正的根因还是在dmesg里。经验补充判断驱逐与OOM还有一个快速方法——看POD的reason。reason: Evicted是kubelet驱逐reason: OOMKilled是容器被内核OOM杀掉两者在kubectl get pod的输出里一眼就能区分。5.6 QoS等级决定驱逐顺序kubelet驱逐POD的顺序不是随机的而是按QoS等级从低到高QoS等级判定条件驱逐顺序BestEffort未设置requests和limits最先被驱逐Burstable设置了部分requests/limits其次被驱逐Guaranteedrequests和limits都设置且值相等最后才被驱逐这意味着如果你希望重要业务不被驱逐至少要让它的QoS等级达到Guaranteed。很多人踩过的坑是POD明明设置了resources.limits.memory却忘了设置requests.memory导致它只是Burstable在内存压力下被kubelet当作二等公民优先驱逐了。6. 防止误杀PriorityClass、PDB与节点内存监控搭配配置了驱逐阈值之后自动驱逐能跑起来但真正的生产环境还要考虑“别把关键依赖杀掉”。“自动驱逐”应该是有优先级、有逃生通道的而不是一锅端。6.1 用PriorityClass保护关键POD创建两个PriorityClass一个给核心业务一个给普通任务apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: 核心业务POD --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: low-priority value: 100 globalDefault: false description: 离线任务或可重启任务关键POD的spec里加上priorityClassName: high-priority调度器会优先保障它在节点内存压力下K8s也会倾向于驱逐低优先级POD而不是高优先级POD。不过注意驱逐优先级和调度优先级是两套体系kubelet在驱逐时优先看QoS等级PriorityClass是第二顺位的判断依据。6.2 PDB对驱逐的影响PodDisruptionBudget可以在“自愿驱逐”时保护POD保证驱逐后仍然有一定数量的副本存活。但kubelet的硬驱逐eviction-hard属于非自愿驱逐PDB通常不会拦住它所以不要指望PDB能挡住内存驱逐。PDB更多的价值在于配合节点维护时的kubectl drain使用让你在主动踢走节点上的POD时不会导致全部副本同时被清理。6.3 监控告警在驱逐发生之前就收到通知自动驱逐之后再去跟踪多少有点被动。更理想的做法是配置节点内存使用的预测告警。一条实用的Prometheus规则示例groups: - name: node-memory.rules rules: - alert: NodeMemoryUsageHigh expr: (1 - (node_memory_Available_bytes{jobnode-exporter} / node_memory_MemTotal_bytes{jobnode-exporter})) * 100 85 for: 10m labels: severity: warning annotations: summary: 节点 {{ $labels.instance }} 内存使用率超过85%建议同时设置两档告警85%为warning提醒扩容或排查内存泄漏95%为critical准备接受节点进入MemoryPressure并可能触发驱逐。这样在驱逐发生前团队就有时间介入。6.4 用ResourceQuota从源头限制内存“上限”如果只是为了限制某个命名空间或业务部门对节点内存的消耗还可以用ResourceQuota在源头锁住apiVersion: v1 kind: ResourceQuota metadata: name: mem-limit spec: hard: requests.memory: 16Gi limits.memory: 32Gi这个配额是“源头限制”的思路只要namespace的POD内存总和超限新的POD就创建失败。它能避免某个业务方大量部署无限制的POD把整个节点内存池打穿。和节点驱逐结合使用才能形成完整闭环——源头限总量运行时防突发。最后再分享一个我在维护多个集群过程中总结出来的小技巧无论驱逐阈值怎么调记得在kubelet启动参数里保留--eviction-pressure-transition-period5m这个默认值不建议为了追求“快速恢复”而把它改小否则节点上的MemoryPressure污点会像幽灵一样反复出现影响调度稳定性。另外建议定期用kubectl get events --all-namespaces --field-selector reasonEvicted检查集群的驱逐记录把驱逐频率作为一项常态化的巡检指标而不是等告警响了才去看。只有把驱逐事件当成一种“资源压力的经济信号”来对待而不是单纯的问题现场集群的内存治理才算真正上了正轨。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价