做 Kubernetes 生产排障这几年,我最大的感受是:大部分故障并不是什么高深莫测的黑科技,而是基础概念没吃透、排查路径不清晰、关键参数没调对。你去看那些线上事故,十次里有七八次最后都能归结到 Pod 生命周期异常、节点资源竞争、etcd 响应变慢这三类问题上。这篇文章我就把从 Pod 崩溃到 etcd 性能调优这条排查链路完整走一遍,每一步都附上实操命令和判断依据,希望能帮你在面对生产告警时少走弯路。
这篇文章适合谁看?刚接手 K8s 集群的运维工程师、正在准备 CKA/CKAD 的开发者,以及那些已经被线上故障折磨过几次、想建立系统排查思路的朋友。我会尽量用“现象 → 排查命令 → 根因分析 → 解决方案”的结构来讲,每个环节都会说到我踩过的坑和事后复盘得出的经验。
1. 排查思路先行:别让“救火”变成“放火”
1.1 故障定位的四个信号维度
接手一个故障集群,第一件事不是急着查日志,而是先确定“故障影响面到底有多大”。我一般按四个维度收集信息:
- 控制面状态:kube-apiserver 是否响应、etcd 是否健康、调度器是否还在正常工作。
- 节点状态:所有 Node 是否 Ready,kubelet 有没有频繁重启,磁盘和内存水位如何。
- 工作负载状态:Deployment 的副本数是否达到期望值,Pod 是否处于 Running,重启次数有没有持续上涨。
- 网络连通性:Pod 之间、Pod 与 Service、集群与外部之间的连通性是否正常。
这四个维度的检查顺序基本就是排查方向的索引。如果 kube-apiserver 本身响应缓慢,那你去查某个具体 Pod 的日志意义不大,得先把控制面拉起来。曾经有一次我们集群出现大面积 Pod 重建,我一开始逐台看节点日志,折腾了半小时没头绪,后来看了一眼 kube-apiserver 指标才发现是 etcd 读延迟飙到几百毫秒,导致 apiserver 的 list-watch 频繁超时,kubelet 拿不到最新的 Pod 状态,才引发了连锁反应。
1.2 从现象反推根因:快速圈定排查范围
K8s 的故障现象和根因之间往往隔了好几层。比如你看到 “Pod 一直 CrashLoopBackOff”,背后可能是镜像不存在、启动命令路径不对、探针配置过严、依赖服务没就绪,甚至是节点磁盘压力导致容器创建失败。单靠一条报错信息很难锁定问题,所以要先圈定范围:
- 如果问题只影响个别 Pod,优先查应用自身,比如启动参数、资源申请、环境变量。
- 如果问题影响某个节点上的所有 Pod,优先查 kubelet、容器运行时、节点磁盘和网络插件。
- 如果问题影响整个集群或多个节点,优先查控制面组件和 etcd 的健康状态。
范围圈定之后,再用工具去验证,而不是到处抓日志。这个习惯帮我省下了大量时间,也避免了在排查过程中误操作导致故障面扩大。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Pod 崩溃类故障:从 CrashLoopBackOff 到 OOMKilled
2.1 先看懂 Pod 生命周期状态
Pod 的状态是排查的第一份“口供”。你对 Kubernetes 的 Pod 生命周期熟悉吗?取个例子,Pending 阶段可能是调度器找不到合适的节点,也可能是镜像拉取失败还在重试;Running 不代表一切正常,容器可能在内部不断崩溃重启;Completed 和 CrashLoopBackOff 的区别在于容器是否正常退出。常见的状态含义我在下表中做了归纳:
| Pod 状态 | 典型含义 | 优先排查方向 |
|---|---|---|
| Pending | 尚未调度成功或正在拉镜像 | 节点资源、污点容忍、PVC 是否可挂载 |
| ContainerCreating | 容器创建中,可能卡在镜像拉取或存储挂载 | 镜像仓库连通性、存储驱动、Volume 权限 |
| Running | 容器在运行 | 通过 logs 和 exec 检查进程健康 |
| CrashLoopBackOff | 容器启动后崩溃,kubelet 在退避重启 | 应用启动命令、探针配置、资源限制 |
| OOMKilled | 容器内存超过 limit 被杀 | 内存 limit 设置、JVM/Node 等运行时参数 |
| Error / Completed | 容器异常退出/正常完成任务 | 退出码、日志、Job 预期 |
| Unknown | kubelet 与 apiserver 失联 | 节点网络、kubelet 进程、CNI 状态 |
CrashLoopBackOff 是生产环境里最常遇到的状态之一。它本身是 kubelet 的一种保护机制:容器启动失败后,kubelet 会按照指数退避策略(10秒、20秒、40秒……上限 5 分钟)不断重启容器,避免无效循环拖垮节点。所以看到这个状态,先别慌,用下面的命令看退出码和日志:
bash复制kubectl describe pod <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous
第一条命令会显示容器上次退出的原因和退出码,第二条命令拿的是容器上一次运行的日志。很多时候容器当前已经退出了,kubectl logs 直接看是没有输出的,必须加 --previous 才能看到崩溃前的日志。
2.2 CrashLoopBackOff 排查实例
举一个我实际处理过的例子。一个 Java 微服务上线后一直 CrashLoopBackOff,describe 显示退出码是 137。这里有个关键信息:退出码 137 通常是 SIGKILL,要么是内存超限被 cgroup OOM killer 杀掉,要么是外部显式 kill。但这个应用的内存 limit 设了 4Gi,实际用量看起来不高,后来我把容器启动命令从 java -jar app.jar 改成了 exec java -jar app.jar 也没效果,才怀疑方向错了。
继续查系统日志,发现是节点磁盘 IO 阻塞导致容器创建超时,containerd 在启动容器时被卡住,kubelet 判定启动失败,触发了重启。
这个例子说明,退出码只是线索,不是结论。要配合 dmesg、kubelet 日志、容器运行时日志一起看。另外一个常见坑是探针配置过严导致的“假崩溃”:应用启动需要 30 秒,但 readinessProbe 的 initialDelaySeconds 只给了 5 秒,探针一直失败,kubelet 就一直重启容器。这种问题的特征是应用日志还没输出多少就被杀,describe 里能看到探针失败的记录。解决方案不是盲目调大延迟,而是先确认应用真正的启动耗时,再设置合理的 initialDelaySeconds 和 periodSeconds。
2.3 OOMKilled 的定位与根治
OOMKilled 是另一类高发问题,典型现象是 Pod 状态显示 OOMKilled,容器重启次数不断增加。排查步骤如下:
- 查看当前 Pod 的 limits 和实际使用量:
bash复制kubectl top pod <pod-name> -n <namespace>
- 如果
top显示的内存使用率接近 limit,再看一下 cgroup 的内存事件统计:
bash复制cat /sys/fs/cgroup/memory/memory.oom_control
cat /sys/fs/cgroup/memory/memory.events
- 查看 kubelet 或系统日志里有没有对应的 OOM 记录:
bash复制journalctl -u kubelet | grep -i oom
dmesg | grep -i "killed process"
有一个 Java 应用 OOMKilled 案例非常典型。容器内存 limit 是 1Gi,但 Java 进程的堆内存启动参数 -Xmx 没有设置,JVM 默认按物理内存的 1/4 来分配,节点是 32Gi 内存,JVM 觉得自己可以用 8Gi,结果一压测就 OOM。问题的根源是容器内进程看到的 /proc/meminfo 是宿主机信息,这个坑在运行任何语言运行时(Java、Node.js、Python 都可能遇到)时都要注意。Java 应用建议在容器环境里显式设置 -Xmx 和 -Xms,并且要小于 Pod 的 limit;Go 应用需要注意 GOMAXPROCS 在容器里默认会读到宿主机 CPU 数量,可以考虑用 automaxprocs 这类库来自动适配。
OOMKilled 的根治方向有两个:一是优化应用内存占用,让它在 limit 内稳定运行;二是根据实际需求调整 limit。不是所有场景都适合无脑调大 limit,Node 的总内存是有限的,调大一个 Pod 可能会挤占其他 Pod 的空间。如果发现大量 Pod 都在接近 limit 的边缘运行,优先考虑优化应用或者拆分服务。
2.4 探针失效引发的“假死”问题
探针问题之所以难排查,是因为它不会直接显示为某一种明确的错误状态,而是表现为服务间歇性不可用、Pod 不断重启,或者流量路由异常。用一个真实场景说明:某个网关服务的 livenessProbe 配置为:
yaml复制livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
高峰期流量上来后,/healthz 响应偶尔会超过 5 秒,连续 3 次失败后 livenessProbe 触发容器重启。但业务本身没有死锁,只是健康检查端点响应慢。更麻烦的是,容器重启瞬间,网关连接全部断开,引发下游雪崩。
排查这类问题,需要在压测或故障时段抓取健康检查端点的响应时间。如果 RT 高是资源竞争导致的,优先解决资源问题;如果只是健康检查实现得太重(比如你去查数据库连通性),那就考虑把健康检查精简到纯粹的进程存活判断,把依赖检查的活交给 readinessProbe。
另外,livenessProbe、readinessProbe、startupProbe 三者的职责很容易搞混。我的经验是:startupProbe 用于慢启动应用的保护,readinessProbe 决定流量是否进入,livenessProbe 决定容器是否重启。不要在 livenessProbe 里做过重的依赖检查,否则很小的抖动就会导致频繁重启。
3. 集群级故障:Node 异常、CNI 网络与 DNS 解析
3.1 Node 进入 NotReady 的处理流程
当某个 Node 变成 NotReady,影响的不只是这台机器上的 Pod,如果 Pod 被驱逐到其他节点,还可能引起集群资源雪崩。我处理 NotReady 的标准流程是这样的:
bash复制# 查看节点状态详情
kubectl describe node <node-name>
# 查看 kubelet 日志
journalctl -u kubelet -f
# 查看节点的资源压力
kubectl describe node <node-name> | grep -A 10 "Conditions"
Conditions 里面的 MemoryPressure、DiskPressure、PIDPressure 是重点。生产环境里 DiskPressure 最常见,通常是 Docker 或 containerd 的日志文件、容器可写层、镜像占用写满了磁盘。kubelet 有默认的 eviction 阈值,比如 imagefs.available < 10% 就会开始清理镜像,但如果你把容器日志输出到了单独的数据盘而没有合理配置日志轮转,很快就能把盘打满。
一个典型的磁盘故障案例:某次集群里有个节点上大量 Pod 被驱逐,但检查发现不是 CPU 也不是内存问题,而是 /var/lib/docker 所在的盘满了。容器 stdout 日志全在宿主机上,一个应用一天产生十几 GB 日志,Docker 自带的 json-file log driver 默认不限制大小,磁盘很快就爆了。后续我们统一配置了 logrotate 和 Docker daemon 的 max-size 参数,才根治了这个问题:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
3.2 CNI 网络异常:Pod 无法互相通信
CNI 网络问题在排查链路上通常比较隐蔽,因为网络是“通”还是“不通”有时候是间歇性的。现象往往是:Service 解析正常,但 curl 不通;或者部分节点上的 Pod 互 ping 不通。排查路径如下:
- 检查 CNI 插件 Pod 状态,比如 Calico 的 felix、kube-proxy 的运行情况:
bash复制kubectl get pods -n kube-system -o wide
- 检查节点上的路由表和 iptables 规则:
bash复制ip route
iptables -L -n -t nat | grep <service-name>
- 检查 kube-proxy 模式。如果 kube-proxy 使用 iptables 模式,Service 多了之后规则量巨大,更新延迟明显;改用 IPVS 模式可以大幅降低规则数量和更新开销,这也是我推荐大规模集群使用 IPVS 的原因。
一个真实案例是:业务反馈某个 Service 偶尔超时,但看 Pod 日志一切正常。后来抓包发现 kube-proxy 在更新 iptables 规则的瞬间,连接被 DROP。问题发生在 Service 后端 Pod 发生变更时,iptables 规则更新采用了“先 flush 再重建”的逻辑,中间出现了极小的时间窗。换成 IPVS 模式后,这个问题自然消失。
3.3 集群 DNS 解析故障排查
DNS 是 Kubernetes 里最容易出“软故障”的组件。现象包括:应用能启动但访问数据库报域名解析失败,或者 Pod 内偶尔出现 Temporary failure in name resolution。
排查时先确认 CoreDNS 本身是不是正常的:
bash复制kubectl -n kube-system get pods -l k8s-app=kube-dns
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50
再用一个测试 Pod 验证解析链路:
bash复制kubectl run -it --rm test-dns --image=busybox -- /bin/sh
nslookup kubernetes.default.svc.cluster.local
如果解析正常但业务还是报 DNS 失败,大概率是 conntrack 表满或者 NodeLocal DNSCache 没有配置导致的高并发丢包。我曾经处理过一个 case:业务高峰时期 DNS 查询大量超时,查 CoreDNS 指标发现每秒查询量其实不高,但节点 conntrack 表满了,新的 DNS UDP 包被丢弃。临时调大 conntrack 参数:
bash复制sysctl -w net.netfilter.nf_conntrack_max=131072
长期方案是配置 NodeLocal DNSCache,把 DNS 查询缓存放到节点上,减少到 CoreDNS 的 UDP 连接数量。这个方案对 DNS QPS 高的集群效果立竿见影。
4. etcd 性能调优:从监控指标到关键参数
4.1 为什么 etcd 会成为集群瓶颈
etcd 是 Kubernetes 控制面的“数据库”,所有 API 对象的读写都要经过它。随着集群规模扩大、资源对象数量增多、变更频率加快,etcd 的性能直接决定整个集群的稳定性。一个集群里大量 Pod 频繁重建、大量 ConfigMap 被更新,都会放大 etcd 的压力。
etcd 的性能取决于三个底层因素。第一是磁盘延迟,etcd 每次写入都要持久化 WAL 日志,如果磁盘 fsync 延迟过高,写入性能直接崩。第二是网络延迟,etcd 集群节点之间需要同步数据,网络抖动会放大到每次写入。第三是内存与 key 数量,etcd 默认限制为 8Gi 内存,如果 backend 数据量持续增大,内存压力会导致频繁 compaction,进而引发读写放大。
4.2 必看的性能指标
排查 etcd 性能问题,我不建议一上来就改参数,而是先看指标。etcd 暴露的 Prometheus 指标里,我重点关注这几个:
| 指标名 | 含义 | 健康参考值 |
|---|---|---|
| etcd_server_leader_changes_seen_total | 主节点切换次数 | 长时间内不增长 |
| etcd_disk_wal_fsync_duration_seconds | WAL 日志落盘耗时 | p99 < 10ms |
| etcd_disk_backend_commit_duration_seconds | 后端事务提交耗时 | p99 < 25ms |
| etcd_network_peer_round_trip_time_seconds | 节点间 RTT | p99 < 50ms |
| etcd_server_health_failures | 健康检查失败次数 | 趋于 0 |
| etcd_server_slow_apply_total | 慢请求应用计数 | 尽量为 0 |
| etcd_mvcc_db_total_size_in_bytes | 数据库大小 | 不要超过 quota |
| grpc_server_started_total / grpc_server_handled_total | gRPC 请求量 | 关注异常增长 |
在线环境里我见过最多的是 etcd_disk_wal_fsync_duration_seconds 和 etcd_disk_backend_commit_duration_seconds 严重超标。查一下底层磁盘,发现用的是机械盘或者共享存储,这在大集群里完全无法满足 etcd 的 IOPS 要求。etcd 社区对磁盘的要求是顺序写吞吐不低于 50MB/s,随机写 IOPS 不低于 500,fsync 延迟越低越好。如果条件允许,给 etcd 单独挂一块 SSD,哪怕容量不大,性能提升也是肉眼可见的。
4.3 慢请求排查与常见根因
如果发现 apiserver 响应变慢,Etcd 慢请求常常是根因。排查时可以分几步看:
- 看 etcd 实例的 grpc 请求延迟。可以通过 etcdctl 直接查看端点状态:
bash复制etcdctl endpoint health --cluster -w table
etcdctl endpoint status --cluster -w table
- 看 etcd 的告警状态,如果触发了
NOSPACE告警,说明数据库超过了配额,需要做碎片整理或压缩历史版本:
bash复制etcdctl alarm list
- 检查 etcd 的 key 数量和版本数量。Kubernetes 的所有资源变更都会产生新的 revision,如果大量资源频繁更新,MVCC 的历史版本会疯狂膨胀,读请求需要扫描大量版本。可以在 etcd 端执行命令查看:
bash复制etcdctl endpoint status --cluster -w json | jq .
常见根因有几个。第一个是 K8s 控制面组件循环更新操作,比如某个 controller 不断更新 Deployment 的 annotation,造成 etcd 写放大。通过 apiserver 审计日志可以定位到是哪个组件在频繁写。第二个是 Event 对象过多,K8s 里 Event 本身是资源对象,如果系统里没人及时清理 Event,或者某次故障导致大量 Event 产生,etcd 的 key 数量会快速上涨。第三个是 compaction 和 defrag 策略配置不当,导致历史版本没有被及时清理。
4.4 关键参数调优与最佳实践
etcd 有几个参数是生产集群必须关注的。第一个是 --quota-backend-bytes,默认值在 etcd 3.4 里是 2Gi,对于生产集群这个值一般不够。推荐根据节点内存和业务增长情况设置为 8Gi 或更大,但要注意 quota 越大,内存占用和后续 defrag 时间也会增加。
第二个是 --auto-compaction-retention。K8s 的 etcd 环境一般建议开启自动压缩,比如保留最近 2 小时的历史版本:
bash复制--auto-compaction-retention=2h
压缩过后,数据库空间不一定立即回收,还需要手动执行 defrag:
bash复制etcdctl defrag --cluster
defrag 在 etcd 3.4 之后支持在线操作,但仍建议在低峰期执行,因为它会短暂影响读写性能。
第三个是请求大小限制。etcd 默认 --max-request-bytes 为 1.5MiB,如果 K8s 里有特别大的资源对象(比如大的 ConfigMap 或 CRD),写入会被拒绝。可以适当调大,但不宜过大,否则单个请求会占用大量内存。K8s apiserver 侧也有对应的 --max-mutating-requests-inflight 和 --max-requests-inflight,这两个参数控制并发请求数。如果 etcd 本身没问题而 apiserver 还是慢,经常是请求量超过了 apiserver 的处理上限,此时优先考虑给 apiserver 扩容,而不是无限调大并发上限。
磁盘 IO 调度也值得优化。etcd 对 fsync 的延迟极其敏感,如果宿主机用 ext4 且开启 barrier=1,会给写入增加额外开销。可以评估是否关闭 barrier,或用 noatime 挂载选项降低写放大。另外,数据盘和 WAL 盘建议分离,这样 WAL 写入不会被数据写入阻塞。在生产环境条件允许的集群中,独立 WAL 卷带来的收益非常明显。
还有一个经常被忽略的做法:etcd 数据目录的备份与恢复演练。调优只能预防性能问题,但真正出大事时你得保证能恢复数据。我建议至少每两周做一次 etcd 快照,并且在一个临时集群中实际演练一次恢复流程:
bash复制ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /backup/etcd-snapshot-$(date +%Y%m%d).db
恢复演练的价值不在于备份本身,而在于验证备份的有效性和恢复流程的可执行性。我见过不止一次,明明有备份,真到恢复时才发现证书路径不对、快照文件损坏、版本不匹配。只有演练过,恢复流程才真正可信。
5. 排障工具箱与实战建议
5.1 高频使用的排查命令组合
有一些命令组合是我每次排查故障都会敲的,整理出来直接抄:
bash复制# 看整体概况
kubectl get nodes -o wide
kubectl get pods -A | grep -v Running | grep -v Completed
# 深入某个 Pod
kubectl describe pod <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous --tail=100
kubectl exec -it <pod-name> -n <namespace> -- sh
# 看节点资源
kubectl describe node <node-name>
kubectl top node
kubectl top pod -A --sort-by=memory
# 看 apiserver 和 etcd 状态
kubectl get --raw=/healthz
kubectl get --raw=/readyz?verbose
5.2 故障复盘的正确姿势
有一个习惯强烈建议培养:无论多小的故障,解决后都写一份复盘记录。复盘的框架可以参考:
- 故障发现的时间、现象、影响面。
- 定位根因的过程,包括走了哪些弯路。
- 最终的恢复动作,精确到命令和配置变更。
- 事后改进项,比如告警补充、监控图表、参数调整。
- 经验沉淀到这里,下次再遇到类似问题可以快速复用。
我曾经复盘过一起 etcd 磁盘慢导致集群不可用的故障。事件发生后我们做了三件事:给 etcd 换 SSD、增加磁盘延迟告警、编写了节点磁盘塞满时的应急清理手册。后续再遇到类似情况,10 分钟就能处理完,不再像第一次那样手忙脚乱。
6. 写在最后:一些掏心窝的建议
排障能力和经验积累是正相关的,但有没有好的方法决定了积累的速度。我在实际处理故障时,最大的体会是:任何操作之前先确认影响面,任何配置变更之前先备份,任何参数调整之后持续观察一段时间。生产环境的故障往往不是单一原因造成的,而是多个小问题的叠加,所以不要满足于“恢复了”,要反复追问“为什么会发生”。系统不会说谎,指标和日志会告诉你真相,只要耐住性子一层层往下查,问题总是可以定位的。
最后分享一个小技巧:遇到 Pod 崩溃,先看退出码,再看探针配置,然后看资源限制,最后看应用日志。这个顺序能覆盖大多数场景,也避免了在错误的方向上浪费时间。希望这篇文章能帮你在 Kubernetes 排障路上少踩一些坑。
