故障排查与最佳实践
这一篇是全系列的收尾,目标只有一个:让每次排障都有迹可循。
每个故障场景都按统一套路展开:典型现象 → 可能原因 → 排查命令 → 解决方法。拿到症状直接对号入座,再顺着命令把证据链补齐。
无论遇到什么故障,下面四板斧几乎都是第一步:
# 1. 看状态、重启次数、落在哪个节点
kubectl get pods -o wide
# 2. 看事件(排障第一现场)
kubectl describe pod <pod-name>
# 3. 看日志,崩溃过的一定要带 --previous
kubectl logs <pod-name> --previous
# 4. 看整个命名空间的事件流
kubectl get events --sort-by=.metadata.creationTimestamp
先收集证据(事件、日志、状态),再下结论。90% 的 Kubernetes 故障,describe 的 Events 区和 --previous 日志里其实已经写好了答案。
kubectl debug
kubectl debug 是排障的瑞士军刀(1.18 引入,临时容器在 1.25 稳定),核心有三种用法:注入临时容器、调试节点、复制 Pod。它解决的最大痛点是:distroless/精简镜像里没有 shell,容器又一直重启,根本进不去现场。
用法一:注入临时容器(Ephemeral Container)
往运行中的 Pod 里塞一个临时调试容器,不重启业务容器:
kubectl debug -it web-7d9c4b6f5-x2v8k --image=busybox:1.36 --target=web
--image:调试容器用什么镜像,busybox 自带ps、netstat、wget,够用且体积小。--target:指定共享哪个业务容器的进程命名空间。不加它只能共享 Pod 的网络命名空间(查网络够用),加了之后能看到目标容器的进程:
# 进入临时容器后
ps aux # 能看到业务容器的 1 号进程
ls /proc/1/root # 通过 /proc 直接访问业务容器的文件系统
cat /proc/1/root/etc/app/config.yaml
注意限制:临时容器不能配置端口和资源配额,kubectl describe pod 也看不到它(在 status.ephemeralContainerStatuses 里),需要集群 1.23 以上版本。
用法二:调试节点(debug node)
没有 SSH 权限或 SSH 不通时,直接在节点上起一个特权 Pod:
kubectl debug node/node-2 -it --image=ubuntu:22.04
这个 Pod 会把宿主机根目录挂载到 /host,进去之后 chroot 一下就等价于登录了宿主机:
chroot /host
# 之后就是节点本地操作
journalctl -u kubelet -n 100 --no-pager
systemctl status containerd
df -h
新版本如果遇到权限受限,可以加 --profile=sysadmin 放开。
用法三:复制 Pod 换镜像/改命令
不动原 Pod,复制一个副本来折腾,适合「换个 debug 镜像试试」「容器秒崩,想用 sh 卡住现场」:
# 复制 Pod,并附加一个共享进程空间的调试容器
kubectl debug web-7d9c4b6f5-x2v8k -it --copy-to=web-debug \
--container=web --share-processes --image=busybox:1.36
# 复制 Pod,把业务容器换成带调试工具的镜像
kubectl debug web-7d9c4b6f5-x2v8k --copy-to=web-debug \
--set-image=web=registry.example.com/web:v1.2.3-debug
# 复制 Pod,把启动命令改成 sh(容器一直崩溃时卡住现场再慢慢查)
kubectl debug web-7d9c4b6f5-x2v8k -it --copy-to=web-debug \
--container=web -- sh
复制出来的 Pod 不受原控制器管理,不会随原 Pod 删除而消失,用完后务必 kubectl delete pod web-debug 手动清理。
Pod Pending
典型现象
$ kubectl get pod web-7d9c4b6f5-x2v8k
NAME READY STATUS RESTARTS AGE
web-7d9c4b6f5-x2v8k 0/1 Pending 0 10m
可能原因
- 资源不足:没有节点放得下(
Insufficient cpu/Insufficient memory),最常见。 - 节点污点没容忍:节点有 taint,Pod 没有对应 toleration。
- PVC 未绑定:Pod 挂的 PVC 处于 Pending,详见下文 PVC Pending 一节。
- 镜像拉取中:大镜像 + 慢网络,调度成功后长时间处于 Pending/ContainerCreating。
- 调度器异常:所有新 Pod 都 Pending 且没有任何事件,见 Scheduler 故障一节。
排查命令
kubectl describe pod web-7d9c4b6f5-x2v8k # 重点看 Events
kubectl get pvc # 看挂载的 PVC 是否 Bound
kubectl describe nodes | grep -A3 Taints # 看节点污点
kubectl top nodes # 看节点资源水位
典型的调度失败事件:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 24s default-scheduler 0/3 nodes are available: 1 Insufficient cpu, 2 Insufficient memory. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.
解决方法
- 资源不足:
FailedScheduling消息里会逐台列出节点为什么不行。降 request、加节点、或开 cluster-autoscaler 三选一。 - 污点:
kubectl describe node确认 taint 后,给 Pod 补 toleration:
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
- PVC 未绑定:
kubectl describe pvc看供给失败原因,跳转 PVC Pending 一节。 - 镜像拉取中:
describe里能看到Pulling image事件,要么等,要么预热镜像、换近一点的仓库。
0/3 nodes are available 后面的列表是关键:1 Insufficient cpu, 2 Insufficient memory 意思是 1 台 CPU 不够、2 台内存不够,按这个原因对症下药,而不是盲目扩容。
CrashLoopBackOff
典型现象
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-7d9c4b6f5-x2v8k 0/1 CrashLoopBackOff 6 (3m20s ago) 18m
可能原因
- 启动命令/入口错误:command、args 拼错,二进制不存在。
- 配置或依赖缺失:环境变量、ConfigMap、数据库连接失败,应用主动退出。
- 存活探针误杀:应用还没起完,livenessProbe 就把它杀了,循环往复。
- 资源问题:实际是 OOM,见 OOMKilled 一节。
排查命令
# 拿崩溃前的日志,这是最关键的一步
kubectl logs web-7d9c4b6f5-x2v8k --previous
# 当前这次运行的日志(可能为空,因为又崩了)
kubectl logs web-7d9c4b6f5-x2v8k
# 看上次的退出状态
kubectl describe pod web-7d9c4b6f5-x2v8k | grep -A8 "Last State"
Last State: Terminated
Reason: Error
Exit Code: 127
Started: Fri, 23 Oct 2026 10:02:11 +0800
Finished: Fri, 23 Oct 2026 10:02:12 +0800
常见退出码速查:
| Exit Code | 含义 | 常见原因 |
|---|---|---|
| 1 | 应用主动退出 | 配置错误、依赖连不上 |
| 126 | 命令不可执行 | 文件权限、挂载覆盖 |
| 127 | 命令不存在 | entrypoint 拼错、镜像里没这个二进制 |
| 137 | SIGKILL | OOMKilled 或被驱逐 |
| 143 | SIGTERM | 正常终止,或被探针/缩容杀死 |
解决方法
- 入口错误:修正 command/args;想进容器确认文件是否存在,用上一节的
kubectl debug --copy-to -- sh卡住现场。 - 探针误杀:调大
initialDelaySeconds/failureThreshold,慢启动应用改用startupProbe,别硬扛 initialDelay。 - 依赖未就绪:让应用带重试启动,或用 initContainer 等待依赖。
- 改好后还在 CrashLoopBackOff:kubelet 的重启是指数退避的,10s、20s、40s 一路退到上限 5 分钟。配置改对后可能要等当前退避周期结束才恢复,不想等就删 Pod 立即重建。
它本身不是错误原因,而是「反复崩溃 + 退避重启」的外在表现。真正的死因永远在 --previous 日志和 Exit Code 里。
ImagePullBackOff
典型现象
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-7d9c4b6f5-x2v8k 0/1 ImagePullBackOff 0 5m
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 4m40s kubelet Pulling image "registry.example.com/web:v1.2.3"
Warning Failed 4m32s kubelet Failed to pull image "registry.example.com/web:v1.2.3": rpc error: code = NotFound desc = failed to pull and unpack image "registry.example.com/web:v1.2.3": manifest unknown: manifest unknown
Warning Failed 4m32s kubelet Error: ErrImagePull
Normal BackOff 58s (x7 over 4m) kubelet Back-off pulling image "registry.example.com/web:v1.2.3"
Warning Failed 58s (x7 over 4m) kubelet Error: ImagePullBackOff
可能原因
- 镜像名/tag 拼错:报
manifest unknown,最常见。 - 私有仓库缺 imagePullSecrets:报
unauthorized/ 401。 - Docker Hub 限速:报
toomanyrequests: You have reached your pull rate limit,匿名拉取按 IP 限流,NAT 出口共享配额很容易撞墙。 - 架构不匹配:arm64 节点拉 amd64-only 的镜像,同样报
manifest unknown(清单里没有对应架构)。 - 仓库网络/证书问题:内网 harbor 证书不受信、域名解析不通。
排查命令
kubectl describe pod web-7d9c4b6f5-x2v8k # Events 里的错误原文最关键
# 本地或节点上直接验证镜像是否存在
crictl pull registry.example.com/web:v1.2.3
# 确认拉取密钥已配置
kubectl get pod web-7d9c4b6f5-x2v8k -o jsonpath='{.spec.imagePullSecrets}'
kubectl get secret regcred -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
解决方法
- 拼错:修正
image字段,注意 tag 大小写、library/前缀。 - 私有仓库:建 Secret 并挂到 Pod 或 ServiceAccount 上:
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=admin \
--docker-password='xxx'
spec:
imagePullSecrets:
- name: regcred
- 限速:配 registry mirror 走内网缓存,或认证账号拉取(配额更高),生产集群统一收口到 Harbor。
- 架构:用
docker buildx出 multi-arch 镜像,或临时用 nodeSelector 把 Pod 钉到 amd64 节点。
ErrImagePull 是第一次拉取失败,ImagePullBackOff 是失败后退避重试中,两者是同一问题的两个阶段,看 Events 原文即可分辨。
OOMKilled
典型现象
Pod 状态在 Running 与重启之间反复,RESTARTS 持续上涨:
$ kubectl describe pod web-7d9c4b6f5-x2v8k
State: Running
Started: Fri, 23 Oct 2026 10:12:05 +0800
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Fri, 23 Oct 2026 09:58:11 +0800
Finished: Fri, 23 Oct 2026 10:12:03 +0800
可能原因
- limit 太小:
limits.memory低于应用正常水位,流量一来就被杀。 - 应用内存泄漏:内存缓慢爬坡,几小时或几天后触顶,每次被杀的时间间隔很有规律。
- JVM 不感知容器:JDK 8u191 之前的版本按宿主机内存算默认堆,容器 limit 形同虚设。
- 节点内存不足被驱逐:节点压力大时 QoS 低的 Pod 先牺牲,这种一般伴随
Evicted而不是 OOMKilled。
排查命令
kubectl describe pod web-7d9c4b6f5-x2v8k | grep -A8 "Last State"
# 当前用量
kubectl top pod web-7d9c4b6f5-x2v8k --containers
# 崩溃前日志
kubectl logs web-7d9c4b6f5-x2v8k --previous
# 节点内核日志里的 OOM 记录
ssh node-2 'journalctl -k | grep -i "killed process"'
kubectl top 只能看瞬时,判断「limit 太小」还是「泄漏」要看趋势:Prometheus 里 container_memory_working_set_bytes 曲线缓慢爬坡后触顶是泄漏,瞬间尖峰是流量或加载行为。
解决方法
- limit 太小:按 P95 用量加 20% 余量重设:
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"
- 内存泄漏:抓 heap dump 或 profiling 定位后修代码;临时手段是加大 limit 并配告警,给修复争取时间。
- JVM:升级到 JDK 8u191+/11+ 开启容器感知,并显式限制堆占 limit 的比例,给堆外内存留空间;更老的版本只能按 limit 手工写死
-Xmx:
-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0
137 = 128 + 9,即进程收到 SIGKILL。cgroup 的 OOM killer 杀超限容器时不会有任何优雅退出过程,应用来不及写最后一行日志。如果 --previous 日志戛然而止且退出码 137,基本就是内存问题。
DNS 故障
典型现象
$ kubectl exec -it web-7d9c4b6f5-x2v8k -- nslookup api.example.com
;; connection timed out; no servers could be reached
或表现为:集群内服务名解析正常,外部域名解析慢、偶发 NXDOMAIN、间歇性超时。
可能原因
- CoreDNS Pod 不健康:副本不够被打爆、 readiness 失败、OOM。
- Pod 的 dnsPolicy 配错:
hostNetworkPod 用默认策略会拿到节点 DNS,解析不了集群内服务名。 - ndots:5 的坑:名字里的点少于 5 个时,会先依次拼接 search 域查询,外部域名白白多查好几轮,慢且偶发超时。
- 节点本地 DNS 缓存、conntrack 表满等底层问题。
排查命令
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system logs deploy/coredns --tail=50
# 看 Pod 实际生效的 DNS 配置
kubectl exec -it web-7d9c4b6f5-x2v8k -- cat /etc/resolv.conf
# 集群内域名与外部域名分别验证
kubectl exec -it web-7d9c4b6f5-x2v8k -- nslookup kubernetes.default.svc.cluster.local
kubectl exec -it web-7d9c4b6f5-x2v8k -- nslookup api.example.com.
典型的 resolv.conf:
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
解决方法
- CoreDNS 不健康:重启或扩容 CoreDNS 副本,检查它自己的资源 limit 与探针。
- dnsPolicy:普通 Pod 保持默认
ClusterFirst;hostNetwork: true的 Pod 想解析集群内服务,必须显式设置dnsPolicy: ClusterFirstWithHostNet。 - ndots 问题,两个解法:
- 应用侧零成本技巧:外部域名写成 FQDN,结尾加点——
api.example.com.。结尾的点让它成为绝对域名,跳过 search 域遍历,curl、Java、Go 全都认。 - 集群侧:调小 ndots,让点较多的名字直接查绝对域名:
- 应用侧零成本技巧:外部域名写成 FQDN,结尾加点——
spec:
dnsConfig:
options:
- name: ndots
value: "2"
api.example.com(2 个点,小于 ndots 5)会先查 api.example.com.default.svc.cluster.local 等 3 个搜索域,全失败后才查真身;api.example.com. 直接一步到位。排障时对比这两个名字的解析耗时,立刻就能确认是不是 ndots 的锅。
Service 不通
典型现象
$ kubectl exec -it web-7d9c4b6f5-x2v8k -- curl http://api-service:8080/health
curl: (7) Failed to connect to api-service port 8080 after 2 ms: Connection refused
或干脆超时无响应。
可能原因
- Endpoints 为空:Service 的 selector 和 Pod 的 label 不匹配,最常见,没有之一。
- targetPort 与容器端口不符:Service 把流量转到了容器没在监听的端口。
- 后端 Pod 没 Ready:readinessProbe 不通过,Pod 不会进 endpoints。
- kube-proxy 异常:iptables/IPVS 规则没下发或残留脏规则。
- 会话保持误导判断:
sessionAffinity: ClientIP把同一客户端固定到同一后端,单客户端测试时像是不通或不均衡。
排查命令
# 第一步:看 endpoints 有没有后端
kubectl get endpoints api-service
# 第二步:核对 selector 与 Pod label 是否对得上
kubectl describe svc api-service | grep -E "Selector|Port|TargetPort"
kubectl get pods -l app=api -o wide --show-labels
# 第三步:绕过 Service 直连 Pod IP,验证应用本身通不通
kubectl exec -it web-7d9c4b6f5-x2v8k -- curl 10.244.2.15:8080/health
# 第四步:怀疑 kube-proxy 时看它的日志
kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=50
endpoints 为空时的样子:
$ kubectl get endpoints api-service
NAME ENDPOINTS AGE
api-service <none> 10m
解决方法
- selector 不匹配:对齐
Service.spec.selector与 Pod 的metadata.labels,注意大小写、多余空格、版本号差异。 - targetPort 错:改成容器真实监听的端口,或干脆用命名端口(
named port)解耦。 - Pod 没 Ready:回头修 readinessProbe 或应用本身,Ready 后自动进 endpoints。
- kube-proxy:重启 kube-proxy;检查模式下的规则是否真的存在,iptables 模式看
iptables -t nat -L KUBE-SERVICES,IPVS 模式看ipvsadm -Ln。 - 会话保持:不需要粘性就关掉:
spec:
sessionAffinity: None
endpoints 有没有 → 直连 Pod IP 通不通 → kube-proxy 规则在不在。三步走下来,90% 的 Service 问题已经定位。
PVC Pending
典型现象
$ kubectl get pvc data-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-pvc Pending fast-ssd 30m
引用它的 Pod 一侧:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m default-scheduler 0/3 nodes are available: 3 pod has unbound immediate PersistentVolumeClaims.
可能原因
- 没有匹配的 PV:静态供给模式下,容量、accessModes、storageClassName 任一不匹配都绑不上。
- StorageClass 不存在或拼错:PVC 指向的 SC 根本不存在。
- provisioner 异常:动态供给的 CSI 组件挂了,没人干活。
- 配额/容量不足:云盘配额用完、存储池满了。
排查命令
kubectl describe pvc data-pvc # Events 通常直接写明原因
kubectl get pv # 有没有可用的 PV
kubectl get sc # SC 存不存在、名字对不对
# 动态供给时看 provisioner 日志
kubectl -n kube-system get pods | grep -E "csi|provisioner"
典型的 StorageClass 拼错事件:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ProvisioningFailed 2m (x12 over 30m) persistentvolume-controller storageclass.storage.k8s.io "fast-ssd" not found
解决方法
- SC 拼错:PVC 的
storageClassName创建后不可修改,只能删了重建(确认里面没数据)。 - 静态 PV 不匹配:补一个满足条件的 PV——容量不小于 request、accessModes 一致、storageClassName 相同:
apiVersion: v1
kind: PersistentVolume
metadata:
name: data-pv
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
hostPath:
path: /data/pv001
- provisioner 异常:按 provisioner 日志修 CSI 组件本身。
- WaitForFirstConsumer:
volumeBindingMode: WaitForFirstConsumer的 SC 下,PVC Pending 是设计如此——要等引用它的 Pod 调度时才供给,别当成故障删了重建。
Node NotReady
典型现象
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
node-1 Ready control-plane 45d v1.28.2
node-2 NotReady <none> 45d v1.28.2
node-3 Ready <none> 45d v1.28.2
可能原因
- kubelet 挂了:进程退出或反复崩溃,节点心跳中断。
- 网络分区:节点与控制面断连超过约 40 秒,状态开始翻 NotReady/Unknown。
- 证书过期:kubelet 的 client 证书过期,连不上 API Server。
- 磁盘压力:DiskPressure 条件为 True,kubelet 主动报 NotReady 或开始驱逐 Pod。
- 容器运行时挂了:containerd/docker 无响应。
排查命令
# 控制面侧:看节点 Conditions,这是节点排障的第一现场
kubectl describe node node-2
# 节点本机
systemctl status kubelet
journalctl -u kubelet -n 100 --no-pager
systemctl status containerd
df -h / /var/lib/containerd
Conditions 区的典型输出:
Conditions:
Type Status Reason Message
---- ------ ------ -------
MemoryPressure False KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure True KubeletHasDiskPressure kubelet has disk pressure
PIDPressure False KubeletHasSufficientPID kubelet has sufficient PID available
Ready False KubeletNotReady container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:cni plugin not initialized
解决方法
- kubelet 挂了:
journalctl -u kubelet定位原因(证书、配置、cgroup driver),修复后systemctl restart kubelet。 - 证书过期:kubeadm 集群执行
kubeadm certs renew all后重启控制面组件;kubelet 证书确认rotateCertificates: true已开启。 - 磁盘压力:
crictl rmi --prune清理无用镜像、清日志文件、扩盘;根治要调整 kubelet 的eviction-hard阈值或应用日志策略。 - 网络分区:恢复网络是第一优先级;短期恢复不了的节点先
kubectl cordon+kubectl drain把负载迁走,避免 Pod 卡在 Terminating。
ETCD 故障
典型现象
API 请求变慢、超时,kubectl 大面积卡住:
$ kubectl get pods
Error from server: etcdserver: request timed out
etcd 日志里的危险信号:
W | etcdserver: read-only range request "key:\"/registry/pods/\"" took too long (1.982763412s) to execute
W | wal: sync duration of 1.023s, expected less than 1s
可能原因
- 磁盘 IO 慢:WAL fsync 延迟高,p99 超过 100ms 就要警惕,秒级必然频繁超时。
- quorum 丢失:多数成员宕机,集群无法选出 leader,整体不可用。
- db 超 quota:默认 2GB 配额被打满,触发 NOSPACE 告警,集群进入只读(写请求被拒绝)。
- 网络分区/时钟漂移:成员间心跳异常,频繁重新选举。
排查命令
ETCDCTL_API=3 etcdctl endpoint status --cluster -w table \
--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
ETCDCTL_API=3 etcdctl endpoint health --cluster
ETCDCTL_API=3 etcdctl alarm list
# Prometheus 指标看 wal_fsync_duration_seconds 和 backend_commit_duration_seconds 的 p99
解决方法
- 磁盘慢:etcd 数据目录迁到 SSD 并独占磁盘,避免与其他 IO 争抢;云盘提高 IOPS 档位。
- db 超 quota:确认
alarm list有 NOSPACE 后,压缩历史版本、整理碎片、解除告警:
# 取当前版本号,压缩掉之前的历史
rev=$(ETCDCTL_API=3 etcdctl endpoint status --write-out="json" | egrep -o '"revision":[0-9]*' | egrep -o '[0-9]*')
ETCDCTL_API=3 etcdctl compact $rev
ETCDCTL_API=3 etcdctl defrag
ETCDCTL_API=3 etcdctl alarm disarm
长期措施:调大 --quota-backend-bytes(比如 8GB)并建立定期 compact/defrag 的运维机制。
- quorum 丢失:先抢救存活成员;确实凑不齐多数时,用最近的快照重建集群(3.5 及以上用
etcdutl snapshot restore,旧版本用etcdctl snapshot restore),再逐台把成员加回来。这也再次说明日常快照备份是保命线:
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db
defrag 会短暂锁库,必须先 compact 再 defrag,否则空间回收不了多少。多成员集群要逐台滚动操作,千万不要同时对所有成员执行。
API Server 故障
典型现象
$ kubectl get nodes
The connection to the server 192.168.1.10:6443 was refused - did you specify the right host or port?
证书过期时:
Unable to connect to the server: x509: certificate has expired or is not yet valid: current time 2026-10-23T10:30:00+08:00 is after 2026-10-20T08:00:00Z
可能原因
- 证书过期:kubeadm 签发的证书默认有效期 1 年,周年庆变事故日。
- 连不上 etcd:etcd 故障传导,API Server 进程活着但请求全部超时。
- 请求风暴:失控的控制器或脚本疯狂 list/watch,把 API Server 打到 OOM 或响应瘫痪。
- static Pod 起不来:手改 manifest 改错,kubelet 反复拉起失败。
排查命令
kubectl 用不了的时候,直接上控制面节点看容器运行时和落盘日志:
# static Pod 的容器状态
crictl ps -a | grep kube-apiserver
# 看 apiserver 日志(两种方式)
crictl logs <apiserver-container-id>
tail -100 /var/log/pods/kube-system_kube-apiserver-*/kube-apiserver/*.log
# 证书有效期
kubeadm certs check-expiration
# static Pod 反复重启时看 kubelet 的拉起记录
journalctl -u kubelet -n 100 --no-pager
解决方法
- 证书过期:
kubeadm certs renew all续期后重启控制面组件(或重启 kubelet 让 static Pod 重建),并建立证书到期巡检,别再等周年。 - etcd 连不上:先按上一节修 etcd,再核对 apiserver 的
--etcd-servers地址与 etcd 证书。 - 请求风暴:开 API Priority and Fairness(1.20+ 默认开启)兜底限流;通过
apiserver_request_total指标找出凶手客户端并掐掉;临时可以加 apiserver 副本、调大内存和--max-requests-inflight。 - manifest 改错:把
/etc/kubernetes/manifests/kube-apiserver.yaml改回正确配置,kubelet 会自动重建 static Pod。
kubectl logs 依赖 API Server,控制面自己出事时这条路就断了。crictl logs、/var/log/pods 落盘日志、journalctl -u kubelet 三条备用路线都要会走。
Scheduler 故障
典型现象
所有新 Pod 卡 Pending,但 describe 里没有任何事件——这是和「资源不足导致的 Pending」最关键的区别:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-7d9c4b6f5-x2v8k 0/1 Pending 0 20m
$ kubectl describe pod web-7d9c4b6f5-x2v8k
...
Events: <none>
可能原因
- kube-scheduler 挂了:没人做调度,自然没有事件。
- leader 选举失败:拿不到租约,多个副本都在旁观,没人干活。
- schedulerName 对不上:Pod 指定了自定义调度器名字,但那个调度器没在运行。
- 调度器连不上 API Server:证书或网络问题。
排查命令
kubectl -n kube-system get pods -l component=kube-scheduler
kubectl -n kube-system logs -l component=kube-scheduler --tail=100
# 看 leader 选举租约
kubectl get leases -n kube-system kube-scheduler -o yaml
# 看 Pod 指定了哪个调度器
kubectl get pod web-7d9c4b6f5-x2v8k -o jsonpath='{.spec.schedulerName}'
选举失败的典型日志:
E1023 02:15:30.123456 1 leaderelection.go:334] error retrieving resource lock kube-system/kube-scheduler: Get "https://10.96.0.1:443/apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-scheduler": dial tcp 10.96.0.1:443: i/o timeout
解决方法
- 选举失败:顺着日志修 API Server 连通性;多副本部署时只要有一个持锁即可,单副本故障不用过度恐慌。
- schedulerName 对不上:核对 Pod 的
spec.schedulerName与实际部署的调度器名称,笔误就让 Pod 永远没人管。 - 调度器进程挂了:同 API Server 一节,控制面 static Pod 用
crictl和/var/log/pods排查。
Pod Pending 且 describe 有 FailedScheduling 事件,说明调度器活着只是没地方放;完全没有事件,说明根本没人干活,直接去查调度器本身。
kubelet 故障
典型现象
节点翻 NotReady、Pod 状态不更新、exec/logs 失败,节点上能看到:
$ journalctl -u kubelet -n 50
Oct 23 10:18:01 node-2 kubelet[1234]: E1023 10:18:01.123456 1234 kubelet.go:2410] "Container runtime sanity check failed" err="rpc error: code = DeadlineExceeded desc = context deadline exceeded"
Oct 23 10:18:05 node-2 kubelet[1234]: I1023 10:18:05.234567 1234 setters.go:xxx] "Node became not ready" node="node-2" condition={Type:Ready Status:False ...}
Oct 23 10:18:05 node-2 kubelet[1234]: PLEG is not healthy: pleg was last seen active 3m5.123s ago; threshold is 3m0s
可能原因
- PLEG is not healthy:容器运行时卡住,Pod 生命周期事件(PLEG)超过 3 分钟没更新,kubelet 判定节点不健康。
- cgroup driver 不一致:kubelet 用
cgroupfs而 containerd 用systemd(或反过来),经典坑,表现为 kubelet 起不来或节点上 cgroup 错乱、内存统计失真。 - 镜像垃圾回收配置不当:磁盘被镜像打满触发 DiskPressure,或 GC 阈值太激进频繁回收。
- 证书过期、配置错误:升级或改配置后起不来。
cgroup driver 不一致的经典报错:
failed to run Kubelet: misconfiguration: kubelet cgroup driver: "cgroupfs" is different from docker cgroup driver: "systemd"
排查命令
systemctl status kubelet
journalctl -u kubelet -n 100 --no-pager
journalctl -u containerd -n 50 --no-pager
# 运行时是否响应
crictl ps
# 两边 cgroup driver 分别是什么
crictl info | grep -i systemd
grep cgroupDriver /var/lib/kubelet/config.yaml
# init 系统确认(systemd 系统上两边都该用 systemd)
ps -p 1 -o comm=
df -h /var/lib/containerd
crictl images | wc -l
解决方法
- PLEG:通常是运行时卡死,先
systemctl restart containerd(containerd 的 shim 独立运行,重启 daemon 不影响业务容器),再systemctl restart kubelet;根治要查磁盘 IO、内核版本和运行时 bug。 - cgroup driver:systemd 作为 init 的系统上,统一两边都为
systemd:
# /var/lib/kubelet/config.yaml
cgroupDriver: systemd
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
改完执行 systemctl restart containerd kubelet。
- 镜像 GC:临时
crictl rmi --prune清理未使用镜像;长期调整 kubelet 的imageGCHighThresholdPercent/imageGCLowThresholdPercent(例如 85/80),并配合磁盘扩容。
企业最佳实践
排障的终点是少出故障。下面这些标配清单,逐条对照自查,能挡掉前面 80% 的事故。
资源配置:request/limit 标配
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "1Gi"
- request 是调度依据,不写等于让调度器盲排,节点必然超售。
- limit 不写,一个内存泄漏的 Pod 就能拖垮整台节点的邻居。
- 命名空间用 LimitRange 设默认值兜底,ResourceQuota 防单团队吃光集群。
探针:三件套标配
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 20
- 慢启动应用必须配 startupProbe,不要用超长
initialDelaySeconds硬扛。 - liveness 探测路径要轻量,不要查数据库等下游依赖——下游抖一下,所有 Pod 被同时重启,就是一场雪崩。
PDB:可用性底线标配
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 1
selector:
matchLabels:
app: web
没有 PDB,节点 drain、集群升级时可能瞬间把全部副本赶光。
镜像:固定版本
- 生产禁止
latest,固定完整版本号nginx:1.25.4,强一致场景用 digest。 - 镜像统一走私有仓库(Harbor),入库同步做漏洞扫描。
日志:输出到 stdout
- 应用日志只写 stdout/stderr,不写容器内文件(文件随容器消失,也绕过了采集链路)。
- 节点 agent(Fluent Bit/Filebeat/Vector)统一采集,结构化 JSON 并带 traceId,排障时跨服务串联。
RBAC:最小权限
spec:
serviceAccountName: web-sa
automountServiceAccountToken: false # 不需要调 API 的应用直接关掉
- 每个应用独立 ServiceAccount,按需绑 Role,严禁图省事绑 cluster-admin。
- 定期审计 ClusterRoleBinding,清退过期授权。
备份:etcd 定期快照
- 定期
etcdctl snapshot save,副本异地存放,控制面节点的 manifests 和 pki 目录一并备份。 - 必须演练恢复:没恢复过的备份约等于没有备份。
变更:一切走 GitOps
- 所有变更先进 Git,由 Argo CD/Flux 同步到集群,禁止
kubectl apply直改生产。 - 集群状态可回溯、可回滚,「谁改了什么」一目了然。
- 结合 code review 与策略引擎(OPA/Kyverno),在入库前卡掉无 limit、latest、hostPath 这类危险配置。
最后汇总一张自查表:
| 项目 | 标配要求 | 防住的坑 |
|---|---|---|
| request/limit | 每个容器都写 | Pod Pending、OOMKilled、节点超售 |
| 探针 | readiness + liveness + startup | CrashLoopBackOff 误杀、流量打到未就绪 Pod |
| PDB | minAvailable 或 maxUnavailable | 升级/驱逐导致服务中断 |
| 镜像 | 固定版本、私有仓库 | ImagePullBackOff、latest 不可回溯 |
| 日志 | stdout + 统一采集 | 排障时无日志可查 |
| RBAC | 最小权限、独立 SA | 凭据泄露后的横向移动 |
| 备份 | etcd 快照 + 恢复演练 | quorum 丢失无法恢复 |
| GitOps | 变更全走 Git | 配置漂移、事故无法回溯 |
这篇里的每个故障场景,几乎都能在这份清单里找到对应的预防措施。把清单落地,排障就会从「救火」变成「走流程」。