你的集群跑了三年,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 双栈实现:引入 ipFamilies
和 ipFamilyPolicy 字段。旧的
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”这么简单。它对集群的多个子系统产生了连锁影响:
- kube-apiserver:
--service-cluster-ip-range接受逗号分隔的两段 CIDR(先 v4 后 v6 或反之,顺序决定”主地址族”)。 - kube-controller-manager:
--cluster-cidr同样接受双段 CIDR。NodeIPAM controller 为每个 Node 分配两个 PodCIDR。 - kube-proxy:同时运行 iptables + ip6tables(或 ipvs 的 IPv4/IPv6 virtual server)。规则量翻倍。
- kubelet:上报
node.status.addresses[]中的 IPv4 和 IPv6 地址;pod.status.podIPs[]包含两条记录。 - CNI 插件:
cmdAdd()必须在Result.IPs中返回两个 IP 条目。 - CoreDNS:为同一个 Service 同时生成 A 和 AAAA 记录。
二、双栈 Pod 的地址分配
整体流程
双栈 Pod 的地址分配可以用下面这张图概括:
流程的核心环节如下:
- kube-apiserver 接受 PodSpec,写入 etcd。
- Scheduler 将 Pod 绑定到某个 Node。
- 该 Node 上的 kubelet 监听到 Pod 事件,创建 pause 容器和 network namespace。
- kubelet 调用 CNI 插件的
cmdAdd()。 - CNI 插件向 IPAM 后端请求地址——在双栈场景下,会分别从 IPv4 Pool 和 IPv6 Pool 各取一个地址。
- CNI 插件将两个 IP 配置到 Pod 的
eth0上,设置两套路由,返回Result。 - 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 256IPv4 的网关通常是 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 0kubelet 上报的 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"
podIP和podIPs[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 数组的第一个元素决定了:
clusterIP(单数形式)的值——即主 ClusterIP。- kube-proxy 生成的 Endpoints 匹配规则中的默认地址族。
- 外部客户端通过 DNS 解析到的”首选”地址。
如果你希望 Service 优先通过 IPv6 对外提供服务:
spec:
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv6
- IPv4已有 Service 的升级
对于在双栈 GA 之前创建的 Service,Kubernetes
会在升级过程中自动将它们标记为
SingleStack。你可以通过
kubectl edit 或 kubectl 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()关键细节:
- Calico 为 IPv6 默认不启用
NAT(
natOutgoing: false),因为 IPv6 的设计哲学是端到端可达,不需要 NAT。 - IPv6 的封装模式支持有限:VXLAN over IPv6 在某些内核版本上可能有问题。推荐使用 BGP 模式直接路由 IPv6 流量。
- Felix 组件同时管理 iptables 和 ip6tables 的策略规则。
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关键细节:
- Cilium 使用 eBPF 实现数据包转发,IPv4 和 IPv6 共享同一套 eBPF 程序,只是 map key 长度不同(4 字节 vs 16 字节)。
- Cilium 的 Service 负载均衡在 eBPF 层完成,不依赖 kube-proxy。双栈 Service 在 eBPF map 中会有两条 frontend 记录。
- Cilium 1.12+ 对 IPv6 BIG TCP(GSO/GRO 大包)有良好支持,可以显著提升 IPv6 吞吐量。
enableIPv6Masquerade控制 IPv6 SNAT 行为。生产环境中建议保持关闭,除非有明确需求。
Flannel
Flannel 对双栈的支持相对基础:
{
"Network": "10.244.0.0/16",
"IPv6Network": "fd00:10:244::/48",
"Backend": {
"Type": "vxlan"
}
}关键细节:
- Flannel 从 v0.12.0 开始支持双栈,但早期版本 bug 较多,建议使用 v0.21.0 以上。
- Flannel 的双栈 VXLAN 会创建两个 VTEP
设备:
flannel.1(IPv4)和flannel-v6.1(IPv6)。 - Flannel 不管理 NetworkPolicy,也不直接操作 ip6tables。IPv6 的 SNAT/MASQUERADE 规则由 kube-proxy 处理。
- Flannel 的 IPAM 比较简单,直接使用 Node 的 PodCIDR 进行子网划分,IPv6 同理。
对比总结
| 特性 | 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 库的实现。
问题场景:
- Pod A 访问
my-svc.default.svc.cluster.local。 - CoreDNS 返回 A 记录
10.96.0.100和 AAAA 记录fd00::96:100。 - Pod A 的 glibc 默认优先尝试 AAAA 记录(IPv6 优先)。
- 如果 IPv6 路径有问题(比如某跳路由器不转发 IPv6、或者 MTU 不匹配导致大包丢失),连接会挂起直到超时(通常 30-75 秒)。
- 超时后才回退到 IPv4,用户感知到的延迟是灾难性的。
Happy Eyeballs(RFC 8305):
现代客户端应该实现 Happy Eyeballs 算法:同时发起 IPv4 和 IPv6 连接,谁先成功用谁,通常给 IPv6 250ms 的先发优势。但是:
- 很多容器镜像中的 glibc 版本并不完整实现 Happy Eyeballs。
- Go 语言的
net.Dialer默认实现了 Happy Eyeballs(DualStack: true是默认值)。 - Python 的
socket.create_connection()在 3.11+ 才开始支持。 - Java 的行为取决于 JVM 参数
java.net.preferIPv6Addresses。 - curl 7.x 默认启用 Happy Eyeballs。
缓解措施:
# 方案一:在 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
规则可能缺失。 |
问题场景:
- 如果 CNI 不做 IPv6 SNAT,外部服务器收到的源地址是 Pod 的
fd00::私有地址。 - 外部服务器可能无法回包(因为没有到
fd00::的路由)。 - 如果使用 GUA(Global Unicast Address,全局单播地址),Pod 直接暴露真实 IPv6 地址可能带来安全顾虑。
缓解措施:
# 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 MASQUERADEcloud-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
- 地址空间:IPv4 地址已经枯竭。大规模集群(数千 Node、数万 Pod)的 IPv4 CIDR 规划越来越困难。
- 运营商推动:中国三大运营商自 2019 年起推进 IPv6 部署。移动互联网客户端的 IPv6 覆盖率已超过 60%。
- 合规要求:部分行业(如政府、金融)要求基础设施支持 IPv6。
- 性能:IPv6 头部固定 40 字节,没有 checksum 计算,路由器处理效率更高。eBPF BIG TCP 在 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. 外部依赖
- 很多外部 API 仍然只提供 IPv4 端点。
- 容器镜像仓库(如 Docker Hub、gcr.io)在 IPv6 的支持上仍然不完善。
- DNS 递归解析器需要支持 IPv6 transport。
3. 基础设施组件
- etcd:从 3.4 开始支持 IPv6 监听,但需要显式配置。
- CoreDNS:完全支持 IPv6。
- Ingress Controller:大部分支持 IPv6,但需确认具体版本。
- 监控组件(Prometheus、Grafana):需要确认抓取目标的 IPv6 可达性。
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:
- DNS64:当 DNS 查询只返回 A
记录时,DNS64 服务器会合成一条 AAAA 记录,将 IPv4 地址嵌入
IPv6 前缀(如
64:ff9b::192.0.2.1)。 - NAT64:当 Pod 向
64:ff9b::前缀发送数据包时,NAT64 网关将其转换为 IPv4 数据包并转发。
# 在 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八、运维清单与最佳实践
开启双栈前的检查清单
- 网络基础设施:确认物理/虚拟网络支持 IPv6 转发。检查交换机、路由器、防火墙的 IPv6 ACL。
- Node IPv6 连通性:所有 Node 之间 IPv6
可达。在每个 Node 上执行
ping6测试。 - CNI 版本:确认 CNI 插件版本支持双栈。参考本文第四节的对比表。
- kube-proxy 配置:确认
--cluster-cidr包含 IPv6 CIDR。 - DNS:CoreDNS 版本 1.8+ 对双栈 Service 的 A/AAAA 记录生成已经很成熟。
- 应用兼容性:审计关键应用是否硬编码了 IPv4 地址或使用了 IPv4-only 的 API。
生产环境建议
# 推荐的 Service 默认策略
# 如果不确定,从 PreferDualStack 开始
spec:
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv4
- IPv6- 渐进式推进:先在非关键 Service 上启用
PreferDualStack,观察一段时间后再全量推广。 - 监控 IPv6 流量:在 Node 上部署
ip6tables -L -v -n定期采集,或使用 Cilium Hubble 观测 IPv6 流量。 - MTU 规划:IPv6 头部比 IPv4 大 20 字节(40 vs 20)。如果使用隧道封装(VXLAN、Geneve),需要重新计算 MTU 以避免分片。关于 MTU 的详细讨论,参见后续文章。
- 安全组/防火墙:IPv6 流量和 IPv4 流量需要分别配置安全组规则。不要遗漏 ICMPv6(NDP 依赖它)。
故障排查要点
# 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 全栈的改造。
核心要点回顾:
地址分配:双栈 Pod 同时持有 IPv4 和 IPv6 地址。CNI 的
cmdAdd()返回两个 IP 条目,kubelet 上报podIPs[]数组。Service 策略:
ipFamilyPolicy的三种取值(SingleStack/PreferDualStack/RequireDualStack)提供了灵活的控制粒度。ipFamilies数组的顺序决定主地址族。CNI 差异:Calico 和 Cilium 的双栈支持成熟度较高,Flannel 相对基础。IPv6 NAT 行为在各 CNI 间不一致,需要显式配置。
现实陷阱:Happy Eyeballs 机制的缺失会导致 IPv6 故障时的严重延迟;IPv6 SNAT 行为不一致可能导致外部通信失败;云厂商 LB 的双栈支持参差不齐。
纯 IPv6:技术上可行但挑战较多。推荐的路径是从纯 IPv4 逐步过渡到双栈,再视业务需求决定是否迁移到纯 IPv6。
渐进式推进:从
PreferDualStack开始,先验证非关键服务的 IPv6 连通性,再逐步扩大范围。
上一篇:kube-proxy 与 Service 网络 下一篇:Flannel 深入解析