Kubernetes 基础体系 · 第 35/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 网络排障:DNS、Service、CNI、MTU、Conntrack 和抓包

Kubernetes 网络故障经常表现为同一句话:“Pod 访问不了某个地址”。但“某个地址”可能是 DNS 名称、Service ClusterIP、Pod IP、NodePort、外部 IP 或入口负载均衡器;对应的数据路径、负责组件和故障证据完全不同。

一次有效的排障,首先要回答四个问题:

  1. 名称是否解析成功?
  2. 目标 IP 是否可达?
  3. 中间是否发生了 Service 转发、SNAT 或 DNAT?
  4. 连接失败发生在应用、内核、CNI、节点网络还是外部网络?

可以把常见路径抽象为:

flowchart LR
    A[应用进程] --> B[Pod 网络命名空间]
    B --> C[Pod veth]
    C --> D[CNI 数据路径]
    D --> E[节点内核路由与防火墙]
    E --> F[kube-proxy Service 转发]
    F --> G[目标 Pod 或外部地址]

    A --> H[CoreDNS Service]
    H --> I[CoreDNS Pod]
    I --> J[集群 DNS / 上游 DNS]

DNS 负责把名称转换成地址,但不负责建立连接;Service 负责把虚拟地址转换到后端端点,但不一定负责跨节点传输;CNI 负责 Pod 网络接入和 Pod 间路径;MTU 决定单个报文能否无分片通过;conntrack 保存连接状态并参与 NAT;抓包则用于验证每一跳实际发生了什么。


一、先建立网络对象与数据路径模型

1. Pod IP、Service IP 和 Node IP 不是同一类地址

Kubernetes 中常见的三类地址如下:

地址 所属对象 典型用途
Pod IP Pod Pod 之间直接通信
ClusterIP Service 集群内部访问 Service
Node IP Node 节点管理、NodePort、节点间网络
LoadBalancer IP 云厂商或外部负载均衡器 集群外部访问
ExternalName 对应名称 Service DNS 别名,不提供四层代理

Pod IP 通常来自集群 Pod CIDR 或插件管理的地址池。Service 的 ClusterIP 来自 Service CIDR,通常不是某个网卡上真实配置的地址,而是由 kube-proxy 等组件实现虚拟转发。

因此:

访问 Pod IP:
客户端 -> 路由/CNI -> 目标 Pod

访问 ClusterIP:
客户端 -> Service 规则 -> 某个 EndpointSlice 中的 Pod

访问 NodePort:
客户端 -> 节点地址和端口 -> kube-proxy 规则 -> Service 后端

不能因为 ping ClusterIP 失败,就直接推断 Service 不可用。Service 的主要语义是传输层转发,ICMP 是否被实现或转发并不是诊断 HTTP/TCP 服务的可靠方法。应使用与真实业务相同的 TCP 或 UDP 协议测试。

2. Service 的后端来自 EndpointSlice,而不是 Service 本身

Service 通过选择器关联 Pod。控制器根据匹配结果生成 EndpointSlice,kube-proxy 再观察 Service 和 EndpointSlice,编程本节点的数据平面。

查看关系:

kubectl get svc web -n demo -o wide
kubectl get endpointslice \
  -n demo \
  -l kubernetes.io/service-name=web \
  -o wide

典型输出会包含:

NAME        ADDRESSTYPE   PORTS   ENDPOINTS        AGE
web-abc123  IPv4          8080    10.244.2.17      3m

这里至少要检查:

  • Service 的 port:客户端访问的 Service 端口;
  • Service 的 targetPort:后端 Pod 监听的端口;
  • EndpointSlice 中的端点地址;
  • 端点是否为 ready;
  • Service 是否为 ClusterIP: None 的 Headless Service;
  • Service 的协议是 TCP、UDP 还是 SCTP。

例如:

kubectl get svc web -n demo \
  -o jsonpath='{.spec.clusterIP}{" "}{.spec.ports[*].port}{" -> "}{.spec.ports[*].targetPort}{"\n"}'

kubectl get endpointslice -n demo \
  -l kubernetes.io/service-name=web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses}{" ready="}{.conditions.ready}{"\n"}{end}'

如果 EndpointSlice 没有端点,问题通常发生在 Service 选择器、Pod 标签、端口声明或 readiness 状态,而不是 iptables、IPVS 或 CNI。此时继续查看节点 NAT 规则通常是错误方向。

3. Service 转发不等于应用监听

假设:

apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: demo
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

客户端访问:

web.demo.svc.cluster.local:80

经过 DNS 后得到 ClusterIP,例如 10.96.12.34。kube-proxy 选择端点 10.244.2.17:8080。这里有两个不同的正确性条件:

Service 正确性:
ClusterIP:80 -> 10.244.2.17:8080

应用正确性:
10.244.2.17:8080 上确实有进程监听,并且返回协议正确

只要 targetPort 配置错误,Service 规则即使完全正常,连接仍会得到 Connection refused 或超时。

在后端 Pod 内检查监听:

kubectl exec -n demo deploy/web -- ss -lntp

如果容器镜像没有 ss,可以使用调试容器或在节点上检查网络命名空间;不要把“镜像缺少诊断工具”误判为“服务没有监听”。


二、DNS:先区分解析失败与连接失败

1. Pod 中的 DNS 配置从哪里来

普通 Pod 通常会获得类似配置:

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

这些字段的含义是:

  • nameserver:Pod 应向哪个 DNS 地址发送查询;
  • search:短名称失败或满足搜索规则时,客户端尝试追加的域名后缀;
  • ndots:名称中点号少于该值时,解析器通常优先尝试 search 列表。

检查实际配置:

kubectl exec -n demo deploy/web -- cat /etc/resolv.conf

10.96.0.10 只是示例,实际地址应以集群中的 DNS Service 为准:

kubectl get svc -n kube-system
kubectl get svc -n kube-system kube-dns -o wide

Kubernetes 使用名为 kube-dns 的 Service 作为集群 DNS 的常见约定,即使实际后端组件是 CoreDNS。不要仅根据 Service 名称推断后端实现。

2. Service DNS 名称的构成

对于命名空间 demo 中名为 web 的普通 Service,常见名称为:

web
web.demo
web.demo.svc
web.demo.svc.cluster.local

完整形式通常是:

<service>.<namespace>.svc.<cluster-domain>

其中 cluster-domain 通常是 cluster.local,但可以在集群配置中修改。

在另一个命名空间中解析 web,短名称可能会先尝试当前命名空间的 web.demo.svc.cluster.local,而不是目标命名空间。排障时应优先使用完整名称:

kubectl run dns-test \
  -n demo \
  --rm -it \
  --restart=Never \
  --image=busybox:1.36 \
  -- nslookup web.demo.svc.cluster.local

预期结果应包含 ClusterIP。这个命令要求节点能拉取镜像,并且集群允许创建临时 Pod;生产环境不应把任意公共镜像直接用于敏感网络区域。

3. ndots 可能制造额外查询和延迟

如果 ndots:5,应用查询:

api.example.com

由于其中只有两个点,某些 libc 或语言运行时会先尝试:

api.example.com.demo.svc.cluster.local
api.example.com.svc.cluster.local
api.example.com.cluster.local
api.example.com

这会导致:

  • DNS 查询数量增加;
  • 外部名称解析延迟增加;
  • 上游 DNS 或 CoreDNS 日志出现许多看似“错误”的搜索域查询;
  • 某些严格限流的 DNS 服务更容易被触发。

这不是 Kubernetes DNS 必然故障,而是解析器、searchndots 共同产生的行为。使用绝对域名时可写成:

api.example.com.

末尾的点表示 DNS 绝对名称,避免搜索域追加;但应用是否保留并正确处理末尾点,仍取决于客户端库。

4. DNS 排障要逐层验证

建议按以下顺序验证:

# 1. DNS Service 是否存在
kubectl get svc -n kube-system kube-dns -o wide

# 2. CoreDNS Pod 是否运行
kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide

# 3. CoreDNS 后端是否有端点
kubectl get endpointslice -n kube-system \
  -l kubernetes.io/service-name=kube-dns -o wide

# 4. Pod 是否使用预期 nameserver
kubectl exec -n demo deploy/web -- cat /etc/resolv.conf

# 5. 从业务网络命名空间实际查询
kubectl run dns-test -n demo --rm -it --restart=Never \
  --image=busybox:1.36 \
  -- nslookup kubernetes.default.svc.cluster.local

若第 5 步超时,再从 CoreDNS 日志判断请求是否到达:

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

日志中没有请求,可能是 Pod 到 DNS Service 的路径、Service 转发、NetworkPolicy 或 DNS 客户端配置问题;有请求但没有响应,则继续检查 CoreDNS 到上游 DNS 的路径、CoreDNS 配置和上游服务。

CoreDNS 的常见配置来源是 ConfigMap:

kubectl get configmap coredns -n kube-system -o yaml

但具体 Corefile、插件和日志格式可能因发行版或云厂商而不同。不能假设所有集群都启用了相同的缓存、转发、DNSSEC 或可观测性配置。

5. 普通 Service、Headless Service 和 ExternalName 的差异

普通 Service 通常返回 ClusterIP:

web.demo.svc.cluster.local -> 10.96.12.34

Headless Service 设置:

spec:
  clusterIP: None

此时 DNS 通常直接返回后端 Pod 地址,而不是虚拟 IP。客户端可能获得多个 A 或 AAAA 记录,并自行选择端点。于是:

  • 客户端是否轮询由 DNS 库和应用决定;
  • 连接是否经过 kube-proxy 取决于客户端访问的地址;
  • 后端 Pod 地址变化会直接影响客户端缓存和连接复用。

ExternalName 则主要返回 CNAME,不创建通常意义上的 ClusterIP 和后端转发规则。对它执行“检查 EndpointSlice、检查 kube-proxy NAT”通常没有意义。


三、CNI:Pod 网络是怎样接入节点的

1. CNI 的职责边界

CNI 是容器网络接口规范及其插件生态。Kubernetes 通过运行时调用 CNI 插件,为 Pod 网络命名空间配置:

  • Pod 网卡;
  • Pod IP;
  • 默认路由;
  • 与节点网络命名空间连接的 veth 或其他设备;
  • 跨节点所需的路由、隧道、桥接、封装或主机网络配置。

Kubernetes API 并不规定某一种 CNI 实现。Calico、Cilium、Flannel、云厂商 VPC CNI 等插件在地址分配、路由、封装、NetworkPolicy 和可观测性方面差异很大。

因此,下面这个抽象路径是常见模型,不是所有插件的硬性实现:

Pod eth0
  -> veth pair
  -> 节点 bridge / eBPF hook / host device
  -> 节点路由或隧道设备
  -> 目标节点
  -> 目标 Pod

2. Pod 创建时的状态变化

Pod 网络初始化通常可理解为以下过程:

  1. kubelet 创建 Pod 沙箱;
  2. 容器运行时创建网络命名空间;
  3. 运行时调用 CNI 主插件;
  4. CNI 调用 IPAM 分配地址;
  5. CNI 将网卡、地址和路由写入 Pod 网络命名空间;
  6. CNI 或其控制器配置节点间路径;
  7. 容器启动并使用该网络命名空间。

如果 CNI ADD 阶段失败,Pod 可能长期处于 ContainerCreating,事件中出现 FailedCreatePodSandBox。此时故障尚未进入 DNS 或 Service 层:

kubectl describe pod <pod> -n <namespace>
kubectl get events -n <namespace> --sort-by=.lastTimestamp

节点侧还要检查 CNI DaemonSet、插件日志和 /etc/cni/net.d//opt/cni/bin/ 等节点文件。但这些路径属于常见部署约定,不是 Kubernetes API 保证的固定路径。

3. 先测试 Pod IP,再测试 Service

假设:

客户端 Pod: 10.244.1.10
目标 Pod:   10.244.2.17
Service:    10.96.12.34:80

应至少做两次测试:

# 访问后端 Pod 的真实端口
kubectl exec -n demo deploy/client -- \
  wget -S -O- -T 3 http://10.244.2.17:8080

# 访问 Service 端口
kubectl exec -n demo deploy/client -- \
  wget -S -O- -T 3 http://10.96.12.34:80

结果解释:

Pod IP Service 常见推断
成功 成功 基础路径和 Service 大致正常
失败 失败 CNI、路由、NetworkPolicy、目标进程或跨节点路径优先
成功 失败 Service 选择器、EndpointSlice、kube-proxy、端口映射或 Service 策略
失败 成功 不常见,可能是 Service 后端选择了其他可用端点,或测试地址不等价

测试必须考虑目标 Pod 是否与客户端同节点。跨节点失败而同节点成功,通常指向节点间路由、Overlay、Underlay、MTU 或跨节点策略;同节点也失败,则先检查 Pod 接口、默认路由、目标监听和本地策略。

4. Overlay 与 Underlay

Underlay 是底层承载网络,例如节点网卡之间的 VPC、物理网络或云网络。
Overlay 是在 Underlay 之上封装的逻辑网络,例如 VXLAN、Geneve、IP-in-IP 或 WireGuard 隧道。

Overlay 报文大致是:

外层以太网/IP/UDP/隧道头
  + 内层原始 Pod 报文

节点间网络可能只看到外层源、目的地址;目标 Pod 看到的是解封装后的内层地址。抓包位置不同,看到的五元组也不同。

跨节点 Pod 通信可能有三类实现:

  1. 纯路由:Underlay 直接知道 Pod CIDR 路由;
  2. Overlay:节点之间发送封装报文;
  3. VPC 原生地址:Pod 地址直接由底层网络承载,可能没有传统隧道。

不能看到 vxlan.calicoflannel.1 或类似设备,就断言所有集群都应有 Overlay 设备。设备名和路径由 CNI 决定。


四、MTU:为什么小请求成功,大响应却失败

1. MTU 的定义和条件

MTU(Maximum Transmission Unit)是某个链路一次能够承载的最大三层 IP 载荷大小,通常指 IP 包总长度上限,不包括外层以太网帧头。

若底层链路 MTU 为 MuM_u,封装额外开销为 OO,则内层路径可用 MTU 近似为:

MiMuOM_i \leq M_u - O

其中:

  • MiM_i:Pod 内层报文允许的最大 IP 包长度;
  • MuM_u:Underlay 路径上的最小 MTU;
  • OO:Overlay 外层 IP、UDP、隧道等头部开销。

“最小”很重要:路径上任一设备 MTU 更小,整个路径的有效 MTU 就由它决定。

2. 完整算例:1500 字节底层网络和 VXLAN

假设:

Underlay MTU = 1500
外层 IPv4 头 = 20
UDP 头       = 8
VXLAN 头     = 8
外层以太网相关开销不计入 IP MTU

则:

Mi15002088=1464M_i \leq 1500 - 20 - 8 - 8 = 1464

但实际插件通常还会为其他封装细节、实现差异或保守余量选择 1450 左右,而不是机械地只减去一个固定数字。

如果 Pod 使用 TCP/IPv4,TCP 最大报文段长度可近似为:

MSS=MTUIPHeaderTCPHeaderMSS = MTU - IPHeader - TCPHeader

当 Pod MTU 为 1450、无 TCP options 的基本头部长度为 20 字节时:

MSS=14502020=1410MSS = 1450 - 20 - 20 = 1410

实际 TCP options 会影响有效空间,Linux 也可能通过 MSS clamping 调整 SYN 中的 MSS。

3. 典型 MTU 故障路径

当报文超过路径 MTU 时,可能发生:

  1. 发送端尝试发送大 IP 包;
  2. 中间设备无法转发;
  3. IPv4 设备可能分片或返回 ICMP Fragmentation Needed;
  4. IPv6 设备依赖路径 MTU 发现,由源端根据 ICMPv6 Packet Too Big 调整;
  5. 如果 ICMP 被防火墙丢弃,发送端可能持续使用过大的报文;
  6. 小报文和 TCP 三次握手成功,但大响应、TLS 握手或文件传输卡住。

因此,“TCP 端口能连通”不能证明 MTU 正常。

检查节点和 Pod 的 MTU:

# Pod 内
kubectl exec -n demo deploy/client -- ip link show eth0

# 节点上
ip link show
ip route

在 Linux 上进行不分片探测时,具体参数要区分 IPv4 和 IPv6:

# IPv4 示例:请求总 IP 包不超过 1400 字节
ping -M do -s 1372 -c 3 <目标IP>

# IPv6 示例:payload 1400 字节,不能使用 IPv4 的 -M 参数
ping6 -s 1400 -c 3 <目标IPv6>

IPv4 中 ping -s 通常指定 ICMP payload,因此总 IP 长度约为 payload + 20 + 8;上例 1372 加上 IPv4 和 ICMP 头正好约 1400。不同 ping 实现参数略有差异,必须先查看本机 ping --help

4. 为什么 Ping 结果不能单独证明 MTU

ICMP Echo 可能:

  • 被 NetworkPolicy 或节点防火墙阻止;
  • 被云网络安全策略限制;
  • 被目标 Pod 或节点忽略;
  • 走与 TCP 不同的路径;
  • 没有触发与业务报文相同的封装和策略。

更接近业务的验证方式是:

# 在客户端 Pod 中测试 TCP
kubectl exec -n demo deploy/client -- \
  curl -v --connect-timeout 3 --max-time 10 http://<service-name>:<port>/large-response

同时在客户端 Pod、节点隧道设备和目标 Pod 处抓包,观察大报文是否发出、是否收到 ICMP、是否在某一跳消失。


五、Conntrack:NAT 和连接状态为什么会影响 Service

1. Conntrack 保存什么

Linux conntrack 是内核连接跟踪子系统。它根据报文的协议和五元组等信息,为连接建立状态,例如:

NEW -> ESTABLISHED -> FIN_WAIT/TIME_WAIT

对 TCP,状态会结合 SYN、ACK、FIN、RST 等标志变化;对 UDP,内核只能根据报文方向和超时近似跟踪“连接”,因为 UDP 本身无握手和关闭过程。

conntrack 通常还保存 NAT 映射。例如客户端访问:

10.244.1.10:43000 -> 10.96.12.34:80

Service 规则可能把它转换为:

10.244.1.10:43000 -> 10.244.2.17:8080

返回包需要沿相反方向恢复地址,使客户端仍认为自己在与 10.96.12.34:80 通信。后续同一连接的报文通常依赖已建立的 conntrack/NAT 状态,而不是每次重新随机选择后端。

2. Service 规则和 conntrack 的关系

kube-proxy 可能使用 iptables、IPVS 或 nftables 等数据平面模式,具体可用模式与版本、发行版和配置有关。无论实现细节如何,排障时都应区分:

  • 规则是否存在:Service 和 EndpointSlice 是否被同步;
  • 报文是否命中规则:计数器或内核观测是否增加;
  • 连接状态是否正确:conntrack 是否建立、是否冲突或耗尽;
  • 后端是否返回:目标 Pod 是否收到并响应。

在节点上可查看 conntrack:

sudo conntrack -L
sudo conntrack -S

常见统计字段可能包括:

  • 当前表项数量;
  • insert failed;
  • drop;
  • early drop;
  • error。

查看当前限制:

sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max

如果 count 接近 max,新连接可能被丢弃,表现为间歇性超时。扩大表容量并不是自动修复:它会增加内核内存消耗,且不能解决应用未关闭连接、UDP 超时不合理或异常流量持续增长的问题。

3. “清空 conntrack”是有破坏性的操作

下面命令会影响节点上的大量连接:

sudo conntrack -F

它可能导致:

  • 现有 TCP 连接中断;
  • NAT 映射丢失;
  • 长连接和数据库连接重建;
  • 大规模重试形成新的连接洪峰;
  • 节点上其他业务同时受影响。

生产环境不应把它作为第一反应。更安全的做法是先记录故障时间、五元组和节点范围,再针对特定流删除;但不同 conntrack 工具版本的过滤参数存在差异,执行前必须查看:

conntrack --help

同时需要注意,删除 conntrack 表项只能改变内核状态,不能修复 EndpointSlice 错误、后端不监听、CNI 丢包或外部防火墙问题。

4. Conntrack 常见故障表现

表耗尽

表现可能是:

  • 新连接间歇性超时;
  • 节点上的多个无关 Service 同时异常;
  • 内核日志出现 conntrack 相关丢包;
  • 已建立连接比新连接稳定。

验证应结合 nf_conntrack_countnf_conntrack_max、内核日志和抓包,而不是只观察应用错误率。

非对称路由

假设请求从节点 A 进入,返回却经节点 B 离开,两个节点上的 conntrack 可能分别只看到半条流。对于依赖状态的防火墙和 NAT,这可能造成:

  • ESTABLISHED 判断失败;
  • 回包被丢弃;
  • Service 访问超时;
  • 只在跨节点或特定方向发生故障。

抓包应同时记录进入、离开节点和 Pod 的路径,比较源目的地址、接口和方向。

长连接与后端变化

Service 选择后端通常发生在连接建立或首次处理阶段;已建立 TCP 连接不会因为 EndpointSlice 后来移除某个端点而自动迁移到另一个 Pod。Pod 滚动发布期间,旧连接可能由应用、连接池或代理负责关闭和重建。不能期待 kube-proxy 把一个已建立的 TCP 流“搬运”到新 Pod。


六、抓包:把“推断”变成路径证据

1. 抓包位置决定你能看到什么

常见抓包点和用途如下:

位置 能观察到的内容
Pod eth0 Pod 看到的内层源/目的地址
节点 veth Pod 与节点之间的报文
节点物理网卡 Underlay 上的报文,可能是封装后的外层地址
Overlay 设备 封装或解封装相关报文
目标 Pod eth0 目标 Pod 实际收到的报文
CoreDNS Pod DNS 请求是否到达 DNS 后端

只在客户端 Pod 中抓包,无法证明报文到达了目标节点;只在物理网卡中抓包,也可能看不到解封装后的 Pod 内层流量。

2. 在 Pod 网络命名空间中抓包

如果镜像包含 tcpdump

kubectl exec -n demo deploy/client -- \
  tcpdump -ni eth0 -vv 'host 10.96.12.34 and tcp port 80'

但业务镜像通常没有抓包工具,也不应为了排障长期修改生产镜像。可使用临时调试容器:

kubectl debug -n demo pod/<pod-name> \
  -it \
  --image=nicolaka/netshoot \
  --target=<container-name> \
  -- bash

这依赖集群支持 ephemeral containers,且运行时、权限、镜像来源和安全策略可能限制其使用。--target 使调试容器尝试进入目标容器的进程命名空间;网络命名空间共享方式和运行时实现应在目标集群中验证。

如果需要进入节点网络命名空间,常见方式是:

kubectl debug node/<node-name> -it \
  --image=nicolaka/netshoot \
  --privileged \
  -- bash

进入后,节点根文件系统通常挂载在 /host,但具体调试 Pod 模板和权限策略可能不同。也可以在节点上直接执行:

sudo tcpdump -ni any -vv \
  'host 10.244.2.17 or host 10.96.12.34'

-i any 方便初步定位,但会合并多个接口,可能重复看到同一报文,也不适合精确判断某个链路上的单次转发。确认范围后,应改用具体接口:

sudo tcpdump -ni eth0 -s 0 -w /tmp/service.pcap \
  'tcp port 80'

-s 0 尽量保留完整报文,-w 写入 pcap 文件,便于 Wireshark 或 tshark 后续分析。抓包本身有 CPU、磁盘和隐私风险,过滤条件应尽可能具体。

3. 使用五元组和 TCP 标志判断故障位置

TCP 五元组为:

协议、源 IP、源端口、目的 IP、目的端口

在 Service NAT 前后,IP 和端口可能改变。因此要根据观察位置判断预期:

客户端 Pod 看到:

10.244.1.10:43000 -> 10.96.12.34:80

目标 Pod 看到的可能是:

10.244.1.10:43000 -> 10.244.2.17:8080

如果客户端发送 SYN,但目标 Pod 完全没有收到:

  • 可能是 Service 规则、节点路由、CNI、NetworkPolicy 或中间防火墙;
  • 也可能是抓包点选错,目标端点并不是你检查的那个 Pod。

如果目标 Pod收到 SYN并回复 SYN-ACK,但客户端收不到:

  • 优先检查返回路径;
  • 检查 SNAT、非对称路由、节点防火墙和 conntrack;
  • 不要只检查入口方向。

如果目标 Pod收到 SYN,却立即返回 RST:

  • 通常说明该地址端口没有监听,或应用主动拒绝;
  • 这与“网络完全不通”的超时不同。

如果三次握手成功,随后大报文重传:

  • 检查 MTU、MSS、PMTUD 和中间设备;
  • 检查应用是否在等待 DNS、TLS 或上游响应;
  • 不要把所有重传都归因于 conntrack。

4. DNS 抓包示例

在 DNS 测试 Pod 或 CoreDNS Pod 中:

tcpdump -ni any -vv \
  'udp port 53 or tcp port 53'

DNS 通常优先使用 UDP;响应过大、截断或客户端策略变化时可能转 TCP。只抓 UDP 53 会漏掉 TCP DNS 流量。

观察顺序:

客户端是否发送查询
  -> 查询目的地址是否为预期 DNS Service
  -> DNS Service 是否转发到 CoreDNS Pod
  -> CoreDNS 是否向上游发送查询
  -> CoreDNS 是否返回客户端

如果客户端发往 ClusterIP,而 CoreDNS Pod看不到请求,不代表 Service 一定没有转发:抓包接口、kube-proxy 模式、节点本地转发路径都可能影响可见性。需要结合 EndpointSlice、节点规则和客户端/节点两侧抓包。


七、kube-proxy:iptables、IPVS 与 nftables 只解决数据平面的一部分

1. iptables 模式

在 iptables 模式中,kube-proxy 为 Service 和端点创建一组规则。规则可能涉及:

  • ClusterIP;
  • NodePort;
  • ExternalIP;
  • DNAT;
  • SNAT;
  • 会话亲和性;
  • 端点选择;
  • 健康检查相关路径。

具体链名和规则细节属于实现行为,不应编写依赖某一个版本的固定链名排障脚本。查看规则时:

sudo iptables-save -t nat
sudo iptables -t nat -L -n -v

如果系统使用 nftables 后端兼容 iptables 命令,还要确认查看的命令是否对应实际规则集:

sudo nft list ruleset

iptables-legacyiptables-nft 和底层 nftables 的组合受发行版配置影响;看到某个命令没有规则,不足以证明 kube-proxy 没有编程规则。

2. IPVS 模式

IPVS 在内核中提供虚拟服务器和后端调度。常见检查命令是:

sudo ipvsadm -Ln

可以观察虚拟服务、后端 Real Server 以及连接计数。IPVS 仍可能依赖 iptables 处理部分流量、SNAT 或其他辅助逻辑,因此不能仅凭 ipvsadm 输出就认为整个路径不受 iptables 影响。

IPVS 的调度器、连接状态和会话保持也会影响“同一个客户端是否总是命中同一个后端”。故障时应将实际连接五元组、后端 Pod 和会话亲和性配置对应起来。

3. nftables 模式

较新的 Kubernetes 版本和 kube-proxy 可提供 nftables 数据平面模式,但可用性取决于 Kubernetes 版本、kube-proxy 配置、内核和发行版。检查时:

sudo nft list ruleset
kubectl -n kube-system get configmap kube-proxy -o yaml

不能因为节点安装了 nft 命令,就断言 kube-proxy 正在使用 nftables 模式;应查看 kube-proxy 实际配置和日志。

三种模式的共同排障原则是:

Service/EndpointSlice 正确
  -> kube-proxy 已同步
  -> 规则或虚拟服务存在
  -> 报文命中
  -> 后端 Pod 收到
  -> 返回路径正确

只验证“规则存在”不等于验证“报文走过规则”。iptables 的计数器、IPVS 连接统计、nftables 计数器或 eBPF 观测工具可以提供命中证据,但计数器读取也可能受规则刷新和实现差异影响。


八、一个完整的排障流程

假设业务报告:

client Pod 访问 http://web.demo.svc.cluster.local/large
偶发超时;直接访问某个 Pod IP 有时正常

第一步:确认名称解析

kubectl exec -n demo deploy/client -- \
  getent hosts web.demo.svc.cluster.local

若镜像没有 getent

kubectl run dns-test -n demo --rm -it --restart=Never \
  --image=busybox:1.36 \
  -- nslookup web.demo.svc.cluster.local

需要记录:

  • 返回的是 ClusterIP 还是 Pod IP;
  • 是否同时返回 IPv4 和 IPv6;
  • 是否存在超时、SERVFAIL 或 NXDOMAIN;
  • DNS 查询是否受 search/ndots 影响。

第二步:确认 Service 和端点

kubectl get svc web -n demo -o yaml
kubectl get endpointslice -n demo \
  -l kubernetes.io/service-name=web -o yaml

核对:

客户端使用的端口 = Service.spec.ports[].port
Service 转发端口 = targetPort 解析后的端口
实际后端地址     = EndpointSlice 中的 addresses
后端状态         = conditions.ready

如果端点为空,先修复标签或 readiness,不要继续修改节点 NAT。

第三步:分别测试 Pod IP 和 Service IP

kubectl exec -n demo deploy/client -- \
  curl -v --connect-timeout 3 --max-time 10 \
  http://10.244.2.17:8080/large

kubectl exec -n demo deploy/client -- \
  curl -v --connect-timeout 3 --max-time 10 \
  http://10.96.12.34:80/large

真实环境中应替换为实际地址,并确保后端端口和 HTTP 协议正确。若业务是 TLS、gRPC 或 UDP,应使用对应协议测试,而不是用 HTTP 工具替代。

第四步:按节点位置缩小 CNI 范围

kubectl get pod -n demo -o wide
kubectl get pod -n demo -l app=client -o wide

比较:

  • 同节点访问;
  • 跨节点访问;
  • 单个目标 Pod;
  • 多个后端 Pod。

如果只有某一节点上的 Pod 失败,应检查该节点的 CNI agent、路由、隧道设备、内核日志和 MTU。若所有节点都失败,应优先看 Service、策略、端点或统一配置。

第五步:检查策略和节点状态

NetworkPolicy 可能允许 DNS,但禁止业务端口;也可能允许到 ClusterIP,却因实现方式对 Pod IP 的规则不同而表现不同。检查:

kubectl get networkpolicy -A
kubectl describe networkpolicy -n demo

NetworkPolicy 的实际执行由 CNI 插件负责,Kubernetes API 只定义对象语义;插件支持范围、对 HostNetwork、Named Port、DNS 名称和集群外流量的处理可能不同。

节点状态与事件:

kubectl get nodes -o wide
kubectl describe node <node-name>
kubectl get pods -n kube-system -o wide

特别关注:

  • CNI DaemonSet 是否在异常节点运行;
  • kube-proxy 是否正常;
  • CoreDNS 是否集中在受影响节点;
  • 节点是否出现 NotReady、网络设备重建或内核错误。

第六步:抓取双向证据

在客户端 Pod、客户端节点和目标 Pod/目标节点分别抓包,至少比较:

客户端是否发送 SYN
节点是否看到 SYN
目标 Pod 是否收到 SYN
目标 Pod 是否返回 SYN-ACK
客户端是否收到 SYN-ACK
大报文是否重传
是否出现 ICMP Fragmentation Needed 或 ICMPv6 Packet Too Big

一次抓包只能说明一个观察点发生了什么;只有把多个观察点按时间和五元组关联,才能定位“报文消失”的边界。


九、按现象反推故障层

1. NXDOMAIN

通常表示名称不存在或查询的名称不对,例如:

  • Service 名称拼写错误;
  • 命名空间错误;
  • 使用短名称访问了其他命名空间;
  • 查询的是尚未创建的 Service;
  • Headless Service 的预期记录与实际状态不符。

先执行:

kubectl get svc -A | grep -w web

不要先重启 CoreDNS。NXDOMAIN 与“DNS 超时”是不同证据:前者通常有明确的否定响应,后者可能是路径或服务不可达。

2. DNS SERVFAIL 或超时

可能来自:

  • Pod 到 DNS Service 不通;
  • CoreDNS Pod 无端点或崩溃;
  • CoreDNS 无法访问上游;
  • UDP 被丢弃而 TCP fallback 也失败;
  • DNS 响应被策略或防火墙阻断;
  • 大 DNS 响应遇到 MTU 或分片问题。

需要同时检查 Pod 内 resolv.conf、CoreDNS 日志、EndpointSlice 和 53 端口抓包。

3. Connection refused

通常表示 TCP RST 到达客户端,常见原因是目标端口没有监听或应用主动拒绝。应核对:

kubectl exec -n demo <pod> -- ss -lnt

以及 Service 的 targetPort。这与静默超时的处理路径不同。

4. 连接超时

超时可能是:

  • SYN 被丢弃;
  • 返回包被丢弃;
  • CNI 或路由故障;
  • NetworkPolicy 或节点防火墙;
  • conntrack 表耗尽;
  • MTU/PMTUD 问题;
  • 外部负载均衡器或云安全组。

“超时”本身定位信息很少,必须通过抓包判断请求包和响应包分别到达了哪里。

5. 小响应成功,大响应卡住

优先检查:

  • Pod MTU 与节点/隧道 MTU;
  • TCP MSS;
  • IPv4 分片;
  • IPv6 Packet Too Big;
  • ICMP 是否被错误阻断;
  • Overlay 外层路径是否存在更小 MTU;
  • TLS 握手或应用响应是否跨越了触发阈值。

不要只用 ping 得出结论,也不要一看到重传就修改 conntrack。

6. 只有长连接或特定客户端失败

应检查:

  • Service sessionAffinity
  • IPVS 或其他实现的连接状态;
  • conntrack 超时;
  • NAT 映射;
  • 客户端连接池;
  • 后端滚动发布期间的连接排空;
  • 非对称路由。

同一个 Service 的新连接和已有连接可能走不同的状态路径,因此“重试后成功”不能证明网络已经恢复。


十、常见误区与生产边界

1. 把 DNS 当成网络连通性测试

DNS 解析成功只说明某个 DNS 服务器返回了记录,不说明:

  • ClusterIP 可达;
  • Endpoint Pod 可达;
  • 目标端口监听;
  • HTTP/TLS 协议正常。

反过来,业务使用 IP 访问成功,也不能证明业务使用的 DNS 名称正确,因为这绕过了名称解析和搜索域行为。

2. 直接修改 Service ClusterIP 或 Pod IP

ClusterIP 和 Pod IP 通常由控制面和网络插件管理。手动在节点上加路由、加 NAT 规则或修改 Pod 地址,可能只修复一个节点上的临时现象,并在 kube-proxy、CNI 或 Pod 重建后消失。应先修复声明式对象、插件状态或底层网络配置。

3. 随意重启 CoreDNS、kube-proxy 或 CNI

重启可能暂时清除缓存、重新编程规则或重建隧道,但同时会:

  • 造成 DNS 短暂不可用;
  • 触发连接重建;
  • 改变节点上的 NAT/conntrack 状态;
  • 掩盖可复现证据。

生产环境应先保留:

kubectl get svc,endpointslice,pod -A -o yaml
kubectl get events -A --sort-by=.lastTimestamp
kubectl logs -n kube-system <相关Pod>

再根据变更窗口和影响范围执行恢复动作。

4. 把实现行为当成 Kubernetes API 保证

以下内容经常被误认为固定规范:

  • iptables 的链名和规则排列;
  • IPVS 一定存在;
  • 节点一定有 VXLAN 设备;
  • DNS Service 一定使用某个固定 ClusterIP;
  • 所有 CNI 都支持相同的 NetworkPolicy 语义;
  • 所有 Service 流量都经过相同的 SNAT 路径。

这些都可能随 Kubernetes 版本、kube-proxy 模式、CNI、Linux 内核、发行版和云厂商网络实现变化。排障命令应先确认实际组件和模式,再解释输出。


十一、把一次故障记录成可验证的证据链

一份有价值的网络故障记录,不应只有“访问失败”,而应至少包含:

时间窗口:
客户端 Pod、节点:
目标名称、解析结果:
目标 Service、ClusterIP、端口:
EndpointSlice 中的实际端点:
Pod IP 直连结果:
Service IP 访问结果:
同节点/跨节点差异:
Pod 与节点 MTU:
kube-proxy 模式:
conntrack count/max 和异常统计:
客户端、节点、目标端抓包:
DNS、CNI、kube-proxy 和内核日志:
采取的恢复动作及其副作用:

例如,下面这条结论才具备可验证性:

客户端 Pod 10.244.1.10 能解析 web.demo.svc.cluster.local,
得到 ClusterIP 10.96.12.34;直接访问后端
10.244.2.17:8080 成功,但访问 ClusterIP:80 超时。
EndpointSlice 有两个 ready 端点,kube-proxy 规则存在。
客户端抓包有 SYN,节点 eth0 能看到请求,目标 Pod eth0
没有看到 SYN。故障边界位于目标节点到 Pod 的转发路径,
优先检查该节点 CNI 路由、NetworkPolicy 和 Service 规则命中,
而不是检查应用监听。

这类描述把 DNS、Service、CNI、MTU、conntrack 和抓包放回同一条数据流中:先确定名称对应的地址,再验证 Service 后端,再比较真实路径,最后用不同网络命名空间和节点接口的报文证据确定丢失位置。只有完成这条链路,才有足够依据选择修改 Service、修复 CNI、调整 MTU、处理 conntrack,或转向外部负载均衡和底层网络。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。