双栈 Service 配置完:IPv6 秒通、IPv4 每次等 5 秒——ipFamilies 的主从陷阱

场景:Service 配置 ipFamilyPolicy: PreferDualStack 后,IPv6 客户端秒通、IPv4 客户端每次等 5 秒才连上——不是 CNI 不支持双栈,是 ipFamilies 的书写顺序把 IPv4 降级成了"备选地址" 路径:坐标(Service 双栈配置 + 症状)→ 分层(Service → DNS → kube-proxy → 集群配置)→ 路径(3 条排查命令)→ 定位(ipFamilies 顺序决定主地址族)→ 标点(修复 + Check-list) K8s 版本:v1.28,kube-proxy iptables 模式

上篇讲了换了 Cilium 以后排查路径要跟着 CNI 一起升级——传统 iptables 工具链在 eBPF 环境下什么都看不到。这次的问题方向相反:工具链没变,但 Service 配置变了,排查路径同样需要调整。

一个 Service 加了 ipFamilies: [IPv6, IPv4],IPv6 客户端秒连、IPv4 客户端永远 Connection Timeout——"CNI 不支持双栈吧?""kube-proxy 没开 dual-stack?"都不是。K8s 按 ipFamilies 的顺序选了一个协议族作为 ClusterIP 的主地址族,而你的网络只准备好了一半。


金句:双栈不是让两个协议族平等——是 K8s 选了一个当"主地址",另一个当"备选"。你写错了顺序,IPv4 就成了二等公民。


【坐标】双栈 Service:IPv6 通、IPv4 不通

Layer 1 — 部署背景

部署项 配置
消费方 Pod B,web-frontend,Namespace prod,10.244.1.15
目标服务 order-svc.prod:8080,HTTP
Service 类型 ClusterIP,ipFamilyPolicy: PreferDualStack
网络插件 Calico v3.26(双栈模式)
集群版本 K8s v1.28,kube-proxy 模式 iptables
集群 IPv4 范围 --service-cluster-ip-range=10.96.0.0/12
集群 IPv6 范围 --service-cluster-ip-range=2001:db8:1::/48

Service YAML:

apiVersion: v1
kind: Service
metadata:
  name: order-svc
  namespace: prod
spec:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
  - IPv6
  - IPv4
  selector:
    app: order
  ports:
  - port: 8080
    targetPort: 8080

Service YAML ipFamilies 顺序

Layer 2 — 症状:IPv6 秒通,IPv4 永远超时

部署后测试:

# Pod A(有 IPv6 地址)→ 秒通
kubectl exec -n prod pod-a -- curl -m 5 http://order-svc:8080/health
# → HTTP 200,响应 < 100ms

# Pod B(IPv4 地址,无 IPv6 路由)→ 超时
kubectl exec -n prod pod-b -- curl -m 5 http://order-svc:8080/health
# → curl: (28) Connection timeout after 5000ms

# 但 Pod B 用 IPv4 ClusterIP 直连 → 秒通
kubectl exec -n prod pod-b -- curl -m 5 http://10.96.1.50:8080/health
# → HTTP 200,响应 < 100ms

症状:IPv6 通 IPv4 不通

规律很明显:走 DNS 名称解析的通(Pod A)、不通(Pod B);用 IPv4 ClusterIP 直连的反而通。 说明不是 kube-proxy 没配好规则,是 DNS 解析回来的地址本身有问题。

Layer 3 — 误判引出

看到这个现象,第一反应往往是"CNI 不支持双栈"或者"kube-proxy 的 IPv6DualStack 特性门没开"。但 Calico v3.26 明确支持双栈,kube-proxy 的 iptables 模式在 K8s v1.28 默认开启 dual-stack。不是组件没开——是配置选错了主地址族。

我刚入行时同事查了三天这类问题,换了 CNI、重启了 kube-proxy、甚至重装了集群——最后发现是 YAML 里两行配置的顺序写反了。


【分层】Pod → DNS → kube-proxy → 集群配置,逐层排除

Layer 1 — 结论前置

Pod 本身没问题,kube-proxy 的转发规则也在——问题出在 ipFamilies 的顺序决定了 Service 的主 ClusterIP 是什么族,而 DNS 优先返回的就是这个主地址。

Layer 2 — 逐层排查

第 1 层(Pod 层):Pod 正常运行

kubectl get pods -n prod -l app=web-frontend
# NAME                            READY   STATUS    RESTARTS   AGE
# web-frontend-6b8f4d8d7f-abc12   1/1     Running   0          3d

kubectl logs -n prod web-frontend-6b8f4d8d7f-abc12
# 日志无异常,应用正常启动

Pod 层确认没问题——不是 Pod 挂了或应用报错。

第 2 层(DNS 层):解析返回两个地址,但优先走 IPv6

kubectl exec -n prod pod-b -- nslookup order-svc.prod 2>/dev/null
# Server:    10.96.0.10
# Address 1: 2001:db8:1::a  order-svc.prod.svc.cluster.local
# Address 2: 10.96.1.50     order-svc.prod.svc.cluster.local

DNS 解析优先返回 IPv6

CoreDNS 同时返回了 AAAA(IPv6)和 A(IPv4)记录,但 Address 1 是 IPv6。大部分操作系统和运行时库优先连接第一个返回的地址(Happy Eyeballs 的 RFC 8305 也默认先尝试第一个)。Pod B 尝试连接 2001:db8:1::a,但这个 IPv6 地址在跨节点间不通——不是 K8s 层面不通,是底层网络不路由 IPv6 流量。

第 3 层(kube-proxy 层):IPv4 和 IPv6 的 iptables 规则都在

# 查 Service 的 IPv4 规则
iptables-save -t nat | grep order-svc
# -A KUBE-SVC-XXXXX -d 10.96.1.50/32 -p tcp -m tcp --dport 8080 -j KUBE-SEP-YYYYY
# -A KUBE-SEP-YYYYY -p tcp -m tcp --dport 8080 -j DNAT --to-destination 10.244.2.10:8080

# 查 IPv6 规则(确认也是存在的)
ip6tables-save -t nat | grep order-svc
# -A KUBE-SVC-XXXXX -d 2001:db8:1::a/32 -p tcp -m tcp --dport 8080 -j KUBE-SEP-YYYYY

iptables 双栈规则

kube-proxy 为两个协议族都创建了 DNAT 规则——不是 kube-proxy 的问题。

第 4 层(集群配置层):问题在 ipFamilies 顺序

kubectl get svc -n prod order-svc -o yaml | grep -A 5 ipFamily
#  ipFamilies:
#  - IPv6
#  - IPv4
#  ipFamilyPolicy: PreferDualStack

# 确认主 ClusterIP
kubectl get svc -n prod order-svc -o jsonpath='{.spec.clusterIP}'
# 2001:db8:1::a

ipFamilies 检查

spec.clusterIP(单数,主地址)是 IPv6 地址 2001:db8:1::a,因为 ipFamilies 列表的第一个元素是 IPv6。这个主地址被 CoreDNS 作为第一个返回的解析结果,客户端优先尝试——但底层 VPC 网络并不路由 IPv6 流量。

Layer 3 — 排查顺序表

层级 命令 预期输出 结论
Pod kubectl logs <pod> 应用正常 ✅ Pod 没问题
DNS nslookup <svc> 返回 AAAA+A ✅ 解析正常,但 IPv6 排第一
kube-proxy iptables-save \| grep <svc> DNAT 规则存在 ✅ 双栈规则都在
集群配置 kubectl get svc -o yaml \| grep ipFamily [IPv6, IPv4] ❌ primary 是 IPv6

【路径】🔍 下次先查 Service 的 ipFamilies

看到"双栈 Service 一边通一边不通",第一件事不是怀疑 CNI 或 kube-proxy——是先看 ipFamilies 的顺序:

# 查主地址族
kubectl get svc <name> -n <ns> -o jsonpath='{.spec.ipFamilies}'
# [IPv6 IPv4]  → IPv6 是 primary
# [IPv4 IPv6]  → IPv4 是 primary

# 查主 ClusterIP
kubectl get svc <name> -n <ns> -o jsonpath='{.spec.clusterIP}'
# 2001:db8::a  → IPv6 是 primary
# 10.96.1.50   → IPv4 是 primary

判断标准:如果 ClusterIP 是 IPv6 地址,但你的底层网络(VPC/underlay)不路由 IPv6 → 这就是根因。

底层网络验证:双栈 Service 不通最隐蔽的根因不是 K8s 配置,是底层网络不支持某个协议族的实际路由。在 Node 上 ping -c 3 -6 <other-node-ip>,如果 connect: Network is unreachable → IPv6 路由不完整,需排查 VPC/underlay。

双栈拓扑:IPv6 在 VPC 层断开


【定位】ipFamilies 的顺序决定了谁是主地址族

Layer 1 — 结论前置

不是 CNI 不支持双栈,不是 kube-proxy 没配好——是 ipFamilies 列表的第一个元素决定了 Service 的主 ClusterIP 地址族,而这个地址被 DNS 作为首选返回。

Layer 2 — 原因拆解

K8s 的行为逻辑:

当 Service 的 ipFamilyPolicy 不是 SingleStack 时,K8s 会分配两个 ClusterIP(每个协议族一个)。但 .spec.clusterIP(单数)只存第一个协议族的地址——这个字段被 CoreDNS、kubelet 等组件用来确定"主地址"。

# 当你写:
ipFamilies:
- IPv6     # ← 第一个 = primary
- IPv4     # ← 第二个 = secondary

# 效果:
spec:
  clusterIP: 2001:db8:1::a    # ← primary(IPv6)
  clusterIPs:                   # ← 全部
  - 2001:db8:1::a
  - 10.96.1.50

为什么 IPv4 不通:

  1. CoreDNS 返回的 Address 1 是 IPv6(因为它是 primary)
  2. 客户端优先尝试 IPv6 地址——2001:db8:1::a:8080
  3. SYN 包离开节点 → 底层 VPC 不路由 IPv6 → 丢包
  4. Happy Eyeballs 等待 3-10 秒后回退到 IPv4
  5. IPv4 连接成功——但每次请求都有 3+ 秒延迟

本质:不是 IPv4 不通——是 IPv4 被降级成了"备选"地址,客户端每次都先试 IPv6 才回退到 IPv4。

Layer 3 — 谁来决定的

这个过程由 kube-controller-manager 的 service controller 执行(基于 K8s v1.23+ dual-stack GA 实现,v1.23 之前 dual-stack 为 alpha 行为不同)。当创建或更新 Service 时,controller 按 ipFamilies 的顺序分配 ClusterIP:

// K8s 源码逻辑(简化)
func allocateClusterIPs(service *v1.Service, ipFamilies []v1.IPFamily) {
    for i, family := range ipFamilies {
        ip := allocator.Allocate(family)  // 按顺序分配
        if i == 0 {
            service.Spec.ClusterIP = ip   // 第一个 = primary
        }
        service.Spec.ClusterIPs = append(service.Spec.ClusterIPs, ip)
    }
}

印证:从源码看,clusterIP 的赋值就是 ipFamilies[0] 分配的地址。没有魔法——就是谁在前面谁做主。

Layer 4 — 错误 vs 正确排查

❌ 错误排查 ✅ 正确排查
怀疑对象 CNI 不支持双栈、kube-proxy 没开特性门 ipFamilies 顺序 + 底层网络能力
检查工具 重装 Calico、调 kube-proxy 参数 kubectl get svc -o jsonpath='{.spec.ipFamilies}'
修复方向 修改 CNI 配置 交换 ipFamilies 顺序或显式指定 SingleStack

【标点】修复 + Check-list + 命令清单

Layer 1 — 修复方案

方案 A:交换 ipFamilies 顺序(推荐)

修复 diff:交换 ipFamilies 顺序

# before
spec:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
  - IPv6
  - IPv4

# after
spec:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
  - IPv4     # ← 调换顺序,IPv4 成为 primary
  - IPv6     # ← IPv6 作为 secondary

⚠️ 注意:ipFamilies 对已有 Service 是 immutable(不可变字段)。修改需要删除 Service 重建。重建期间 Service 不可用——请在维护窗口操作。

方案 B:如果不需要双栈,降级到 SingleStack

spec:
  ipFamilyPolicy: SingleStack
  ipFamilies:
  - IPv4

Layer 2 — 上线前确认清单

在创建双栈 Service 之前,逐项确认底层网络就绪:

  • [ ] kubectl get nodes -o jsonpath='{.items[*].spec.podCIDRs}' → 确认节点有双栈 CIDR(应包含 IPv4 和 IPv6 两段)
  • [ ] 在任一节点上 ping -c 3 -6 <其他节点 IP> → 确认 IPv6 跨节点路由通
  • [ ] ip6tables-save -t nat | head -5 → 确认 ip6tables 可用,kube-proxy 已为 IPv6 创建规则
  • [ ] 确定 ipFamilies 顺序:先写你底层网络最稳定的协议族

Layer 3 — 故障排查 Check-list

新部署双栈 Service 后发现不通时,逐项排查:

  • [ ] kubectl get svc <name> -o jsonpath='{.spec.clusterIP}' → 确认主地址族是你期望的
  • [ ] kubectl exec <pod> -- curl -4 http://<svc>:<port> → IPv4 直连正常
  • [ ] kubectl exec <pod> -- curl -6 http://<svc>:<port> → IPv6 直连正常
  • [ ] kubectl exec <pod> -- nslookup <svc> → 检查 DNS 返回的地址顺序,Address 1 应该是网络实际支持的协议族
  • [ ] 跨节点验证:Pod 分布在两个不同 Node 上,分别测试
  • [ ] 底层网络确认:ping -c 3 -6 <其它 Node IP> → 确认底层 VPC 确实路由 IPv6

Layer 4 — 完整命令清单

# 查 Service ipFamilies 顺序
kubectl get svc <name> -n <ns> -o jsonpath='{.spec.ipFamilies}'

# 查主 ClusterIP
kubectl get svc <name> -n <ns> -o jsonpath='{.spec.clusterIP}'

# 分别测试 IPv4 和 IPv6
kubectl exec -it <pod> -n <ns> -- curl -4 -m 3 http://<svc>:<port>/health
kubectl exec -it <pod> -n <ns> -- curl -6 -m 3 http://<svc>:<port>/health

# 查 kube-proxy 双栈规则
iptables-save -t nat | grep <svc-name>
ip6tables-save -t nat | grep <svc-name>

# 查 CoreDNS 解析顺序
kubectl exec -it <pod> -n <ns> -- nslookup <svc>.<ns>

# 查 Service YAML 完整配置
kubectl get svc <name> -n <ns> -o yaml

# 查节点双栈 CIDR 配置
kubectl get nodes -o jsonpath='{.items[*].spec.podCIDRs}'

下篇我们来聊双栈场景的第二个常见坑:CNI 双栈模式下 NetworkPolicy 只写了 IPv4 规则,IPv6 流量直接绕过策略控制。


📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」