
K8s集群里最磨人的告警不是CPU飙到99%也不是磁盘快满了而是POD在那里反反复复重启——你以为它挂了它又起来了你刚想忽略它它又挂一次。更麻烦的是重启本身有时候是自愈机制在正常工作有时候却是一个正在恶化的隐患。我见过不少刚接触K8s的同学一看到CrashLoopBackOff就慌先把POD删了再说结果业务越删越乱。这篇文章就用一次真实排查经历做主线把POD重启问题的完整排查链路讲清楚包括怎么从错误码和事件里提取线索、常见重启原因的分类和证据形态、以及如何把一次排障转化成长期的监控和治理手段。适合正在维护生产集群的运维、或者刚从传统部署切到K8s的开发者看。1. 先分清三层状态POD重启到底属于哪种“死法”排查任何问题之前先定义问题。POD“重启”是一个结果表述它背后的机制完全不同有的是容器进程自己退出被kubelet拉起有的是被探针判定不健康后强制杀死有的是因为节点压力被驱逐后重新调度还有的是镜像拉取失败导致根本启动不了。你如果不先弄清楚到底是哪一种后面所有的排查方向都是跟着感觉走效率极低。1.1 从CrashLoopBackOff和容器退出码提取第一线索拿到一个反复重启的POD我一般先不看业务日志先看状态。kubectl get pod会直接告诉你它当前卡在哪个阶段最常见的就是CrashLoopBackOff。这个词的意思是容器启动后短时间内退出kubelet尝试重启重启后再次退出于是进入一个指数退避的循环——10秒、20秒、40秒、80秒……所以你会发现它越重启越慢这不是系统变卡了是它在保护节点资源避免自杀式循环消耗CPU。但CrashLoopBackOff只告诉你了“在崩溃循环”没告诉你“为什么崩”。这时要看容器的退出码。屏幕前的你可能用过docker run看到过exited with code 1K8s里同理容器退出时有一个exit code代表进程以什么状态结束。排障第一步就是把这个数字查出来kubectl get pod pod-name -n namespace -o yaml在status.containerStatuses下面能看到lastState.terminated.exitCode。我再给你我常用的退出码对照参考退出码常见含义排查方向0正常退出可能是任务完成或主动关闭检查是否有进程主动调用exit或探针/脚本触发了关闭1通用错误看业务日志第一行报什么错一般是应用自身启动失败2参数错误/语法错误检查启动命令、脚本语法、配置文件解析127命令找不到启动命令里的可执行文件路径不对或镜像里根本没有这个命令137被SIGKILL杀死内存溢出OOM最常见也可能被手动kill143被SIGTERM终止优雅停机超时后被kill或探针强制杀进程137/135组合疑似OOM必须去describe确认是否为OOMKilled我踩过一个典型的坑有个服务一直显示137我第一反应就是内存爆了结果仔细看describe才发现是节点上的其他进程吃完了内存把Pod挤死了。所以说exit code只是线索不是结论它指向哪个方向你就要去那个方向找证据。1.2 不只是崩溃Completed、Evicted、Failed也会造成重启记录很多人有个误区以为只有CrashLoopBackOff才算“重启”。实际上你在kubectl get events里看到Pod被反复重建还有另外两种情况容易被忽略:第一种是Evicted。节点内存或磁盘资源不足时kubelet会根据优先级驱逐POD。被驱逐的POD不会原地重启而是进入Failed状态后重新调度到其他节点。如果调度后又遇到同样的问题你就会看到一个Pod走马灯一样在不同的节点上挂掉重生。这种情况单看kubectl get pod很容易误判为“莫名其妙重启”实际上问题在节点层面。第二种是Completed之后被重启。如果你的控制器是Deployment或StatefulSet容器主进程正常退出后exit code 0控制器会认为这个实例已经终止然后依照副本数要求重新创建一个POD于是你也看到了“重启”。尤其是有那种跑批任务的镜像跑完就退出如果被塞进了Deployment就会无限循环重建。这种最坑——因为业务日志显示一切正常怎么查都查不出异常最后才发现是把一次性任务错当常驻服务来部署了。所以拿到“POD重启”这个现象第一件事永远是问这个POD现在的RESTARTS字段是加在同一个容器上原地重启还是AGE变成了新的重建这两个是完全不同的排查路径。1.3 用kubectl describe把重启前后的时间线拼出来明确状态之后下一步是把时间线拼起来。kubectl describe pod pod-name -n namespace是我在排障时使用频率最高的命令没有之一。它下面的Events字段记录了最近的事件比如Scheduled调度成功Pulled镜像拉取成功Started容器启动Killing容器被终止Unhealthy探针判定不健康OOMKilled内存超限被杀死读Events的时候是有技巧的注意时间排序和相互间隔。例如你看到Started之后几秒钟就出现Unhealthy然后Killing这就说明容器启动本身没问题但对于“健康”的定义没过关。如果你看到Pulled反复出现说明镜像在反复拉取那问题可能在镜像仓库或镜像标签。还有一个容易忽视的字段是containerStatuses.restartCount。注意看它是持续增长还是稳定不变。如果重启次数在增加但Events里没有新的Killing事件可能要去看是不是有别的控制器比如运维平台、Operator在背后悄悄操作这种情况我后面会展开讲。2. 四大典型重启场景从证据到根因的完整排查链路症状是表象根因才是治疗的靶点。根据我接触过的生产案例POD重启的根因九成以上能归到四类内存超限被杀、探针误杀、节点驱逐、外围依赖失效。下面这四节每一节都按“判定依据 → 为什么发生 → 怎么确认 → 怎么修复”来讲你可以当成一份菜单来对照自己的症状。2.1 OOMKilled内存限制设得太死是头号杀手判定依据kubectl describe里Last State的Reason为OOMKilledExit Code为137。或者Events里直接出现OOMKilled。为什么容易发生K8s通过容器的limits.memory来限制内存上限而limits通常和requests一起配置。后端的机制是cgroup给容器设置一个内存上限一旦容器内存使用量超过这个值内核会触发OOM killer把超出限制的进程直接杀掉。你在宿主机上可能看到内存还很充裕但容器还是被杀——因为限制不是看节点剩多少内存而是看cgroup内用了多少。很多业务代码有内存尖峰平时1G够用一到大促或大批量处理时就涨到2G如果不设limit倒没事设了1G就会反复被杀。怎么确认除了看describe的OOMKilled以外还有一个高级确认法看container_memory_working_set_bytes监控曲线。如果这个指标在容器被杀前呈陡增并顶到limit线那么根因基本锁定。如果指标还没到limit就被杀那可能是节点本身内存不足触发了系统级OOM要去看宿主机dmesg里的oom-killer记录。怎么修复临时方案把limits上调但不要拍脑袋调建议结合历史监控数据的P99内存用量×1.5来设置。你还可以开启kubectl exec进去跑一下进程商用的诊断命令看堆内存占用。如果是JVM类应用还要注意它是否向OS申请了远超实际使用的内存——我之前有个Java服务-Xmx设置成4G但实际堆占用只有1.5G没限制时SGA疯涨把容器内存打爆了。调优的方向是让-Xmx和容器的limits.memory配合给JVM留足堆外内存Metaspace、线程栈、DirectBuffer等一般建议容器limit是-Xmx的1.21.5倍。2.2 Liveness探针误杀服务还活着但K8s觉得它死了判定依据Events里有Unhealthy且事件显示的探针类型是livenessProbe后面跟着Killing。容器退出码取决于探针触发的杀进程方式有时是137有时是143取决于进程收到什么信号。为什么容易发生Liveness探针是用来判断容器是否“死锁”的如果它失败kubelet会杀容器并重启。问题是很多人把Liveness和Readiness的语义混在一起或者参数设置不科学。最典型的三个错initialDelaySeconds太小容器启动需要15秒你设了5秒探针在应用还没监听端口时就请求直接失败然后杀进程重启重启后又是这样CrashLoop。periodSeconds太短每2秒探一次一旦服务偶发GC停顿或慢查询超过timeoutSeconds就判定失败误杀。探针路径和服务真实状态脱节比如健康检查接口是/health但这个接口内部又依赖数据库或下游服务数据库稍微抖动接口就返回5xx——但容器本身进程是健康的数据库恢复后业务也能自愈这时候Liveness一刀切下去反而把一个本来能自愈的服务杀掉了。怎么确认拿到Unhealthy事件后别急着改探针先去确认事件发生时应用是否真的不可用。看两条证据一是应用日志在Killing前后有没有正常请求处理记录二是看这个POD当时有没有被readiness探针摘除流量如果Events里没出现Unhealthy对应的readiness说明请求可能还在持续进来。如果业务日志在事件前后没有任何异常几乎可以断定是探针参数设置不当。怎么修复合理调整探针参数。我贴一组通用配置思路供参考livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 20 # 根据应用启动实测时间预留20%余量 periodSeconds: 15 # 不要太频繁15秒一次很合理 timeoutSeconds: 5 # 单次探活超时5秒足够 failureThreshold: 3 # 连续3次失败才杀避免偶发抖动误杀 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 2 # 连续2次失败就摘流量敏感些没关系这里有个关键点探针路径最好是一个轻量的本地状态检查不要包含太多下游依赖判断。你可以在应用里把健康检查接口拆成两个——/health/live和/health/ready前者只测进程存活后者才去做依赖检查liveness指向livereadiness指向ready。这个习惯能帮你避免掉一大批“躺枪式重启”。2.3 节点压力驱逐与资源竞争POD被从节点上“请走”了判定依据kubectl get events里出现Evicted或NodeHasDiskPressure、NodeHasMemoryPressure随后POD状态变为Failed然后在另一节点重新Scheduled。kubectl describe node node-name能看到Conditions里的压力标记。为什么容易发生节点是共享的POD的资源请求只是“预约”而不是“占用”。当节点实际使用量超过预留能力时kubelet会触发驱逐。驱逐顺序是先淘汰BestEffort没设requests/limits的再淘汰Burstable最后才会动Guaranteedrequestslimits的。如果你发现自己的POD比其他POD更容易被驱逐先看看自己是不是属于Burstable这一档或者干脆没设置limits那会被当作BestEffort。除了内存和磁盘压力还有一种被很多人忽略的是PID压力——节点上跑了太多进程/sys/fs/cgroup/pids达到上限也会导致新POD无法启动然后一直被调度到其他节点表现为“重启不断但都没起来”。怎么确认除了看节点Condition还有一个关键工具是看kubelet的日志。在节点上执行journalctl -u kubelet -n 100 --since 10 minutes ago搜索关键词evict或者pressure能看到kubelet驱逐时的完整决策过程和原因。另外别忘了看被驱逐POD之前所在节点的dmesg有没有其他进程大量占用内存或IO这些信息能帮你判断是节点本身的问题比如跑了大任务还是系统预留不足。怎么修复这类问题没有“一次调参解决”的捷径要做的是资源配置治理为每个工作负载设置合理的requests和limits不要用裸奔式部署。给系统组件如kubelet、dockerd、网络插件预留系统资源在kubelet启动参数里配置--system-reserved和--kube-reserved。给节点打标签划分环境不让测试任务和核心业务混部避免资源竞争但最治本的还是监控节点资源水位在触顶之前扩容节点或调度削峰。2.4 镜像、存储、外围依赖失效还没跑到业务层就倒下了判定依据Events里出现Failed to pull image、ImagePullBackOff、FailedMount或MountVolume.SetUp failed。容器可能处于Waiting十几分钟然后被拉起重试。为什么容易发生这类重启和业务代码一点关系都没有问题出在POD“出生”那一刻的依赖条件上。镜像仓库拉取慢或权限不足导致容器一直启动不了PV存储卷挂载失败导致容器启动后读写盘报错DNS、时间同步等系统级依赖失效导致应用启动后马上因为网络初始化失败而退出。我遇到过最离谱的一次是某个POD反复重启查了半天才发现是镜像仓库里同一个tag被覆盖成坏镜像拉到一半就EOFPOD被反复拉起拉取而缓存镜像又是旧的——这种镜像不可变性问题在CI/CD流程里尤其常见。怎么确认kubectl describe能帮你定位到具体阶段。另外观察一个细节如果POD在Pending和ContainerCreating之间反复切换优先怀疑存储或镜像拉取问题如果状态已经变成Running但几秒后退出再去怀疑应用启动时的外围依赖比如连数据库、初始化配置文件等。怎么修复镜像层面用不可变tag替换latest配置镜像拉取超时和重试策略存储层面检查PV/PVC的容量、StorageClass配置、节点上的挂载点是否残留必要时手动mount -a验证DNS层面检查CoreDNS的副本数和日志。这一类问题虽然不复杂但排起来最烦人因为现象和根因之间隔着好几层。3. 一次生产事故完整复盘压测扩容引发的POD连环重启前面讲的是原理和分类这一节我用一个去年实际处理过的案例把从报警到修复的完整流程串起来。案例不复杂但它几乎同时踩中了探针、内存、资源竞争三个坑对理解“多因一果”的排查很有帮助。3.1 异常表现从监控告警到第一手现场证据事情发生在一个大促前的压测阶段。当时我们对某个核心订单服务进行扩容从4个副本加到10个压测一跑监控页面上突然弹出大量重启告警告警粒度到了POD级别大概每分钟有三四个POD在重启。我第一反应是看kubectl get pod状态栏里混着CrashLoopBackOff和Running但Running的POD里也有好几个RESTARTS已经累计到十几。我随即用describe拉了一个典型POD的Events。时间线是这样排的09:30:01Started09:30:05UnhealthylivenessProbe09:30:06Killing09:30:40Started第二次启动09:30:44Unhealthy09:31:20OOMKilled这里能看出两个信号第一启动后仅4秒就被探针判死明显是initialDelaySeconds不足第二在探针事件之后还出现了OOMKilled说明内存也超标了。前者是“教唆犯”后者才是“真凶”——因为探针太急躁容器还在进行初始化就杀了导致业务线程还没完成缓存预热就被终止下一次启动时缓存未命中内存被重复加载直接冲破了limit。这就是多因叠加的效果。3.2 定位过程探针、资源、日志三层逐步排除我先看日志kubectl logs pod-name --previous能拿到上一次退出前的日志。日志显示应用启动流程正常走到了“开始加载分布式缓存”然后就没有后续了——也就是进程在预热阶段就被杀了。这就把探针误杀的范围锁定在启动过程。然后看资源监控。内存用量曲线在重启前确实从300M一路涨到900M但limit是1G照理说不至于OOM。问题出在哪我特意看了下同一节点上其他POD的占用发现压测扩容后新调度的两个POD刚好落在同一台机器上而那台机器本身还跑着几个没设limits的老服务节点内存使用率已经到89%。这时候limits虽然标了1G但节点整体可分配内存不足内核在节点层面杀掉了超得最离谱的进程表现就是OOMKilled。最后核对探针配置。服务启动时间经过实测大概1215秒预热缓存但liveness的initialDelaySeconds用的是默认值10秒timeoutSeconds又是2秒这种配置下稍慢一点就误杀。三层线索拼在一起根因清楚了探针过早介入预热期内存尖峰节点资源紧张三者共同把POD推向了重启循环。3.3 根因确认与快速止血方案快速止血我用三步完成第一步先给这批POD换个节点池调度把压测流量引到资源更富裕的机器上避免和没设置limits的老服务混布。第二步调整探针参数initialDelaySeconds改到25秒timeoutSeconds改到5秒periodSeconds改到15秒同时增加startupProbe专门给预热阶段兜底。第三步给那几个裸奔老服务补上requests/limits防止它们继续蚕食节点资源。这步是比较费劲的因为要业务方配合压测但关键在于如果不补只要再扩容一次同样的表面现象就会重新出现。稳定运行半小时后重启次数归零压测吞吐恢复预期。过了两天我又回去看了下监控确认没有复发才把方案固化为配置模板。事后复盘的时候我把这次事故的时序、止损动作、根因验证三条线各自拉了出来对应到监控、探针、资源三个模块分别补了体检项。4. 把“问题排查”变成“问题预防”排障后的长期修修补补一次排障成功如果不沉淀成预防体系下次换个壳照样踩坑。这一节聊聊我在排完这类问题之后一定会做的三件事——监控补全、探针治理、资源配额化。任何一项都不是一次性动作而是持续的配置运维习惯。4.1 给监控补上重启次数和事件采集很多团队的基础监控只有CPU、内存、磁盘这种“猪均指标”看不见POD级别的重启趋势等告警响了往往已经重试了一二十次。我的建议是基础监控至少加两块一是kubelet的container_restart_count指标。Prometheus配上这组告警规则- alert: PodRestartingFrequently expr: rate(kube_pod_container_status_restarts_total[15m]) 0.5 for: 10m labels: severity: warning annotations: summary: POD重启次数异常 ({{ $labels.namespace }} / {{ $labels.pod }}) description: POD在15分钟内重启超过0.5次/分钟即累计超过7次请检查探针和日志。二是事件采集。事件在K8s里默认只保留一小时不接采集等于丢证据。我推荐用eventrouter或kube-eventer这类工具把Events全部送进日志系统或对象存储里排障时能直接按时间回溯——这一步在追那些“半夜悄悄重启了一次”的诡异问题时特别好使。4.2 用探针设计规范避免误杀我见过很多团队的探针配置都是从网上复制来的完全没有结合自身服务特性。长期来看探针的设计应该固化到发布流程里。我的建议是以服务启动时间、优雅停机时间、健康检查接口的真实语义为事实基础形成一套可检查的配置模板。启动慢的服务必须配startupProbe它的failureThreshold * periodSeconds要大于实测冷启动时间。探针接口的语义要拆分成live和ready两层live层只做进程自检ready层才去查依赖。还要建立评审机制凡是新增或修改探针的MR必须附带启动耗时截图和压测下的探针失败率数据没有证据不给合入。4.3 资源配额与节点预留的治理清单最后聊资源。前面已经说过OOM、驱逐和资源竞争经常纠缠在一起所以资源配置的治理要当作持续事项来做。我习惯用一张清单逐项过所有生产工作负载是否有requests和limitsrequests不能拍脑袋要按监控的P95用量上浮20%30%。对每个namespace设置ResourceQuota防止某个团队无节制扩容把节点吃穿。节点层面配置--system-reserved给系统进程、网络插件、容器运行时留足余量否则节点被压垮时所有POD都会遭殃。定期用kubectl top nodes和监控系统核对节点水位如果某个节点持续超过80%主动疏散POD或扩容。这套治理不复杂难的是一以贯之地执行。但只要你认真做一轮之后再遇到POD重启大部分情况都能在五分钟里定位到根因而不是又一次陷入通宵捞日志的循环。我自己的体会是K8s排障的本质是“证据链推理”而不是凭感觉猜。每次重启背后都有明确的信号——退出码、事件、时间戳、监控曲线把这些证据摆齐了原因自己就会浮出来。如果你也在被POD重启折磨建议先从kubectl describe那一步开始把你手头那个POD的完整Events贴到文档里对着这篇文章的分流图逐项排查。你可以试着给它配上startupProbe再观察一个周期这个不起眼的参数能省下你大量凌晨被电话叫醒的时间。