ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Kubernetes Pod反复重启?从退出码到探针的排障全指南

Kubernetes Pod反复重启?从退出码到探针的排障全指南 早上九点多监控群里的告警就把我拽起来了订单服务在生产环境的PodRESTARTS已经涨到37。刚开始群里还有人说是“临时抖动先观察”结果两个小时之后Pod又重启了一轮业务开始出现间歇性超时。类似场景在Kubernetes环境里真不算新鲜Pod反复重启是排障频率最高、也最容易误判的问题之一。这篇文章我想把排查Pod重启的完整思路从头捋一遍从底层机制到实际取证手段再到几个我踩过的坑和可复现的排查步骤。不管你是刚入门Kubernetes、还在读Pod基础概念的同学还是已经被线上POD重启折磨到焦头烂额的运维按这个顺序走一遍基本能把大部分问题定位出来。1. Pod为什么重启先理解“重启”背后的机制1.1 容器退出不等于应用崩溃Kubernetes里最小的调度单位是Pod真正跑业务进程的是Pod里的容器。每个节点上都有kubelet守护进程时刻盯着本机容器的运行状态。容器主进程一旦退出kubelet就要根据Pod的restartPolicy决定要不要把它重新拉起来。Deployment创建的Pod默认restartPolicy是Always翻译成人话就是“不管容器因为什么原因退出都先重启了再说”。所以你在kubectl get pod里看到的RESTARTS字段严格意义上表示“容器主进程退出并被重新创建的次数”而不是“应用崩溃次数”。这两个概念经常被混为一谈但排障时必须分清楚。这个区别直接决定了排查方向。容器被重启可能是因为进程自己的逻辑错误比如启动时配置读错、端口被占、依赖没起来也可能完全不是应用的问题比如宿主机内存不足触发内核OOM Killer、Liveness探针连续失败被kubelet强制杀掉、节点磁盘压力触发了驱逐。如果一上来就盯着应用日志读很容易被日志里海量的“常规报错”带偏浪费大量时间。不管Pod里跑的是Nginx、Java服务还是SAP这类重量级业务系统看容器退出原因的方法都是一样的先看机制再下结论。1.2 Pod重启的几种典型状态在kubectl get pod的输出里STATUS列往往直接透露了问题类型。我把最常见的几种状态整理成了一张速查表排障时可以对着看状态/事件现象典型根因CrashLoopBackOff容器启动后几秒内退出事件里反复出现Back-off restarting failed container启动命令错误、配置错误、依赖服务未就绪、主进程没有常驻前台OOMKilled容器的Last State里Reason是OOMKilled退出码137内存limit设置过小、应用内存泄漏、JVM堆外内存超限UnhealthyEvents里反复出现Liveness probe failed随后容器被kill探针路径/端口配置错误、超时太短、服务真的不健康EvictedPod状态变为Failed或Evicted节点上有驱逐事件节点内存、磁盘或PID资源压力过大ImagePullBackOff容器一直处于等待拉取镜像的状态镜像名不存在、仓库认证失败、网络无法访问镜像仓库要特别提醒的是这些状态经常叠加出现。比如探针失败把容器杀了一次容器重启后因为配置问题又起不来最终呈现出来的就是CrashLoopBackOff。只看STATUS列是不够的得结合后面的事件时间线来综合判断。状态只是表象事件才是根因所在。1.3 RestartPolicy和退出码先排除方向性问题Pod的restartPolicy一共有三个值Always、OnFailure、Never。Deployment和ReplicaSet管理的Pod默认是AlwaysJob类任务默认是Never或OnFailure。排障时先确认一下当前Pod的restartPolicy是否符合预期防止有人临时改动后忘了恢复。我就遇到过一回某同学为了测试方便把Pod的restartPolicy改成了Never结果Pod挂掉之后完全没有重启线上恢复全靠手动拉起来直到监控报警才发现属于自己给自己挖坑的典型。退出码则直接反映容器主进程是被谁结束的。看退出码时需要跟Last State里的Reason字段一起看因为在Kubernetes里同一个退出码也可能对应完全不同的原因。常见的几个退出码如下表所示退出码信号/含义常见场景0正常退出主进程主动退出。业务上不该退出时往往是启动脚本用后台方式启动子进程后自己退出了1程序内部错误配置错误、启动失败、运行时未捕获异常1301282SIGINT收到CtrlC或人为中断1371289SIGKILLOOM被杀或者外部kill -914312815SIGTERM收到优雅终止信号常见于滚动更新、节点驱逐看到137不要立刻断定是OOM先看Reason是不是OOMKilled看到143也不要急着怪应用先想这个时间点是不是正好在做Deployment滚动更新或者节点是不是正在驱逐Pod。方向判断对了后面才不至于白忙。2. 第一现场取证别急着翻日志2.1 先看RESTARTS再看节点分布拿到告警后的第一件事我建议先执行这条命令kubectl get pod -n namespace -o wide主要关注四个字段STATUS、RESTARTS、AGE、NODE。RESTARTS如果在持续上涨说明容器正处在“重启循环”里事态比较紧急如果RESTARTS已经固定住说明当前容器稳定运行了一段时间之前发生过重启可以稍微放缓节奏分析。AGE和RESTARTS放在一起看更有价值一个刚创建10秒就重启了20次的Pod和一个跑了三天只重启1次的Pod背后的问题逻辑完全不一样。前者多半是配置或启动命令有问题后者多半是内存泄漏、依赖劣化或节点压力。不要忽略NODE字段。如果多个异常Pod恰好分布在同一个节点上那问题可能不在Pod本身而在节点。先把节点的名字记下来后面大概率用得上。这个习惯在排查大规模“集体重启”时尤其重要可以帮你快速从“逐个看Pod”的琐碎工作里跳出来。2.2 describe里的Events时间线是核心证据这是整个排障过程里最关键的一步kubectl describe pod pod-name -n namespace输出内容很多但重点就两块。第一块是Containers段看里面Last State的描述。如果Last State显示Terminated会给出Reason和Exit Code比如Reason是OOMKilled、Exit Code是137基本可以直接锁定内存问题如果Reason是Error、Exit Code是1那大概率是应用自身的启动逻辑出了问题。第二块是Events段它记录了Pod从创建到现在的关键事件调度到哪里、镜像有没有拉成功、容器有没有启动、健康检查是否失败、被谁杀掉、为什么要重启。从下往上读Events基本就是在重放这个Pod的“案发现场”。使用describe时有两个细节要小心。Events是有限时保留的过了大概一小时早期的事件会被清理掉所以遇到线上事故第一时间先把describe输出保存成文件再慢慢分析。另外长时间运行的Pod事件非常多describe里展示的可能是不完整的尾部记录。想看更完整的事件列表我用这条命令kubectl get events -n namespace --sort-by.lastTimestamp它会列出该命名空间最近的所有事件并按时间倒序排比describe展示的信息更全。我甚至遇到过靠事件里的FailedScheduling直接定位到“节点CPU资源不足”的案例这类信息藏在describe的尾部时很容易被忽略但单独查事件时一目了然。2.3 日志要看“上一次”而不是“现在”一个非常常见的误区是容器崩溃后直接执行kubectl logs查看日志。问题在于容器崩溃之后kubelet已经重新创建了一个新容器此时你看到的是新容器的日志。如果新容器几秒内就再次崩溃你只能看到它刚刚启动时那点可怜的输出真正导致崩溃的报错根本没机会出现。正确做法是加--previous参数读取上一次容器的日志kubectl logs pod-name -n namespace --previous如果是多容器Pod还要加上-c参数指定崩溃的是哪个容器kubectl logs pod-name -n namespace -c container-name --previous举一个实际场景一个Java服务反复崩溃用户查看当前日志只能看到JVM启动横幅什么报错都没有加上--previous之后立刻看到了Spring Boot启动时报“数据库连接池初始化失败”的完整堆栈根因一瞬间就明了了。这个参数值得被刻进脑子里。如果应用没有把日志写到stdout或stderr而是写进了容器的日志文件那容器一旦重建文件系统里的数据就会消失再exec进去也找不到。这种情况下可以到节点上用容器运行时命令查看比如containerd环境crictl ps -a | grep pod-name crictl logs container-idcrictl logs不仅能获取当前容器的日志也能拿到已经退出容器的日志是补救最后一个证据缺口的重要手段。2.4 抓不到应用日志时转向kubelet日志偶尔会碰到一种更头疼的情况容器启动就崩或者崩得太快连日志都没来得及写。此时应用日志这条线索基本断了需要去节点上看kubelet的日志。kubelet是节点上管理Pod和容器的核心组件它杀容器、重启容器、驱逐Pod时都会留痕。journalctl -u kubelet -f在kubelet日志里经常会看到类似这样的记录Pod容器被OOM Kill、Liveness探针检查失败、Failed to create pod sandbox、Back-off restarting failed container。这些信息是kubelet视角的故障说明粒度虽然不如应用日志细但足够帮你把排查方向收敛。如果kubelet日志里出现大量与容器运行时交互失败的错误还要顺带确认一下containerd或者docker服务本身是否正常有时候容器运行时挂了Pod也会表现为反复重建这时问题的根源根本不在应用。3. 高频根因拆解配置、探针、资源3.1 ConfigMap/Deployment配置变更引发的“重启风暴”生产环境里一大半Pod重启的根因都和配置变更有关。最常见的流程是这样有人改了ConfigMap里的数据比如数据库地址、开关标识、超时时间改完后Deployment因为模板没有变化所以不会自动滚动更新于是又手动删掉一批Pod或者执行了rollout restart。新Pod起来后读取的配置却是坏的或者配置指向的依赖服务还没就绪进程一启动就报错退出形成“启动-崩溃-重启-再崩溃”的死循环。这类问题最明显的特征是事件里反复出现Back-off restarting failed container或者新Pod的启动日志里直接抛出配置解析错误、连接失败之类的异常。如果确认问题在配置最快的止损方式是回滚Deployment到上一个正常版本kubectl rollout history deployment/name -n namespace kubectl rollout undo deployment/name -n namespace --to-revisionversion回滚之后观察RESTARTS是否停止上涨。如果Pod稳定了再慢慢对比新旧配置找出具体是哪个值坏了。不要一上来就在生产环境里调试配置文件那样会把问题放大。还有一个老生常谈的坑Kubernetes里ConfigMap更新不会自动触发Pod重启。有人改完ConfigMap发现Pod还在使用旧配置以为删Pod就能强制刷新结果新Pod挂载的ConfigMap如果使用的是subPath方式它只会拿到之前的缓存内容并不会随ConfigMap更新而刷新。想让配置变更真正落地要么给Deployment执行kubectl rollout restart要么在设计阶段就顺手给Deployment模板加上ConfigMap内容的哈希注解例如annotations: configmap/config: sha256:xxxx每次ConfigMap内容变化时哈希值变化Deployment就会自动滚动更新。这个方案能同时解决“配置改了但Pod没变”和“配置不好无论如何都要手动重启”两个问题算是配置治理里性价比很高的一招。3.2 Liveness探针误杀参数需要认真算不能随便填Liveness探针设计的初衷是检测容器是否“还活着”一旦失败次数达到阈值kubelet会主动杀掉容器重建。这套机制本身没有问题但在实际配置中探针是最容易被误配的地方。我见过一个支付网关的例子健康检查接口正常响应需要2秒探针的timeoutSeconds却设成了1秒结果探针每次请求都超时Pod隔几分钟就被重启一次。业务没有崩完全是被探针搞崩的——这个形容一点不夸张。设置探针时至少要考虑三个时间因素服务启动时间、单次响应时间、可容忍的瞬时抖动时间。对于启动很慢的应用用startupProbe来兜底把启动时间拉长避免livenessProbe在启动阶段就误杀实例。下面这个配置是我在实际项目里验证过的startupProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 30 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 15 timeoutSeconds: 3 failureThreshold: 3startupProbe里failureThreshold设成30等于给慢启动应用最多150秒的准备时间。livenessProbe的periodSeconds设成15秒、timeoutSeconds设3秒、failureThreshold设3意味着一次抖动只会在后续检查里被自然纠正不会立刻触发重启。参数之间是相互牵连的periodSeconds太小、failureThreshold太大会让一个已经不健康的容器拖很久才被处理等于失去了“快速兜底”的意义periodSeconds太小、failureThreshold太小又可能因为一次网络抖动就杀掉一个正常服务。具体的值要根据业务响应速度来调但上述配置是一个比较好的起点。另外一个经典案例是服务监听地址和探针地址不匹配。健康检查接口只绑定了127.0.0.1但httpGet探针访问的是Pod IP连接自然失败。这类现象表面上和“健康检查失败”一样但本质是服务根本没监听在预期地址上。遇到探针显示连接被拒绝、但服务进程还活着的情况先确认监听地址再决定是改服务配置还是改探针配置不要一上来就重启。3.3 内存Limit与OOM先确认是“泄漏”还是“不够用”OOM导致的Pod重启从kubectl describe输出到Last State里会写得非常明确Reason是OOMKilledExit Code是137。确认是OOM之后真正要回答的问题是这个进程是被容器内存Limit卡死的还是被节点内存压力杀掉的又或者是代码真的存在内存泄漏。看实际内存使用趋势是最直观的手段kubectl top pod -n namespace --sort-bymemory如果内存从启动后一路上升直到贴近limit时崩溃那基本可以判定是无限增长型的内存问题比如缓存没清理、连接池泄漏、日志采集线程堆积。调大limit只能推迟崩溃时间治标不治本。如果内存一直平稳但limit本身就设置得过小那属于资源配置不合理给Pod扩容即可。有一个Java场景很典型容器limit设成2G但JVM启动参数里没配置-XX:MaxRAMPercentage或-XmxJVM在容器里默认感知到的内存大小可能远大于2G导致堆内存疯狂扩张最终被容器Limit直接杀掉。对这种应用要显式设置JVM参数让堆大小在容器限制内自我约束。还有JVM的堆外内存、线程栈、元空间也都算在容器内存里只看-Xmx是不够的。内存问题的排查本质上就是区分“不够用”和“真的在漏”两个方向方向错了调参没有意义。4. 视线放到Pod外面节点、kubelet、DNS和网络4.1 节点级故障会带来整批Pod重启单个Pod重启优先怀疑容器本身但如果是同一节点上一批Pod同时重启就几乎可以认定问题发生在节点层面。节点级故障的常见类型包括节点变成NotReady、kubelet与容器运行时失联、containerd服务异常、磁盘压力或PID压力触发驱逐。遇到这种场面不要继续逐Pod排查直接看节点状态kubectl get nodes kubectl describe node node-name重点看Conditions这一段MemoryPressure、DiskPressure、PIDPressure是否为TrueReady状态是否正常。一旦节点有压力kubelet会优先驱逐那些超过资源请求的Pod或者整批Pod被标记为Failed等待重新调度到其他节点。如果Events里出现类似“The node was low on resource: memory”的驱逐记录那根因已经非常明确了跟应用代码没有半毛钱关系。节点本身的初始化质量也容易埋雷。我排过一个问题集群是用kubeadm初始化的v1.26.0preflight检查全过但上线后节点Pod频繁重启。后来定位到kubelet和容器运行时的cgroup driver不一致一边用的systemd一边用的cgroupfs。这个不一致平时很难察觉但在内存和CPU统计上会造成混乱进而引发资源控制异常。Kubernetes官方文档里明确要求kubelet与容器运行时使用同一个cgroup驱动。如果你发现节点的Pod重启没有明显规律先核对这个配置。4.2 DNS与网络配置隐藏的启动依赖很多服务启动时都要解析其他服务的地址最常见的就是启动时连接数据库或注册中心。如果应用配置里写的是Service域名或主机名那就要依赖CoreDNS来做解析。CoreDNS某个时间段内异常、或者节点上的DNS解析路径不通Pod启动时解析失败进程就会报错退出并重启。这类问题的特点是等网络恢复后Pod又能正常起来但过一会儿可能又因为同样的原因崩掉看起来随机且反复。查看Pod内的resolv.conf时要注意它由kubelet根据Pod的dnsPolicy自动生成。默认的ClusterFirst策略下会指向集群内部的CoreDNS地址。如果有人在Pod里手工改动了resolv.confPod一旦重启或重建配置会被重置相当于Linux系统里“改了DNS重启网络后又还原”的老问题。在容器环境里这不算什么罕见事。排查这类问题要看集群级的CoreDNS状态和节点网络而不是去改Pod内部文件。安全组和防火墙配置同样会伪装成应用启动问题。健康检查端口被防火墙挡住探针访问不到端口容器就会被标记为不健康并杀掉业务服务之间的端口未放通连接超时最终也会引发探针失败。排查思路是探针路径、Service端口、防火墙规则三件事要同步核对只看其中一项很容易产生误判。4.3 宿主机不稳定节点重启才是真根因还有一种比较隐蔽的情况Pod的重启其实是因为宿主机本身在重启。比如内核panic、电源问题、磁盘控制器故障、网卡驱动异常节点连同上面的kubelet一起消失等系统恢复后Pod再被重新调度回来。这种故障从应用视角看就是“全部Pod莫名其妙集体重启”而且往往不只是一个节点。判断方法很简单看节点启动时间uptime last reboot dmesg -T | tail -100如果多个节点都显示最近发生过重启或者dmesg里有明显的kernel panic、驱动报错痕迹那就可以把排查重心从Kubernetes转向宿主机的硬件和操作系统层面。这类问题虽然频率低但一旦出现破坏力极强。像某些网卡驱动在开机时未被识别、特定硬件在压力下反复重启都是能引发Pod批量重启的宿主机因素千万别只顾着看容器日志。5. 有状态服务与“重启后一切正常”的假象5.1 重启后数据丢失临时文件、状态目录的坑还有一类问题表面看是“重启后一切正常”但实际上状态丢了。比如Trino/Presto这类引擎如果动态Catalog配置没有持久化到配置中心或PVC一旦Pod重启所有Catalog配置全部消失查询全部失败需要人工重新配置。再比如ClickHouse这类服务上次退出不是干净退出重启时可能碰到旧状态残留导致的启动报错类似system log相关的表已经存在。本质都是“进程重构了但旧的运行状态没有处理好”。对于这些服务核心原则是凡是重启后必须保留的数据和配置都不要放在容器可写层。容器可写层是临时的Pod重建后会被清空。日志用emptyDir只是临时保存配置用ConfigMap或外部配置中心数据用PVC或外部存储才能跨Pod生命周期存活。Kubernetes里的“自愈”从来不是指应用自动变无状态而是指通过架构让状态可以被妥善保存和恢复。5.2 用优雅终止把重启的影响降到最低重启无法完全避免但可以在重启过程中把对业务的影响降到最低。常见做法是给Pod加preStop钩子在kubelet发送SIGTERM之前先执行一段清理逻辑。比如通知注册中心下线、等待正在处理的请求完成、把内存中的数据刷到磁盘。配合terminationGracePeriodSeconds来控制等待时间防止应用还没清理完就被强杀。一个实用的配置示例lifecycle: preStop: exec: command: [/bin/sh,-c,sleep 5 /opt/app/graceful-shutdown.sh] terminationGracePeriodSeconds: 30preStop里加sleep是在给Service和Endpoint的更新留传播时间避免Service还在把流量往后端列表里即将删除的Pod转发。这个细节在流量较大的服务上尤为重要。如果每次重启都能做到优雅下线用户侧基本无感即使偶发重启也不会成为事故。5.3 自愈设计不只有“重启”组合拳才有意义最后聊一下系统级的自愈设计。Liveness探针负责发现不健康并重启容器readinessProbe负责把不健康的Pod从Service后端摘除副本数保证同一时刻至少有两个实例在承接流量PodDisruptionBudgetPDB控制自愿驱逐场景下最多可以同时失去多少个副本。这些机制单独看都不复杂但组合起来才能让“Pod重启”变成一个低成本、无感化的自愈动作。我见过太多团队把所有可靠性寄托在“Pod崩了会自动重启”这一个兜底上结果一次重启引发了更大的雪崩流量瞬间集中到新实例连接数暴涨缓存失效后压力传导到数据库最终整个服务不可用。重启不是银弹它是兜底里的最后一环。要避免这种雪崩还得靠readiness探针把不健康的实例及时摘除靠合理的副本容量来吸收单点故障靠依赖服务的降级和重试来缓冲抖动。最后分享一个我自己的排查习惯。收到Pod重启告警后我给自己定了一条“三分钟规则”第一分钟看kubectl get pod -o wide和describe输出把事件时间线保存下来第二分钟看崩溃前日志加--previous参数获取上一次容器的输出第三分钟看节点状态和最近十分钟是否有配置变更、发布动作。三分钟没有结论就立刻扩大视野而不是反复重启同一个Pod。很多Pod重启问题之所以难不是它有多深而是我们一开始把视野缩得太窄。希望这篇能在下次你面对RESTARTS不停上涨时帮你少走一点弯路。
返回列表