土法炼钢兴趣小组的算法知识备份

IPv4/IPv6 双栈:K8s 的地址空间演化

目录

你的集群跑了三年,Pod CIDR 早就从 /16 裁到 /20,地址快用完了。运维在群里喊”加 IPv6 吧”,你点头同意,然后发现事情远没有 --service-cluster-ip-range 后面多写一段 CIDR 那么简单:CNI 要不要升级?Service 怎么分配两个 ClusterIP?DNS 返回 AAAA 记录后客户端是走 v4 还是 v6?cloud-provider 的 LoadBalancer 能不能同时监听两个地址族?

本文从 Kubernetes 双栈特性的演化历程讲起,拆解 Pod 双栈地址分配的内部流程,梳理 Service ipFamilyPolicy 的三种取值,对比主流 CNI 的双栈实现差异,最后用 kind 搭建双栈集群做端到端验证。

本文基于 Kubernetes 1.29 / 1.30。双栈特性自 1.23 GA,本文不再讨论 feature gate 开关。 实验环境:Ubuntu 22.04, Docker 24.x, kind v0.22, kubectl 1.29。


一、双栈特性的演化:从 alpha 到 GA

Kubernetes 的 IPv6 时间线

Kubernetes 对 IPv6 的支持并不是一蹴而就的。在早期版本中,集群要么纯 IPv4,要么纯 IPv6(且后者几乎没有人在生产环境使用)。真正意义上的”双栈”——一个 Pod 同时拥有 IPv4 和 IPv6 地址——经历了漫长的孵化期。

版本 阶段 关键变化
1.16 Alpha 引入 IPv6DualStack feature gate,默认关闭。Pod 可以拿到两个 IP,但 Service 只支持单栈。
1.17-1.19 Alpha 迭代 修复若干 kube-proxy 双栈 iptables/ipvs 规则生成 bug;CNI 规范开始讨论多 IP 返回。
1.20 Alpha (重构) 完全重写 Service 双栈实现:引入 ipFamiliesipFamilyPolicy 字段。旧的 ipFamily 字段被废弃。
1.21 Beta feature gate 默认开启。kube-proxy 同时维护 iptables + ip6tables 规则集。
1.22 Beta 迭代 EndpointSlice 原生支持双栈地址。kubelet 上报 status.podIPs[]
1.23 GA IPv6DualStack feature gate 锁定为 true 并在后续版本中移除。双栈成为内建能力。

架构层面的影响

双栈并不只是”给 Pod 多加一个 IP”这么简单。它对集群的多个子系统产生了连锁影响:

  1. kube-apiserver--service-cluster-ip-range 接受逗号分隔的两段 CIDR(先 v4 后 v6 或反之,顺序决定”主地址族”)。
  2. kube-controller-manager--cluster-cidr 同样接受双段 CIDR。NodeIPAM controller 为每个 Node 分配两个 PodCIDR。
  3. kube-proxy:同时运行 iptables + ip6tables(或 ipvs 的 IPv4/IPv6 virtual server)。规则量翻倍。
  4. kubelet:上报 node.status.addresses[] 中的 IPv4 和 IPv6 地址;pod.status.podIPs[] 包含两条记录。
  5. CNI 插件cmdAdd() 必须在 Result.IPs 中返回两个 IP 条目。
  6. CoreDNS:为同一个 Service 同时生成 A 和 AAAA 记录。

二、双栈 Pod 的地址分配

整体流程

双栈 Pod 的地址分配可以用下面这张图概括:

双栈 Pod 地址分配流程

流程的核心环节如下:

  1. kube-apiserver 接受 PodSpec,写入 etcd。
  2. Scheduler 将 Pod 绑定到某个 Node。
  3. 该 Node 上的 kubelet 监听到 Pod 事件,创建 pause 容器和 network namespace。
  4. kubelet 调用 CNI 插件的 cmdAdd()
  5. CNI 插件向 IPAM 后端请求地址——在双栈场景下,会分别从 IPv4 Pool 和 IPv6 Pool 各取一个地址。
  6. CNI 插件将两个 IP 配置到 Pod 的 eth0 上,设置两套路由,返回 Result
  7. kubelet 将 Result 中的 IP 写入 pod.status.podIP(主 IP)和 pod.status.podIPs[](全部 IP)。

两个 IP

在双栈集群中,Pod 的 eth0 接口上会同时配置 IPv4 和 IPv6 地址:

# 在 Pod 内部执行
$ ip -4 addr show eth0
    inet 10.244.1.5/24 brd 10.244.1.255 scope global eth0

$ ip -6 addr show eth0
    inet6 fd00:10:244:1::5/128 scope global
    inet6 fe80::a8f3:c1ff:fe2b:7d01/64 scope link

注意:IPv6 通常还会有一个 fe80:: 的链路本地地址,这是内核自动生成的,不计入 IPAM 分配。

两条路由

CNI 插件会分别设置 IPv4 和 IPv6 的默认路由:

$ ip -4 route
default via 10.244.1.1 dev eth0
10.244.1.0/24 dev eth0 proto kernel scope link src 10.244.1.5

$ ip -6 route
default via fe80::1 dev eth0
fd00:10:244:1::/64 dev eth0 proto kernel metric 256
fe80::/64 dev eth0 proto kernel metric 256

IPv4 的网关通常是 Node 上 cni0 或 veth 对端的地址;IPv6 的网关通常是 fe80::1(由 CNI 插件硬编码或通过 NDP Router Advertisement 下发)。

两套 iptables 规则

kube-proxy 在双栈模式下同时维护 iptables(IPv4)和 ip6tables(IPv6)的规则链。以一个 ClusterIP Service 为例:

# IPv4 规则
$ iptables -t nat -L KUBE-SERVICES | grep my-svc
-A KUBE-SERVICES -d 10.96.0.100/32 -p tcp --dport 80 -j KUBE-SVC-XXXXXX

# IPv6 规则
$ ip6tables -t nat -L KUBE-SERVICES | grep my-svc
-A KUBE-SERVICES -d fd00::96:100/128 -p tcp --dport 80 -j KUBE-SVC-YYYYYY

如果使用 IPVS 模式,kube-proxy 会为同一个 Service 创建两个 virtual server——一个绑定在 IPv4 ClusterIP 上,一个绑定在 IPv6 ClusterIP 上:

$ ipvsadm -Ln | grep -A2 10.96.0.100
TCP  10.96.0.100:80 rr
  -> 10.244.1.5:80         Masq    1      0      0

$ ipvsadm -Ln | grep -A2 fd00::96:100
TCP  [fd00::96:100]:80 rr
  -> [fd00:10:244:1::5]:80 Masq    1      0      0

kubelet 上报的 Pod 状态

status:
  podIP: "10.244.1.5"            # 主 IP,与 Node 的主地址族一致
  podIPs:
    - ip: "10.244.1.5"
    - ip: "fd00:10:244:1::5"
  hostIP: "192.168.1.10"
  hostIPs:
    - ip: "192.168.1.10"
    - ip: "fd00::192:168:1:10"

podIPpodIPs[0] 永远相同。主地址族由集群的 --service-cluster-ip-range 参数中第一段 CIDR 决定。如果第一段是 IPv4,则 podIP 就是 IPv4 地址。


三、双栈 Service:ipFamilyPolicy 的三种取值

字段定义

从 1.20 开始,Service 使用两个新字段来声明双栈行为:

apiVersion: v1
kind: Service
metadata:
  name: my-svc
spec:
  ipFamilyPolicy: PreferDualStack    # 关键字段
  ipFamilies:                        # 地址族顺序
    - IPv4
    - IPv6
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

三种策略

ipFamilyPolicy 行为 分配的 ClusterIP 数量
SingleStack 只分配一个地址族的 ClusterIP。使用 ipFamilies[0] 指定的族,或者默认为集群主地址族。 1
PreferDualStack 尽量分配两个 ClusterIP。如果集群不支持双栈(比如只配了单段 CIDR),退化为单栈,不报错。 1 或 2
RequireDualStack 必须分配两个 ClusterIP。如果集群不支持双栈,创建 Service 会失败。 2

实际效果

# 创建一个 PreferDualStack Service
$ kubectl get svc my-svc -o jsonpath='{.spec.clusterIPs}'
["10.96.0.100","fd00::96:100"]

$ kubectl get svc my-svc -o jsonpath='{.spec.ipFamilies}'
["IPv4","IPv6"]

ipFamilies 的顺序很重要

ipFamilies 数组的第一个元素决定了:

如果你希望 Service 优先通过 IPv6 对外提供服务:

spec:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv6
    - IPv4

已有 Service 的升级

对于在双栈 GA 之前创建的 Service,Kubernetes 会在升级过程中自动将它们标记为 SingleStack。你可以通过 kubectl editkubectl patch 将其改为 PreferDualStack

$ kubectl patch svc my-legacy-svc -p '{"spec":{"ipFamilyPolicy":"PreferDualStack"}}'

注意:从 SingleStack 切换到 PreferDualStack 不会更改已分配的主 ClusterIP,只会追加第二个地址族的 ClusterIP。

Headless Service 的双栈行为

Headless Service(clusterIP: None)同样支持 ipFamilyPolicy

spec:
  clusterIP: None
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6

此时 CoreDNS 会为每个 Endpoint 同时返回 A 和 AAAA 记录。客户端拿到多条记录后的行为取决于其自身的地址选择算法。


四、CNI 插件的双栈支持现状

不同 CNI 插件对双栈的支持程度和实现方式存在差异。以下对比三个主流插件。

Calico

Calico 从较早版本就支持双栈。其关键配置点:

# calico IPPool 资源(IPv4)
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: default-ipv4-pool
spec:
  cidr: 10.244.0.0/16
  encapsulation: VXLAN
  natOutgoing: true
  nodeSelector: all()

---
# calico IPPool 资源(IPv6)
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: default-ipv6-pool
spec:
  cidr: fd00:10:244::/48
  encapsulation: None         # IPv6 VXLAN 支持有限
  natOutgoing: false           # IPv6 通常不做 NAT
  nodeSelector: all()

关键细节

Cilium

Cilium 对双栈的支持依赖其 eBPF 数据平面,实现相对优雅:

# Helm values 摘要
ipam:
  mode: cluster-pool
  operator:
    clusterPoolIPv4PodCIDRList:
      - "10.244.0.0/16"
    clusterPoolIPv4MaskSize: 24
    clusterPoolIPv6PodCIDRList:
      - "fd00:10:244::/48"
    clusterPoolIPv6MaskSize: 64
ipv4:
  enabled: true
ipv6:
  enabled: true
enableIPv6Masquerade: false     # 同样默认不做 IPv6 NAT

关键细节

Flannel

Flannel 对双栈的支持相对基础:

{
  "Network": "10.244.0.0/16",
  "IPv6Network": "fd00:10:244::/48",
  "Backend": {
    "Type": "vxlan"
  }
}

关键细节

对比总结

特性 Calico Cilium Flannel
IPv6 封装 BGP 直连推荐;VXLAN 有限 eBPF 原生;Geneve/VXLAN 可选 VXLAN(双 VTEP)
IPv6 NAT 默认关闭,可配置 默认关闭,可配置 依赖 kube-proxy
NetworkPolicy IPv6 支持(Felix ip6tables) 支持(eBPF) 不支持
IPAM 模式 CRD-based IPPool cluster-pool / CRD Node PodCIDR
双栈成熟度

五、现实中的双栈陷阱

双栈的”理论”很美好,但在生产环境中,有若干你必须知道的陷阱。

DNS 返回 AAAA 优先导致的延迟问题

当 CoreDNS 为一个双栈 Service 同时返回 A(IPv4)和 AAAA(IPv6)记录时,客户端的行为取决于其 DNS resolver 和 socket 库的实现。

问题场景

  1. Pod A 访问 my-svc.default.svc.cluster.local
  2. CoreDNS 返回 A 记录 10.96.0.100 和 AAAA 记录 fd00::96:100
  3. Pod A 的 glibc 默认优先尝试 AAAA 记录(IPv6 优先)。
  4. 如果 IPv6 路径有问题(比如某跳路由器不转发 IPv6、或者 MTU 不匹配导致大包丢失),连接会挂起直到超时(通常 30-75 秒)。
  5. 超时后才回退到 IPv4,用户感知到的延迟是灾难性的。

Happy Eyeballs(RFC 8305)

现代客户端应该实现 Happy Eyeballs 算法:同时发起 IPv4 和 IPv6 连接,谁先成功用谁,通常给 IPv6 250ms 的先发优势。但是:

缓解措施

# 方案一:在 Service 层面控制——只暴露 IPv4
spec:
  ipFamilyPolicy: SingleStack
  ipFamilies:
    - IPv4

# 方案二:在 CoreDNS 配置中过滤 AAAA 记录(不推荐,但应急可用)
# Corefile 片段
template IN AAAA {
    rcode NOERROR
}

# 方案三:在 Pod 层面配置 gai.conf
# /etc/gai.conf
precedence ::ffff:0:0/96  100    # 优先选择 IPv4-mapped 地址

建议:在开启双栈之前,确保集群内 IPv6 的端到端连通性已经验证通过。不要在 IPv6 路径不通的情况下暴露 AAAA 记录。

某些 CNI 的 IPv6 SNAT 行为不一致

IPv4 世界中,Pod 访问集群外部地址时,节点会对源地址做 SNAT/MASQUERADE(将 Pod IP 替换为 Node IP)。但在 IPv6 中,各 CNI 的行为并不统一:

CNI IPv6 SNAT 默认行为
Calico 关闭(natOutgoing: false)。Pod 的 IPv6 地址直接暴露给外部。
Cilium 关闭(enableIPv6Masquerade: false)。与 Calico 类似。
Flannel 取决于 kube-proxy 的 --cluster-cidr 配置。如果 kube-proxy 不知道 IPv6 CIDR,MASQUERADE 规则可能缺失。

问题场景

缓解措施

# Calico:启用 IPv6 natOutgoing
$ calicoctl patch ippool default-ipv6-pool \
    -p '{"spec":{"natOutgoing":true}}'

# Cilium:Helm values 中启用
enableIPv6Masquerade: true

# 手动添加 ip6tables 规则(应急)
$ ip6tables -t nat -A POSTROUTING \
    -s fd00:10:244::/48 \
    ! -d fd00:10:244::/48 \
    -j MASQUERADE

cloud-provider LoadBalancer 的双栈支持差异

不同云厂商对双栈 LoadBalancer 的支持程度参差不齐:

云厂商 IPv6 LB 支持 双栈 LB 支持 注意事项
AWS NLB/ALB 支持 IPv6 NLB 支持 dualstack 需要 VPC 开启 IPv6;service.beta.kubernetes.io/aws-load-balancer-ip-address-type: dualstack
GCP 支持 IPv6 支持双栈 需要 VPC dual-stack subnet;Internal LB 双栈支持有限
Azure 支持 IPv6 支持双栈 需要 dual-stack VNet;Standard SKU LB
阿里云 部分支持 有限 CLB 不支持 IPv6,ALB 支持;需要开通 IPv6 网关

常见问题

apiVersion: v1
kind: Service
metadata:
  name: my-lb-svc
  annotations:
    # AWS NLB 双栈配置
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
    service.beta.kubernetes.io/aws-load-balancer-ip-address-type: "dualstack"
spec:
  type: LoadBalancer
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: my-app

注意:即使 Service 设置了 RequireDualStack,如果 cloud-provider 不支持双栈 LB,创建出来的 LB 可能只监听一个地址族。Kubernetes 不会因此报错——这个不一致需要你自己检查。


六、纯 IPv6 集群:可行性分析与迁移路径

为什么要考虑纯 IPv6

纯 IPv6 集群面临的挑战

尽管双栈是推荐的过渡方案,但如果你考虑纯 IPv6 集群,需要面对以下问题:

1. 容器镜像和应用兼容性

// 常见的硬编码问题
listener, err := net.Listen("tcp4", "0.0.0.0:8080")  // 只监听 IPv4
// 应该改为
listener, err := net.Listen("tcp", ":8080")            // 同时监听 IPv4 和 IPv6

很多老旧应用在代码中硬编码了 0.0.0.0 或使用了 IPv4-only 的 socket API。迁移前需要全面审计。

2. 外部依赖

3. 基础设施组件

4. Node 本身的 IPv6 连通性

# 检查 Node 是否有 IPv6 地址
$ ip -6 addr show scope global
# 检查 IPv6 默认路由
$ ip -6 route show default
# 检查 IPv6 DNS 解析
$ dig AAAA kubernetes.default.svc.cluster.local @::1

推荐的迁移路径

阶段 1:纯 IPv4 集群
    |
    v
阶段 2:双栈集群(IPv4 为主)
    - 开启双栈 CIDR
    - CNI 配置双栈 IPAM
    - Service 设为 PreferDualStack
    - 验证 IPv6 端到端连通
    |
    v
阶段 3:双栈集群(IPv6 为主)
    - 调整 --service-cluster-ip-range 顺序(IPv6 在前)
    - Service 的 ipFamilies 改为 [IPv6, IPv4]
    - 验证所有应用在 IPv6 优先下正常工作
    |
    v
阶段 4:纯 IPv6 集群(可选)
    - 移除 IPv4 CIDR
    - 对仍需 IPv4 的外部访问使用 NAT64/DNS64
    - 配置 NAT64 网关处理 IPv4-only 外部服务

NAT64 和 DNS64

纯 IPv6 集群访问 IPv4-only 外部服务时,需要 NAT64 + DNS64:

# 在 CoreDNS 中配置 DNS64 插件
# Corefile
.:53 {
    dns64 64:ff9b::/96 {
        prefix 64:ff9b::/96
        translate_all
    }
    forward . /etc/resolv.conf
}

现阶段建议:除非有强烈的业务驱动,生产环境优先选择双栈而非纯 IPv6。NAT64 引入的额外复杂性和潜在故障点往往得不偿失。


七、实验:用 kind 创建双栈集群

下面我们用 kind(Kubernetes IN Docker)创建一个双栈集群,验证 Pod 和 Service 的 IPv4/IPv6 连通性。

前置条件

# 安装 kind
$ go install sigs.k8s.io/kind@v0.22.0
# 或者下载二进制
$ curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
$ chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind

# 确认 Docker 的 IPv6 支持
$ cat /etc/docker/daemon.json
{
  "ipv6": true,
  "fixed-cidr-v6": "fd00:docker::/64",
  "experimental": true,
  "ip6tables": true
}
# 修改后重启 Docker
$ sudo systemctl restart docker

创建双栈集群配置

# kind-dual-stack.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  ipFamily: dual
  podSubnet: "10.244.0.0/16,fd00:10:244::/48"
  serviceSubnet: "10.96.0.0/16,fd00:10:96::/112"
nodes:
  - role: control-plane
  - role: worker
  - role: worker
$ kind create cluster --name dual-stack --config kind-dual-stack.yaml
Creating cluster "dual-stack" ...
 [ok] Ensuring node image (kindest/node:v1.29.2)
 [ok] Preparing nodes
 [ok] Writing configuration
 [ok] Starting control-plane
 [ok] Installing CNI
 [ok] Installing StorageClass
 [ok] Joining worker nodes
Set kubectl context to "kind-dual-stack"

验证集群双栈配置

# 检查 Node 的 PodCIDR 分配
$ kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDRs}{"\n"}{end}'
dual-stack-control-plane    ["10.244.0.0/24","fd00:10:244::/64"]
dual-stack-worker           ["10.244.1.0/24","fd00:10:244:1::/64"]
dual-stack-worker2          ["10.244.2.0/24","fd00:10:244:2::/64"]

# 检查 Node 地址
$ kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.addresses}{"\n"}{end}'

部署测试 Pod

# dual-stack-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pod-a
  labels:
    app: ds-test
spec:
  containers:
    - name: nettools
      image: nicolaka/netshoot:latest
      command: ["sleep", "infinity"]
---
apiVersion: v1
kind: Pod
metadata:
  name: test-pod-b
  labels:
    app: ds-test
spec:
  containers:
    - name: nettools
      image: nicolaka/netshoot:latest
      command: ["sleep", "infinity"]
  nodeName: dual-stack-worker2    # 确保调度到不同节点
$ kubectl apply -f dual-stack-test.yaml

# 查看 Pod 的双栈地址
$ kubectl get pod test-pod-a -o jsonpath='{.status.podIPs}'
[{"ip":"10.244.1.5"},{"ip":"fd00:10:244:1::5"}]

$ kubectl get pod test-pod-b -o jsonpath='{.status.podIPs}'
[{"ip":"10.244.2.3"},{"ip":"fd00:10:244:2::3"}]

验证 Pod 间 IPv4 和 IPv6 连通性

# 从 test-pod-a ping test-pod-b 的 IPv4 地址
$ kubectl exec test-pod-a -- ping -c 3 10.244.2.3
PING 10.244.2.3 (10.244.2.3) 56(84) bytes of data.
64 bytes from 10.244.2.3: icmp_seq=1 ttl=62 time=0.452 ms
64 bytes from 10.244.2.3: icmp_seq=2 ttl=62 time=0.318 ms
64 bytes from 10.244.2.3: icmp_seq=3 ttl=62 time=0.295 ms

# 从 test-pod-a ping test-pod-b 的 IPv6 地址
$ kubectl exec test-pod-a -- ping6 -c 3 fd00:10:244:2::3
PING fd00:10:244:2::3(fd00:10:244:2::3) 56 data bytes
64 bytes from fd00:10:244:2::3: icmp_seq=1 ttl=62 time=0.489 ms
64 bytes from fd00:10:244:2::3: icmp_seq=2 ttl=62 time=0.341 ms
64 bytes from fd00:10:244:2::3: icmp_seq=3 ttl=62 time=0.307 ms

创建双栈 Service 并验证

# dual-stack-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: ds-test-svc
spec:
  ipFamilyPolicy: RequireDualStack
  ipFamilies:
    - IPv4
    - IPv6
  selector:
    app: ds-test
  ports:
    - port: 80
      targetPort: 80
$ kubectl apply -f dual-stack-svc.yaml

# 验证 Service 分配了两个 ClusterIP
$ kubectl get svc ds-test-svc -o wide
NAME          TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
ds-test-svc   ClusterIP   10.96.0.100   <none>        80/TCP    5s

$ kubectl get svc ds-test-svc -o jsonpath='{.spec.clusterIPs}'
["10.96.0.100","fd00:10:96::100"]

$ kubectl get svc ds-test-svc -o jsonpath='{.spec.ipFamilies}'
["IPv4","IPv6"]

验证 DNS 解析双栈记录

# 在 Pod 内部验证 DNS
$ kubectl exec test-pod-a -- dig +short A ds-test-svc.default.svc.cluster.local
10.96.0.100

$ kubectl exec test-pod-a -- dig +short AAAA ds-test-svc.default.svc.cluster.local
fd00:10:96::100

# 通过 IPv4 ClusterIP 访问 Service
$ kubectl exec test-pod-a -- curl -s --connect-timeout 5 http://10.96.0.100
# 通过 IPv6 ClusterIP 访问 Service
$ kubectl exec test-pod-a -- curl -s -6 --connect-timeout 5 http://[fd00:10:96::100]

验证 kube-proxy 规则

# 进入 Node 容器检查 iptables/ip6tables 规则
$ docker exec -it dual-stack-worker bash

# IPv4 NAT 规则
root@dual-stack-worker:/# iptables -t nat -L KUBE-SERVICES | grep ds-test
-A KUBE-SERVICES -d 10.96.0.100/32 -p tcp --dport 80 \
    -j KUBE-SVC-XXXXXXXXXXXXXXXX

# IPv6 NAT 规则
root@dual-stack-worker:/# ip6tables -t nat -L KUBE-SERVICES | grep ds-test
-A KUBE-SERVICES -d fd00:10:96::100/128 -p tcp --dport 80 \
    -j KUBE-SVC-YYYYYYYYYYYYYYYY

测试三种 ipFamilyPolicy

# SingleStack(只要 IPv4)
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: svc-single
spec:
  ipFamilyPolicy: SingleStack
  ipFamilies: [IPv4]
  selector: { app: ds-test }
  ports: [{ port: 80 }]
EOF

$ kubectl get svc svc-single -o jsonpath='{.spec.clusterIPs}'
["10.96.0.101"]

# PreferDualStack
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: svc-prefer
spec:
  ipFamilyPolicy: PreferDualStack
  ipFamilies: [IPv6, IPv4]
  selector: { app: ds-test }
  ports: [{ port: 80 }]
EOF

$ kubectl get svc svc-prefer -o jsonpath='{.spec.clusterIPs}'
["fd00:10:96::102","10.96.0.102"]

# RequireDualStack
$ kubectl get svc ds-test-svc -o jsonpath='{.spec.clusterIPs}'
["10.96.0.100","fd00:10:96::100"]

清理实验环境

$ kubectl delete pod test-pod-a test-pod-b
$ kubectl delete svc ds-test-svc svc-single svc-prefer
$ kind delete cluster --name dual-stack

八、运维清单与最佳实践

开启双栈前的检查清单

  1. 网络基础设施:确认物理/虚拟网络支持 IPv6 转发。检查交换机、路由器、防火墙的 IPv6 ACL。
  2. Node IPv6 连通性:所有 Node 之间 IPv6 可达。在每个 Node 上执行 ping6 测试。
  3. CNI 版本:确认 CNI 插件版本支持双栈。参考本文第四节的对比表。
  4. kube-proxy 配置:确认 --cluster-cidr 包含 IPv6 CIDR。
  5. DNS:CoreDNS 版本 1.8+ 对双栈 Service 的 A/AAAA 记录生成已经很成熟。
  6. 应用兼容性:审计关键应用是否硬编码了 IPv4 地址或使用了 IPv4-only 的 API。

生产环境建议

# 推荐的 Service 默认策略
# 如果不确定,从 PreferDualStack 开始
spec:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6

故障排查要点

# 1. 检查 Pod 是否拿到了双栈地址
$ kubectl get pod <name> -o jsonpath='{.status.podIPs}'

# 2. 在 Pod 内检查接口地址和路由
$ kubectl exec <pod> -- ip -4 addr show eth0
$ kubectl exec <pod> -- ip -6 addr show eth0
$ kubectl exec <pod> -- ip -4 route
$ kubectl exec <pod> -- ip -6 route

# 3. 检查 Service 是否分配了双栈 ClusterIP
$ kubectl get svc <name> -o jsonpath='{.spec.clusterIPs}'

# 4. 检查 CoreDNS 是否返回 A 和 AAAA 记录
$ kubectl exec <pod> -- dig A <svc>.<ns>.svc.cluster.local
$ kubectl exec <pod> -- dig AAAA <svc>.<ns>.svc.cluster.local

# 5. 检查 kube-proxy 规则
$ iptables -t nat -L KUBE-SERVICES -n | grep <clusterIP-v4>
$ ip6tables -t nat -L KUBE-SERVICES -n | grep <clusterIP-v6>

# 6. 抓包确认 IPv6 流量是否到达
$ kubectl exec <pod> -- tcpdump -i eth0 ip6 -c 10

# 7. 检查 Node 的 IPv6 转发是否开启
$ sysctl net.ipv6.conf.all.forwarding

九、总结

Kubernetes 双栈网络从 1.16 的 Alpha 走到 1.23 的 GA,经历了四年的打磨。它不仅仅是给 Pod 多加一个 IP,而是对 apiserver、controller-manager、kube-proxy、kubelet、CNI、CoreDNS 全栈的改造。

核心要点回顾:

  1. 地址分配:双栈 Pod 同时持有 IPv4 和 IPv6 地址。CNI 的 cmdAdd() 返回两个 IP 条目,kubelet 上报 podIPs[] 数组。

  2. Service 策略ipFamilyPolicy 的三种取值(SingleStack / PreferDualStack / RequireDualStack)提供了灵活的控制粒度。ipFamilies 数组的顺序决定主地址族。

  3. CNI 差异:Calico 和 Cilium 的双栈支持成熟度较高,Flannel 相对基础。IPv6 NAT 行为在各 CNI 间不一致,需要显式配置。

  4. 现实陷阱:Happy Eyeballs 机制的缺失会导致 IPv6 故障时的严重延迟;IPv6 SNAT 行为不一致可能导致外部通信失败;云厂商 LB 的双栈支持参差不齐。

  5. 纯 IPv6:技术上可行但挑战较多。推荐的路径是从纯 IPv4 逐步过渡到双栈,再视业务需求决定是否迁移到纯 IPv6。

  6. 渐进式推进:从 PreferDualStack 开始,先验证非关键服务的 IPv6 连通性,再逐步扩大范围。


上一篇kube-proxy 与 Service 网络 下一篇Flannel 深入解析


By .