ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

K8s节点内存限制与自动驱逐:kubelet驱逐策略配置与生产实践

K8s节点内存限制与自动驱逐:kubelet驱逐策略配置与生产实践 做过K8s运维的朋友应该都遇到过这种场景半夜两点手机上弹出告警节点内存使用率飙到95%登录上去一看某几个业务POD状态变成了Evicted客户端那边已经在报超时了。更麻烦的是如果节点内存被彻底耗尽内核的OOM Killer会把进程直接杀掉杀谁完全不可控可能正好把数据库、关键网关给杀了整个集群跟着雪崩。这个问题的根源在于集群缺少一套“限制节点内存使用率、内存不足时自动驱逐POD”的机制也就是Kubernetes原生的驱逐Eviction体系。这篇文章我打算把这块讲透。我会先从为什么要做节点内存限制说起然后拆解kubelet驱逐策略的核心参数再带大家一步一步完成配置和压测验证最后整理我在生产环境里踩过的坑和排查方法。适合K8s运维、SRE、平台工程师或者正在从测试集群走向生产集群的人参考。不管你是刚入门K8s还是已经维护着几十个节点的集群看完都应该能直接照做把驱逐机制落到自己的环境里。1. 为什么需要节点内存限制与自动驱逐——问题背景与方案选型思路1.1 没有内存限制时的真实场景OOM与连锁故障我先说个我自己遇到的真实案例。之前维护的一套K8s集群节点规格是32C64G上面跑着一个Java写的内部系统还有几个Python脚本任务和Nginx网关。某个Java服务一直有轻微的内存泄漏平时看不出来到了业务高峰期内存一点点涨上去最后把整台节点的64G内存全部吃光。这时候内核的OOM Killer介入了。OOM Killer选进程的规则是按优先级和内存占用来的但它不会管你的业务重要性。那次它直接把一个缓存中间件进程杀了这个缓存中间件恰恰是很多服务依赖的一瞬间所有依赖它的请求全部报错连锁反应一直蔓延到整个集群。更难受的是这个中间件是Deployment管理的容器被杀后Kubelet会重新拉起一个新POD新POD又继续吃内存继续被OOM Kill反复重启反复抖动线上持续了将近一个小时才恢复。这个案例暴露了几个问题第一很多POD根本没设置内存limit导致某个容器可以把节点的全部内存吃光第二节点没有资源预留系统关键进程和相关组件的内存也是裸奔的第三没有驱逐机制非要等内核OOM Killer出手灾难就免不了。可以说K8s集群在生产环境里要稳定节点内存的防线必须提前搭建而不是寄希望于“不要出事”。1.2 从“内核杀死进程”到“平台调度驱逐”为什么选驱逐既然内存不够直接把超限的POD杀掉不就行了吗问题是让内核OOM Killer去杀它是“低层次”的动作它只能杀掉某个进程而且它不具备业务视角。比如节点上同时跑着关键数据库POD和跑批任务PODOOM Killer可能会优先杀掉数据库因为数据库进程占用的内存多、得分高。这种“随机性”在生产环境是完全不可接受的。Kubernetes的驱逐机制就不一样。kubelet是站在“管理平台”的高度来做决策的它可以根据POD的服务质量等级QOS、优先级PriorityClass、实际内存使用量来决定先驱逐谁。驱逐之前它还会尝试优雅退出给应用时间做清理比如把连接池断开、刷新缓冲数据到磁盘。而且驱逐是可以用配置来控制的作为运维人员我们可以明确告诉kubelet内存阈值设多少、超过多久开始驱逐、哪些POD优先保留全部可以按策略来。所以我的结论是kubelet驱逐是K8s集群内存保护的“第一道防线”内核OOM Killer是“最后一道绝对不能先用的防线”。我们要做的就是把第一道防线搭建好让它尽量在内存真正耗尽之前就把风险化解掉把话事权从内核手里拿回来。1.3 方案选型只设limit不够需要三层防线配合很多人一开始的想法是给每个POD设置内存limit不就行了吗比如给每个容器限制2G内存超了就重启。这个思路是对的但它只是基础配置不能完全解决节点整体的内存安全。为什么首先一个节点上除了业务POD还有系统进程sshd、systemd-journald、监控agent、kubelet、容器运行时等这些都不受POD的资源隔离限制。其次Linux的内核page cache会占用大量内存这部分内存虽然是可以回收的但回收需要时间。第三不是所有团队都能保证每个POD都规规矩矩地设置limits和requests漏配一个风险就还在。所以只靠配置limit解决不了“节点内存整体失控”的问题。我的做法是三层防线第一层给每个POD设置合理的requests和limits尤其是limits必配关键业务做成Guaranteed等级。第二层给节点配置system-reserved和kube-reserved把系统组件和关键进程的内存预留出来不跟业务抢资源。第三层配置kubelet的驱逐阈值当节点可用内存低于阈值时由kubelet自动驱逐低优先级POD保住高优先级POD和节点本身的稳定性。三层配合才能真正实现标题里说的“限制节点内存使用率、内存不足时自动驱逐POD”。下文我会重点讲第三层驱逐策略怎么配置因为第一层和第二层相对简单驱逐策略反而是大家最容易忽枊、坑最多的地方。2. 驱逐策略核心参数解析搞懂驱逐信号、阈值与驱逐顺序2.1 驱逐信号kubelet拿什么判断内存不足Kubernetes驱逐机制的核心是“驱逐信号Eviction Signal”。kubelet不是简单地看“内存使用率”这个百分数而是基于一组称之为信号的指标来决策的。和内存相关的驱逐信号是memory.available它的计算方式如下memory.available node.status.capacity.memory - node.status.allocatable.memory - memory.working_set这个公式里capacity是节点物理内存总量allocatable是kubelet可分配给业务POD的内存总量capacity减去kube-reserved和system-reservedworking_set则是节点当前实际被占用的内存工作集。通俗点说kubelet关心的是“还可以分给新POD用的内存余量还剩多少”而不是物理内存用了百分之几。这里的memory.working_set需要解释一下它等于当前进程占用的内存RSS加上匿名内存再加上内核的最近未被回收的page cache。因为page cache在内存紧张时可以被内核迅速回收所以working_set比纯粹的used值更能反映“真实占用”。这也是为什么有时候你看到free -h里面used很高但kubelet认为内存还不紧张因为相当一部分是page cache回收一下就出来了。实际操作中可以用kubectl describe node查看节点的Allocatable和System Reserved信息用kubectl top node查看节点实时使用量用kubectl describe node | grep -A10 Allocatable确认容量分配情况。理解这些之后再去配置驱逐阈值就有的放矢了。2.2 eviction-hard与eviction-soft阈值如何配置才合理kubelet的驱逐阈值分两种硬阈值eviction-hard和软阈值eviction-soft我分别来说。eviction-hard是指在可用资源低于阈值时kubelet会立即采取驱逐动作不给POD优雅退出的时间容器会直接收到SIGKILL信号。配置示例evictionHard: memory.available: 500Mi nodefs.available: 10% imagefs.available: 15%memory.available: 500Mi的意思是当节点可用内存低于500MiB时kubelet立即开始驱逐POD。这个值设多大合适我个人的经验是生产环境建议至少留500Mi到1Gi的内存余量如果是内存型节点建议留到2Gi。设太小的坏处是page cache回收、进程内存释放都需要时间等阈值被越过再去驱逐很可能已经跟OOM Killer的触发窗口重叠了该杀进程还是会被杀。设太大的坏处是资源利用率下降节点明明还有不少内存余量呢就开始驱逐POD造成不必要的业务中断。eviction-soft是指当可用资源低于某个阈值并且持续一段时间之后kubelet才会驱逐POD并且驱逐时会尊重POD的优雅退出期限terminationGracePeriodSeconds给容器一个清理现场的机会。配置示例evictionSoft: memory.available: 1Gi evictionSoftGracePeriod: memory.available: 1m30s evictionMaxPodGracePeriod: 120这段配置的意思是当节点可用内存低于1Gi并持续90秒后开始驱逐POD并且POD最大宽限期不超过120秒。软阈值适合那种“内存波动一下马上回来”的场景比如POD启动时内存瞬间升高但随后回落到正常硬阈值就可能误伤软阈值多一层观察期更稳妥。我的建议是软硬结合硬阈值兜底保证不出大事软阈值提前干预尽量让影响范围可控。注意evictionHard和evictionSoft的memory.available要设置成不同级别软阈值要比硬阈值更宽裕比如软阈值1Gi、硬阈值500Mi这样才有“先预警后兜底”的层次感。2.3 驱逐顺序与QOS等级为什么我的POD总是先被驱逐kubelet决定驱逐哪些POD时遵循一套明确的优先级顺序去重后大致是优先驱逐BestEffort等级的POD然后是Burstable等级中内存使用量超出请求量的POD最后是Guaranteed等级的POD。同一等级的POD中再按照PriorityClass的值排序优先级低的先被驱逐。所以如果你发现某个POD总是先被驱逐大概率是它的QOS等级是BestEffort或者Burstable。怎么判断POD的QOS等级可以这样看kubectl get pod pod-name -n namespace -o jsonpath{.status.qosClass}Guaranteed等级的要求是容器必须同时设置requests和limits并且requests等于limits。Burstable等级是设置了requests或者limits但两者不相等或者有的容器设置了、有的没有。BestEffort等级是所有容器都没有设置requests和limits。实战含义很明确核心业务POD比如数据库、网关、注册中心一定要做成Guaranteed等级否则内存压力一来kubelet可能会把你最重要的业务给驱逐了虽然相比BestEffort它们直接被杀的概率小但Burstable等级中如果内存用量超过requests依然会被优先清理。我的习惯是核心服务一律requestslimits宁可多留一点资源也要保障稳定性。2.4 nodefs与imagefs不只是内存需要保护说到驱逐大家第一个想到的往往是内存但实际上kubelet的驱逐机制还覆盖了磁盘。磁盘相关的驱逐信号主要有两组nodefs.available和nodefs.inodesFreenodefs通常是根分区/或者kubelet的--root-dir所在分区里面存放着POD的日志、emptyDir卷和临时文件。imagefs.available和imagefs.inodesFreeimagefs是容器运行时存放镜像层的分区一般是/var/lib/docker或者/var/lib/containerd所在的磁盘。两个分区的地位不同。nodefs满了会导致POD无法创建、无法写入日志甚至kubelet自身都可能出问题所以它的驱逐阈值必须比imagefs更保守。imagefs满了主要影响镜像的拉取和垃圾回收影响面相对小一些但也不能放任不管。我生产环境的常用配置是这样的evictionHard: memory.available: 500Mi nodefs.available: 10% nodefs.inodesFree: 5% imagefs.available: 15% imagefs.inodesFree: 10%很多人会忽略inodes耗尽的问题。数据块没满但inode数量耗尽了文件系统一样报“No space left on device”而且这个问题比磁盘满更隐蔽因为df -h根本看不出来要df -i才看得到。所以配置驱逐阈值时最好把inodesFree也带上防患于未然。3. 实操配置从kubelet参数到压测验证全流程3.1 环境准备与前置检查配置之前先确认一下你的环境支持程度。Kubernetes的驱逐机制从1.18开始基本稳定如果是1.20以上的版本KubeletConfiguration结构和字段都更加完善建议至少用1.24以上的版本操作起来省心很多。第一步是确认kubelet当前的配置方式。现在主流发行版都是通过/var/lib/kubelet/config.yaml这个文件来配置KubeletConfiguration启动参数里只留一个--config指向该文件。如果你们的kubelet还是老式的纯命令行参数建议先升级迁移到配置文件方式因为纯参数方式没法配置完整的驱逐策略。查看现有驱逐阈值的方法是kubectl describe node node-name | grep -i -A5 Eviction如果输出里能看到类似memory.available500Mi这样的信息说明驱逐阈值已经在生效。如果没有看到任何驱逐相关输出说明当前使用默认策略默认值往往是memory.available100Mi和nodefs.available10%默认的100Mi几乎等于没有保护内存一紧张就是OOM Killer的舞台这种状态一定要调整。另外建议排查一下容器运行时类型是containerd还是docker这影响imagefs的路径判断。如果docker和containerd的数据目录都在根分区同一块盘上那么nodefs和imagefs的驱逐阈值应该统筹考虑不要分别设得很激进。3.2 修改kubelet驱逐参数下面是我在一台48C128G节点上实际使用的/var/lib/kubelet/config.yaml片段可以直接参考kind: KubeletConfiguration apiVersion: kubelet.config.k8s.io/v1beta1 evictionHard: memory.available: 500Mi nodefs.available: 10% nodefs.inodesFree: 5% imagefs.available: 15% imagefs.inodesFree: 10% evictionSoft: memory.available: 1Gi nodefs.available: 15% imagefs.available: 20% evictionSoftGracePeriod: memory.available: 1m30s nodefs.available: 2m imagefs.available: 2m evictionMaxPodGracePeriod: 120 systemReserved: cpu: 500m memory: 1Gi kubeReserved: cpu: 200m memory: 512Mi这里我做了几个设计决策内存的硬阈值设在500Mi软阈值设在1Gi中间留出500Mi的缓冲地带。因为128G节点内存够大1Gi不算什么但能防止page cache瞬时波动导致误驱逐。systemReserved留了1Gi内存和500m CPU这部分是给系统进程和内核用的。如果不提前预留逻辑上驱逐机制会把节点上除了POD之外的内存全算进可用量但实际上系统进程也要吃内存预留出来更安全。evictionMaxPodGracePeriod设120秒限制POD优雅退出的最大时间避免某些POD的terminationGracePeriodSeconds设置过长导致驱逐卡住。改完配置后重启kubelet并检查生效systemctl restart kubelet systemctl status kubelet kubectl describe node node-name | grep -i -A10 Eviction如果配置没有生效多半是config.yaml的字段名或apiVersion不对kubelet会拒绝加载甚至起不来。此时去journalctl -u kubelet日志里看具体报错。顺便说一句如果你的K8s集群是云厂商或者公司平台托管的可能不能直接改节点上的kubelet配置。这种情况一般是通过节点池的启动参数或者自定义kubelet配置来下发思路是一样的只是入口不同建议先看平台的节点池配置文档。3.3 压测验证驱逐用stress工具制造内存压力配置完不能光看一定要实际压测验证效果。我先说思路在一个节点上部署一个没有内存limit或limit较高的测试POD然后在这个POD里跑stress-ng把内存吃上去观察节点的驱逐动作。步骤如下创建一个测试POD里面安装stress-ng也可以用busybox镜像手动写脚本但stress-ng更省事kubectl run memory-stress-test --imagepolinux/stress-ng --command -- sleep 600 kubectl exec -it memory-stress-test -- bash在容器内压内存stress-ng --vm 2 --vm-bytes 3G --vm-hang 0 -t 120这个命令会启动2个虚拟内存stress进程每个吃3G内存持续120秒。如果你的节点可用内存只有5G左右压力很快就上去了。打开另一个终端实时观察节点状态kubectl top nodes kubectl describe node node-name | tail -50 journalctl -u kubelet -f | grep -i evict正常情况下你会看到MemoryPressure这个状态变成True然后kubelet日志里出现Evicting container相关的记录最后测试POD会被终止状态变为Evicted。查看POD状态和事件kubectl get pod -o wide | grep Evicted kubectl get events --sort-by.lastTimestamp | grep -i evict看到测试POD被Evicted说明驱逐机制正常工作了。压测时一定要选测试环境节点千万别在生产节点上直接压。3.4 配合监控告警从“事后驱逐”到“事前感知”驱逐机制解决了“内存不够时该怎么办”的问题但运维更希望提前发现问题、在驱逐发生之前就把内存风险处理掉。我建议给集群配上Prometheus那一套监控体系配合驱逐机制使用。关键监控指标有这么几个node_memory_Available_bytes节点可用内存量直接对应memory.available驱逐信号。kube_node_status_condition{conditionMemoryPressure}节点是否处于内存压力状态一旦为1就说明已经触发阈值。kube_pod_container_status_terminated_reason{reasonEvicted}记录所有因驱逐而终止的POD。node_filesystem_avail_bytes磁盘分区可用空间对应nodefs和imagefs。告警规则我来一条常用的- alert: NodeMemoryPressure expr: kube_node_status_condition{conditionMemoryPressure,statustrue} 1 for: 5m labels: severity: warning annotations: summary: 节点 {{ $labels.node }} 内存压力持续5分钟再配一条驱逐POD数量突增的告警- alert: ManyPodsEvicted expr: sum(increase(kube_pod_container_status_terminated_reason{reasonEvicted}[15m])) 3 labels: severity: critical annotations: summary: 15分钟内驱逐了超过3个POD监控只是辅助驱逐机制才是兜底。两者配合后你可以在节点内存使用率还不到危险线时就收到报警从容处理内存异常而不是在驱逐已经发生时手忙脚乱。4. 常见问题与排查技巧驱逐的避坑指南4.1 问题速查驱逐相关的典型症状与处理建议我在维护集群的过程中整理了一份驱逐相关的排查速查表先列给大家症状可能原因处理建议POD状态为Evictedkubelet驱逐触发先看节点MemoryPressure状态确认是内存还是磁盘压力再决定是否扩容或调阈值POD被驱逐后一直Terminating优雅退出超时、PDB阻止、某finalizer卡住kubectl describe pod看事件必要时kubectl delete pod --force --grace-period0强制删除节点MemoryPressure是True但内存不高百分比阈值设置过小、page cache波动、nodefs也同时紧张检查阈值配置建议用绝对量而不是百分比检查nodefs和imagefs占用驱逐POD后内存还是高系统进程消耗、page cache回收需要时间、有其他POD也在吃内存检查systemReserved是否预留用free -h配合ps aux --sort-%mem排查多个节点连续驱逐POD驱逐风暴被驱逐POD重调度到其他节点设置PDB、调整PriorityClass、使用descheduler或适当扩容节点4.2 POD被驱逐后一直Terminating如何排查驱逐过后POD显示Evicted是正常的但有时候会有POD状态一直卡在Terminating。我遇到过一次罪魁祸首是POD里的业务进程没有响应SIGTERM加上该Deployment又设置了PodDisruptionBudget要求最小可用数为1kubelet等了一段时间后发现不能驱逐就直接卡住。排查思路分三步先看POD事件kubectl describe pod pod-name -n namespace重点看Events里有没有FailedPrecondition、Evicted、DeadlineExceeded等关键词。再看kubelet日志journalctl -u kubelet -f | grep pod-name看驱逐过程中卡在哪一步。如果确认业务进程不会响应优雅退出或者PDB配置不合理可以考虑强制删除kubectl delete pod pod-name -n namespace --force --grace-period0。需要注意的是强制删除有丢失数据的风险尤其是POD里有状态存储的场景执行前要确认该POD的存储是否已经持久化。另外一个容易被忽略的点如果POD挂载了某个存储卷而该存储卷所在的后端存储集群也遇到压力POD的卸载或重挂载也会卡住驱逐过程同样走不完。这种情况要先去处理存储侧的问题。4.3 驱逐风暴与节点抖动如何避免雪崩最怕的一种故障是节点A内存满了驱逐了10个POD这些POD被调度到节点B和节点C结果把节点B和节点C的内存也吃满了又开始驱逐整个集群像多米诺骨牌一样一个个节点轮流失血。这就是典型的驱逐风暴。驱逐风暴很难单靠驱逐阈值解决它需要从容灾和调度策略层面入手给核心应用设置PodDisruptionBudget比如minAvailable: 2确保一次性最多只能驱逐1个副本留出至少2个在运行。为关键服务设置较高PriorityClass让kubelet在驱逐时把他们排在最后。为Deployment设置拓扑分布约束topologySpreadConstraints把副本尽量打散到不同节点避免单节点挂了把所有副本都带崩。条件允许的情况下为节点预留一定的资源余量比如让节点内存水位保持在60%以下给调度器留足重调度的缓冲空间。我生产环境里比较喜欢用topologySpreadConstraints它对驱逐风暴的抑制非常有效。因为它能保证同一份服务的副本不会堆在同一个节点上每个节点被分到的副本数量更均衡即使某个节点触发驱逐影响的面也不会太大。4.4 阈值设置不当导致的误驱逐与虚高报警驱逐阈值设置是个很有讲究的活设不好会有各种奇怪的表现。我早期踩过一个坑把memory.available的hard阈值设置成了5%。看起来没什么问题对吧但实际操作中发现节点一旦有批处理任务启动page cache会瞬间冲高working_set波动特别大available值经常在几个百分点之间剧烈跳动结果就是节点MemoryPressure频繁变TruePOD被反复驱逐。后来我总结出一个经验像内存这种抖动剧烈的资源尽量不要用百分比去设hard阈值用绝对量比如500Mi、1Gi更稳定。百分比适合磁盘这种波动相对平滑的资源。另一个问题是system-reserved没配置到位导致的虚高报警。有一次节点明明没有多少业务POD内存却持续告警后来排查发现是监控agent、日志采集器、sshd这些系统进程加起来吃掉了好几个G内存而kubelet计算available的时候没有把系统进程的占用单独扣除导致实际可用内存比预期少很多。解决办法就是在KubeletConfiguration里配置好systemReserved和kubeReserved让kubelet意识到系统组件的内存开销。4.5 几条生产环境的实战经验总结最后分享几条我一直在用的实战经验不算什么高深理论但都是实打实踩出来的核心业务POD一律做成Guaranteed即requests等于limits不给自己留“被优先驱逐”的借口。给所有POD设置内存limit哪怕是跑批任务也至少设个limit: 2Gi防止某个脚本把节点内存吃满。定期检查kubectl describe node里的Allocated resources和Events在POD数量变多、容量不足时提前扩容而不是等驱逐发生再处理。把驱逐阈值写入代码仓库GitOps思路谁要改阈值都要走评审避免有人为了压榨资源利用率把阈值调得极端激进。有条件的话在测试环境模拟一次“内存耗尽”演练让团队每个人都熟悉驱逐日志长什么样、事件怎么查、POD怎么恢复真出事的时候不会慌。驱逐机制不是一锤子买卖它需要根据业务特性和节点规格持续调优。没有一个参数组合适用于所有集群但把握住QOS等级、资源预留、软硬阈值这几个核心点再配合监控和演练你的集群在内存压力下就能保持稳定而不是等着内核的OOM Killer来点名。我个人的体会是处理K8s的内存问题重点不是“出问题了怎么紧急处理”而是“出问题之前怎么通过策略把影响范围降到最小”。驱逐机制就是这句话的最佳实践载体把底层内核那些不可控的行为变成平台层可配置、可预测、可观测的调度策略。把这个机制配好之后你会发现晚上接到的报警电话少了一大半即使偶尔真发生了内存异常POD也能在几秒钟内被自动驱逐并重新调度业务影响被牢牢控制在一个很小的范围内。
返回列表