运维知识
悠悠
2026年3月1日

别再瞎猜了!K8s生产环境排错实战手册:从Pod卡住到服务503,这些坑我都替你踩过了

说实话,K8s这玩意儿,部署起来可能半小时搞定,但真到了生产环境出问题,那可真是“一小时定位,三小时复盘,五小时背锅”。

今天这篇文章,不讲理论,不画架构图,就聊点实在的——生产环境真实遇到的排错案例 + 详细处理过程 + 踩过的坑。内容全来自我亲手处理过的故障,有些甚至半夜三点被电话叫醒去救火。如果你也在搞K8s,建议收藏,说不定哪天就用上了。


🧯 场景一:Pod 卡在 Pending,你以为是资源不够?

前几天,测试团队反馈新上线的服务一直起不来,kubectl get pod 看到状态是 Pending。第一反应:是不是节点资源不够?赶紧 kubectl describe pod xxx 一看:

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  2m    default-scheduler  0/5 nodes are available: 5 Insufficient cpu.

看起来确实是 CPU 不够。但奇怪的是,集群明明还有两台空闲节点,CPU 使用率不到30%。这时候很多人就懵了:“资源明明够啊,为啥调度不过去?”

真相其实是:QoS 和 PriorityClass 在背后搞鬼。

我们项目里为了保障核心服务,给某些 Pod 设置了高优先级(PriorityClass)。而这个新上线的服务没配,结果被调度器直接“降级”了——即使有资源,也得等高优先级任务跑完再说。

排查命令:

# 查看当前集群所有 PriorityClass
kubectl get priorityclass

# 查看 Pod 是否绑定了 PriorityClass
kubectl get pod <pod-name> -o jsonpath='{.spec.priorityClassName}'

解决办法:

  • 如果是非核心业务,可以临时调低其他高优 Pod 的副本数;
  • 或者给这个新服务也配上合适的 PriorityClass;
  • 更彻底的做法是:在命名空间级别设置 ResourceQuota + LimitRange,避免资源被无限制抢占。
💡 小提醒:别只看 kubectl describe 的 Events 表面信息。有时候“Insufficient cpu”只是障眼法,真正原因是亲和性(affinity)、污点(taint)或调度插件策略。

🌪️ 场景二:服务间歇性 503,日志却一切正常?

这是最让人抓狂的问题之一。用户访问接口,偶尔返回 503 Service Unavailable,但 kubectl logs 看应用日志,完全正常,连 error 都没有。重启 Pod?暂时好了,几小时后又来。

一开始以为是应用 Bug,后来发现:根本不是应用的问题,而是 conntrack 表溢出了!

K8s 默认用 iptables 做 Service 转发,而 iptables 依赖内核的 conntrack 模块记录连接状态。当并发连接数暴增(比如大促、压测、爬虫攻击),conntrack 表满了,新连接直接被丢弃,表现为“服务不可达”,但容器本身毫无感知。

怎么确认?

登录到 Pod 所在节点,执行:

# 查看当前 conntrack 连接数
sysctl net.netfilter.nf_conntrack_count

# 查看最大限制
sysctl net.netfilter.nf_conntrack_max

如果 count 接近 max(默认一般是 65536),基本就是它了。

解决方案:

  1. 临时扩容(应急):

    sysctl -w net.netfilter.nf_conntrack_max=262144
  2. 长期优化

    • 改用 IPVS 模式(性能更好,不依赖 conntrack);
    • 或者调整 kube-proxy 配置,启用 strictARP: true + 合理设置 conntrack 参数;
    • 对外暴露的服务,前面加一层 Nginx 或 LVS 做连接收敛。
📌 血泪教训:有一次我们没注意,大促当天凌晨 conntrack 满了,导致支付回调失败,损失几十万订单。从此以后,监控里必加 nf_conntrack_count 指标。

🔌 场景三:PVC 挂载失败,Pod 卡在 ContainerCreating

这种情况太常见了。你写了个 StatefulSet,挂了个 PVC,结果 Pod 一直卡在 ContainerCreating,describe 一看:

MountVolume.SetUp failed for volume "pvc-xxx" : mount failed: exit status 32

或者:

Unable to attach or mount volumes: unmounted volumes=[data], unattached volumes=[...]: timed out waiting for the condition

这时候很多人第一反应是:“存储后端挂了?” 但其实,90% 的情况是 StorageClass 配置问题或权限不对

举个真实例子:我们用的是 NFS 作为后端存储,StorageClass 里写了 mountOptions: ["vers=4.1"],但某台 NFS 服务器只支持 vers=3。结果新 Pod 调度到那台机器上就挂载失败。

排查步骤:

  1. 看 PV 和 PVC 是否绑定成功

    kubectl get pvc -n your-ns
    kubectl get pv

    如果 PVC 状态是 Pending,说明没找到匹配的 PV。

  2. 检查 StorageClass 是否存在且配置正确

    kubectl get storageclass
    kubectl describe storageclass your-sc-name
  3. 手动在节点上测试挂载(关键!):
    登录到目标节点,用 root 执行:

    mount -t nfs -o vers=4.1,nolock,proto=tcp your-nfs-server:/path /mnt/test

    如果报错,就能定位是网络、权限还是协议问题。

  4. 如果是云厂商(如 AWS EBS、阿里云云盘),还要检查:

    • 节点是否在同一可用区(AZ)?
    • IAM 权限是否允许挂载?
    • 磁盘是否已被其他实例占用?

修复建议:

  • 统一 StorageClass 的 mountOptions;
  • 对 NFS 存储,建议用 nolock,intr,soft 参数避免死锁;
  • 生产环境尽量用 CSI 驱动,而不是 in-tree 插件(已废弃)。

🌐 场景四:DNS 解析时有时无,nslookup 正常但 curl 失败?

这个问题特别诡异。你在 Pod 里执行 nslookup kubernetes.default,返回 IP 正常;但 curl http://my-service 却超时。

很多人以为是 CoreDNS 崩了,其实更可能是 DNS 缓存 + ndots 陷阱

K8s 默认给 Pod 的 /etc/resolv.conf 加了 ndots:5,意思是:如果域名中点少于 5 个,就先尝试拼接 search domain(比如 my-service.namespace.svc.cluster.local)。但如果 search domain 太多,或者网络延迟高,就会导致 DNS 查询超时重试,最终失败。

验证方法:

# 进入 Pod
kubectl exec -it your-pod -- sh

# 查看 resolv.conf
cat /etc/resolv.conf

# 手动测试带完整域名的解析(绕过 ndots)
nslookup my-service.namespace.svc.cluster.local

# 对比短域名
nslookup my-service

如果短域名慢或失败,长域名正常,基本就是 ndots 问题。

解决方案:

  1. 应用层改用 FQDN(完整域名):比如代码里写 http://my-service.namespace.svc.cluster.local:8080
  2. 调整 Pod 的 dnsConfig

    spec:
      dnsConfig:
        options:
          - name: ndots
            value: "1"
          - name: timeout
            value: "2"
  3. 升级 CoreDNS 到最新版,并开启缓存插件(cache 30);
  4. 监控 CoreDNS 的 latency 和 error rate,用 Prometheus + Grafana 做告警。
🧠 冷知识:K8s 1.27+ 已支持 dnsPolicy: ClusterFirstWithHostNet,对 hostNetwork Pod 更友好。

⚙️ 场景五:节点 NotReady,kubelet 日志全是 “PLEG is not healthy”

某天早上收到告警:多个节点状态变成 NotReady。登录节点一看,systemctl status kubelet 显示 active (running),但 kubectl get node 就是 NotReady。

查 kubelet 日志:

journalctl -u kubelet -n 100 | grep -i pleg

输出:

PLEG is not healthy: pleg was last seen active 3m ago

PLEG(Pod Lifecycle Event Generator) 是 kubelet 用来监听容器生命周期变化的组件。它卡住,通常是因为 容器运行时响应太慢

我们当时用的是 containerd,排查发现:

  • 节点上跑了太多 Pod(200+);
  • containerd 的 oom_score_adj 没调,被系统 OOM killer 干掉过一次;
  • 磁盘 IO 高,导致 containerd 状态同步延迟。

处理流程:

  1. 先隔离节点,防止调度新 Pod:

    kubectl cordon node-name
    kubectl drain node-name --ignore-daemonsets --delete-local-data --force
  2. 重启 containerd 和 kubelet

    systemctl restart containerd
    systemctl restart kubelet
  3. 优化配置

    • 给 containerd 设置更高的 oom_score_adj(比如 -999);
    • 限制单节点 Pod 数量(通过 kubelet 的 --max-pods=110);
    • 使用 SSD 磁盘,避免 IO 瓶颈。
✅ 经验:生产环境单节点 Pod 数别超过 100,否则 kubelet 容易卡死。别信“理论上支持 110”,那是理想值。

🧨 场景六:镜像拉取失败,但本地 docker pull 正常?

kubectl describe pod 显示:

Failed to pull image "registry.corp.com/app:v1": rpc error: code = Unknown desc = failed to pull and unpack image: failed to resolve reference "...": pull access denied

但你在节点上手动 crictl pull registry.corp.com/app:v1 却成功了!

原因:Pod 没配 imagePullSecrets!

K8s 拉镜像时,会用 Pod 所在命名空间下的 secret 来认证。如果你没显式指定 imagePullSecrets,它就用默认的(通常是空的)。

解决办法:

  1. 创建 secret:

    kubectl create secret docker-registry regcred \
      --docker-server=registry.corp.com \
      --docker-username=user \
      --docker-password=pass \
      -n your-ns
  2. 在 Deployment 中引用:

    spec:
      imagePullSecrets:
        - name: regcred
💥 坑点:Helm chart 里经常漏掉 imagePullSecrets,导致内网部署失败。建议在 values.yaml 里统一配置。

🛠️ 我的私藏排错工具箱

最后分享几个我常用的“暗黑”命令,关键时刻能救命:

  1. 绕过容器直接抓包(不用进 Pod):

    kubectl debug node/<node> -it --image=nicolaka/netshoot -- tcpdump -i eth0 port 80
  2. 查看 Pod 被哪个控制器管理

    kubectl get pod <pod> -o jsonpath='{.metadata.ownerReferences}'
  3. 快速导出所有事件(按时间排序)

    kubectl get events --sort-by=.metadata.creationTimestamp -A
  4. 检查 kubelet 是否上报心跳

    kubectl get --raw='/api/v1/nodes/<node>/proxy/stats/summary'
  5. 诊断镜像(自己打包的)

    FROM alpine:3.18
    RUN apk add --no-cache curl jq bind-tools net-tools iproute2 tcpdump
    CMD ["sleep", "infinity"]

    用法:

    kubectl run diag --image=my-diag-tools -it --rm --restart=Never -- sh

总结:排错不是玄学,是系统工程

K8s 排错,说难也难,说简单也简单。关键在于:

  • 别慌:先看状态(STATUS),再看事件(Events),最后看日志(Logs);
  • 分层排查:从 Pod → Node → Network → Storage → Control Plane,一层层剥;
  • 善用工具:kubectl + journalctl + tcpdump + Prometheus,组合拳打满;
  • 预防大于治疗:做好监控、告警、资源限制、健康检查,很多问题根本不会发生。

生产环境没有“不可能”,只有“还没遇到”。你今天省下的一个探针配置,可能就是明天半夜三点的夺命连环 call。


如果你觉得这篇文章帮你避开了一个坑,欢迎转发给你的同事(尤其是那个总说“K8s 很稳定”的开发)。也别忘了 关注 @运维躬行录,我会持续更新更多硬核实战经验,带你少走弯路,多睡好觉。

毕竟,运维的终极目标,不是解决问题,而是——让问题别发生

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码