1. 故障现场Pod像薛定谔的猫一样反复消失前几天线上K8s集群报警响了运维同事在群里甩了一张图某个核心服务的Pod频繁被杀重启状态持续了快一个小时。我接手排查第一反应是代码写炸了但看业务日志又没发现明显异常进程起来没多久就消失。后来定位发现问题根源不在业务代码而在K8s资源限制配置上。这个坑值得写出来因为很多人包括我自己都只关注了资源限制能防止Pod吃光节点却忽略了一个关键点资源限制设置得不对K8s自己的调度和回收机制也会反过来杀Pod。先说结论现象那批Pod显示状态是CrashLoopBackOffkubectl describe拉出来Last State是TerminatedReason是OOMKilledExit Code是137。看到这个组合问题基本就锁定在资源限制的范畴了。如果你也遇到过类似情况这篇文章能帮你少走弯路。主要内容包括资源限制里requests和limits到底怎么影响Pod存活、QoS等级决定Pod被杀的顺序、完整排查流程和常用命令、以及几个真实踩过的配置坑。这篇文章适合K8s运维、后端开发、以及所有给应用做容器化的同学。不管你是刚开始接触K8s还是已经在生产环境折腾过一轮里面的排障思路和命令都可以直接抄作业。2. 资源限制的两个维度requests和limits少一个都不行2.1 requests决定调度limits决定运行上限先讲最基础但最容易混淆的概念。在Pod定义里每个容器都可以配置两个维度的资源参数requests和limits。requests是至少预留的意思它告诉调度器给我这个Pod安排一个能满足这些资源的节点。比如一个容器requests.memory256Mi调度器会找一个剩余内存大于等于256Mi的节点来放它。这个值是调度时用的也是资源不足时抢占优先级的重要依据。limits则是最多能用多少它是硬上限。如果容器运行时的实际资源使用量超过limits指定的值K8s底层的cgroup就会强制执行限制。CPU超了会被throttle限流内存超了会直接触发OOM Killer把进程杀掉。很多人在配置时容易犯两个错。第一个是只设limits不设requests这样调度器不知道容器到底需要多少资源可能把Pod调度到一个剩余资源很少的节点运行时又因为limits限制被杀死形成调度没问题、运行就崩的诡异现象。第二个是requests和limits差距过大导致调度器认为节点资源很充裕疯狂往节点上塞Pod结果节点实际内存被打满触发节点级驱逐一批Pod跟着陪葬。举一个生活中比较好理解的例子requests相当于你去酒店订房间时跟前台说我要一个双床房前台得保证有这间房给你limits相当于入住后酒店规定房间最多住3个人你硬塞10个人进去酒店保安就会来赶人。前者管住不住得下后者管超不超员。2.2 QoS等级Pod在K8s里的社会阶级围绕requests和limitsK8s会把Pod划分成三个QoSQuality of Service等级这个等级直接决定了节点资源紧张时谁先被杀Guaranteed每个容器都设置了requests和limits并且两者相等。这类Pod等级最高资源紧张时最后才被杀。Burstable至少有一个容器设置了requests或limits但没满足requestslimits的条件。这类Pod是中产资源紧张时比Guaranteed先被杀。BestEffort所有容器都没有设置requests和limits。这类Pod基本就是路边摊节点资源一紧张第一个被清理的就是它们。我这次排查的Pod就是个典型的Burstable配置requests.memory256Milimits.memory512Mi。表面看弹性给得很合理但实际上业务是Java服务JVM堆内存加线程栈加元数据区正常跑起来常驻就要700Mi左右。512Mi的地板上限根本盖不住cgroup一看到内存用量超过limits立刻把进程杀了。更惨的是requests和limits不一样QoS是Burstable节点内存压力大时驱逐排序又往前靠了一档。这里有个容易被忽视的细节QoS等级不是只看Pod开头的配置而且要逐个容器判断。如果一个Pod里有多个容器每个容器都必须是requestslimits才能算Guaranteed。只要有一个容器不满足整个Pod的QoS就降级。很多多容器Pod比如业务容器和sidecar容器共存都会在无声无息中掉到Burstable排查时要特别注意。2.3 CPU是弹性压缩内存是一刀切资源限制还要区分一个更底层的特性CPU和内存对超限的反应完全不同。CPU是可压缩资源。容器超过limits.cpu时cgroup会限制CPU时间片容器表现为变慢、延迟变高但进程不会死。CPU的limits设置得越小throttle次数越多服务响应越慢。如果这个服务还挂着了就绪探针readinessProbe和存活探针livenessProbe慢到探针超时K8s就会认为容器不健康自动重启它。杀你的不是OOM而是慢性CPU饥饿导致的自愈机制。内存是不可压缩资源。容器超过limits.memory时没有降速这个选项cgroup直接杀掉进程。进程收到的信号是SIGKILL所以Exit Code会是137。与此同时内核日志里会留下Out of memory记录。如果你连logs都看不到业务异常输出大概率就是这里的问题。所以排查Pod被杀第一步不是去看业务代码而是看清Status、Reason、Exit Code和Events。这四个信息能把问题范围迅速收窄方向对了后面就是体力活。3. 完整排查过程从现象到根因3.1 用三个命令快速锁定问题范围接手一个Pod频繁被杀的问题我一般按下面的顺序执行命令每一步都能滤掉一批可能性kubectl get pod -o wide看Pod的当前状态挂在哪个节点上、重启了多少次。如果能看到CrashLoopBackOff基本说明是起来就死的循环。kubectl describe pod pod-name -n namespace看Last State和Exit Code。这一步能把问题分裂成三类业务异常退出、资源限制杀掉、被驱逐。kubectl logs pod-name --previoustrue -n namespace如果容器还在循环重启用--previous可以拉取上一轮进程退出前的日志。这个参数是很多新手不知道的默认的kubectl logs只能看到当前容器的输出而循环重启时当前容器往往不打印任何东西。我当时执行describe看到的关键信息是Last State: Terminated Reason: OOMKilled Exit Code: 137OOMKilled这个Reason是cgroup层面的明确信号含义是这个容器的内存使用量超过了它自身的limits.memory上限被内核杀掉。注意这不是节点内存不足导致的节点可能内存还很充裕单纯是容器自己的房顶太矮。接下来我用kubectl top pod -n namespace看实时使用量发现容器的内存使用量曲线一直在500Mi-800Mi之间波动而limits只给了512Mi。到了内存高峰使用量超过512Mi的瞬间cgroup触发OOM进程被杀然后重启再涨再被杀。这个循环的图谱跟业务日志里的看起来正常完全吻合——业务代码确实没有抛异常是底层资源裁决强制断电。3.2 区分OOMKilled、Evicted和Exit Code 137这里补充一个容易混淆的知识点。新手容易把OOMKilled和Evicted混为一谈但它们发生在不同层面OOMKilled容器自身使用内存超过limits.memorycgroup杀进程。跟节点其他Pod无关只跟本容器有关。Evicted节点层面的资源压力通常是全局内存不足或磁盘空间不足调度器/节点回收机制把Pod从节点上驱逐出去。触发原因是节点级的kubelet策略比如memory.available低于阈值或者nodefs.inodesFree不足。这类Pod的Events里会写明原因比如The node was low on resource: memory。Exit Code 137表示进程被SIGKILL杀掉但这只是表象它既可能来自cgroup OOM也可能来自节点OOM还可能因为手动kubectl delete时Pod被强制终止。需要结合Reason和节点内核日志才能完全确定。区分它们有一个实用技巧kubectl describe的Events里如果出现Killing而且Reason是OOMKilled说明是容器自身内存超限如果Events里出现Evicted或者FailedScheduling则要去检查节点状态和节点上的其他Pod。这次排查中我特意登录节点看了内核日志journalctl -k | grep oom输出里能看到Out of memory: Killed process的记录并且日志中标注了被杀的进程PID和对应的cgroup路径。cgroup路径里通常带有Pod的UID和容器名可以直接对应到具体是哪个容器的哪个进程被杀。这一步做完根因基本死锁容器的内存上限和实际需求严重不匹配。3.3 用监控曲线验证而不是拍脑袋调参排查到这一步有人可能会直接说把limits调大不就行了。但我建议先看一下监控曲线再把数值夯实。因为调大limits有两种可能一是上限确实小于实际需求调大能解决二是业务本身存在内存泄漏调大只是延迟爆炸时间。我当时打开Prometheus看的是这几个指标container_memory_working_set_bytes容器的真实内存工作集比container_memory_rss更能反映会被OOM计入的用量。container_memory_max_usage_bytes容器的历史峰值内存判断该把limits调到多少。container_cpu_usage_seconds_totalCPU累计使用量用于分析是否还有CPU throttle的问题。kube_pod_container_status_restarts_total重启次数时间线确认问题是否在某个节点状态变化或流量高点集中爆发。曲线出来后非常清晰Pod在每天流量高峰期都会内存爬升到峰值然后瞬间归零归零的节奏与重启次数完全重合。这说明不是偶发抖动而是规律性超限被杀。通过max_usage算出的峰值在780Mi左右考虑到安全余量我把limits调整到1Gi才是比较稳妥的选择而不是象征性调到600Mi。另外一个加分操作是看节点上的/sys/fs/cgroup/memory.eventscgroup v2或memory.oom_controlcgroup v1它们会记录OOM触发的次数和原因。虽然kubectl describe已经给了Reason但这类系统级文件能帮你确认是不是还有别的容器在同一个cgroup路径下被误杀。3.4 根因复盘为什么这个坑这么深复盘这次故障最核心的问题出在资源配额的设定逻辑上。团队当初给这个Java服务配limits时参考的是开发环境里JVM参数-Xmx512m。所有人都觉得JVM最大堆512M那容器内存给512Mi肯定够。但这是典型的只看堆内存不看全貌。Java进程在JVM堆之外还需要线程栈、元数据区、GC相关数据结构、JIT编译缓存、直接缓冲区。加上容器里可能还有其他子进程或sidecar-Xmx512m不等于整个容器只需要512Mi。在K8s中limits.memory限制的是整个cgroup的内存用量包括页缓存。就算JVM堆只用了512M页缓存、线程栈、网络缓冲区加起来很容易超过限制。而cgroup的OOM判定主要看实际分配的内存页不看JVM堆还剩多少。这个坑的本质是资源限制不只是一个数字而是与业务运行特征、语言运行时、节点调度策略耦合的综合问题。单看任何一面都会掉坑。4. 解决方案与配置参数把资源限制从坑变成护城河4.1 案例一内存型应用设置合理的内存上限对于本次Java服务我最终的修复配置是这样resources: requests: memory: 800Mi cpu: 500m limits: memory: 1Gi cpu: 1为什么这样定有三个依据监控峰值在780Mi左右limits给1Gi留出约30%余量应对GC抖动和流量毛刺。requests内存给到800Mi使得调度器能提前为这个Pod预留真实所需的内存避免节点超卖后在高峰时段出现节点级内存压力。requests和limits不完全相等QoS是Burstable。对于这类对延迟敏感的核心服务后续我其实更推荐直接做成Guaranteed即requests.memorylimits.memory、requests.cpulimits.cpu。虽然可调度性变低但在节点资源紧张时被杀风险最小。这里要特别提醒调整内存不是改完yaml就完事了要改的是需求评估方法。以后再有人问这个服务该配多少内存上限我会让他先跑一周监控拿出max_usage再说。没有监控数据支撑的资源限制基本等同于拍脑袋。4.2 案例二CPU limits过低引发雪崩另一个我遇到过的经典场景是CPU限制杀Pod。有个Go服务单实例平时CPU占用很低但秒杀活动时会跑满多个核。配置文件里CPU limits写的是500m相当于0.5个核。活动流量一来容器CPU被强制throttle大量请求积压响应时间从几十毫秒一路涨到几秒。存活探针的timeoutSeconds是3秒探针连续几次超时后K8s直接判定容器不健康开始重启。重启过程中流量又被分发到其他Pod形成雪崩。这种情况从kubectl describe看Last State的Reason是Completed或ErrorExit Code也可能不是137而是正常退出码但其实触发点是CPU饥饿。排查方法是在监控里看container_cpu_usage_seconds_total有没有周期性平台期以及container_cpu_cfs_periods和container_cpu_cfs_throttled_periods这两个指标中throttled的时间占比是否异常高。如果throttled period占比超过50%说明CPU限制形同虚设甚至压制了业务能力。CPU是压缩资源超限不会直接杀进程但会通过健康检查链路间接杀。很多K8s用户查Pod被杀只盯着内存和OOM忽略了CPU throttle这条隐蔽路径。我在实际排障中见过服务被重启时业务日志毫无异常最后定位到CPU throttle的案例不在少数。4.3 通用配置建议给经验不足的团队一个安全公式如果你的团队刚从传统部署迁移到K8s对资源需求没有数据积累我建议先用一套保守安全但不过分浪费的公式内存限额取监控max_usage的1.5倍且不低于当前配置的2倍。宁可先给足稳定运行后再逐步下调。CPU限额取监控平均CPU的2倍但要注意单核服务的CPU上限不要低于1000m。requests设置内存requests建议等于或略低于limits比如limits的80%CPU requests可以用实际平均值的70%到80%。推荐Guaranteed对于核心服务、有状态服务、主备切换依赖IP/域名不变的服务把requests和limits设成相等尽量把QoS拉到Guaranteed降低被驱逐概率。临时存储不能忘Pod声明里还可以配置ephemeral-storage的requests和limits不然容器写临时文件打满节点磁盘时会触发节点级驱逐连带整个节点的Pod遭殃。还有一点要提醒设置了limits后K8s会把Pod对应的cgroup写入内存限制但并不是所有语言运行时都会自动感知cgroup限制。Java的默认行为是在物理机内存很大的节点上按节点内存百分比计算堆大小。如果你只给容器设置了1Gi limits但节点有64G内存JVM可能把堆初始化成16G一启动就撞上cgroup限制被杀。这类问题在Java 8/9的老版本容器里特别常见解决办法是显式设置-Xmx、-XX:MaxRAMPercentage等参数让JVM读取cgroup限制而不是节点物理内存。配置示例resources: requests: memory: 1Gi cpu: 500m limits: memory: 1Gi cpu: 500m同时JVM启动参数配合java -Xms512m -Xmx700m -XX:MaxRAMPercentage65.0 -XX:MaxMetaspaceSize256m -jar app.jarMaxRAMPercentage加上MaxMetaspaceSize的组合可以确保JVM整体内存占用被压在一个可控区间而不是无脑吃满节点内存。4.4 配角也很重要sidecar容器的资源限制前面提到QoS是按Pod中所有容器共同计算的这里再展开讲一个大坑。很多Pod会带sidecar容器比如Istio的Envoy、日志采集agent、配置热更新agent。主容器配置完整但sidecar容器经常是裸奔的既没有requests也没有limits或者只有limits没有requests。这种配置会让Pod在整体QoS评估时掉档。假设主容器是requestslimits的Guaranteed只要sidecar是BestEffort整个Pod就变成Burstable。节点内存紧张时本来不该被杀的核心服务因为sidecar的连累反而被优先驱逐。我曾经帮一个团队排查过奇怪的Pod漂移现象每隔一周左右Pod就会从节点A漂移到节点B没有OOM没有CrashLoopBackOff就是Evicted。查到最后发现是日志采集sidecar没设limits在日志量大的时候把节点临时存储打满触发了nodefs驱逐整个Pod被连带清理。所以配置资源限制时要把Pod里的每个容器都纳入考虑不能只管主容器。统一给sidecar设一个合理的requests和limits哪怕稍微保守一点也比完全不设要好。5. 常见问题与排查技巧实录5.1 排查命令速查表把这次排查中高频使用的命令整理成一张速查表遇到Pod被杀问题时按顺序执行命令作用关键输出kubectl get pod -o wide查看Pod状态和所在节点CrashLoopBackOff、Evictedkubectl describe pod pod -n ns查看退出原因和事件Last State、Reason、Eventskubectl logs pod --previoustrue -n ns拉取上一轮退出前日志业务侧是否有异常kubectl top pod -n ns查看Pod实时资源用量内存/CPU实时值kubectl top node查看节点资源使用率是否节点级超卖journalctl -k | grep -i oom登录节点查看内核OOM记录Out of memory: Killed processcat /sys/fs/cgroup/memory.eventscgroup v2 内存事件oom_kill计数kubectl get events --sort-by.lastTimestamp查看集群事件时间线驱逐/调度失败记录5.2 常见问题速查表现象可能原因排查方向Exit Code 137ReasonOOMKilled容器内存超过limits调大limits或优化内存占用Exit Code 137ReasonEvicted节点内存/磁盘不足检查节点资源、其他Pod占用进程没被杀但频繁重启CPU throttle导致健康检查失败看throttle指标调大CPU limitsThe node was low on resource: memory节点内存压力过大增加节点或降低Pod requestsThe node was low on resource: ephemeral-storage临时存储超限清理日志、配置临时存储limits启动即死日志无明显异常JVM/运行时未感知cgroup限制检查-XX:MaxRAMPercentage等参数5.3 三个真实的我以为不会犯的坑第一个坑是把limits当成运行标配而不是紧急预案。很多人配limits的初衷是防止内存泄漏拖垮节点但在业务高峰期这个limits反而成了业务扩张的天花板。调大limits之前一定要确认请求量上涨带来的内存增长是否还在良性范围内不能一超限就调否则会掩盖内存泄漏问题。第二个坑是只调Pod配置没看节点分配。某次我调整完limits后Pod还是被杀点开节点指标发现节点本身内存已经用了95%。这种情况下就算Pod上限给到很大节点级驱逐依然会执行。资源限制是Pod和节点两个层面的共同约束只修一头没用。第三个坑是以为重启就好了。CrashLoopBackOff的自愈逻辑确实会自动拉起Pod但如果没有找到并消灭根本原因每次重启都只是无限循环。我在排查中习惯把每个被杀Pod的Events截图留档对比多次事件的规律。比如不同节点的Pod被杀时间是否集中在同一时段是否与某些慢查询、大流量、定时任务重合。规律越明显根因定位越快。我觉得排查K8s Pod被杀问题最怕的不是技术复杂而是方向错了还在努力。先把describe和日志看明白再结合监控曲线和数据做判断资源限制这个坑是完全可以避免的。现在再遇到Pod又双叒叕挂了的告警我会先问三个问题Exit Code是多少OOM还是Evicted监控曲线有没有规律这三个问题问完一半的答案已经在我心里了。