跳转至

Kubernetes 网络排障实战:逐层决策树 + CNI 对比 + 5 个真实事故

干运维十年,K8s 网络问题是我最不愿意半夜爬起来接警的一类。不是难,是玄学——症状和根因之间隔着五六层抽象,你 ping 通了,业务还是不通;你抓包了,包是通的,但服务就是 5xx。

上周二凌晨 1 点,客户群里喊"订单服务连不上 MySQL",我登录集群一看,Pod 全是 Running,kubectl get svc 列表正常,kubectl get endpoints 里 MySQL 的 Endpoint 也有 IP。按直觉应该去查应用连接池,结果拿 netshoot 进 Pod 一 nslookup,域名解析出来的 ClusterIP 根本不对——是 CoreDNS 缓存了旧的 headless 记录。

这种"看起来全对,实际全错"的场景,就是 K8s 网络排障最折磨人的地方。这篇文章把我这些年处理过的真实事故串成一条逐层决策树,从 DNS 一路打到云防火墙,每一层都告诉你"查什么命令、看到什么算正常、下一步去哪"。

我不是在复述官方文档,下面是我亲手敲过、在客户生产上跑过的命令和输出。

1. 为什么 K8s 网络问题最"玄学"

传统网络排障,ping 不通基本就锁定在 IP/路由/防火墙三选一。K8s 不是。一个"Pod A 访问 Service B 超时"的症状,背后可能是:

应用(Pod A)
  → DNS 解析(域名→ClusterIP,可能被 CoreDNS 缓存/上游污染)
  → Service 转发(ClusterIP→Endpoint IP,由 kube-proxy 写 iptables/IPVS)
  → 跨节点转发(CNI 数据面,VXLAN 8472 / BGP 179 / eBPF)
  → 网络策略(NetworkPolicy 的 ingress/egress 双向过滤)
  → 目标 Pod 就绪(readinessProbe 没过就没 Endpoint)
  → 目标端口(listening 没起 / 容器端口写错)
  → 云平台安全组(节点安全组没放行 NodePort 或跨 VPC)

七层里任何一层出问题,症状都一样:连接超时或拒绝。你不可能靠"超时还是拒绝"区分是哪层,因为每一层的失败都表现为这两种之一。

这就是为什么我坚持一个原则:K8s 网络排障不许猜,必须逐层验证,每层留证据。

很多同学一上来 kubectl exec 进 Pod ping 一下对端,通了就觉得"网络没问题",这是最大的坑。Pod 之间网络通,不代表 Service 转发通,更不代表 DNS 解析对。你 ping 的是 IP,业务走的是域名→ClusterIP→Endpoint,中间全是抽象层。

1.1 症状与层之间的对应关系(我的经验表)

我习惯先把"症状"和"可能的层"做一张映射表贴在看板上,接警时先对照:

症状表现 优先怀疑的层 快速验证命令
Pod 一直 Pending CNI 没分配 IP kubectl describe pod 看事件
Pod CrashLoopBackOff 容器端口 / 探针 kubectl logs
svc 的 Endpoint 为空 Service selector kubectl get endpoints
域名解析超时 CoreDNS / DNS nslookup
域名解析出的 IP 不对 CoreDNS 缓存/上游 对比多次 nslookup
ClusterIP 直连不通 kube-proxy iptables-save / ipvsadm
跨节点 Pod 不通 CNI 数据面 测 8472 / 179 / cilium status
同节点通、跨节点断 CNI 或防火墙 nc 测数据面端口
ping 通但业务不通 NetworkPolicy kubectl get netpol
集群外访问 404/502 Ingress / 安全组 kubectl get ingress -A -o wide

这张表不是绝对真理,但能让你在报警的头 60 秒里把搜索范围从"七层"缩到"一两层"。剩下的就是把这一两层查透。

1.2 为什么"超时"和"拒绝"不能区分层

很多人试图用"连接超时 vs 连接拒绝"来猜问题。这在传统网络里有效,在 K8s 里基本失效。原因:

  • 连接拒绝(RST):可能是目标 Pod 没监听端口,也可能是 NetworkPolicy 显式 drop,还可能是 Service 的 targetPort 写错。
  • 连接超时(无响应):可能是 CNI 跨节点丢包,也可能是 kube-proxy 没写 DNAT 规则,还可能是云安全组直接丢包(云上安全组默认 drop 而不是 reject)。

所以"超时 vs 拒绝"只能缩小到数据面丢包 vs 应用层拒绝,定位不到具体层。真正的定位手段是逐层排除 + 抓包看是谁发的包。

2. 60 秒决策:先建立"访问链路"心智模型

接到报警第一件事不是敲命令,是在脑子里画一张访问链路图。我一般拿张纸(或记事本)写:

客户端
  └─ 域名 www.example.com
       └─ Ingress(80/443)  ── 外部流量入口
            └─ Service: myapp-svc(ClusterIP)
                 ├─ selector 匹配 → Endpoint(10.244.1.15:8080)
                 ├─ kube-proxy 写入转发规则
                 └─ CNI 数据面(跨节点 or 同节点)
                      └─ Pod: myapp-xxxxx
                           ├─ NetworkPolicy 过滤
                           └─ 容器端口 8080

这张图决定你下一步敲什么命令。我按"从应用往外、从现象往根因"的顺序,把整条链路切成 7 层,每层一个决策节点:

症状:Pod 内访问服务失败
  │
  ├─ 第 1 层:Pod 是不是 Ready?容器端口对不对?──┐
  ├─ 第 2 层:Service 有没有 Endpoint?          │ 由内到外
  ├─ 第 3 层:DNS 解析对不对?                   │ 逐层排除
  ├─ 第 4 层:kube-proxy 转发规则在不在?        │
  ├─ 第 5 层:CNI 跨节点通不通?                 │
  ├─ 第 6 层:NetworkPolicy 拦没拦?             │
  └─ 第 7 层:Ingress / 云防火墙                  ┘

注意顺序是从客户端视角的应用层往底层打,不是从 CNI 开始。原因很简单:空 Endpoint 的概率远大于 CNI 故障。我的统计数据里,K8s 网络事故里 40% 是 selector 不匹配或 Pod 不 Ready 导致空 Endpoint,只有 15% 左右真正落在 CNI 数据面。

2.1 三种常见的排障起点,别选错

接警后到底从哪层开始,我按"是不是新变更"来定:

  1. 刚做过发布/升级:从第 1、2 层开始(Pod Ready + Endpoint)。发布引起的网络问题,八成是 selector 或镜像标签变了。
  2. 老系统突然挂:从第 3、4 层开始(DNS + kube-proxy)。没有变更的网络问题,多半是 CoreDNS 或 kube-proxy 的状态漂移。
  3. 集群刚上线/扩容:从第 5 层开始(CNI 数据面)。新节点加入后跨节点不通,优先查防火墙和 BGP peer。

这个"变更驱动"的选层法,比"从第 1 层开始死磕"快很多。发布挂了查发布相关的层,没变更查状态层,扩容挂了查数据面层。

2.2 访问链路每一层对应的 K8s 对象

很多同学排障时混淆"对象"和"数据面",这里必须分清。K8s 对象是声明,数据面是实际转发。比如 Service 对象声明了"我要转发到 selector 匹配的 Pod",但真正的转发规则是 kube-proxy 写的 iptables。排障时你要验证的是数据面,不是对象:

访问链路段 对应 K8s 对象 实际数据面 验证命令
域名→ClusterIP Service + CoreDNS CoreDNS 的 kubernetes 插件 nslookup
ClusterIP→Pod IP Service.selector kube-proxy 的 DNAT 规则 iptables-save
Pod IP→目标 Pod (无对象) CNI 路由/veth ip route、ping
过滤 NetworkPolicy CNI 的策略引擎 kubectl get netpol
外部→集群 Ingress ingress controller kubectl get ingress -o wide

排障时永远问自己"我现在验证的是对象声明,还是数据面实际"。很多人 kubectl get svc 看到 Service 存在就以为转发正常——这是最大的认知误区。Service 对象存在 ≠ 转发规则已写入。你必须验证到数据面(iptables/ip route/CNI),才算真正确认。

3. 第 1 层:Pod Ready 与容器端口

排障从 Pod 本身开始,因为如果 Pod 都不 Ready,后面 Service/Endpoint 全是空的,查了也白查。

3.1 看 Pod 状态与 IP

kubectl get pods -n myapp -o wide

正常输出长这样:

NAME                        READY   STATUS    RESTARTS   AGE   IP             NODE        NOMINATED NODE   READINESS GATES
myapp-6d8f4c9d5-abc12       1/1     Running   0          3h    10.244.1.15    node-01     <none>           <none>
myapp-6d8f4c9d5-def34       1/1     Running   0          3h    10.244.2.22    node-02     <none>           <none>

关键看三列:READY 是不是 1/1、STATUS 是不是 Running、IP 是不是分配了 10.x 的 Pod 网段地址。

3.2 READY=0/1:readinessProbe 没过

如果 READY 是 0/1,说明 readinessProbe 没过,这个 Pod 不会进 Service 的 Endpoint。这时候别急着查网络,先看探针:

kubectl describe pod myapp-6d8f4c9d5-abc12 -n myapp | grep -A 20 "Readiness"

看到类似下面的,就是探针配置或应用自身的问题:

Readiness:  http-get http://:8080/healthz delay=0s timeout=1s period=10s #success=1 #failure=3
  Ready:  False

探针失败但容器在跑是最隐蔽的。应用进程活着,日志正常,就是 readinessProbe 的路径返回非 200,Endpoint 被摘掉。这时候网络层全是好的,问题在应用健康检查。排查:

kubectl exec -it <pod> -- curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/healthz

返回非 200 就是探针路径问题,跟网络无关。返回 200 但 Pod 还是 0/1,再查探针的端口和 scheme(http vs https、tcp vs http)。

3.3 STATUS 不是 Running:看事件定位

STATUS 是 Pending、CrashLoopBackOff、ImagePullBackOff 时,直接看事件:

kubectl describe pod <pod> -n myapp | tail -30

Pending 且没有 IP,十有八九是 CNI 没分配地址。事件里会出现:

  Warning  FailedCreatePodSandBox  1m   kubelet  Failed to create pod sandbox: 
  rpc error: code = Unknown desc = failed to setup network for pod ... 
  network plugin cni: failed to set up pod ... 
  failed to allocate for range 0: no IP addresses available

no IP addresses available 说明 IPAM 地址池耗尽——这其实也是网络问题(第 5 层 CNI 的 IPAM),但很多人第一反应是"扩容"。排查:看 CNI 的 IPPool 容量,calicoctl get ippool 或 flannel 的 subnet.env,是不是地址用完了。

CrashLoopBackOff:容器反复重启,查日志,多半是应用崩溃或端口冲突,不是网络问题。

3.4 容器端口写错

READY=1/1 但业务还是不通,看容器真正监听的端口。很多人 containerPort 写 8080,应用实际监听 9090,Service 的 targetPort 也写 8080,就全对不上了。

# 进 Pod 看实际监听端口
kubectl exec -it myapp-6d8f4c9d5-abc12 -n myapp -- ss -tlnp

正常输出(容器内可能没有 ss,用 netstat 或 /proc 兜底):

State   Recv-Q   Send-Q     Local Address:Port     Peer Address:Port   Process
LISTEN  0        128              0.0.0.0:8080            0.0.0.0:*       users:(("myapp",pid=1,fd=8))

如果容器里没 ss,直接看容器暴露的端口和 Pod 的端口映射是否一致。containerPort 只是声明,不限制实际监听,所以实际以应用监听的端口为准。

# 容器内没有 ss 时,用 netstat 或直接看 /proc
kubectl exec -it <pod> -- netstat -tlnp
kubectl exec -it <pod> -- cat /proc/net/tcp | head

这一层最容易忽略的细节:containerPort 和 targetPort 是两回事。containerPort 写在 Deployment 的容器定义里,targetPort 写在 Service 里。Service 的 targetPort 必须等于容器实际监听的端口,而不是 containerPort。很多人照着 containerPort 填 targetPort,结果 containerPort 写错了,Service 转发就全错。

3.5 镜像拉取失败:ImagePullBackOff 与网络的关系

虽然 ImagePullBackOff 严格说是镜像问题,但它和网络排障的边界很模糊,我把它放在这里一起讲。因为很多时候"Pod 起不来"是拉镜像的网络问题,不是应用问题。

kubectl get pods -n myapp | grep ImagePull
kubectl describe pod <pod> -n myapp | grep -A5 "Events"

事件里如果出现:

  Warning  Failed  2m  kubelet  Failed to pull image "registry.example.com/app:v2": 
  rpc error: code = DeadlineExceeded desc = failed to pull and extract image ... 
  context deadline exceeded

DeadlineExceeded 或 pull access denied,要区分两类:

  1. 网络不通:节点拉不到镜像仓库(curl -I https://registry.example.com 测一下)。
  2. 鉴权失败:ImagePullSecrets 缺失或 registry 凭证过期(pull access denied)。

验证节点到镜像仓库的网络:

# 在节点上测,不是 Pod 内
curl -fsSI https://registry.example.com/v2/ | head -5

注意:拉镜像走的是节点的网络,不是 Pod 网络。如果节点能拉(节点上 curl 通)但 Pod 报 ImagePullBackOff,那是鉴权问题;如果节点上也拉不动,才是节点网络问题。这一步别在 Pod 里测,要在节点上测。

3.6 Pod 一直 Pending:CNI 没分配 IP 的判据

Pending 且事件里出现 FailedCreatePodSandBox + no IP addresses available,这是第 5 层 CNI 的 IPAM 问题,但症状出现在第 1 层。判断口诀:

Pending + 有 IP 分配事件失败 = CNI IPAM 问题(地址池耗尽 / CNI 组件挂)
Pending + 无 IP 字段           = 调度问题(资源不足 / 污点 / 节点亲和)

区分方法:

# 看是不是资源不足
kubectl describe pod <pod> | grep -E "Unschedulable|Insufficient|Taint"

# 看 CNI 的 IP 池有没有耗尽
kubectl get pods -n kube-system | grep -iE "calico|flannel|cilium"

如果是 Insufficient cpu / Insufficient memory,是调度问题不是网络问题;如果是 no IP addresses available,才是 CNI 的 IPAM。这两个都会表现为 Pending,但一个是 K8s 调度层,一个是 CNI 网络层,别混。

4. 第 2 层:Service selector 与 Endpoint(空 Endpoint 是最常见根因)

这一层是 K8s 网络事故的"重灾区"。我的经验里,空 Endpoint 是最常见的根因,没有之一。

4.1 先看 Endpoint 有没有 IP

kubectl get svc myapp-svc -n myapp -o wide
kubectl get endpoints myapp-svc -n myapp -o wide

正常情况 Service 和 Endpoint 的 IP 列表应该一致:

NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE   SELECTOR
myapp-svc    ClusterIP   10.96.88.123   <none>        8080/TCP   5d    app=myapp

NAME         ENDPOINTS                    AGE
myapp-svc    10.244.1.15:8080,10.244.2.22:8080   5d

Endpoint 为空(ENDPOINTS <none>)时,这就是根因所在,直接跳 4.2。如果 Endpoint 有 IP,跳过这一层去第 3 层 DNS。

4.2 空 Endpoint 的排查:selector 对不上

# 看 Service 的 selector
kubectl get svc myapp-svc -n myapp -o jsonpath='{.spec.selector}'
map[app:myapp]

再看 Pod 的实际 labels:

kubectl get pods -n myapp --show-labels
NAME                        READY   STATUS    RESTARTS   AGE   LABELS
myapp-6d8f4c9d5-abc12       1/1     Running   0          3h    app=myapp,version=v1
myapp-6d8f4c9d5-def34       1/1     Running   0          3h    app=myapp,version=v1

一眼就能看出 selector 的 app=myapp 和 Pod label 的 app=myapp 是否匹配。最常见翻车点:selector 写 app=myapp,version=v1,但 Pod 只有一个 app=myapp;或者 Pod 升级后 label 从 app=myapp 改成了 app=myapp-v2,Service 没跟着改。

我处理过最隐蔽的一个:Helm 升级时 app.kubernetes.io/version 变了,selector 用的 version: v1,Pod 升级后变成 version: v2,Endpoint 一夜之间全空,业务全挂。kubectl rollout status 显示升级成功,但没人注意到 Service 的 selector 没跟着模板走。

4.3 Endpoint 有 IP 但端口不对

Endpoint 不空,但 ENDPOINTS 列显示的端口和目标 Pod 实际监听端口不一致,比如显示 10.244.1.15:8080 但 Pod 实际监听 9090。这是 targetPort 写错。验证:

# 看 Service 的 port/targetPort
kubectl get svc myapp-svc -n myapp -o jsonpath='{.spec.ports[0]}'

输出 map[name:http port:8080 targetPort:9090],而 Pod 实际监听 8080,targetPort: 9090 就错了。

4.4 Service 定义片段(生产级,供对照)

一个正确的 Service 长这样:

apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
  namespace: myapp
spec:
  selector:
    app: myapp          # 必须与 Pod label 完全一致
  ports:
    - name: http
      port: 80          # Service 端口,ClusterIP 上的端口
      targetPort: 8080  # 转发到 Pod 的容器端口
  type: ClusterIP

排查口诀:spec.selector 必须与 Pod labels 键值完全相等,targetPort 必须等于容器实际监听端口。三处对不上任何一处,Endpoint 就空。

4.5 多端口 Service 的 selector 陷阱

如果一个 Service 有多个端口(比如 80 和 443),ports 数组里每一项的 targetPort 都要单独对上。最容易错的是:ports[0].name 和 ports[1].name 不同,但 targetPort 都填了同一个值。还有 appProtocol 字段(http/https),如果写了但和实际不符,部分 CNI(尤其 cilium 做 L7 策略时)会按 appProtocol 误判协议。多端口场景建议:

kubectl get svc myapp-svc -n myapp -o jsonpath='{.spec.ports[*].name}' 
kubectl get svc myapp-svc -n myapp -o jsonpath='{.spec.ports[*].targetPort}'

逐项核对 name 与 targetPort 的对应关系。

5. 第 3 层:CoreDNS 与 Service DNS

Endpoint 没问题了,接下来验证域名解析。业务用的是 myapp-svc.myapp.svc.cluster.local 这类域名,解析不对,IP 对也白搭。

5.1 进 Pod 解析验证

# 用 netshoot 起临时诊断 Pod(后面第 10 章详细讲)
kubectl run netshoot-$RANDOM -it --rm \
  --image=nicolaka/netshoot \
  -- /bin/bash -c "nslookup myapp-svc.myapp.svc.cluster.local"

正常输出:

Server:         10.96.0.10
Address:        10.96.0.10#53

Name:   myapp-svc.myapp.svc.cluster.local
Address: 10.96.88.123

重点看两处:Server 是不是 10.96.0.10(kube-dns 的 ClusterIP),Address 是不是等于 Service 的 ClusterIP 10.96.88.123。

如果解析出的 IP 和 kubectl get svc 显示的 ClusterIP 不一致,这就是根因——见 5.3。如果解析正常,去第 4 层。

5.2 CoreDNS 本身健不健康

解析失败(server can't find 或超时)时,先确认 CoreDNS Pod 活着:

kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl get endpoints -n kube-system kube-dns

正常:

NAME                       READY   STATUS    RESTARTS   AGE
coredns-7db6d8ff4d-abc12   1/1     Running   0          8d
coredns-7db6d8ff4d-def34   1/1     Running   0          8d

NAME         ENDPOINTS                AGE
kube-dns     10.244.0.2:53,10.244.0.3:53   8d

再看日志里有没有异常:

kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50

5.3 真实事故 ②:CoreDNS 解析错 IP(缓存污染)

这个事故我印象特别深。症状是新部署的服务偶尔连不上旧版本的 Pod,重启后又能连上,过几分钟又乱。

我拿 netshoot 进去反复解析:

$ nslookup myapp-svc.myapp.svc.cluster.local
Server:         10.96.0.10
Address:        10.96.0.10#53

Name:   myapp-svc.myapp.svc.cluster.local
Address: 10.96.88.123          # 第一次正常

再解析一次:

$ nslookup myapp-svc.myapp.svc.cluster.local
Server:         10.96.0.10
Address:        10.96.0.10#53

Name:   myapp-svc.myapp.svc.cluster.local
Address: 10.96.88.200          # 第二次变了!但 svc 的 ClusterIP 是 10.96.88.123

ClusterIP 是静态分配的,同一个 Service 的 ClusterIP 永远不变。解析结果在两个 IP 之间跳,说明上游解析链路有问题,不可能是 CoreDNS 正常返回。

进一步查,发现是团队之前在 CoreDNS 的 ConfigMap 里加了 forward . /etc/resolv.conf 之外的转发规则,把 cluster.local 也转给了外部 DNS,外部 DNS 里残留了同名旧记录。CoreDNS 默认只把 cluster.local 留给集群内部 Service 处理,一旦有人改了 Corefile 的转发逻辑,外部记录就会渗进来。

修复:

kubectl edit configmap coredns -n kube-system

确认 Corefile 里的转发段(正常应该是这样,cluster.local 走集群内部逻辑):

.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
      pods insecure
      fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153
    forward . /etc/resolv.conf
    cache 30
    loop
    reload
}

关键:kubernetes cluster.local 这一行必须存在,它负责把 *.svc.cluster.local 解析成 ClusterIP。如果被 forward 抢先或删掉,cluster.local 就会漏到上游。改完 Corefile 后重启 CoreDNS 让配置生效:

kubectl rollout restart deployment/coredns -n kube-system
kubectl rollout status deployment/coredns -n kube-system

这次事故的教训就一句话:cluster.local 永远只该由 CoreDNS 的 kubernetes 插件处理,任何人把它 forward 到外部都是事故。

5.4 headless Service 与普通 Service 的解析差异

排查 DNS 时,一定要先分清目标是不是 headless Service(clusterIP: None)。headless 的解析行为完全不同:

kubectl get svc -n myapp | grep None
  • 普通 Service:解析返回单个 ClusterIP,kubectl get svc 的 CLUSTER-IP 列有值。
  • headless Service:解析返回所有 Pod IP 的列表(多个 A 记录),CLUSTER-IP 列是 None。

如果业务连的是 headless Service,但你在 kubectl get svc 里看到有 ClusterIP,说明 Service 定义错了;反之,你按 ClusterIP 去对,结果解析出一堆 Pod IP,也会对不上。headless 场景,A 记录应该等于 Endpoint 的 IP 列表:

kubectl get endpoints myapp-headless -n myapp

解析出的多 IP 应该和 ENDPOINTS 列一致。不一致,看 Endpoint 和 Pod 是否 Ready。

5.5 search 域与 ndots:解析慢/解析错的隐形杀手

Pod 内 /etc/resolv.conf 默认带 search 域:

search myapp.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

ndots:5 是坑点:如果应用请求的域名含 4 个点以上(如 a.b.c.d.e),Pod 会直接按 FQDN 处理,不走 search 追加;如果域名点少(如 myapp-svc),会依次尝试 myapp-svc.myapp.svc.cluster.local、myapp-svc.svc.cluster.local、myapp-svc.cluster.local、myapp-svc。

解析慢的根因:应用用了相对短名(不带 FQDN),每次解析都要尝试多个 search 域,每个都等超时。业务报"DNS 慢"但 nslookup 正常,多半是这个。验证:

# 在 Pod 内看 resolv.conf
kubectl exec -it <pod> -- cat /etc/resolv.conf

# 分别解析短名和 FQDN,看耗时差异
kubectl exec -it <pod> -- time nslookup myapp-svc
kubectl exec -it <pod> -- time nslookup myapp-svc.myapp.svc.cluster.local

如果短名解析明显慢于 FQDN,就是 search 域逐级尝试导致的。修复:应用侧改用 FQDN,或调整 dnsConfig.options 的 ndots 值。生产级 Pod 配置:

spec:
  dnsConfig:
    options:
      - name: ndots
        value: "2"   # 减少 search 域尝试次数

5.6 区分"解析超时"与"解析不到记录"(NXDOMAIN)

排查 DNS 时,先分清两种失败,它们的根因完全不同:

  • 解析超时(no servers could be reached 或一直卡住):DNS 服务器不可达,CoreDNS 挂了,或 Pod 到 10.96.0.10 的流量被 NetworkPolicy 拦了。
  • 解析 NXDOMAIN(server can't find):DNS 服务器活着,但查不到记录,Service 名写错 / namespace 错 / 域名拼错。
# 分别验证
nslookup <svc>.<ns>.svc.cluster.local
# 超时 → 查 CoreDNS 与 NetworkPolicy(第 5 章 + 第 8 章)
# NXDOMAIN → 核对域名拼写与 Service 是否存在
kubectl get svc <svc> -n <ns>

NXDOMAIN 但 Service 明明存在,多半是域名格式不对。cluster.local 域要写全:<svc>.<namespace>.svc.cluster.local。漏掉 namespace 或写成 <svc>.svc.cluster.local,在跨 namespace 访问时就会 NXDOMAIN。验证正确域名:

# 跨 namespace 访问必须带完整域名
kubectl run netshoot-$RANDOM -it --rm --image=nicolaka/netshoot \
  -- nslookup <svc>.<target-ns>.svc.cluster.local

5.7 外部 DNS 的 forwarding:forward . 到底指哪

集群要访问外部域名(如 api.example.com 这种集群外的),走 CoreDNS 的 forward . /etc/resolv.conf。这里有个常见坑:

  • forward . /etc/resolv.conf:转发到节点 /etc/resolv.conf 里的上游(通常是宿主机 DNS,可能是 systemd-resolved 的 127.0.0.53 或云厂商 DNS)。
  • forward . 8.8.8.8:显式转发到公网 DNS,绕过节点配置。

如果节点 /etc/resolv.conf 指向 127.0.0.53(systemd-resolved),但 CoreDNS 在容器里无法访问 127.0.0.53(那是宿主机 loopback),转发就会失败。这是 K8s 1.22+ 配合 systemd-resolved 的经典坑。验证:

# 节点上看
cat /etc/resolv.conf
# nameserver 127.0.0.53  ← 容器里访问不到这个!

# CoreDNS 日志里会报
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50 | grep -i "forward"

修复:把 CoreDNS 的 forward 改为真实的上游 IP,或用 dnsPolicy: Default 的节点 DNS。生产级 Corefile 修正:

.:53 {
    kubernetes cluster.local in-addr.arpa ip6.arpa
    forward . 10.0.0.2   # 改为真实上游 DNS IP,不要用 127.0.0.53
}

6. 第 4 层:kube-proxy 与 iptables 规则

域名解析对了,Service ClusterIP 也对了,下一步验证 kube-proxy 到底有没有把"ClusterIP:port → Endpoint IP:port"的转发规则写进数据面。

6.1 先确认 kube-proxy 模式

kubectl logs -n kube-system -l k8s-app=kube-proxy --tail=5 | grep -i "using"

或者:

kubectl get cm -n kube-system kube-proxy -o yaml | grep -i mode

输出类似 Using iptables Proxier 或 Using ipvs Proxier。iptables 和 ipvs 模式查规则的方式不一样,先分清模式再动手。

6.2 iptables 模式:grep ClusterIP

在任意工作节点上执行(ClusterIP 换成你的 Service IP):

iptables-save -t nat | grep -E "10.96.88.123" | head -20

正常会看到一串 KUBE-SERVICES / KUBE-SVC-xxx / KUBE-SEP-xxx 链:

-A KUBE-SERVICES -d 10.96.88.123/32 -p tcp -m comment --comment "myapp/myapp-svc:http" -m tcp --dport 80 -j KUBE-SVC-XXXXX
-A KUBE-SVC-XXXXX -m comment --comment "myapp/myapp-svc:http" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-YYYYY
-A KUBE-SEP-YYYYY -p tcp -m tcp -j DNAT --to-destination 10.244.1.15:8080

关键看 KUBE-SEP 链里的 DNAT --to-destination,它必须是真实 Endpoint IP:port。如果这里没有规则,或 DNAT 目标不是 Endpoint,说明 kube-proxy 没同步,见 6.3。

6.3 kube-proxy 没同步的排查

iptables 规则缺失,常见原因:

# 1. kube-proxy Pod 是不是 CrashLoop
kubectl get pods -n kube-system | grep kube-proxy

# 2. 看 kube-proxy 日志有没有报错
kubectl logs -n kube-system -l k8s-app=kube-proxy --tail=100 | grep -iE "error|fail|warn"

常见报错是 conntrack 或 nftables 兼容问题,或者节点时钟漂移导致证书验证失败。修复:

# 重启 kube-proxy 让它重读 ConfigMap 重建规则
kubectl rollout restart daemonset/kube-proxy -n kube-system

重启后再 iptables-save -t nat | grep 10.96.88.123 确认规则回来了。

6.4 ipvs 模式:用 ipvsadm 查

如果是 ipvs 模式,iptables 里看不到 DNAT,要用:

ipvsadm -ln | grep 10.96.88.123

正常输出:

TCP  10.96.88.123:80 rr
  -> 10.244.1.15:8080            Masq    1      0          0
  -> 10.244.2.22:8080            Masq    1      0          0

如果 ipvsadm 列表为空,说明 ipvs 的 Service 没同步,同样检查 kube-proxy 日志。ipvs 模式还有一个坑:ip_vs 内核模块没加载。验证:

lsmod | grep ip_vs
modprobe ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh

6.5 用 conntrack 验证 NAT 是否命中

当 iptables 规则存在但流量还是不通,查 conntrack 表看连接有没有被正确 NAT:

conntrack -L | grep 10.96.88.123

正常会看到 dst=10.96.88.123 且 src/dst 做了 DNAT 转换:

tcp      6 431999 ESTABLISHED src=10.244.1.15 dst=10.96.88.123 sport=56784 dport=80 src=10.244.1.15 dst=10.244.1.15 sport=56784 dport=8080 [ASSURED]

关键看转换后的 dst(最后一段)是不是真实 Pod IP。如果 conntrack 里只有 ClusterIP 没有转换后的 Pod IP,说明 DNAT 没生效,回查 kube-proxy。

6.6 kube-proxy 与 CNI 的边界(重要)

这里要特别说明一个很多人误解的点:kube-proxy 管的是 ClusterIP→Endpoint 的转发,CNI 管的是 Pod 之间的路由和数据面。 两者是串联关系,不是同一件事。

客户端 → ClusterIP → [kube-proxy 的 DNAT 规则] → Endpoint IP → [CNI 数据面] → 目标 Pod

所以"Service 能通但跨节点 Pod 直连不通",问题在 CNI 不在 kube-proxy;"ClusterIP 都不通",才轮到 kube-proxy。这就是为什么我强调逐层验证——很多人把 Service 不通直接甩锅给 CNI,其实 DNAT 规则根本没写对。

反过来,如果 iptables 的 DNAT 规则正常(能查到 KUBE-SEP),但 ClusterIP 还是不通,那问题在连接跟踪或 conntrack 表溢出。查:

# conntrack 表是否满了(满了会丢新连接)
dmesg | grep -i conntrack | tail -5
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count

如果 nf_conntrack_count 接近 nf_conntrack_max,就是表满了,新连接被丢,表现为间歇性不通。这时候不是 kube-proxy 的问题,是 conntrack 容量。调大或排查是谁占满了连接。

6.7 iptables 规则逐段解读(看懂才能排障)

很多人 iptables-save 出来一堆看不懂,其实 kube-proxy 的规则结构很固定,记住四段链即可:

KUBE-SERVICES   → 入口链,匹配 ClusterIP:port
KUBE-SVC-XXX    → 服务链,按权重随机挑一个后端
KUBE-SEP-YYY    → 端点链,做 DNAT 到真实 Pod IP
KUBE-POSTROUTING → 出方向做 SNAT(Pod 网段到外部时)
# 完整看一个 Service 的四段链
iptables-save -t nat | grep -E "10.96.88.123|KUBE-SVC|KUBE-SEP"

一个正常的多后端 Service 长这样(概率 0.5 表示两个后端各 50%):

-A KUBE-SERVICES -d 10.96.88.123/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-XXXXX
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-AAA
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BBB
-A KUBE-SEP-AAA -p tcp -m tcp -j DNAT --to-destination 10.244.1.15:8080
-A KUBE-SEP-BBB -p tcp -m tcp -j DNAT --to-destination 10.244.2.22:8080

排障时逐段对比:

  1. KUBE-SERVICES 有这条吗?没有 → kube-proxy 没同步,跳 6.3。
  2. KUBE-SEP 的 DNAT 目标是真实 Endpoint 吗?不是 → Service 定义或 Endpoint 不一致。
  3. 概率段正常吗?后端多于一个但只有一个 KUBE-SEP → 其他 Pod 没进 Endpoint。

注意:--probability 是 kube-proxy 按后端数算的负载均衡权重。如果一个 Service 有 2 个后端但只看到 1 个 KUBE-SEP,说明另一个 Pod 掉了(不 Ready 或 selector 不匹配),回第 2 章查 Endpoint。这是"Service 有 2 个副本但流量总打到 1 个"的常见根因。

6.8 MASQUERADE 与 SNAT:跨节点回包问题

Service 转发还有一种"回包走错路由"的坑,尤其跨节点。kube-proxy 默认对 ClusterIP 发起的、目标不是本节点 Pod 网段 的流量做 SNAT(MASQUERADE)。如果 MASQUERADE 规则丢了,会出现:

  • 能建连但请求超时/卡死:请求到了 Pod,但回包时源 IP 还是 ClusterIP,对端节点路由表没有 ClusterIP 网段路由,回包丢弃。

验证 MASQUERADE 规则:

iptables-save -t nat | grep -E "MASQUERADE|KUBE-POSTROUTING"

正常应该有:

-A KUBE-POSTROUTING -m comment --comment "kubernetes postrouting rules" -m addrtype ! --dst-type LOCAL -j MASQUERADE

如果缺失,kubectl rollout restart daemonset/kube-proxy -n kube-system 重建。这个症状特别容易误判成 CNI 问题——抓包能看到请求到达 Pod,但 Pod 的响应回不来,回查 kube-proxy 的 SNAT 规则,别只盯 CNI。

7. 第 5 层:CNI 与跨节点(flannel / calico / cilium 对比)

到了这一层,Service 的 ClusterIP 和转发规则都正常,但跨节点访问还是不通。这时候才轮到 CNI。不同 CNI 的排障入口完全不同,这是本文最想讲清的一点。

7.1 三种 CNI 数据面对比

CNI 数据面机制 默认端口/协议 排障入口命令 说明
flannel VXLAN overlay(也支持 host-gw) 8472/udp(VXLAN) ip route \| grep podCIDR、测 8472 连通性 最易被云防火墙挡
calico BGP 路由(默认)或 IPIP 179/tcp(BGP)、4/proto(IPIP) calicoctl node status、birdcl BGP peer 未建立则跨节点不通
cilium eBPF 数据面(无传统端口转发) 无固定 overlay 端口(隧道可选) cilium status、cilium bpf、hubble eBPF 绕过 iptables,规则查法不同

核心差异一句话:flannel/calico 走传统内核网络栈(iptables/路由),cilium 用 eBPF 直接在 socket 层做转发,kube-proxy 在 cilium 里甚至可以关掉。

7.2 先确认 CNI 类型

kubectl get pods -A | grep -iE "flannel|calico|cilium" | head -5

或者看 Pod 的网卡与路由:

# 进任意 Pod,看它的路由表
kubectl exec -it <pod> -- ip route

7.3 flannel:VXLAN 8472 排障

flannel 最典型的跨节点故障就是 8472/udp 被防火墙挡住。验证方法:

# 在节点 A 上测试到节点 B 的 8472 端口
nc -vz -u <node-B-IP> 8472

或者用 timeout 包测:

# 节点 A 发一个 UDP 包到节点 B 的 flannel 端口,看 B 有没有收到
nc -u -w 1 <node-B-IP> 8472 < /dev/null

被挡的表现:命令挂起或返回 Connection timed out。再看路由表有没有下发 Pod 网段:

# 节点 A 上应该有到 Pod 网段的 VXLAN 路由
ip route | grep -E "10.244"

正常输出(flannel VXLAN 模式):

10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 10.244.0.0 dev flannel.1 onlink
10.244.2.0/24 via 10.244.0.0 dev flannel.1 onlink

如果只有本节点网段,没有 via flannel.1 的路由,说明 flannel 的 route 没同步,查 flannel Pod 日志:

kubectl logs -n kube-flannel -l app=flannel --tail=50

flannel 的 host-gw 模式是另一个坑:host-gw 不走 VXLAN,靠节点间直连路由,要求节点间二层可达。如果节点跨三层(VPC 间),host-gw 直接不通。判断是不是 host-gw:

# 看 flannel 的 backend 配置
kubectl get cm -n kube-flannel kube-flannel-cfg -o yaml | grep -A2 backend

输出 backend: host-gw 时,跨三层节点必不通,需改回 VXLAN 或换 calico。

7.4 calico:BGP 179 排障

calico 跨节点靠 BGP 建立 peer。BGP peer 没起来,跨节点就全断。排障入口:

# 在节点上查看 BGP 状态
calicoctl node status

或者进 calico-node Pod(老版本用 birdcl,新版用 calicoctl):

kubectl exec -n kube-system <calico-node-pod> -- birdcl show protocols

正常输出里每个 peer 应该是 Established:

BGP node-to-node mesh        up     up

如果 peer 是 Active 或 Start,说明 BGP 没建立。常见原因:

  • 179/tcp 被防火墙挡(云安全组最常见)
  • 节点 IP 对不上(calico 用的 IP vs 路由表 IP 不一致)
  • CALICO_IPV4POOL 网段和路由冲突

验证 179 端口连通性:

nc -vz <node-B-IP> 179

calico 的 IPIP 模式:走 4/proto 的 IP-in-IP,防火墙挡的是 IPIP 协议不是普通端口。判断是不是 IPIP:

# 看路由表,IPIP 模式的路由带 dev tunl0
ip route | grep tunl0

如果有 dev tunl0 的路由,跨节点走 IPIP,云安全组要放行 IPIP(protocol 4),不是 179。很多人查了 179 通了却还是不通,就是因为实际是 IPIP 模式。验证 IPIP :

# 抓 IPIP 封装包
tcpdump -nn -i any proto 4 -c 20

7.5 cilium:eBPF 数据面排障

cilium 的排障入口完全不同,没有 iptables 规则可查(eBPF 接管了)。用 cilium CLI:

# 看 cilium 整体健康状态
cilium status

正常输出会显示 Controller Status、Cluster health、BPF 全部 OK:

Cluster health:   All 3 nodes reachable, 0 not reachable
BPF:              OK

看 Cluster health 里有没有 not reachable,有就是跨节点数据面断了。再往深了查 eBPF 转发规则:

# 看某个 Service 的 eBPF 后端映射
cilium bpf lb list
cilium bpf lb list | grep <ClusterIP>

# 查端点健康
cilium endpoint list

跨节点问题用 hubble 看流量方向:

# 需要 hubble 组件(cilium 安装时启用 hubble)
kubectl exec -it -n kube-system <cilium-pod> -- hubble observe --follow

cilium 的关键点:它默认用 eBPF 做 kube-proxy 的活,所以 Service 转发规则不在 iptables 里。如果你在 cilium 集群里 iptables-save 查不到 ClusterIP 规则,不要以为 kube-proxy 坏了——那是正常的,eBPF 在数据面直接处理了。这是 cilium 用户最容易踩的认知坑。

cilium 还有一个坑:如果 kube-proxy 还开着,会和 eBPF 的转发冲突。cilium 官方建议关闭 kube-proxy。如果集群里 kube-proxy 和 cilium 同时存在,Service 转发可能走两套逻辑,出现间歇性不通。验证:

# 看 kube-proxy 是否还在跑(cilium 集群里应该被移除或禁用)
kubectl get ds -n kube-system kube-proxy

如果 kube-proxy 还在,确认 cilium 安装时是不是没设 kubeProxyReplacement=strict。

7.6 通用跨节点验证:从 Pod 内 ping 对端 Pod

不管哪种 CNI,跨节点不通先做这个对照实验:

# 从 Pod A ping Pod B 的 IP
kubectl exec -it <pod-A> -- ping -c 3 <pod-B-IP>
  • ping 通:Pod 间数据面通,问题在 Service/kube-proxy/DNS 层,回退到第 3、4 层。
  • ping 不通:Pod 间数据面断,问题在 CNI,用上面 7.3-7.5 对应分支查。

这个对照实验能把"CNI 问题"和"非 CNI 问题"一刀切开,少走一半弯路。

7.7 CNI 故障的兜底检查:Pod 网卡与路由

如果以上都查不出,回到 Pod 本身看网卡和路由是否正常:

kubectl exec -it <pod> -- ip addr
kubectl exec -it <pod> -- ip route

正常 Pod 应该有 eth0(或 veth 端点),IP 在 Pod 网段,默认路由走网关(通常是 10.244.0.1)。如果 Pod 内 ip addr 只有 lo 没有 eth0,是 CNI 没把 veth 插进去,属 CNI 故障,查 CNI Pod 日志。

7.8 三种 CNI 的选型与排障难度对照

选 CNI 时的权衡,和排障难度直接相关。我列一张自己用过的对比:

维度 flannel calico cilium
数据面 VXLAN/host-gw BGP/IPIP eBPF
NetworkPolicy 支持 需配 calico 完整(含 L7) 完整(含 L7)
排障心智负担 低(传统网络栈) 中(BGP 状态) 高(eBPF 无 iptables)
常见故障点 8472 防火墙、host-gw 三层不通 179 防火墙、BGP peer 断 kube-proxy 冲突、隧道未启
抓包可读性 高(iptables 可见) 中 低(需 hubble)

给团队的选型建议(基于我踩过的坑):

  • 云上多子网、网络策略要求不高:flannel,简单,但记得放行 8472/udp。
  • 需要 NetworkPolicy 做微隔离:calico,BGP 模式,放行 179/tcp。
  • 大规模、要可观测性、要 eBPF 性能:cilium,但排障门槛高,团队得先学 hubble。

排障难度和功能丰富度成正比,这点团队要认。cilium 功能最强,但"iptables 查不到规则"会让习惯传统排障的同事直接懵。我建议:用 cilium 的团队,把 cilium status 和 cilium bpf lb list 写进标准排障手册的第一页,别等出了事再学。

7.9 节点网络与 Pod 网络的边界:CNI 不通 还是 节点不通

跨节点不通还有个容易混淆的点:到底是 Pod 网段路由没下发(CNI 问题),还是节点间物理网络不通(节点问题)。区分方法:

# 1. 节点之间 ping(物理层)
ping -c 3 <node-B-IP>

# 2. 节点之间测数据面端口(CNI 层)
nc -vz -u <node-B-IP> 8472

# 3. Pod 之间 ping(Pod 层)
kubectl exec -it <pod-A> -- ping -c 3 <pod-B-IP>

三层对照:节点 ping 不通 → 节点网络/安全组问题;节点 ping 通但 8472 不通 → CNI 端口被防火墙挡;8472 通但 Pod ping 不通 → CNI 路由/转发问题。这三步能精确切开"云网络问题"和"CNI 问题",是跨节点排障的核心对照实验。

8. 第 6 层:NetworkPolicy 与防火墙

CNI 数据面通了,接下来最容易忽视的一层:NetworkPolicy。它是在 CNI 之上叠加的 L3/L4 过滤,默认不拦截,但只要有一条 default-deny 策略,就全拦。

8.1 列出所有策略

kubectl get netpol -A

正常输出(如果没有策略,列表为空):

NAMESPACE     NAME                      POD-SELECTOR             AGE
myapp         allow-ingress-8080        app=myapp                3d
kube-system   default-deny-ingress      <none>                   8d

看到 default-deny 就要警惕,它意味着该 namespace 默认拒绝所有 ingress,需要显式放行。

8.2 看策略具体规则

kubectl get netpol allow-ingress-8080 -n myapp -o yaml

一个生产级放行策略(允许来自任何 namespace、带 app=frontend 的 Pod 访问 8080):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-8080
  namespace: myapp
spec:
  podSelector:
    matchLabels:
      app: myapp
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector: {}      # 任意 namespace
          podSelector:
            matchLabels:
              app: frontend           # 来源 Pod 必须带这个 label
      ports:
        - protocol: TCP
          port: 8080

重点:policyTypes 如果同时有 Ingress 和 Egress,且没有对应的 egress 规则,那么所有出方向流量(包括 DNS 53)都会被拒。

8.3 真实事故 ④:NetworkPolicy 默认拒绝误伤 DNS(53)

这个事故是同行群里看到的经典案例,我自己也踩过一次。症状是新加的一个 Namespace 里所有业务 Pod 突然无法解析域名,nslookup 超时,但 Pod 之间 ping 是通的。

排查过程:Pod 间网络通说明 CNI 没问题;域名解析超时指向 DNS 层。我 kubectl get netpol -A 一看,这个 Namespace 里有三条策略:

NAMESPACE      NAME                    POD-SELECTOR    AGE
prod           default-deny-ingress    <none>          1d
prod           default-deny-egress     <none>          1d
prod           allow-ingress-8080      app=web         1d

根因一目了然:团队下了 default-deny-egress,但只配了 ingress 的放行规则,忘了给 egress 放行 DNS 53/udp。Pod 想解析域名,出方向的 DNS 查询被 egress 策略拦死,nslookup 自然超时。Pod 间 ping 是 ICMP,不在 NetworkPolicy 的 TCP/UDP 端口过滤范围内(默认 NetworkPolicy 不管 ICMP),所以 ping 通、DNS 不通,症状极具迷惑性。

修复——加一条放行 DNS 的 egress 规则:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-dns
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

快速止血法(业务先恢复,再精细重写):

# 临时撤掉该 namespace 所有策略
kubectl delete netpol --all -n prod

教训:default-deny-egress 是双刃剑。它比 default-deny-ingress 危险得多,因为出方向默认拒绝会把 DNS 也掐死。所以加了 egress 拒绝的策略,第一条放行规则永远是 DNS 53,不是业务端口。

8.4 NetworkPolicy 与 CNI 的兼容性

不同 CNI 对 NetworkPolicy 的支持差异巨大,这是很多人忽略的:

  • calico / cilium:完整支持 NetworkPolicy(calico 还能做 L7 HTTP 策略)。
  • flannel:默认不支持 NetworkPolicy。flannel 本身没有数据面策略引擎,需要配合 calico 或使用 flannel+calico 混合方案。

所以如果集群是 flannel,kubectl get netpol 能看到对象但策略根本不生效。这是排查 NetworkPolicy 时先要确认的:你的 CNI 支持 NetworkPolicy 吗? 不支持的话,策略对象存在但无任何过滤效果,反而会误导你认为"策略拦了"。

验证 CNI 是否支持:

# flannel 集群:看有没有安装 calico 的 policy 组件
kubectl get pods -A | grep -iE "calico|policy"
# 如果只有 flannel,没有任何 policy 组件,NetworkPolicy 无效

8.5 云平台安全组(第 6.5 层)

NetworkPolicy 是集群内的,集群外的防火墙是云安全组。如果 NodePort 或 LoadBalancer 的 Service 从集群外访问不通,而集群内访问通,基本就是云安全组没放行。

# 确认 Service 类型和 NodePort
kubectl get svc -A -o wide | grep -E "NodePort|LoadBalancer"

然后上云控制台或 CLI 检查节点安全组是否放行了对应端口。这一步容易漏,因为它不在集群内,kubectl 看不到。我通常会在排障清单最底下记一行:"集群外不通,先查安全组。"

NodePort 的端口范围要特别注意:K8s 默认 NodePort 是 30000-32767,云安全组如果只放行了某个特定 NodePort(比如 30080),但 Service 实际分配的是 31080,就会不通。验证:

kubectl get svc <svc> -n <ns> -o jsonpath='{.spec.ports[0].nodePort}'

LoadBalancer 场景还要多查一层:云厂商的 LB 后端是否注册了节点(health check 是否通过)。LB 健康检查失败,节点被摘,也会表现为"集群外不通、集群内通"。

8.6 NetworkPolicy 常见误配清单(我踩过的坑)

NetworkPolicy 的坑很多是"看起来对、实际错"。我把常见误配列成清单,逐条对着排:

① namespaceSelector 空 {} 到底匹配谁?

- from:
    - namespaceSelector: {}    # 匹配所有 namespace

namespaceSelector: {}(空对象)匹配所有 namespace。但如果你只想匹配特定 namespace,写 namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: prod}}。很多人以为空对象匹配"当前 namespace",实际匹配"所有 namespace",这是最隐蔽的误配——策略比预期宽松或严格得多。

② podSelector 为空 = 匹配该 namespace 所有 Pod

spec:
  podSelector: {}   # 匹配整个 namespace 的所有 Pod

podSelector 不写或空对象,都匹配当前 namespace 的全部 Pod。如果本意是匹配特定 Pod,必须写 matchLabels。误配后果:策略作用范围比预期大,误伤其他 Pod。

③ 多个 from 项是 OR,多个 ports 项也是 OR

ingress:
  - from:
      - podSelector: {matchLabels: {app: a}}
      - podSelector: {matchLabels: {app: b}}
    ports:
      - port: 8080
      - port: 9090

from 里的两个 podSelector 是或(来自 a 或 b),ports 里两个端口是或(8080 或 9090)。但 from 和 ports 之间是与(来自 a/b 且 端口 8080/9090)。很多人以为 from 和 ports 也是或,结果策略全拦或全放。这条最容易看错。

④ 多个 ingress 项之间是 OR

ingress:
  - from: [{podSelector: {matchLabels: {app: a}}}]
  - from: [{podSelector: {matchLabels: {app: b}}}]

两个 ingress 数组项是或关系(满足任一即可)。但注意和③的区别:③是一个 ingress 项里有两个 from(还是或),④是两个 ingress 项。效果一样但写法不同,排障时容易数错层级。

⑤ default-deny 与 allow 的叠加

default-deny-ingress  +  allow-ingress-8080

default-deny 是 podSelector: {} + 空 ingress,它拦截所有。allow-ingress-8080 单独放行 8080。两者叠加后,只有匹配 allow 的流量放行,其余全拦。所以 kubectl get netpol 看到一个 default-deny 和一个 allow,不是 default-deny 覆盖 allow,而是 allow 在 default-deny 上开了口子。理解这点才能判断策略到底放行了什么。

8.7 NetworkPolicy 排障的三板斧

真遇到策略问题,按这三步走:

# 1. 列出所有策略,看 namespace 里有没有 default-deny
kubectl get netpol -n <ns>

# 2. 用 describe 看策略的生效范围(会显示匹配的 Pod 数)
kubectl describe netpol <name> -n <ns>

# 3. 数一下策略实际命中了多少 Pod(对比预期)
kubectl get pods -n <ns> -l <policy-podSelector> --show-labels

kubectl describe netpol 会输出 Pods: X selected,如果这个数字和预期不符,就是 podSelector 写错了。这是最直观的验证,比看 yaml 快。

9. 第 7 层:Ingress 与外部流量

Ingress 是外部流量进集群的入口。这里最常见的坑:Ingress 对象存在,但 ingress controller 没在跑,或规则指向的 Service 名写错。

9.1 Ingress 对象有没有绑定到 controller

kubectl get ingress -A -o wide

正常输出(ADDRESS 列有 LB IP 或 hostname):

NAMESPACE   NAME         CLASS    HOSTS                        ADDRESS          PORTS   AGE
myapp       myapp-ing    nginx    www.example.com              192.168.1.100    80      5d

如果 ADDRESS 列是 <none> 或空,说明没有 controller 处理它。直接看 9.2。

9.2 Ingress controller 是否运行

kubectl get pods -n ingress-nginx
  • 如果 Pod 是 Running,但 Ingress 的 ADDRESS 还是空,看 controller 日志:
  • 如果 Pod 不存在或 CrashLoopBackOff,这是根因。
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx --tail=50

9.3 真实事故 ⑤:Ingress controller 未运行但 Ingress 对象存在

这个事故非常典型。症状:域名 www.example.com 访问 502,但 kubectl get ingress 能看到对象。

我先查对象,ADDRESS 列是 <none>——立刻怀疑 controller。再查 Pod:

kubectl get pods -n ingress-nginx
NAME                                        READY   STATUS             RESTARTS   AGE
ingress-nginx-controller-7c9f6b8d4-abc12    0/1     CrashLoopBackOff   17         3h

根因:controller 一直 CrashLoop,没人注意到。17 次重启说明不是偶发。看日志:

kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx --tail=30
Error: listening on 0.0.0.0:80 failed: address already in use

address already in use —— 80 端口被宿主机上的 nginx 或 hostNetwork 冲突占了。修复:停掉冲突进程,或改 controller 的 hostPort。

# 看谁占着 80
ss -tlnp | grep :80

这个案例的教训:Ingress 对象存在 ≠ Ingress 生效。判断 Ingress 是否生效,看 kubectl get ingress -A -o wide 的 ADDRESS 列,这一列是空的,说明 controller 压根没处理这个对象,别去查规则细节。

9.4 Ingress 规则指向的 Service 是否正确

controller 正常但 404,查规则:

kubectl get ingress myapp-ing -n myapp -o yaml

检查 backend.service.name 是否等于真实 Service 名,backend.service.port.number 是否等于 Service 的 port。Ingress 里写错 Service 名是最常见的 404 根因。

一个生产级 Ingress 片段:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ing
  namespace: myapp
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: www.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp-svc        # 必须与 Service 同名
                port:
                  number: 80           # 必须与 Service 的 port 一致

9.5 Ingress 的 host 冲突与 path 匹配

404 还有两个常见原因:

  1. host 冲突:两个 Ingress 用了同一个 host 但 path 重叠,后创建的覆盖先创建的。查:

    kubectl get ingress -A -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.spec.rules[*].host}{"\n"}{end}'
    
    看同一个 host 是否被多个 Ingress 引用。

  2. pathType 不匹配:pathType: Prefix 要求 path 有明确前缀语义,pathType: Exact 要求完全匹配。如果后端期望 /api 但配了 Exact 且请求是 /api/,会 404。

9.6 Ingress TLS 与证书

如果域名能到 controller 但报 TLS 错误,查 Ingress 的 tls 段:

kubectl get ingress myapp-ing -n myapp -o jsonpath='{.spec.tls}'

检查 secretName 指向的 Secret 是否存在、证书 SAN 是否覆盖域名:

kubectl get secret <tls-secret> -n myapp
kubectl get secret <tls-secret> -n myapp -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

SAN 里没有域名,浏览器会报证书不匹配,但集群内可能正常。这和 SSL/TLS 握手排障 里讲的证书链问题相通。

10. Pod 内抓包:kubectl debug / netshoot + tcpdump

排障到一半,光看 iptables 和路由还不够,有时候必须抓包看真实流量。K8s 里进 Pod 抓包,标准做法是用诊断镜像。

10.1 netshoot 临时 Pod(推荐,最通用)

kubectl run netshoot-$RANDOM -it --rm \
  --image=nicolaka/netshoot \
  -- /bin/bash

进去后就是完整的排障工具箱(tcpdump、dig、nslookup、nc、iftop、iperf 都有):

# 抓 Pod 里所有发往目标 ClusterIP 的流量
tcpdump -nn -i any host 10.96.88.123 -w /tmp/myapp.pcap

# 另一个终端看实时流量
tcpdump -nn -i any host 10.96.88.123 -c 20

抓包看什么:

# 正常 SYN 应该能到目标并收到 SYN-ACK
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
IP 10.244.1.15.56784 > 10.96.88.123.80: Flags [S], seq 123456, win 64240, ...
IP 10.96.88.123.80 > 10.244.1.15.56784: Flags [S.], seq 654321, ack 123457, win 64240, ...

关键判断: - 只有 S 没有 S.:SYN 发出去了但没回应,网络层丢包(CNI 或防火墙)。 - 有 RST:目标端口拒绝了(Pod 没监听该端口)。 - 完全看不到流量:DNS 解析出来的 IP 不对,或 kube-proxy 没转发,回退到第 3、4 层。

10.2 kubectl debug 临时容器(更贴近生产 Pod)

netshoot 是独立 Pod,网络命名空间和业务 Pod 不同。如果想抓业务 Pod 自己的网络命名空间,用 kubectl debug(K8s 1.23+):

kubectl debug -it <myapp-pod> --image=nicolaka/netshoot -- /bin/bash

进去后 ip addr 会看到和业务 Pod 完全一样的网卡和 IP,tcpdump 抓的就是业务 Pod 真实流量:

ip addr show | grep inet
tcpdump -nn -i eth0 port 8080 -c 20

10.3 在节点上抓(跨节点场景)

如果怀疑跨节点丢包,直接在节点上抓 VXLAN 或物理网卡:

# 抓 flannel VXLAN 的 8472 端口
tcpdump -nn -i any port 8472 -c 50

# 抓宿主机网卡上的真实流量(注意不是 Pod 网卡)
tcpdump -nn -i eth0 host <target-IP> -c 50

节点上抓包和 Pod 内抓包看到的流量不同:Pod 内看到的是原始容器流量(ClusterIP),节点上看到的是 SNAT/DNAT 之后的真实 IP。两相对照能判断是 kube-proxy 的 DNAT 没做,还是 CNI 转发丢了。

10.4 常用抓包 filter 速查

# 抓某个 Pod IP 的所有流量
tcpdump -nn -i any host <pod-IP>

# 抓某个 Service ClusterIP 的流量(看 kube-proxy DNAT 是否发生)
tcpdump -nn -i any host <ClusterIP>

# 抓指定端口
tcpdump -nn -i any port 8080

# 抓 DNS 查询(看解析流量)
tcpdump -nn -i any port 53 -c 20

# 抓 TCP 握手包
tcpdump -nn -i any 'tcp[tcpflags] & tcp-syn != 0' -c 20

# 抓 RST 包(看谁在拒绝)
tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0' -c 20

10.5 抓包结果的典型判读表

抓包现象 含义 下一步
只有 SYN 无 SYN-ACK 网络层丢包(无 RST) 查 CNI/防火墙/安全组
SYN 后收到 RST 目标端口拒绝 查 Pod 是否监听该端口
有 SYN-ACK 但业务失败 网络通,应用层问题 查应用日志/探针
Pod 内看不到目标 IP 流量 解析或转发未发生 查 DNS/ClusterIP
节点上看到但 Pod 内没有 DNAT 后丢失 查 CNI 数据面
抓到 8472 但无响应 VXLAN 对端不可达 查防火墙 8472/udp

这张表是我抓包时的对照卡。记住:抓包不是为了抓而抓,是为了确认"包走到哪一层停了"。

10.6 netshoot 里的其他排障工具

tcpdump 之外,netshoot 还带了一整套工具,针对不同症状:

# 带宽测试:Pod A 起服务端,Pod B 测客户端
# Pod A 内:
iperf3 -s
# Pod B 内:
iperf3 -c <pod-A-IP> -t 5

# DNS 压力/延时:dig 带统计
dig @10.96.0.10 <svc>.<ns>.svc.cluster.local +stats

# TCP 连接连通性:nc 探测
nc -vz <pod-B-IP> 8080

# 路由跟踪:traceroute(看包走哪条路)
traceroute -n <pod-B-IP>

iperf3 是区分"通不通"和"慢不慢"的关键工具。ping 通但业务慢,用 iperf3 测带宽,如果带宽远低于预期,是 MTU 或数据面封装的问题(VXLAN 会加 50 字节头部,MTU 要相应调小)。

MTU 问题的判据:

# 大包测试(1472 = 1500 - 28,VXLAN 下要减到 1450 左右)
ping -M do -s 1450 <node-B-IP>
ping -M do -s 1472 <node-B-IP>   # VXLAN 下会失败

如果 -s 1472 失败但 -s 1450 成功,就是 MTU 黑洞——VXLAN overlay 的标准 MTU 应该设 1450 而不是 1500。这是 flannel/calico VXLAN 模式的高频坑,跟 TCP 排障里的 MTU 黑洞 是同一类问题。

10.7 抓包与 CNI 数据面的交叉验证

最后补一个高级技巧:同时抓 Pod 内和节点上两个位置,对比判断包在哪丢。

# 终端 1:Pod 内抓(netshoot,业务 Pod 命名空间)
kubectl debug -it <pod> --image=nicolaka/netshoot -- \
  tcpdump -nn -i eth0 host <target-IP> -c 20

# 终端 2:节点上抓(物理网卡)
tcpdump -nn -i eth0 host <target-IP> -c 20

对照结果:

Pod 内抓到 节点上抓到 判断
有 有 正常转发,问题在更上层
有 无 Pod 发出但没出节点,CNI 数据面丢了
无 有 节点收到但没进 Pod,CNI veth 问题
无 无 源端就没发出,问题在应用或 kube-proxy

"Pod 内抓到、节点上抓不到"是最典型的 CNI 数据面断——包从容器出来,但 CNI 的 veth/路由没把它送出去。这时候查 CNI 的路由和 veth,别查应用。

11. 5 个真实事故复盘

下面把全文串起来,复盘 5 个我处理过的真实事故。每个按"症状 → 排查过程 → 根因 → 修复"四段走。

11.0 综合决策树:一张图走完七层

先给一张综合决策树,把前 10 章的逐层验证收成一张图。你遇到任何 K8s 网络问题,按这张图从上往下走,每一步只回答一个"是/否":

症状:Pod 内访问服务超时/拒绝
  │
  ├─ Pod 是 Running 且 READY=1/1 吗?
  │    否 → describe pod 看探针/事件 → 修探针或换镜像
  │    是 ↓
  ├─ kubectl get endpoints <svc> 有 IP 吗?
  │    否 → 对比 selector 与 Pod labels → 修 selector
  │    是 ↓
  ├─ Pod 内 nslookup 解析出的 IP == ClusterIP 吗?
  │    否 → 查 CoreDNS ConfigMap / 缓存 → 修 Corefile
  │    是 ↓
  ├─ iptables-save 里有 KUBE-SEP DNAT 到 Endpoint 吗?
  │    否 → 重启 kube-proxy / 查 conntrack
  │    是 ↓
  ├─ 跨节点 Pod 之间 ping 通吗?
  │    否 → 测 CNI 端口(8472/179) → 放行防火墙
  │    是 ↓
  ├─ kubectl get netpol -A 有 default-deny 吗?
  │    是 → 检查是否放行 DNS 53 与业务端口
  │    否 ↓
  └─ 外部访问不通?查 Ingress ADDRESS 列 + 云安全组

这张图是我的"肌肉记忆"。凌晨接警,脑子不清醒的时候,就按图从上往下走,每步一个命令,不会漏层也不会乱猜。你把它截图当屏保都不过分。

下面是五个事故的详细复盘。

事故 ①:Service 无 Endpoint(selector 不匹配)

症状:微服务 A 调用服务 B 超时,curl 到 B 的域名一直 Connection timed out。

排查过程:

# 1. Pod 状态,B 的 Pod 都是 Running
kubectl get pods -n prod -o wide | grep svc-b

# 2. Service 和 Endpoint —— 发现问题了
kubectl get svc svc-b -n prod
kubectl get endpoints svc-b -n prod
NAME    TYPE        CLUSTER-IP     PORT(S)    AGE
svc-b   ClusterIP   10.96.33.77    80/TCP     5d

NAME    ENDPOINTS    AGE
svc-b   <none>       5d      ← Endpoint 空

根因定位:

kubectl get svc svc-b -n prod -o jsonpath='{.spec.selector}'
# map[app:svc-b version:v1]

kubectl get pods -n prod -l app=svc-b --show-labels
# 结果 Pod 的 label 是 app=svc-b,version=v2  ← 版本从 v1 升到 v2

根因:Helm 升级把 version label 从 v1 改成 v2,但 Service 的 selector 还写死 version=v1。selector 匹配不到任何 Pod,Endpoint 全空。

修复:

kubectl patch svc svc-b -n prod -p '{"spec":{"selector":{"app":"svc-b","version":"v2"}}}'
kubectl get endpoints svc-b -n prod   # 确认 Endpoint 回来了

教训:升级后一定要 kubectl get endpoints <svc> 确认 Endpoint 非空,这是升级后健康检查的第一道关卡。

补充:这个事故还有个连带症状。当时除了业务超时,kubectl rollout status 一直是绿的——因为 Deployment 的 rollout 只看 Pod 起来没起来,不看 Service 有没有 Endpoint。所以我把"升级后验证 Endpoint 非空"写进了团队的发布 checklist:kubectl rollout status 通过后,必须再加一条 kubectl get endpoints <svc> -o wide,两个都过了才算发布成功。这条救了我们后面至少三次。

事故 ②:CoreDNS 解析错 IP(详见 5.3)

症状:新部署的服务连不上,重启后短暂恢复,几分钟后随机失联。

排查:netshoot 内反复 nslookup,同一域名在两个 IP 之间跳变,而 ClusterIP 是静态唯一的。检查 CoreDNS ConfigMap,发现 cluster.local 被错误地 forward 到外部 DNS,外部残留旧记录渗入。

根因:CoreDNS Corefile 的 kubernetes cluster.local 插件段被破坏/删除,cluster.local 域漏到上游 DNS。

修复:恢复 Corefile 的 kubernetes cluster.local in-addr.arpa ip6.arpa 段,kubectl rollout restart deployment/coredns -n kube-system。

教训:cluster.local 只能由集群内部 CoreDNS 处理,任何把它转发到外部的做法都是定时炸弹。排查解析错 IP,先对比"宿主机解析结果 vs Pod 内解析结果",两者不一致必是链路差异。

事故 ③:跨节点不通(VXLAN 8472 被防火墙挡)

症状:集群刚上线,Pod 同节点互通,跨节点 ping 不通。

排查过程:

# 1. 从 Pod A ping Pod B —— 不通
kubectl exec -it pod-a -- ping -c 3 10.244.2.22
# ping: 10.244.2.22: Destination Host Unreachable

# 2. 看路由表,只有本节点网段,没有 via flannel.1
ip route | grep 10.244
# 10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
# (缺少 10.244.2.0/24 via flannel.1 的 VXLAN 路由)

# 3. 测 8472 端口 —— 不通!防火墙拦了
nc -vz -u 192.168.1.102 8472
# nc: connect to 192.168.1.102 port 8472 (udp) failed: Connection timed out

根因:云安全组/宿主机防火墙没放行 8472/udp(flannel VXLAN 的默认端口),跨节点的 VXLAN 隧道建立不起来。

修复:在安全组和每台节点防火墙放行 8472/udp:

# 节点防火墙(firewalld 示例)
firewall-cmd --permanent --add-port=8472/udp
firewall-cmd --reload

# 再验证
nc -vz -u 192.168.1.102 8472
ip route | grep 10.244.2   # 现在应该有 via flannel.1 的路由了

教训:跨节点不通,第一时间测 CNI 数据面端口。flannel 测 8472/udp,calico 测 179/tcp(或 IPIP 4),cilium 看 cilium status。这个端口被挡是云上 K8s 最常见的跨节点故障。

补充:这个事故还踩过 flannel host-gw 的版本。另一个客户的集群跨了三个 VPC,flannel 配的是 host-gw 模式(不走 VXLAN),结果跨 VPC 节点死活不通。判断时 ip route 里没有 flannel.1 设备,全是直连路由,查 kube-flannel-cfg 的 backend 发现是 host-gw。host-gw 要求节点二层可达,跨三层必挂。改回 VXLAN 模式后恢复:

kubectl edit cm kube-flannel-cfg -n kube-flannel
# 把 backend 从 host-gw 改成 vxlan
kubectl rollout restart ds/kube-flannel-ds -n kube-flannel

所以跨节点排障,除了测端口,还要先确认 CNI 的模式——同样的"跨节点不通",VXLAN 和 host-gw 的根因和修法完全不一样。

事故 ④:NetworkPolicy 默认拒绝误伤 DNS(53)(详见 8.3)

症状:新增 Namespace 的业务 Pod 无法解析域名,nslookup 超时,但 Pod 间 ping 通。

排查:kubectl get netpol -A 发现该 Namespace 有 default-deny-egress,且只放行了 ingress,没有 egress 放行 DNS 53/udp。

根因:default-deny-egress 把出方向全拦,DNS 查询(53/udp)也被掐死。ICMP 不在 NetworkPolicy 过滤范围,所以 ping 通、DNS 不通,极具迷惑性。

修复:先 kubectl delete netpol --all -n prod 恢复业务,再按最小权限补放行 DNS 的 egress 策略(见 8.3 的 yaml)。

教训:加 egress 默认拒绝时,第一条放行规则必须是 DNS 53。排查"ping 通但业务不通",永远把 NetworkPolicy 放进行动列表——它只拦 TCP/UDP,不拦 ICMP,症状非常隐蔽。

事故 ⑤:Ingress controller 未运行但 Ingress 对象存在(详见 9.3)

症状:域名访问 502,kubectl get ingress 能看到对象。

排查:kubectl get ingress -A -o wide 的 ADDRESS 列为空 → 怀疑 controller → kubectl get pods -n ingress-nginx 发现 CrashLoopBackOff,重启 17 次。

根因:controller 的 80 端口被宿主机其他进程占用,address already in use 反复崩溃。

修复:ss -tlnp | grep :80 找到冲突进程并处理,或改 controller hostPort,重启后 ADDRESS 列出现 LB IP。

教训:Ingress 对象存在 ≠ Ingress 生效。判断标准是 ADDRESS 列是否有值,空就是 controller 没处理。

补充:这个事故还有个"假象"要提醒。当时我看到 kubectl get ingress 有对象,第一反应是"Ingress 配错了吧",差点去改规则。后来看 ADDRESS 列是空的才转向查 controller。很多同学在这个坑里浪费半小时——对象存在不等于被处理,kubectl get ingress -A -o wide 的 ADDRESS 列是唯一能一眼判断"controller 有没有处理"的字段。另外补一句:Ingress 的 ingressClassName 必须等于 controller 的 class(默认 nginx),如果 Ingress 写了 ingressClassName: traefik 但集群装的是 nginx controller,也会出现"对象存在、ADDRESS 空"的现象。验证:

# 看集群里注册的 IngressClass
kubectl get ingressclass
# 看 Ingress 用的 class 是否匹配
kubectl get ingress <name> -n <ns> -o jsonpath='{.spec.ingressClassName}'

class 不匹配和 controller 崩溃,都会让 ADDRESS 为空,但修法不同——先看 class 匹配没,再看 controller 死没死。

12. 20+ 条速查命令清单

把全文的命令收成一张速查表,复制即用。

12.1 集群与 Pod 基础

kubectl get pods -A -o wide | grep <关键字>        # 看 Pod 状态/IP/节点
kubectl get pods -n <ns> --show-labels              # 看 Pod labels(对 selector)
kubectl describe pod <pod> -n <ns>                  # 看探针/事件/端口
kubectl exec -it <pod> -n <ns> -- ss -tlnp          # 看容器实际监听端口
kubectl get nodes -o wide                           # 看节点 InternalIP
kubectl exec -it <pod> -- ip route                  # 看 Pod 内路由

12.2 Service 与 Endpoint

kubectl get svc <svc> -n <ns> -o wide                # 看 ClusterIP/Selector
kubectl get endpoints <svc> -n <ns> -o wide          # 看 Endpoint 是否为空
kubectl get svc <svc> -n <ns> -o jsonpath='{.spec.selector}'
kubectl get pods -n <ns> -l <selector> --show-labels # 验证 selector 匹配
kubectl patch svc <svc> -n <ns> -p '{"spec":{"selector":{...}}}'   # 修 selector
kubectl get svc <svc> -n <ns> -o jsonpath='{.spec.ports[0]}'       # 看 port/targetPort
kubectl get svc -n <ns> | grep None                  # 找 headless Service

12.3 DNS / CoreDNS

kubectl run netshoot-$RANDOM -it --rm --image=nicolaka/netshoot -- /bin/bash
nslookup <svc>.<ns>.svc.cluster.local               # Pod 内解析
kubectl get pods -n kube-system -l k8s-app=kube-dns # CoreDNS Pod
kubectl get cm coredns -n kube-system -o yaml       # 看 Corefile
kubectl rollout restart deployment/coredns -n kube-system   # 重启 CoreDNS
kubectl exec -it <pod> -- cat /etc/resolv.conf      # 看 search 域/ndots

12.4 kube-proxy / 转发规则

kubectl logs -n kube-system -l k8s-app=kube-proxy --tail=5 | grep -i using  # 模式
iptables-save -t nat | grep <ClusterIP>              # iptables 模式查 DNAT
ipvsadm -ln | grep <ClusterIP>                       # ipvs 模式查转发
conntrack -L | grep <ClusterIP>                      # 看 NAT 是否命中
kubectl rollout restart daemonset/kube-proxy -n kube-system  # 重建规则
lsmod | grep ip_vs                                   # 检查 ip_vs 模块

12.5 CNI 数据面

kubectl get pods -A | grep -iE "flannel|calico|cilium"   # 确认 CNI 类型
nc -vz -u <node-B> 8472                              # flannel 测 VXLAN
nc -vz <node-B> 179                                  # calico 测 BGP
calicoctl node status                                # calico BGP 状态
ip route | grep <podCIDR>                            # 看 CNI 路由下发
ip route | grep tunl0                                # calico 是否走 IPIP
cilium status                                        # cilium 健康
cilium bpf lb list | grep <ClusterIP>                # cilium eBPF 转发规则
kubectl exec -it <pod> -- ping -c 3 <pod-B-IP>       # 跨节点对照实验

12.6 NetworkPolicy 与防火墙

kubectl get netpol -A                                # 看所有策略(重点 default-deny)
kubectl get netpol <name> -n <ns> -o yaml            # 看规则细节
kubectl delete netpol --all -n <ns>                  # 紧急止血
kubectl get pods -A | grep -iE "calico|policy"       # 确认 CNI 是否支持 NetPol
ss -tlnp | grep :80                                  # 查端口冲突
kubectl get svc -A -o wide | grep -E "NodePort|LoadBalancer"  # 看外部端口

12.7 Ingress

kubectl get ingress -A -o wide                       # 看 ADDRESS 列(关键)
kubectl get pods -n ingress-nginx                    # controller 是否运行
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx --tail=50
kubectl get ingress <name> -n <ns> -o yaml           # 看 backend.service 是否正确
kubectl get ingress <name> -n <ns> -o jsonpath='{.spec.tls}'   # 看 TLS

12.8 Pod 内抓包

kubectl run netshoot-$RANDOM -it --rm --image=nicolaka/netshoot -- /bin/bash
tcpdump -nn -i any host <ClusterIP> -c 20            # Pod 内抓 ClusterIP 流量
tcpdump -nn -i any port 53 -c 20                     # 抓 DNS 查询
tcpdump -nn -i any 'tcp[tcpflags] & tcp-rst != 0' -c 20  # 抓 RST
kubectl debug -it <pod> --image=nicolaka/netshoot -- /bin/bash   # 抓业务 Pod 命名空间
tcpdump -nn -i any port 8472 -c 50                   # 节点上抓 VXLAN
tcpdump -nn -i eth0 host <IP> -c 50                  # 节点上抓真实流量

12.9 关键端口速查表(排障时要放行的端口)

端口/协议 用途 放行位置
6443/tcp kube-apiserver 节点安全组(master)
10250/tcp kubelet 节点安全组
8472/udp flannel VXLAN 节点安全组(最容易漏)
179/tcp calico BGP 节点安全组
4/proto calico IPIP 节点安全组
53/udp+tcp CoreDNS NetworkPolicy egress(必须放)
30000-32767/tcp NodePort 范围 节点安全组
10255/tcp kubelet 只读(旧) 节点安全组
80/443/tcp Ingress controller 节点安全组 + LB

这张表的意义:跨节点不通、集群外不通,八成是上面某个端口没放行。云安全组漏放 8472/udp 是我遇到最多的跨节点故障,没有之一。

12.10 一条命令判断"卡在哪一层"(黄金一条)

如果你只记一条命令,记这条——它把七层问题压缩成一次判断:

# 从 Pod 内依次测:Pod 直连 → ClusterIP → 域名
# 三个结果能区分 网络层/转发层/解析层
kubectl run netshoot-$RANDOM -it --rm --image=nicolaka/netshoot -- bash -c '
  echo "=== 1. 直连目标 Pod IP ===";        nc -vz -w 2 <pod-IP> 8080;
  echo "=== 2. 直连 Service ClusterIP ==="; nc -vz -w 2 <cluster-IP> 80;
  echo "=== 3. 通过域名访问 ===";            nc -vz -w 2 <svc>.<ns>.svc.cluster.local 80;
'

结果判读:

1 直连 Pod 2 ClusterIP 3 域名 结论
通 通 通 全链路正常,问题在应用层
通 通 不通 DNS 解析问题(第 5 章)
通 不通 不通 kube-proxy 转发问题(第 6 章)
不通 不通 不通 CNI 数据面问题(第 7 章)

三条命令,一次定位。这比从头逐层排查快得多,是我凌晨接警的第一条命令。先用它定位到层,再翻到对应章节深挖。

附录:故障现象 → 根因 → 修复速查表

把全文的根因和修复收成一张对照表,接警时直接按症状查表:

故障现象 最可能根因 修复动作
Pod 一直 Pending,无 IP CNI IPAM 地址池耗尽 扩 IPPool 或查 no IP addresses available
Pod CrashLoopBackOff 应用崩溃/端口冲突 kubectl logs 看崩溃原因
READY=0/1,容器在跑 readinessProbe 失败 修探针路径/端口
Endpoint 为空 selector 不匹配 对比 selector 与 Pod labels
Endpoint 端口不对 targetPort 写错 改 targetPort 为实际监听端口
解析 IP ≠ ClusterIP CoreDNS 缓存/上游污染 修 Corefile 的 kubernetes 段
解析 NXDOMAIN 域名/namespace 写错 核对完整 FQDN
解析超时 CoreDNS 挂或 NetPol 拦 DNS 查 CoreDNS Pod + 放行 53
ClusterIP 直连不通 kube-proxy 规则缺失 重启 kube-proxy
间歇性不通 conntrack 表满 调大 nf_conntrack_max
跨节点 ping 不通 8472/179 被防火墙挡 放行数据面端口
跨节点通但业务慢 MTU 黑洞(VXLAN) 节点 MTU 调 1450
ping 通但业务不通 NetworkPolicy 拦截 放行 DNS 53 + 业务端口
集群外访问不通 云安全组未放行 放行 NodePort/LB 端口
Ingress ADDRESS 为空 controller 未运行 修 controller CrashLoop
Ingress 404 Service 名/path 写错 核对 backend.service

这张表是本文的"最后一公里"。前 10 章教你怎么走到层,这张表让你从"症状"直接跳到"根因+修复"。接警时先查表定位方向,再按前 10 章的章节深入验证,效率和准确率都能上来。

排障中的三个错误心态(写给带新人的你)

最后我想单独写一节,专门讲我带新人时反复纠正的三个错误心态。技术是次要的,方向错了才是灾难。

错误一:一上来就 ping 对端,通了就觉得网络没问题。

这是最致命的。ping 验证的是 IP 层的连通,而业务走的是"域名→ClusterIP→Endpoint→Pod"的完整链路。ping 通只说明第 5 层 CNI 的 IP 路由是通的,完全不代表 Service 转发、DNS 解析、NetworkPolicy 是好的。 我在第 1 章反复强调这点。正确做法:用 12.10 的"黄金一条命令"从应用层往下测,而不是从 IP 层往上猜。

错误二:找到一个疑似根因就急着改,不验证是不是"真凶"。

比如看到 NetworkPolicy 有 default-deny 就删了,结果业务恢复了——你以为找到了根因,其实是误打误撞,下次换一个 namespace 又挂。正确做法:改之前先留证据(kubectl get netpol -o yaml > /tmp/before.yaml),改完验证,最后把策略精修后恢复最小权限。排障不是碰运气,是假设检验——每一个"根因"都要能解释所有症状,解释不了的就不是根因。

错误三:只改不测,恢复业务后就走人。

恢复 ≠ 根除。kubectl delete netpol --all 恢复了业务,但你没有补上最小权限的精确策略,三天后安全扫描又告警,或者下次有人误配又挂。正确做法:止血后必须回填——写一条符合最小权限的精确规则,验证它只放行需要的流量,然后才叫真正修复。这也是我在事故 ④ 里强调的:先 delete netpol --all 止血,再补 allow-egress-dns 精修。

这三个心态,比任何一条命令都重要。命令可以查文档,方向错了就是浪费时间。

总结:排障不是背命令,是走链路

写到这里回看一下,K8s 网络排障的底层逻辑就一句话:每一步都问自己"我现在验证的是访问链路里的哪一段",然后只查那一段的证据。

别再一上来就 ping、iptables-save 乱翻。先画链路图:

  1. Pod Ready + 容器端口(第 3 章)
  2. Service selector + Endpoint(第 4 章,空 Endpoint 最高频)
  3. CoreDNS 解析(第 5 章,cluster.local 别乱 forward)
  4. kube-proxy 转发规则(第 6 章,iptables vs ipvs 分开查)
  5. CNI 数据面(第 7 章,flannel 测 8472 / calico 测 179 / cilium 看 status)
  6. NetworkPolicy + 云安全组(第 8 章,default-deny-egress 别忘放行 DNS)
  7. Ingress + 外部流量(第 9 章,ADDRESS 列空就是 controller 没跑)

这套流程我用了五年,从 flannel 时代的 8472 排查到 cilium 时代的 eBPF 数据面,换 CNI 不换思路——因为抽象层没变,变的只是每层的排障命令。你把这七层刻进肌肉记忆,下次凌晨接警,十分钟内能定位到层,而不是陪着用户一起猜。

再补一句最实在的:排障的终点不是找到根因,是让业务先恢复。 遇到 ClusterIP 不通先 kubectl rollout restart 试试 kube-proxy;遇到 NetworkPolicy 误伤先 kubectl delete netpol --all;遇到 Ingress 挂了先重启 controller。先止血、再深挖根因、最后写修复, 这是我十年运维最值钱的一条经验。

FAQ

Q1:Service 有 ClusterIP 但 Endpoint 为空,是 CNI 坏了吗?

A:大概率不是。空 Endpoint 九成是 selector 没匹配上 Pod 的 label,或 Pod 本身没 Ready(readinessProbe 失败)。先 kubectl get pods -o wide 看 Pod 是否 Ready,再看 svc 的 spec.selector 和 Pod 的 labels 是否一致。CNI 出问题通常连 Pod IP 都分配不出来,表现为 Pod 一直 Pending 或 CrashLoopBackOff,不会只影响 Endpoint。

Q2:Pod 之间跨节点不通,但同节点通,第一件事查什么?

A:先确认 CNI 用的什么数据面。flannel 走 VXLAN 8472/udp,calico 走 BGP 179 或 IPIP 4,cilium 走 eBPF 不占传统端口。跨节点不通第一步在节点上 nc 测 8472,被防火墙挡了直接能看到拒绝。再看 node 的 InternalIP 与 Pod 的 podCIDR 是否在路由表里,ip route | grep podCIDR 就能验证 CNI 路由有没有下发。

Q3:CoreDNS 解析出的 IP 和 Service ClusterIP 不一样,怎么定位?

A:先分清是宿主机 /etc/resolv.conf 的 search 域问题,还是 CoreDNS 本身缓存了旧记录。nslookup 到 10.96.0.10 直接查,和宿主机默认 resolver 查做对比,两个结果不同就是链路差异。再 kubectl get endpoints -n kube-system kube-dns 确认解析后端是否正常。最后查 coredns 的 ConfigMap 与日志,确认是不是改过 upstream 或缓存参数。

Q4:NetworkPolicy 误伤 DNS 导致业务全挂,怎么快速止血?

A:最快验证:kubectl get netpol -A 列出所有策略,看有没有 namespace 级别的 default deny。临时加一条宽限策略允许 kube-system 的 kube-dns 53/udp 放行,或者直接 kubectl delete netpol --all -n <ns> 先撤掉策略恢复业务,再按最小权限重写。注意很多团队在 Namespace 里同时下了 default-deny-ingress 和 default-deny-egress,egress 会把 DNS 也掐死。

Q5:Ingress 对象存在但访问 404,是 Ingress 本身的问题还是后端?

A:先 kubectl get ingress -A -o wide 看 ADDRESS 列有没有 LB 地址,空说明 ingress controller 没运行或没注册。再 kubectl get pods -n ingress-nginx 确认 controller 是否 Running。若 controller 正常但 404,多半是 path 或 service 名字写错,kubectl describe ingress 看 Events 里 controller 的报错信息。最后确认 Ingress 的 backend.service 指向的 Service 有没有 Endpoint。

相关文章