NNetpath
原理预计 35 分钟

Service:一个不存在的 IP 是怎么工作的

ClusterIP ping 不通却能访问。把这件事讲透,Service 的四种类型就都通了。

学完这节你能做到

  • 解释 ClusterIP 为什么 ping 不通,以及 DNAT 发生在哪一侧
  • 区分 ClusterIP、NodePort、LoadBalancer、Headless 的适用场景
  • 说清 externalTrafficPolicy 与源 IP 保留之间的取舍

建议先学跳过这几节会看不懂本节的部分推导

从一个奇怪的现象开始

$ kubectl get svc web
NAME   TYPE        CLUSTER-IP     PORT(S)
web    ClusterIP   10.96.132.41   80/TCP

$ ping 10.96.132.41
PING 10.96.132.41: 56 data bytes
^C  --- 100% packet loss

$ curl 10.96.132.41
<html>...   # 却能访问

ping 不通但能访问,这不是故障。ClusterIP 不是任何网卡上的地址, 没有任何设备会响应它的 ICMP。它只是一条规则的索引

Service 的本质:一条分布式的 DNAT 规则。每个节点上都装着同一份"看到这个 VIP 就改写成某个 Pod IP"的规则。

转换发生在客户端一侧

这是最关键、也最容易搞错的一点:

客户端 Pod ──► [ 本节点完成 DNAT ] ──► 后端 Pod IP ──► 网络 ──► 后端 Pod
              ↑
        包在这里就已经不带 ClusterIP 了

推论:在后端 Pod 里抓包,永远抓不到 ClusterIP。新人经常在服务端 tcpdump 找 VIP, 找不到就以为流量没到——其实早就到了,只是目的地址已经被改写。

三种实现方式,改写发生的位置不同:

模式怎么改写特点
iptables在转发路径上按规则链匹配Service 多了会退化
IPVS内核里的哈希表查找规模友好,支持多种调度算法
eBPF 替换connect() 的 socket 层就改掉目的地址包根本没进转发路径

eBPF 模式最彻底:应用以为自己连的是 VIP,实际内核在建连时就把地址换了, 后续的包一路都是 Pod IP,连一次 DNAT 都省了

四种类型,四种用途

ClusterIP —— 集群内默认

给一组 Pod 一个稳定的虚拟地址。Pod 换 IP、扩缩容、重启都不影响这个地址。

Headless(clusterIP: None)—— 把 Pod IP 直接给你

不分配 VIP,DNS 查询直接返回全部 Pod IP。适合客户端自己管连接的场景:

  • 数据库集群(客户端需要区分主从)
  • Kafka / Zookeeper 这类要连特定成员的中间件
  • StatefulSet 里需要按序号访问某个副本
# ClusterIP 类型:返回一个 VIP
nslookup web.default.svc.cluster.local     # → 10.96.132.41

# Headless 类型:返回所有 Pod IP
nslookup db.default.svc.cluster.local      # → 10.244.1.7, 10.244.2.9, 10.244.3.4

NodePort —— 在每个节点占一个静态端口

构建在 ClusterIP 之上,在宿主机的 root netns 里开一个端口(默认 30000–32767), 访问任意节点的这个端口都会被转发到后端 Pod。

问题:端口号是随机高位端口,需要外部记住"哪个服务是哪个端口",且节点 IP 变化时客户端要改。 通常只用于测试或作为 LoadBalancer 的底座。

LoadBalancer —— 一个可路由的 IP

云上由云厂商的 L4 负载均衡器提供。裸金属集群没有这个东西,要靠 MetalLB 自己实现。

MetalLB 从预留的地址池里分配一个 IP,然后用两种方式把它宣告出去:

模式怎么宣告效果
L2某个 speaker 用 gARP 声称"这个 IP 归我"单节点承载全部流量,只是高可用,不是负载均衡
BGP多个节点把同一个 /32 宣告给上游路由器ECMP 真正分担流量

这里只讲到"有这两种模式"。gARP 的宣告与抢占细节、BGP 的对等配置与 ECMP、 以及地址池规划和完整排障流程,见下一组的 MetalLB:裸金属集群的 LoadBalancer

# 地址池要从机房网络组要一段空闲的可路由地址
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default
  namespace: metallb-system
spec:
  addresses:
    - 172.18.15.200-172.18.15.210
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
    - default
!L2 模式下的 VIP 只在一个节点上

很多人以为配了 LoadBalancer 就有负载均衡了。L2 模式下 VIP 只落在一个 speaker 节点上, 所有流量先进那台机器,再由 Service 转发到各 Pod。单节点带宽就是这个服务的上限。

查 VIP 当前在哪个节点,三种办法:

# 新版本 MetalLB 直接有 CRD
kubectl get servicel2statuses.metallb.io -n metallb-system

# 或者从 speaker 日志里找
kubectl logs -n metallb-system <speaker-pod> | grep serviceAnnounced | grep <vip>

# 或者用 ARP 反查
arping <vip>          # 拿到 MAC
arp -n | grep <mac>   # 反查出物理节点 IP

externalTrafficPolicy:源 IP 与均衡的取舍

取值源 IP均衡代价
Cluster(默认)被 SNAT 成节点 IP,后端看不到真实客户端全集群 Pod 都参与多一跳转发
Local保留真实客户端 IP只转发给本节点的 Pod后端分布不均时流量倾斜

Local 还有一个连带效果:只有跑着后端 Pod 的节点才会宣告 VIP。 所以如果某个节点上没有该服务的 Pod,它就不参与承载——这既是特性也是坑, 后端 Pod 分布不均时会造成明显的流量倾斜。

×改成 Local 之后 VIP 突然不通了

如果 speaker 所在节点上恰好没有该服务的后端 Pod,它不会宣告这个 VIP。 再加上另一个常见问题:节点带了 node.kubernetes.io/exclude-from-external-load-balancers 标签时 speaker 也不宣告。两种情况都表现为"VIP 完全不通",但和网络链路毫无关系。

走一遍完整路径

左边是集群内访问 ClusterIP,右边是集群外经 LoadBalancer 进来。对照看两条路径的差别:

路径推演一个包经过的每一跳

ClusterIP ping 不通却能连上 —— 因为它不是一个真的地址。

5延迟量级 百微秒级
这条路径要记住的一句话

DNAT 发生在客户端所在节点,出了那台机器包上就没有 ClusterIP 了。所以在服务端抓包永远抓不到 VIP。

排障顺序

Service 不通时按这个顺序查,每一步都能排除一大块:

# 1. DNS 解析成功了吗(集群里一半的"网络故障"止步于此)
kubectl exec -it client -- nslookup web.default

# 2. Endpoint 里有 Pod 吗(就绪探针全挂 = Endpoint 为空 = 连接直接被拒)
kubectl get endpointslices -l kubernetes.io/service-name=web

# 3. selector 和 Pod 标签真的匹配吗
kubectl get svc web -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels | grep web

# 4. 规则装上了吗
cilium-dbg service list | grep 10.96.132.41
# 或 iptables 模式:iptables-save | grep web
# 或 IPVS 模式:ipvsadm -Ln | grep -A3 10.96.132.41

# 5. conntrack 是否撑不住(表现为新连接随机失败)
conntrack -S | head
Endpoint 为空是最高频的原因

kubectl get endpointslices 返回空,意味着没有任何 Pod 被认为就绪。 这时候连接会立刻被拒绝(不是超时),错误信息是 connection refused。 根因往往在 readinessProbe 配错,而不在网络。

检查点

检查点单选

在后端 Pod 里 tcpdump 抓不到 ClusterIP 的包,但服务明显在正常工作。为什么?

检查点单选

需要客户端自己感知集群成员(比如连数据库主节点),应该用哪种 Service?

检查点单选

裸金属集群把某个 Service 的 externalTrafficPolicy 改成 Local 后,VIP 变得完全不通。最可能的原因?

这节课的落点

  • ClusterIP ping 不通是正常的:它不是网卡地址,只是一条 DNAT 规则的索引
  • DNAT 发生在客户端所在节点,所以后端抓不到 VIP
  • 四种类型:ClusterIP(集群内)、Headless(客户端自管)、NodePort(静态端口)、LoadBalancer(可路由 IP)
  • 裸金属的 LoadBalancer 靠 MetalLB:L2 模式是高可用,BGP 模式才是负载均衡
  • externalTrafficPolicy: Local 换来真实源 IP,代价是宣告节点受限、流量可能倾斜
  • 排障顺序:DNS → Endpoint → selector → 转发规则 → conntrack

延伸资料