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、2 层开始(Pod Ready + Endpoint)。发布引起的网络问题,八成是 selector 或镜像标签变了。
- 老系统突然挂:从第 3、4 层开始(DNS + kube-proxy)。没有变更的网络问题,多半是 CoreDNS 或 kube-proxy 的状态漂移。
- 集群刚上线/扩容:从第 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
正常输出长这样:
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。这时候别急着查网络,先看探针:
看到类似下面的,就是探针配置或应用自身的问题:
Readiness: http-get http://:8080/healthz delay=0s timeout=1s period=10s #success=1 #failure=3
Ready: False
探针失败但容器在跑是最隐蔽的。应用进程活着,日志正常,就是 readinessProbe 的路径返回非 200,Endpoint 被摘掉。这时候网络层全是好的,问题在应用健康检查。排查:
返回非 200 就是探针路径问题,跟网络无关。返回 200 但 Pod 还是 0/1,再查探针的端口和 scheme(http vs https、tcp vs http)。
3.3 STATUS 不是 Running:看事件定位
STATUS 是 Pending、CrashLoopBackOff、ImagePullBackOff 时,直接看事件:
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,就全对不上了。
正常输出(容器内可能没有 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 起不来"是拉镜像的网络问题,不是应用问题。
事件里如果出现:
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,要区分两类:
- 网络不通:节点拉不到镜像仓库(
curl -I https://registry.example.com测一下)。 - 鉴权失败:
ImagePullSecrets缺失或 registry 凭证过期(pull access denied)。
验证节点到镜像仓库的网络:
注意:拉镜像走的是节点的网络,不是 Pod 网络。如果节点能拉(节点上 curl 通)但 Pod 报 ImagePullBackOff,那是鉴权问题;如果节点上也拉不动,才是节点网络问题。这一步别在 Pod 里测,要在节点上测。
3.6 Pod 一直 Pending:CNI 没分配 IP 的判据
Pending 且事件里出现 FailedCreatePodSandBox + no IP addresses available,这是第 5 层 CNI 的 IPAM 问题,但症状出现在第 1 层。判断口诀:
区分方法:
# 看是不是资源不足
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
正常情况 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 对不上
再看 Pod 的实际 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 写错。验证:
输出 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 活着:
正常:
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
再看日志里有没有异常:
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 的转发逻辑,外部记录就会渗进来。
修复:
确认 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 的解析行为完全不同:
- 普通 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 列表:
解析出的多 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 配置:
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 模式
或者:
输出类似 Using iptables Proxier 或 Using ipvs Proxier。iptables 和 ipvs 模式查规则的方式不一样,先分清模式再动手。
6.2 iptables 模式:grep ClusterIP
在任意工作节点上执行(ClusterIP 换成你的 Service IP):
正常会看到一串 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 兼容问题,或者节点时钟漂移导致证书验证失败。修复:
重启后再 iptables-save -t nat | grep 10.96.88.123 确认规则回来了。
6.4 ipvs 模式:用 ipvsadm 查
如果是 ipvs 模式,iptables 里看不到 DNAT,要用:
正常输出:
如果 ipvsadm 列表为空,说明 ipvs 的 Service 没同步,同样检查 kube-proxy 日志。ipvs 模式还有一个坑:ip_vs 内核模块没加载。验证:
6.5 用 conntrack 验证 NAT 是否命中
当 iptables 规则存在但流量还是不通,查 conntrack 表看连接有没有被正确 NAT:
正常会看到 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 之间的路由和数据面。 两者是串联关系,不是同一件事。
所以"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 长这样(概率 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
排障时逐段对比:
KUBE-SERVICES有这条吗?没有 → kube-proxy 没同步,跳 6.3。KUBE-SEP的 DNAT 目标是真实 Endpoint 吗?不是 → Service 定义或 Endpoint 不一致。- 概率段正常吗?后端多于一个但只有一个 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 规则:
正常应该有:
-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 类型
或者看 Pod 的网卡与路由:
7.3 flannel:VXLAN 8472 排障
flannel 最典型的跨节点故障就是 8472/udp 被防火墙挡住。验证方法:
或者用 timeout 包测:
被挡的表现:命令挂起或返回 Connection timed out。再看路由表有没有下发 Pod 网段:
正常输出(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 日志:
flannel 的 host-gw 模式是另一个坑:host-gw 不走 VXLAN,靠节点间直连路由,要求节点间二层可达。如果节点跨三层(VPC 间),host-gw 直接不通。判断是不是 host-gw:
输出 backend: host-gw 时,跨三层节点必不通,需改回 VXLAN 或换 calico。
7.4 calico:BGP 179 排障
calico 跨节点靠 BGP 建立 peer。BGP peer 没起来,跨节点就全断。排障入口:
或者进 calico-node Pod(老版本用 birdcl,新版用 calicoctl):
正常输出里每个 peer 应该是 Established:
如果 peer 是 Active 或 Start,说明 BGP 没建立。常见原因:
- 179/tcp 被防火墙挡(云安全组最常见)
- 节点 IP 对不上(calico 用的 IP vs 路由表 IP 不一致)
CALICO_IPV4POOL网段和路由冲突
验证 179 端口连通性:
calico 的 IPIP 模式:走 4/proto 的 IP-in-IP,防火墙挡的是 IPIP 协议不是普通端口。判断是不是 IPIP:
如果有 dev tunl0 的路由,跨节点走 IPIP,云安全组要放行 IPIP(protocol 4),不是 179。很多人查了 179 通了却还是不通,就是因为实际是 IPIP 模式。验证 IPIP :
7.5 cilium:eBPF 数据面排障
cilium 的排障入口完全不同,没有 iptables 规则可查(eBPF 接管了)。用 cilium CLI:
正常输出会显示 Controller Status、Cluster health、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 安装时是不是没设 kubeProxyReplacement=strict。
7.6 通用跨节点验证:从 Pod 内 ping 对端 Pod
不管哪种 CNI,跨节点不通先做这个对照实验:
- ping 通:Pod 间数据面通,问题在 Service/kube-proxy/DNS 层,回退到第 3、4 层。
- ping 不通:Pod 间数据面断,问题在 CNI,用上面 7.3-7.5 对应分支查。
这个对照实验能把"CNI 问题"和"非 CNI 问题"一刀切开,少走一半弯路。
7.7 CNI 故障的兜底检查:Pod 网卡与路由
如果以上都查不出,回到 Pod 本身看网卡和路由是否正常:
正常 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 列出所有策略
正常输出(如果没有策略,列表为空):
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 看策略具体规则
一个生产级放行策略(允许来自任何 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
快速止血法(业务先恢复,再精细重写):
教训: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 从集群外访问不通,而集群内访问通,基本就是云安全组没放行。
然后上云控制台或 CLI 检查节点安全组是否放行了对应端口。这一步容易漏,因为它不在集群内,kubectl 看不到。我通常会在排障清单最底下记一行:"集群外不通,先查安全组。"
NodePort 的端口范围要特别注意:K8s 默认 NodePort 是 30000-32767,云安全组如果只放行了某个特定 NodePort(比如 30080),但 Service 实际分配的是 31080,就会不通。验证:
LoadBalancer 场景还要多查一层:云厂商的 LB 后端是否注册了节点(health check 是否通过)。LB 健康检查失败,节点被摘,也会表现为"集群外不通、集群内通"。
8.6 NetworkPolicy 常见误配清单(我踩过的坑)
NetworkPolicy 的坑很多是"看起来对、实际错"。我把常见误配列成清单,逐条对着排:
① namespaceSelector 空 {} 到底匹配谁?
namespaceSelector: {}(空对象)匹配所有 namespace。但如果你只想匹配特定 namespace,写 namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: prod}}。很多人以为空对象匹配"当前 namespace",实际匹配"所有 namespace",这是最隐蔽的误配——策略比预期宽松或严格得多。
② 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 是 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
正常输出(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 是否运行
- 如果 Pod 是
Running,但 Ingress 的 ADDRESS 还是空,看 controller 日志: - 如果 Pod 不存在或
CrashLoopBackOff,这是根因。
9.3 真实事故 ⑤:Ingress controller 未运行但 Ingress 对象存在
这个事故非常典型。症状:域名 www.example.com 访问 502,但 kubectl get ingress 能看到对象。
我先查对象,ADDRESS 列是 <none>——立刻怀疑 controller。再查 Pod:
根因:controller 一直 CrashLoop,没人注意到。17 次重启说明不是偶发。看日志:
address already in use —— 80 端口被宿主机上的 nginx 或 hostNetwork 冲突占了。修复:停掉冲突进程,或改 controller 的 hostPort。
这个案例的教训:Ingress 对象存在 ≠ Ingress 生效。判断 Ingress 是否生效,看 kubectl get ingress -A -o wide 的 ADDRESS 列,这一列是空的,说明 controller 压根没处理这个对象,别去查规则细节。
9.4 Ingress 规则指向的 Service 是否正确
controller 正常但 404,查规则:
检查 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 还有两个常见原因:
-
host 冲突:两个 Ingress 用了同一个 host 但 path 重叠,后创建的覆盖先创建的。查:
看同一个 host 是否被多个 Ingress 引用。 -
pathType 不匹配:
pathType: Prefix要求 path 有明确前缀语义,pathType: Exact要求完全匹配。如果后端期望/api但配了Exact且请求是/api/,会 404。
9.6 Ingress TLS 与证书
如果域名能到 controller 但报 TLS 错误,查 Ingress 的 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(推荐,最通用)
进去后就是完整的排障工具箱(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+):
进去后 ip addr 会看到和业务 Pod 完全一样的网卡和 IP,tcpdump 抓的就是业务 Pod 真实流量:
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 乱翻。先画链路图:
- Pod Ready + 容器端口(第 3 章)
- Service selector + Endpoint(第 4 章,空 Endpoint 最高频)
- CoreDNS 解析(第 5 章,
cluster.local别乱 forward) - kube-proxy 转发规则(第 6 章,iptables vs ipvs 分开查)
- CNI 数据面(第 7 章,flannel 测 8472 / calico 测 179 / cilium 看 status)
- NetworkPolicy + 云安全组(第 8 章,default-deny-egress 别忘放行 DNS)
- 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。