Kubernetes在环境里跑起来之后,真正的挑战往往不是“怎么部署”,而是“出了问题之后怎么定位”。我接手过不少容器化部署的项目,大部分时间风平浪静,真正让人头疼的是那种凌晨两点的告警电话:监控面板上整片整片地变红,Pod在CrashLoopBackOff里反复重启,你盯着屏幕却不知道先看哪儿。
这就是我写这份故障排查指南的原因。它不是一份“按这里按那里”的操作手册,而是把我这些年排查Kubernetes容器环境问题时反复用到的思路、命令、判断逻辑和踩过的坑,整理成一套可以直接照用的方法。无论是刚接触Kubernetes的开发者,还是已有几年经验、想系统化自己排查流程的运维同学,都值得花十几分钟过一遍,遇到问题时能少走不少弯路。
需要先说清楚的是,Kubernetes的故障排查和传统单机环境有本质区别。传统环境里,你直接SSH到机器上看日志、看进程、看端口就行;而Kubernetes里,应用被包进容器,容器被调度器放到某个节点上,外面还有Service、Ingress、探针、网络插件一大堆东西围着。排查对象不是单个进程,而是一整条链路。所以这篇文章会从最底层的方法论讲起,再逐步覆盖Pod生命周期、镜像、资源、网络、安全这几类高频故障,最后附上可以直接抄作业的问题速查表。
1. 排查前的准备:先把自己手上的牌理清
1.1 不只盯kubectl get pods,事件才是第一现场
很多朋友排查问题的第一步就是kubectl get pods,看到CrashLoopBackOff或者Pending就开始慌,然后一股脑去看日志。这个习惯可以改一改。日志固然重要,但Kubernetes本身已经把大量线索放在事件(Events)里,你却没有看,等于放着地图不用,非要去山里绕。
我通常的做法是,接到一个故障后先在终端里开一个固定窗口跑:
kubectl get events --all-namespaces -w --sort-by='.lastTimestamp'这个命令会实时滚动集群里的全部事件。为什么要按时间戳排序?因为排查故障时,时间线是判断因果关系的命根子。某个节点NotReady、某个镜像拉取失败、某个探针连续超时,这些事件是按秒发生的,你只有在时间线上把它们串起来,才能判断谁是因、谁是果。
在不方便开多个终端窗口的场合,至少也要执行一次非实时的查看:
kubectl describe pod <pod-name> -n <namespace>describe会把Pod当前状态、节点、容器、探针、挂载卷、QoS等级、最近事件全部列出来。遇到任何异常状态,我建议先describe再logs,因为describe里已经有大量诊断结论,能帮你把日志排查范围缩小一大半。
1.2 读懂Pod状态的“潜台词”
kubectl get pods列出的状态字段,很多人只把它当成“跑没跑起来”,其实每个状态背后都对应着一类明确问题。我把高频状态整理成了一张对照思路的表:
| Pod状态 | 潜台词 | 优先排查方向 |
|---|---|---|
| Pending | 调度器还没把Pod放到节点上 | 节点资源、污点、配额、调度器状态 |
| ContainerCreating | 正在拉镜像或初始化容器 | 镜像地址、存储卷、容器运行时异常 |
| Running | 容器已启动,但不代表健康 | 探针状态、应用日志、资源水位 |
| CrashLoopBackOff | 容器反复崩溃,重启后又被Kill | 容器入口、启动命令、依赖服务、OOM |
| ImagePullBackOff | 镜像无法拉取 | 镜像名/标签、仓库凭证、网络策略 |
| Terminating | 容器长时间无法结束 | 强制删除、挂载卷占用、finalizer卡住 |
这套状态机看着简单,但实际排查时经常出现“状态和直觉不符”的情况。我遇到过Running状态的Pod,业务却完全不通,一看readiness探针已经连续失败几十次了。所以记住一句话:状态只是表象,事件和日志才是证据链。
1.3 排查前必须养成的三个“肌肉记忆”
一是带时间戳。默认的kubectl logs不带时间,排查时很难对齐容器重启前后。用kubectl logs <pod> --timestamps或者--tail=200就能看到截至目前最近200行,同时配合--since=10m只看最近十分钟的日志,能迅速过滤掉大量干扰信息。
二是有壁纸先记现场。问题定位到一半时,容器一崩溃重启,日志全部被冲掉,现场就丢了。所以我每次排查崩溃类问题,都是先执行:
kubectl logs <pod> -n <ns> --previous --timestamps > /tmp/pod-pre.log有--previous,拿到的是崩溃前那一次容器的日志,而这往往才是崩溃真正的根因。
三是区分“集群问题”和“应用问题”。应用自己炸了,你再怎么调调度器也没用;节点资源被打满,应用日志再健康也没办法。每次先问自己:这个问题是Kubernetes层能解决的,还是应用本身的问题?这个分诊判断做对了,排查效率至少翻倍。
2. 高频故障场景拆解:从现象找根因
2.1 CrashLoopBackOff:启动即崩溃的循环
这是Kubernetes里最经典、也最让新人头疼的状态。容器起来几秒钟,退出,过一会儿重启,再退出,循环往复,“CrashLoopBackOff”就是这层循环的名称。碰到它,第一件事不是翻代码,而是拿崩溃日志。
先看常规日志:
kubectl logs <pod> -n <ns>如果日志只有几行或者根本是空的,说明进程可能在入口阶段就挂了,这时候必须看上一次的日志:
kubectl logs <pod> -n <ns> --previous拿到日志后,常见根因就几类。第一类是启动命令写错,比如Dockerfile里写了ENTRYPOINT ["/app/start"],但镜像里根本没有这个文件,容器一启动就报exec: "/app/start": stat /app/start: no such file or directory。这类错误最直接,日志里一眼就能看到。
第二类是配置模板拉不到环境变量。很多应用启动时会连数据库、连配置中心,如果连不上就直接抛异常退出。我在一个Java容器上遇到过这种典型问题:应用启动时读Nacos配置,网络策略把到配置中心的流量给挡了,应用反复重试几次之后自杀。从业务日志看全是连接超时,但你要是不去查网络策略,这种问题能排查一整天。
第三类是资源类崩溃,容器本身被OOM Kill。这种在日志里往往看不到应用报错,反而在describe里能看到Last State: Terminated,Reason: OOMKilled,或者退出码是137。这里需要特别提醒:137本身不等于OOM,必须结合事件确认。137是SIGKILL的退出码,容器被外部强杀就是137,但它也可能来自OOM Kill,还可能来自手动kubectl kill。光看137猜不出原因,要看describe里的状态描述。
还有一类Java容器特别容易踩坑:启动参数里的堆内存设得比容器内存Limit还大,结果容器一启动,内存使用瞬间顶到Limit,被直接杀死。我用Shutdown、闲时并发不高的小服务都见过这种问题,堆内存直接设了-Xmx2g,容器Limit只有1Gi,启动不了几次就崩溃。解决方法是把-Xmx调低,或者给容器Limit适当上调,然后让JVM自动感知容器内存限制。
实操建议:排查CrashLoopBackOff,我固定按“日志 → 上一次日志 → describe事件 → 手动跑一次启动命令”的顺序来。前两步能解决八成问题,剩下两成基本是资源和权限问题,再往后就要进容器手动验证了:
kubectl exec -it <pod> -n <ns> -- /bin/sh进去之后直接跑应用启动命令,看它到底报什么错。这种办法比反复重启容器快得多,因为它能让你脱离Kubernetes的调度逻辑,直接在容器内部复现问题。
2.2 ImagePullBackOff:镜像拉不下来
ImagePullBackOff字面意思是“镜像拉取退避”,原因是Kubelet拉取镜像失败,失败后按指数退避策略重试,但一直失败。这类问题大多不是网络不通,而是一些看着很小的配置错误。
先检查镜像地址本身。最常见的是镜像Tag写错,比如写成了myapp:latest-linux,仓库里根本没有这个Tag;或者没有指定Tag,默认拉latest,但生产仓库里根本没推latest版本。这类问题执行kubectl describe pod,在事件里能看到Failed to pull image "x/y:tag": manifest unknown之类的报错,基本可以确认是Tag的问题。
第二类是私有仓库凭证问题。容器镜像放在私有仓库里,Pod需要提前通过imagePullSecrets引用一个dockerconfigjson类型的Secret。这个Secret如果过期、写错仓库地址或者根本没配,Pod就会一直imagePullBackOff。看事件会提示pull access denied或unauthorized。
我实际排查过不少这样的案例:镜像本身存在,仓库凭证看起来也没问题,但Pod就是拉不下来。后来发现Secret里保存的dockerconfigjson是用错误的registry地址生成的,看起来和实际仓库域名一样,阴差阳错少了个端口号或多了个子路径,导致鉴权永远不通过。这里建议新建Secret时拿明文Docker配置做自动化校验,而不是靠肉眼比对。
第三类是镜像安全扫描和签名拦截。现在容器化部署越做越规范,很多团队在拉取镜像时会走镜像扫描和签名校验流程。如果镜像没有扫描结果,或签名不对,Kubelet可能直接拒绝拉取。这类问题的事件提示往往比较模糊,常见的是failed to verify image signature。排查时要先确认是否启用了镜像签名策略,再检查镜像的签名记录和仓库配置。
还有一个很常见的坑,是镜像拉取时触发了限流或白名单限制。比如Docker Hub匿名拉取有速率限制,生产环境拉取量大,容易遇到toomanyrequests报错。解决办法是把镜像同步到内部镜像仓库,Pod直接从内网仓库拉取,既稳定又安全。
实操建议:碰到ImagePullBackOff,我建议直接在节点上手动试拉一次镜像。先在kubectl get pods -o wide里确认Pod调度到了哪个节点,然后SSH上去执行docker pull或crictl pull,直接拉到报错就一目了然,避免了在Kubernetes层反复排查。注意,有些环境下节点上装了kata容器或者远程容器运行时,docker命令可能不存在,这时候用crictl更通用。
2.3 Pending与资源不足:Pod被卡在调度阶段
Pod长时间Pending是让新手比较困惑的问题,它可能表示调度器压根没找到合适的节点,也可能表示Pod本身配置有问题。在kubectl get pods里看到Pending,建议第一时间执行:
kubectl describe pod <pod-name> -n <ns>在Events里的FailedScheduling行会显示调度失败的详细原因。最常见的原因是节点资源不足:Requests(申请)量超过了所有可用节点的可分配资源。比如节点可用内存只有8Gi,Pod申请了12Gi,调度器自然找不到地方放它。还有一种情况是节点有Taint(污点),Pod没有对应的Toleration(容忍),调度器也会拒绝把它放在那个节点上。
这里有个细节需要注意:Kubernetes判断节点资源是否充足,用的是Requests而不是实际使用量,控制器会持续追踪每个节点上所有Pod的Requests总和。所以有时候整台机器CPU使用率很低,Pod仍然Pending,原因可能就是别人的Pod把CPU Requests占满了。这时候用kubectl describe node <node-name>查看Allocated resources,就能看到节点已经被多少Pod“预定”了。
还有一个很容易被忽略的原因是ResourceQuota,也就是命名空间层面的资源配额。Pod的Requests总和超过了命名空间的quota,调度器会直接拒绝创建,事件里提示exceeded quota。检查命名字空间配额很简单:
kubectl get resourcequota -n <ns>实操建议:Pending问题的排查核心就是“调度器在拒绝谁、为什么拒绝”。我建议每次碰到Pending,都先把describe的事件截图保存,再按“节点资源 → 污点容忍 → 配额 → 调度器日志”的顺序排查。如果节点上明明有足够资源却始终调度不上去,还可以看调度器日志,确认是调度器挂了还是被自定义调度策略挡住了。
2.4 探针失败:服务在跑,但业务不通
探针是Kubernetes里最容易“因小失大”的配置。Pod明明Running,服务却时好时坏,甚至完全不通,问题十有八九出在readiness(就绪)或liveness(存活)探针上。
先说清楚三类探针的区别:
livenessProbe:探测失败就重启容器,负责“坏了就换新”。readinessProbe:探测失败就把Pod从Service端点里摘掉,负责“好了才接流量”。startupProbe:启动阶段用的探针,专门给启动慢的应用争取时间。
我在实际项目里见过一个典型的错误配置:一个Java服务启动需要80秒,但livenessProbe的initialDelaySeconds只写了10秒,结果Pod一启动就被探针判死,反复重启,永远起不来。后来的修复方案是加一个startupProbe,给它120秒的启动窗口,liveness和readiness在启动成功后才开始工作。
探针本身的配置参数也很容易踩坑。timeoutSeconds设为默认的1秒,很多应用在高峰期请求一慢,1秒内没响应就被判定失败;periodSeconds设得太频繁,比如每1秒探测一次,正常服务可能因为并发波动偶尔超时就被误杀。这些参数要根据服务实际响应时间调,不要照抄文档默认值。
还有一种常见问题是探针路径写错。readiness探针配的路径是/health,但应用实际注册的接口是/actuator/health,结果探针一直404,Pod一直被摘流量。这类问题从应用日志里看,会发现大量来自Kubelet的探测请求。排查探针问题时,先看应用日志里有没有来自探针路径的请求记录,有就说明网络和端口通,问题出在探针配置对不上。
实操建议:调整探针参数后,记得观察一段时间再固化到配置里。我个人的调参顺序是:先把startupProbe调宽给足启动时间,再调liveness的initialDelaySeconds和failureThreshold来容忍慢性波动,最后才调readiness的阈值来控制流量切换节奏。千万别一上来就改所有探针,否则问题会越改越乱。
3. 资源隔离与安全:容器看不见的墙
3.1 资源限制与OOMKilled
容器跑得好好的,突然没了,代码看起来没有任何问题,日志也干干净净——这种诡异情况,十有八九是OOMKilled。Kubernetes通过cgroup限制每个容器能用的CPU和内存,一旦容器实际使用超过了memory Limit,内核的OOM Killer就会挑选进程把它杀掉。
怎么确认?最简单的方式是:
kubectl describe pod <pod-name> -n <ns>在Last State一栏如果看到:
Reason: OOMKilled Exit Code: 137那就说明容器确实是因为内存超限被杀的。很多人看到137就慌,但137只是SIGKILL的代号,真正有意义的是Reason: OOMKilled这两行。
确认后要排查的是“为什么超限”。先看应用日志里有没有内存相关错误,再看监控上Pod的内存走势曲线,确认是持续增长还是瞬间暴涨。如果是持续增长,大概率是应用内存泄漏;如果是瞬间暴涨,要查是不是有突发的批量任务或连接数飙升。
除了应用自身问题,还有一类情况是Limit设置得和实际需求不匹配。我见过一个服务,实际峰值内存也就300MiB,但运维图省事写了个512Mi,结果服务在业务高峰期多缓存了一些数据就超限被杀了。这种问题调Limit就能解决,但记得要配合监控观察一段时间。
实操建议:这里要特别提醒一个“资源隔离”的细节。很多人以为容器Limit设成2Gi,容器里就最多用2Gi,这其实是对的,但很多人忽略了——合理设置Requests比Limits更重要。Requests是调度依据,决定了Pod能不能被放到某个节点;Limits是运行约束,决定了Pod能不能在这个节点上稳定运行。如果Requests设置过高,节点明明很空,Pod却被卡在Pending;如果Requests设置过低,节点上所有Pod加起来请求超了,调度器也拒。这两个值一定要根据实际水位来定。
3.2 从容器内部看资源隔离
聊到资源隔离,很多人有个误解:容器是不是真的能“隔离”到底?实际上,容器依赖的namespace和cgroup做了隔离,但内核是共享的。进程、文件系统、网络栈都被隔离了,但CPU、内存等资源只是“限制”而不是“隔离”。理解这点,对排查很多疑难故障很有帮助。
比如有个Java容器在节点上总是偶尔卡顿几秒,直觉是CPU不够用。进容器执行:
cat /sys/fs/cgroup/cpu/cpu.stat如果nr_throttled数值很大,说明这容器的CPU使用被cgroup限制得很厉害,它在“被打压”而不是没CPU。实际排查中,这类throttled问题经常出现在limit设得刚好但CPU突发需求高的应用上,修复办法往往是把CPU Requests和Limits调一致,或者给CPU留一点buffer。
内存排查也是类似。在容器里执行:
cat /sys/fs/cgroup/memory/memory.current可以直接看到容器当前真实使用的内存,比free -m更贴近Kubernetes的视角,因为free看到的是整个宿主机的内存,而不是容器被cgroup限制的那部分。
实操小结:排查资源问题时,不要只看宿主机监控,一定要进容器里看cgroup的数据。因为Kubernetes判断的资源上限,就是cgroup文件里的数值。你把这两层对应上,很多“机器很空闲但容器就是跑不动”的怪问题都能找到答案。
3.3 镜像安全与容器安全:排查时也别忘了底线
排查故障时,安全这个话题往往被放得比较靠后,但容器环境里安全配置一旦出问题,表现和普通故障一模一样,很容易误判。
举例来说,如果镜像扫描工具发现应用镜像里有高危漏洞,仓库平台直接拒绝拉取,Pod表现就是ImagePullBackOff,跟凭证错误一模一样。这类问题不看仓库平台的后台日志根本发现不了。镜像安全关注的不只是“有没有漏洞”,还包括镜像来源是不是可信、是否有人篡改过。我给团队的强制要求是:镜像必须走内部仓库,禁止生产直接拉公网镜像;每次发版前跑一轮镜像扫描,把漏洞报告存到发布记录里。
容器运行时的安全配置也要注意。很多镜像默认用root用户跑容器,一旦容器被攻破,攻击者就有了节点上的高权限。我见过一个应用,因为要写宿主机路径,直接把CAP_SYS_ADMIN给了容器,这等于在隔离墙上开了个大洞。建议至少做到:容器以非root用户运行、去掉不需要的Linux Capability、开启seccomp默认配置文件、关闭特权模式。这些配置出问题时,现象往往是“容器启动异常”或“权限不足”,但日志里又不明说。排查时如果遇到无解的Permission Error,可以看一眼SecurityContext有没有误限制。
在这里多提一句:排查故障时,不要为了“让问题先过去”随便加特权、关安全配置。我踩过这种坑——给一个业务Pod加了特权模式后问题确实不报了,但后面一次排查安全事件,发现那个Pod早就成了集群里的高风险口子。正确的做法是定位到真正原因,再决定需要开哪一项权限。
4. 网络故障:Pod之间通信为什么这么难
4.1 DNS解析失败:最隐蔽的故障源
容器化部署里,应用之间的调用大多依赖Service域名,比如order-service.default.svc.cluster.local。一旦DNS解析出问题,应用日志里就会报“UnknownHostException”或“connection refused”,表现千奇百怪,但根子都在CoreDNS身上。
排查DNS的第一步是确认Pod能不能解析Service名。先起一个临时调试Pod,或者直接在目标Pod里执行:
kubectl exec -it <pod> -n <ns> -- nslookup myservice.default.svc.cluster.local如果解析失败,优先检查CoreDNS到底还活着没有:
kubectl get pods -n kube-system | grep coredns kubectl logs -n kube-system <coredns-pod> --tail=50CoreDNS挂了或者频繁重启,所有Service域名都会解析不出来。很多时候CoreDNS挂掉又和节点DNS缓存、网络插件本身的问题互相纠缠,排查时要先确认一层,再怀疑下一层。
还有一种很常见的配置问题:Pod里的/etc/resolv.conf配置的search域和ndots参数不对。默认情况下,Kubernetes会在/etc/resolv.conf里添加一大堆search域和ndots:5。这意味着应用访问短域名时,本地库会先尝试拼出一长串域名向DNS服务器查询,每次解析都带来额外延迟。如果应用大量使用短域名且解析超时,可以优化Pod的dnsConfig,把ndots调低到2或1,并显式配置search域。
实操建议:排查DNS问题时,先验证集群内域名连通性,再跳到节点上看CoreDNS的解析日志,最后看Pod内部/etc/resolv.conf。三步走完,DNS问题基本都能定位到某一层。
4.2 Service到Pod的连接失效:选择器和端点才是关键
Service配置看着没错,Pod也Healthy,但业务就是不通,这类“Service级故障”排查时要盯两个点:Endpoints和选择器。
kubectl get endpoints <service-name> -n <ns>如果Endpoints列表是空的,那说明Service选不到任何Pod,最可能的原因是选择器和Pod标签对不上。我曾经排查过一个大半夜的故障:Service的selector写的是app: my-web,但Pod的标签是app.kubernetes.io/name: my-web,两边差了一个层级,结果Service的Endpoints一直为空,流量只进Service不到Pod。这类问题执行kubectl get pods --show-labels对比一下就一目了然。
如果Endpoints非空但业务还是不通,就要继续排查端口问题。Service的targetPort和Pod里容器实际监听的端口如果不一致,流量会被直接丢弃。还有一些CI/CD流程会自动生成Service,模板里端口写错也很常见。Windows下跑容器还会碰到端口占用,表现就是容器监听不了端口,但这种在Kubernetes集群里很少见,可以放到最后再怀疑。
Service后面的EndpointSlice是较新的实现,, 检查时也可以用kubectl get endpointslices`看看有没有被拆成多个Slice的情况。注意,如果Pod的readiness探针失败,它会被自动从Endpoint里摘除,流量自然切不到它,但Pod本身还是Running——这就回到了第2.4节说的探针问题。Service不通时,第一反应不是看网络插件,而是看Endpoints里到底有没有健康的Pod。
4.3 K8s内部抓包的快捷方式
网络问题到了需要抓包那一步,很多人会直接跳到节点上用tcpdump。但Kubernetes里更快的办法,是起一个临时调试容器挂到目标Pod的同一个网络命名空间里:
kubectl debug -it <pod> -n <ns> --image=nicolaka/netshoot --target=<container-name>netshoot镜像里预装了tcpdump、nc、dig、curl等工具,插进Pod后直接就能抓包。这个操作习惯我强烈推荐给所有K8s排障人员,因为直接在节点上抓包要过滤一堆别的Pod流量,而进了目标Pod的网络命名空间,看到的就是这个Pod自身的通信,干净利落。
在容器里执行:
tcpdump -i eth0 -nn port 8080配合另一侧模拟请求,很快就能判断是建立连接失败、重传,还是应用层协议问题。
5. 常见问题与排查技巧实录
5.1 问题速查表
这一段我把日常里最高频的问题整理成一张表,方便大家直接对照参考。查询要点是:先看状态,再对齐“最可能的根因”,然后按“验证手段”里的命令去确认。
| 故障现象 | 优先检查 | 最可能的根因 | 验证手段 |
|---|---|---|---|
| Pod处于Pending | describe node、ResourceQuota | 节点资源不足/污点不容忍 | kubectl describe pod 看FailedScheduling |
| 容器反复重启 | kubectl logs --previous | 应用启动崩溃/参数错误 | 查看上次日志和退出码 |
| 镜像拉取失败 | describe pod 事件 | Tag写错/私有仓库凭证失效 | 节点上手动docker pull/crictl pull |
| 服务时好时坏 | readiness探针状态 | 探针配置过严或路径错误 | kubectl get endpoints 看Pod是否被摘除 |
| 应用连不上数据库 | 网络策略/Service解析 | CoreDNS解析不了服务名 | kubectl exec 里 nslookup、nc 测试端口 |
| 突然被系统杀掉 | describe pod 状态 | OOMKilled/内存超Limit | 查看Reason和监控内存曲线 |
| 日志正常但业务不通 | 选择器和Endpoints | Service selector写错 | kubectl get endpoints 对比标签 |
| 容器权限报错 | SecurityContext | 非root用户和Capability丢失 | 进容器执行特权操作复现 |
这张表不能覆盖所有情况,但能覆盖日常七八成的故障。关键是无论哪类问题,都要按“事件 → 日志 → 参数配置”的顺序交叉验证,不要只盯单一信息来源。
5.2 现场排查的标准动作
故障发生后,团队最容易乱成一锅粥,几个人同时上手,排查路径不同,信息又开始断裂。我慢慢把团队排查故障的标准动作固定成一套“三板斧”流程:
第一,先截现场。执行kubectl get pods -A -o wide把当前所有Pod的状态快照保存下来,再kubectl get events -A --sort-by='.lastTimestamp'保存事件快照。这两份数据是后续分析的基础,无论问题是否在线上解决,都要留档。
第二,看时间线。把事件和Pod状态变换按时间排列,找出第一次出现异常的时间点。这个时间点前后的变更纪录(发布、配置改动、节点变更)往往就是根因。
第三,收敛变量。确定根因方向后,逐步排除。比如网络不通,就先验DNS,再验Service,再验到Pod的网络,一层层缩小范围,不要一上来就抓包一分钟。
这套流程看上去朴素,但真的能减少无效排查时间。我自己经历过好多次本来半小时能定位完的问题,因为几人同时上手、信息没同步,多花了三个小时的现象。故障排查最怕的不是技术不懂,而是流程乱了。
5.3 那些年踩过的坑
写到最后,分享几个我亲手踩过、且很难在文档里找到答案的坑。
第一个是“日志都正常”的容器崩溃。有一回排查一个Go服务频繁重启,kubectl logs --previous里什么都看不到,describe也没有明确的OOM事件,只有一串Voluntary Preemption。后来发现是节点上的磁盘压力太大,容器被运行时主动杀掉。这类故障藏得深,排查时要看节点监控,而不是只盯Pod。
第二个是“探针把应用治死了”。一个团队给Java服务配了liveness探针,因为探针的HTTP请求里携带了一个无关紧要的Header,而应用的安全组件正好拦截这个Header,直接返回500,导致容器不断重启。探针看起来完备,实际和业务逻辑冲突。每次配探针之前,一定先用curl手动模拟探测请求,确认返回码和响应体符合预期。
第三个是“Java容器永远慢半拍”。Pod的CPU Limit设得很小,应用跑起来之后CPU被持续throttled,明明节点上有大量空闲CPU,应用却频繁超时。这个问题的根子在于:CPU Limit是动态公平调度的,不是“独占”,你设了1核,系统在节点繁忙时会把你的容器压到1核,节点空闲时也未必一直给满。排查它需要进容器看cpu.stat里的throttled计数,这个坑对Java和Node.js这些偏CPU敏感的运行时而言尤其明显。
第四个是关于“容器化部署是个整体”。别只看单个Pod,很多“故障”其实是部署流水线自动伸缩过度、镜像库限流、节点批量更新组合出来的结果。集群里的问题往往是系统性故障,单一Pod的状态只是它在外层的一个投影。
5.4 最后再给一个实用小习惯
我强烈建议团队在应用里预留一个“诊断接口”,比如一个只允许集群内部访问的/healthz和/debug/vars。这样排查问题时,可以直接通过探针和调试工具读取应用状态,而不需要每次都在生产环境冒险远程调试。
容器环境的故障排查,说到底是一个“快速验证假设”的过程。你看到的每个异常,都是Kubernetes层和应用层的某种信号;你能做的,就是顺着信号一层一层往下挖,直到找到那个真正的根因。这个思路建立起来之后,再多的故障案例翻来覆去也就是那几类问题在变着花样反复出现。
保持冷静,先把事件和日志对齐,再决定下一步动哪里。这就是这几年我在Kubernetes容器环境里,最值钱的一句经验。