mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4408 字
12 分钟
Kubernetes 故障排查与性能优化
2021-12-17

Kubernetes 的故障排查之所以困难,不是因为缺少工具,kubectl describekubectl logskubectl exec 就摆在那里,而是因为问题的因果链往往跨越多层:一个 Pod 崩溃可能源于节点内存不足,而内存不足又可能源于相邻 Pod 的资源限制设置不当,资源限制不当又可能追溯到初始部署时的估算偏差。这篇文章不打算做命令速查表(那种东西 kubectl --help 就有),而是建立一个从症状回溯根因的诊断思维:看到什么现象,该往哪个方向查,为什么要查那里而不是别处。

一、Pod 调度与运行时问题#

Pod 一直卡在 Pending 状态,是大多数人遇到的第一个 Kubernetes 故障。调度失败的原因看起来很多,资源不足、亲和性冲突、污点不匹配、拓扑约束,但它们的共同特征只有一个:调度器找不到一个愿意接纳这个 Pod 的节点。理解这一点,排查就有了方向:不是去猜原因,而是去问调度器”你为什么拒绝了”。

graph TB subgraph "调度失败原因" A["资源不足"] --> E["Pending Pod"] B["亲和性/反亲和性"] --> E C["污点未容忍"] --> E D["拓扑约束"] --> E end

调度器拒绝一个 Pod 时,会在 Event 中写下理由。kubectl describe pod <pod-name> 的 Events 段是最直接的诊断入口,常见的事件类型包括 FailedScheduling(调度失败)、NetworkNotReady(网络未就绪)、FailedMount(挂载失败)。这些事件不是装饰性的,它们就是调度器的”拒信”,里面写明了拒绝原因。

# 查看 Pod 调度事件
kubectl describe pod <pod-name> | grep -A 10 "Events:"
# 查看调度器日志(控制平面层面)
journalctl -u kube-scheduler -n 100
# 模拟调度:在目标节点上启动一个调试 Pod 验证环境是否就绪
kubectl debug node/<node-name> -it --image=busybox

资源不足是最常见的调度失败原因。这里的”不足”指的是节点上已被 requests 占用的资源,而不是实际使用量,Kubernetes 调度器看的是 requests 而非 usage,即使一个 Pod 实际只用了 100Mi 内存,如果它 requests 了 2Gi,调度器也会认为那 2Gi 已被占满。这是很多人困惑的根源:节点明明还有空闲内存,为什么调度不过去?因为调度器不信任运行时用量,它只信任声明值。这个设计选择有明确的工程理由:运行时用量是波动的,如果调度器基于瞬时用量做决策,刚调度完一个 Pod,另一个 Pod 的用量突然上升,节点就会超载。requests 是静态契约,调度器基于契约做决策,才能保证不超卖。

Note

requestslimits 的区别在此处尤为关键:requests 决定调度,limits 决定运行时上限。一个 Pod 可以 requests: 256Mi, limits: 2Gi,调度时只占 256Mi 的位置,但运行时最多能用 2Gi。

污点(Taint)和容忍(Toleration)是另一类常见的调度障碍。节点被打上污点后,只有声明了对应容忍的 Pod 才能被调度上去,这个机制的初衷是把专用节点(如 GPU 节点、Master 节点)隔离出来,但如果忘记给 Pod 添加容忍,或者污点打错了 key,就会导致调度失败。亲和性和拓扑分布约束的逻辑更复杂,但排查方法相同:读 Events

当集群资源确实不足时,优先级抢占(PriorityClass)可以让高优先级工作负载驱逐低优先级工作负载。这不是一个应该随意使用的机制,每次抢占都会导致被驱逐的 Pod 中断服务,但在资源紧张的场景下,它保证了关键路径不被阻塞。

# 优先级调度
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 100000
globalDefault: false
description: "高优先级任务"
---
apiVersion: v1
kind: Pod
metadata:
name: high-priority-app
spec:
priorityClassName: high-priority
containers:
- name: app
image: my-app
Warning

抢占是有代价的:被驱逐的 Pod 会收到 SIGTERM 信号,有 terminationGracePeriodSeconds(默认 30 秒)的时间做清理。如果被驱逐的 Pod 正在处理请求,这些请求可能会失败。不要把所有 Pod 都设成高优先级,那等于没有优先级。

二、网络问题排查#

Kubernetes 网络问题的排查难度在于故障点太多:Pod 内部、Pod 到 Service、Service 到 Endpoints、跨 Namespace、跨 Node、出集群,每一层都有自己的故障模式。与其逐层枚举命令,不如建立一个从内到外的诊断流程:先确认 Pod 自身的网络栈是否正常,再查 Service 路由是否生效,最后看 NetworkPolicy 或外部网关是否阻断了流量。

graph LR A["Pod 内"] --> B["Service"] B --> C["外部"] B --> D["Endpoints 检查"] B --> E["DNS 解析"] C --> F["NetworkPolicy"]

第一步:确认 Pod 网络栈。进入 Pod 的网络命名空间,检查网络接口和路由表。如果 ip addr 显示网卡没分配到 IP,问题出在 CNI 插件,而不是应用;如果 ip route 缺少默认路由,Pod 连网关都到不了。CNI 插件负责为 Pod 分配 IP 和配置路由,如果 CNI Pod 本身异常(比如 Calico 的 calico-node 或 Cilium 的 Agent 崩溃),新创建的 Pod 就拿不到网络配置。

# 进入 Pod 网络空间
kubectl exec -it <pod-name> -- sh
# 检查网络接口和路由
ip addr
ip route
# 测试基本连通性
ping <target-ip>
curl -v <service-name>

第二步:检查 DNS 解析。Kubernetes 集群内部的服务发现依赖 CoreDNS。如果 nslookup <service-name> 返回 NXDOMAIN,问题可能出在 CoreDNS 本身(检查 kube-system 命名空间下 CoreDNS Pod 的状态),也可能出在 Pod 的 /etc/resolv.conf 配置,特别是 ndots 设置导致的过度搜索域查询,会让每次解析都尝试多个后缀,拖慢 DNS 响应甚至超时。ndots: 5 是默认值,意味着任何包含少于 5 个点的主机名(比如 backendbackend.default)都会依次尝试追加 backend.default.svc.cluster.localbackend.default.svc.cluster.local.<节点域> 等后缀,直到匹配或全部失败。对于短名称来说,前几次尝试必然失败,白白增加了延迟。

# 检查 DNS 解析
nslookup <service-name>
cat /etc/resolv.conf

第三步:Service 和 Endpoints。Service 本身不转发流量,它只是 iptables/IPVS 规则的入口。流量能否到达后端 Pod,取决于 Endpoints 是否健康。kubectl get endpoints <service-name> 如果显示空列表,说明没有 Pod 匹配 Service 的 Selector,这是最常见的 Service 故障,通常是因为标签拼写错误或 Pod 还没 Ready。

# 检查 Endpoints
kubectl get endpoints <service-name>
# 检查 Selector 匹配的 Pod
kubectl get pods -l app=<selector> --show-labels
# 查看 Service 详情
kubectl describe svc <service-name>
# 临时起一个调试 Pod 测试 Service 连通性
kubectl run curl --image=curlimages/curl -it --rm -- \
curl -v http://<service-name>:<port>

第四步:NetworkPolicy。如果 Endpoints 健康但流量仍然不通,NetworkPolicy 是最可能的拦截者。NetworkPolicy 默认允许所有流量,一旦任何 Namespace 中存在针对某个 Pod 的 Policy,该 Pod 的入站或出站流量就必须被显式允许,这是一条容易被忽视的规则。排查时先确认是否有 Policy 存在,再检查 Policy 的 podSelectoringress/egress 规则是否覆盖了你的流量路径。

# 检查 NetworkPolicy
kubectl get networkpolicy
kubectl describe networkpolicy <policy-name>
# 在 frontend Pod 内测试到 backend 的连通性
kubectl exec -it frontend -- curl -v http://backend:80
Tip

排查网络问题时,iptables -L -n -t nat | grep <service-name> 可以直接看到 kube-proxy 生成的 NAT 规则。如果规则不存在,说明 kube-proxy 没有正确同步 Service 配置,这通常指向 kube-proxy 本身的故障。

三、存储问题排查#

存储问题的表象通常很统一:Pod 启动失败或挂载卷报错。但底层原因差异很大,PVC Pending 意味着没有可用的 PV,PVC Lost 意味着绑定的 PV 被删除了,而 PVC Bound 但 Pod 无法挂载则指向 CSI 驱动或节点层面的故障。这三者的排查路径完全不同,不能一概而论。

PVC 处于 Pending 状态,首先要看的是 kubectl describe pvc 输出中的 Event。最常见的两个原因是 StorageClass 不存在(拼写错误或未安装)和 PV 不足(静态供给场景下没有可用 PV,动态供给场景下底层存储后端资源不足)。如果是动态供给,还要检查 CSI 驱动是否正常运行,驱动 Pod 挂了,Provisioner 就无法创建 PV。

# 查看 PVC 状态和详情
kubectl get pvc
kubectl describe pvc <pvc-name>
# 常见 PVC 状态含义
# Pending: StorageClass 不存在或 PV 不足
# Lost: PV 被意外删除
# Bound: PVC 已绑定,但 Pod 仍可能挂载失败

PVC Bound 但 Pod 报 FailedMount,问题出在节点层面。CSI 驱动需要在目标节点上运行 Node Plugin,负责执行实际的挂载操作。如果 Node Plugin 未运行或与存储后端的通信异常,挂载就会失败。此时需要检查 CSI 驱动 Pod 的日志,而不是继续在 PVC 上打转。

# 检查 StorageClass 和 PV
kubectl get storageclass
kubectl get pv
# 检查 CSI 驱动状态
kubectl get csidriver
kubectl logs -n kube-system -l app=csi-driver
Important

存储问题的排查有一个关键原则:PVC 状态和实际挂载是两回事。PVC Bound 只说明声明和 PV 绑定成功了,不代表任何节点上完成了挂载。如果遇到 Pod 报挂载错误,不要因为 PVC 是 Bound 状态就跳过存储排查。

四、性能分析与优化#

性能问题很少以”慢”这种清晰的方式出现,更多时候它伪装成 Pod 重启、请求超时、调度延迟等现象。定位性能瓶颈的第一步不是找优化手段,而是确认瓶颈到底在哪一层:CPU、内存、磁盘 I/O 还是网络。不同瓶颈的优化方向完全不同,搞错了方向只会浪费时间。

graph TB subgraph "性能瓶颈定位" A["CPU throttling"] --> D["应用层瓶颈"] B["OOMKilled"] --> D C["I/O wait 飙高"] --> D D --> E["网络延迟放大"] end

kubectl top 是最快的资源概览工具。但要注意,它显示的是实际使用量,而调度决策基于 requests,两者之间的差距才是性能问题的温床。一个 Pod 实际 CPU 使用 100m 但 requests 声明了 2000m,它在调度层面浪费了 1900m 的配额;反过来,实际使用 2000m 但 limits 只有 1000m,就会触发 CPU throttling,导致应用响应变慢,而 kubectl top 显示的 CPU 使用率可能并不直观地反映这一点。

# 查看资源使用概览
kubectl top nodes
kubectl top pods
# 按命名空间查看
kubectl top pods -n <namespace>
# 查看节点级别的资源分配(requests vs allocatable)
kubectl describe node <node-name> | grep -A 10 "Allocated resources"

CPU throttling 是一个尤其隐蔽的问题。Linux CFS 调度器通过 CFS quota 机制实现 CPU 限流:当一个容器在 quota 周期内用完了配额,它会被 throttled 直到下一个周期。这意味着即使节点 CPU 空闲,容器也会被限流,limits 是硬天花板,跟节点负载无关。很多团队发现应用延迟抖动,排查了一圈网络和依赖,最后才发现是 limits 设置过低导致的周期性 throttling。这也是为什么有些团队选择在生产环境中不设 CPU limits,让容器在节点空闲时能借用多余 CPU,只在节点资源紧张时由 CFS 自然调度。这个做法有争议(不设 limits 意味着单个容器可能独占节点 CPU),但对于延迟敏感型工作负载,它比 throttling 带来的影响更小。

内存瓶颈的表现更粗暴:直接 OOMKilled。Linux 内核在物理内存不足时触发 OOM Killer,选择占用内存最多的进程杀掉。Kubernetes 通过 limits.memory 设置容器的内存上限,容器的 memory.limit_in_bytes cgroup 参数被设为此值,超过就会被杀。和 CPU throttling 不同,内存没有”借用”机制,超了就是超了,没有缓冲期。这也是为什么内存 limits 通常需要比 CPU limits 更保守地设置:CPU throttling 只是变慢,OOMKill 是直接死亡,后者没有恢复机会。

资源请求和限制的设置没有万能公式,但有一个原则:requests 应该接近实际稳态用量,limits 应该覆盖峰值。如果 requests 过高,浪费调度空间;如果 limits 过低,运行时被限流或杀掉。很多集群的问题不是资源不够,而是资源分配不合理。确定合理值的方法是观察:用 kubectl top 收集一周的实际用量数据,P99 作为 limits 的起点,P50 作为 requests 的起点,再根据应用特性微调。

# 资源请求与限制:requests 看稳态,limits 看峰值
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
Caution

CPU limits 和内存 limits 的行为完全不同。CPU limits 导致 throttling(变慢),内存 limits 导致 OOMKill(直接死亡)。调优时的策略也应不同:CPU limits 可以适当放宽,避免不必要的 throttling;内存 limits 则必须留出安全余量,因为 OOMKill 没有恢复机会。

五、应用层调试与监控联动#

集群层面的排查工具(kubectl describekubectl top)能定位”Pod 为什么不正常”,但无法回答”应用内部哪里出了问题”。应用层调试需要进入容器内部,使用语言特定的诊断工具。这是从运维视角切换到开发视角的转折点。

最基础的调试手段是启动一个临时工具 Pod。busybox 适合快速网络诊断,nicolaka/netshoot 则是一个功能更全的网络诊断工具箱,内置了 tcpdumpconntracknslookup 等常用工具。选择哪个取决于问题的复杂度,如果只是 ping 一下,busybox 就够了;如果需要抓包分析,就得用 netshoot

# 轻量调试:busybox
kubectl run debug --image=busybox --rm -it -- sh
# 网络诊断工具箱:netshoot
kubectl run netshoot --rm -it --image=nicolaka/netshoot -- bash
# 常用诊断命令
ip link # 网络接口
ip addr # IP 地址
ss -tulpn # 端口监听
conntrack -L # 连接跟踪
tcpdump # 抓包

对于 Go 应用,如果启动时开启了 net/http/pprof,可以直接通过 HTTP 端点获取 CPU profile 和 goroutine 堆栈。Java 应用则可以通过 jstack 获取线程堆栈、jmap 获取内存映射。这些工具的使用方式与在物理机上无异,区别在于你需要先 kubectl exec 进入容器,前提是容器镜像里包含这些工具,或者你可以用 kubectl debug 附带一个 sidecar 容器来注入调试工具。

# 进入目标 Pod
kubectl exec -it <pod-name> -- sh
# Go 应用:pprof
curl http://localhost:6060/debug/pprof/profile # CPU profile
curl http://localhost:6060/debug/pprof/goroutine?debug=1 # goroutine 堆栈
# Java 应用
jstack <pid> # 线程堆栈
jmap -heap <pid> # 内存映射
Tip

生产环境的容器镜像通常不包含调试工具(jstacktcpdump 等),这是出于安全性和镜像大小的考虑。kubectl debug 命令可以在运行中的 Pod 上附加一个 ephemeral container,与目标 Pod 共享网络和 PID 命名空间,这样即使原容器里什么工具都没有,也能进行调试。

前面所有排查手段都是事后响应式的,出问题了再查。在规模较大的集群中,靠人工巡检发现问题的效率极低,故障往往在造成影响后才被注意到。监控体系的价值在于缩短从故障发生到被发现的时间窗口

1
故障现象出现
2
Prometheus 指标触发异常
3
Grafana Dashboard 可视化确认
4
定位问题根因
5
告警规则通知值班人员
6
快速响应处置

不同故障类型对应不同的关键指标。调度失败关注 kube_pod_status_pending,OOMKilled 关注 container_memory_working_set_bytes,网络问题关注 container_network_receive_bytes_total 的异常变化,存储问题关注 kube_persistentvolumeclaim_status。这些指标不是孤立的,一个指标异常往往是另一个指标异常的原因或结果,关联分析才能还原完整的故障链。比如 kube_pod_status_pending 上升的同时 kube_node_status_allocatable 下降,说明是节点资源不足导致调度失败;如果 kube_pod_status_pending 上升但节点资源充足,就要去看污点和亲和性相关的指标。

故障类型关键指标排查方向
调度失败kube_pod_status_pending资源、污点、亲和性
OOMKilledcontainer_memory_working_set_bytes内存限制、泄漏
网络不通container_network_receive_bytes_totalNetworkPolicy
存储问题kube_persistentvolumeclaim_statusStorageClass
Tip

完整的监控配置和告警规则请参考 Kubernetes 监控与可观测性

六、故障案例:从症状到根因#

前面的章节按问题类型分类讨论了排查方法,但真实故障很少乖乖地待在某个类别里。这一节通过三个实际案例,展示从症状到根因的完整诊断过程。

Case 1: Pod 一直 Pending。一个名叫 my-app 的 Pod 部署后始终处于 Pending 状态。kubectl describe pod my-app 的 Events 段显示 0/3 nodes are available: 1 Insufficient cpu, 2 node(s) had taints that the pod didn't tolerate。这条信息同时给出了两个原因:一个节点 CPU 不足,两个节点有未被容忍的污点。进一步检查 kubectl get nodes -o json | jq '.items[].spec.taints' 发现那两个节点被打了 dedicated=special-user:NoSchedule 污点,这是运维为专用工作负载预留的节点。解决方案取决于业务需求:如果 my-app 确实需要跑在这些节点上,就添加对应的 Toleration;如果不需要,就增加普通节点的资源或降低 Pod 的 requests。这里的关键不是选哪个方案,而是理解调度器已经告诉你原因了,Events 就是第一手诊断信息,不需要猜测。

# 检查节点污点
kubectl get nodes -o json | jq '.items[].spec.taints'
# 如果需要,移除污点(通常不推荐在生产环境中这样做)
kubectl taint node <node> dedicated=special-user:NoSchedule-

Case 2: Service 无法访问。前端 Pod 通过 curl backend:80 访问后端服务,返回 Connection timed out。按照网络排查流程,第一步检查 Endpoints:kubectl get endpoints backend 返回空列表。Endpoints 为空意味着 Service 的 Selector 没有匹配到任何 Pod。kubectl get pods -l app=backend --show-labels 发现 Pod 的标签是 app=backend-api,而 Service 的 Selector 写的是 app=backend,一个后缀差异导致了路由断裂。修复 Selector 后 Endpoints 自动填充,流量恢复。这个案例说明,网络问题不一定在网络层,很多时候只是配置不匹配。排查时先查 Endpoints 再查 NetworkPolicy,能避免在错误的方向上浪费时间。

# 排查步骤
kubectl get endpoints backend # 空 → Selector 没匹配到 Pod
kubectl get pods -l app=backend --show-labels # 发现标签不匹配
kubectl get networkpolicy # 确认 NetworkPolicy 没有额外阻断

Case 3: Pod 被 OOMKilled。Pod my-app 不断重启,kubectl get pods 显示 RESTARTS: 3,状态 OOMKilledkubectl top pod my-app 显示实际内存使用接近 limits 设置值。这里有两条可能的路径:一是应用存在内存泄漏,使用量随时间增长最终触顶;二是 limits 设置本身偏低,正常峰值就超过了限制。区分两者的方法是在调高 limits 后观察:如果内存使用持续增长不回落,是泄漏,需要修代码;如果稳定在新水平线上,是 limits 太紧,调整资源配额即可。这个区分方法看似简单,但在实际操作中经常被跳过,很多人看到 OOMKilled 就直接调大 limits,如果真是泄漏,调大只是推迟了崩溃时间。

# 查看重启状态
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# my-app 0/1 OOMKilled 3 10m
# 查看实际内存使用
kubectl top pod my-app
# 临时调高内存限制以区分"泄漏"和"配额不足"
kubectl patch pod my-app -p '{"spec":{"containers":[{"name":"app","resources":{"limits":{"memory":"2Gi"}}}]}}'

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Kubernetes 故障排查与性能优化
https://blog.souloss.cn/posts/kubernetes/k8s-troubleshooting/
作者
Souloss
发布于
2021-12-17
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐