K8s Service 访问链路:域名如何解析到 ClusterIP,再转发到 Pod
目录
背景:一个 DNS 短视频把我带进了整条链路#
起因很小:我刷到 KodeKloud 的一个短视频《Pod DNS Not Working - Part 1》,讲的是 Pod 解析不了 DNS 时怎么一步步排查。视频本身不长,给的排查技巧也很实用,但真正勾住我的是它顺带提到的一句话——业务流量通常不会直接依赖 Pod 原始 IP,而是更常通过 Service DNS 名称访问,因为 Pod IP 会随着重建变化。
于是我冒出一个问题:CoreDNS 解析一个普通 Service 的 DNS 名称,返回给我的只是一个 ClusterIP,而这个 ClusterIP 是个虚拟地址,通常并不直接挂在任何节点的物理网卡上——那发往它的包,到底是怎么找到并送到某个真实 Pod 的?
这里说的 Service DNS 名称,既包括 my-svc.default.svc.cluster.local 这种完整域名,也包括同 namespace 里常写的 my-svc 这种短名;后者会被 Pod 里的 resolver 按 search domain 补成完整域名再查询。
顺着这个问题往下挖,我发现它其实串起了 K8s Service 网络的一整条链路。这篇就按这条链路走一遍:
- Pod 怎么把域名变成 IP(DNS / CoreDNS)
- ClusterIP 这个虚拟 IP 的包怎么到达 Pod(kube-proxy + 内核 DNAT)
- kube-proxy 凭什么知道后端有哪些 Pod(EndpointSlice)
- 负载均衡到底是谁在做
- 为什么 2026 年这套 dataplane 正在从 iptables / IPVS 往 nftables 和 eBPF 迁移
我自己 homelab 那几个 K3s 集群已经换成了 Cilium,所以最后一节会和我之前那几篇实战文接上。
先把结论说清楚#
整条链路其实可以一句话概括:CoreDNS 负责「Service DNS 名称 → 虚拟 IP」的翻译,kube-proxy(配合内核 iptables / IPVS / nftables)负责「虚拟 IP → 真实 Pod IP」的路由和负载均衡,而后端 Pod 列表来自 EndpointSlice。 至少在这条链路里,三个角色的职责边界是比较清晰的。
iptables / IPVS / nftables 规则"] K -->|"4. DNAT 换成某个 Pod IP"| T["目标 Pod 10.244.1.25"] E["EndpointSlice
(后端 Pod 列表)"] -.被 kube-proxy watch.-> K KP["kube-proxy
(规则配置员)"] -.写规则.-> K
下面把每一步拆开讲。
第一站:Pod 怎么把域名变成 IP#
当 Pod 发起一次域名解析,它先读 /etc/resolv.conf,这个文件指明该问哪个 DNS server——在 K8s 里通常指向 kube-dns 这个 Service(它背后是 CoreDNS 的 Pod)。如果集群开了 NodeLocal DNSCache,这里也可能先指向本地缓存地址。请求到了 CoreDNS 之后分两种情况:
- 集群内部域名(如
kubernetes.default):CoreDNS 直接翻译成对应的 Service IP。 - 集群外部域名(如
google.com):CoreDNS 把请求 forward 给上游的外部 DNS server。
排查技巧:一次测两个域名#
短视频里我觉得最实用的一招,是在出问题的 Pod 里同时解析两个域名:一个集群内的 kubernetes.default,一个集群外的 google.com。两者的组合结果能很快圈定故障范围:
内部 kubernetes.default |
外部 google.com |
大概率的问题 |
|---|---|---|
| 失败 | 失败 | Pod 大概率已经用不了集群 DNS:CoreDNS 挂了,或网络/防火墙挡了流量 |
| 成功 | 失败 | 集群内 DNS 正常,外部 DNS 坏了:CoreDNS 上游 forward 配置有问题,或集群断了外网 |
| 失败 | 成功 | Pod 能摸到某个 DNS,但不是集群的:Pod 里大概率配错了 DNS server |
| 都成功但很慢 | 都成功但很慢 | DNS 没坏,可能只是解析请求量过大、负载偏高 |
再叠加一层报错语义,定位会更准:
- 返回
No such host:请求送到了,但这个 DNS 名称确实不存在。 - 返回
Timeout:通常说明 Pod 自始至终都没摸到 DNS server。
这一段如果想看真实环境里 CoreDNS 出问题的连锁反应,可以翻我之前那篇 从 Cilium Gateway 到 CoreDNS 的跨层级故障排查;另外 DNS 解析链路本身(stub / recursive / authoritative)我在 CDN 与 DNS 的存活性 里也单独梳理过。
第二站:ClusterIP 只是个虚拟 IP#
拿到 ClusterIP(比如 10.96.0.100)之后,客户端 Pod 就直接往这个地址发连接了。问题在于:ClusterIP 是个虚拟 IP,它不属于任何一台机器的物理网卡。 所以这里发生的不是「CoreDNS 再帮你去找 Pod」,而是数据包在节点内核网络层被改写后转发。
幕后推手是每个节点上的 kube-proxy。但要强调一点:kube-proxy 自己并不处理数据包,它只是个「规则配置员」。 它从 API Server 拿到 Service 和后端列表,翻译成内核规则;真正执行转发的是 Linux 内核。流程大致是:
- 客户端 Pod 向
10.96.0.100发起连接。 - 包离开 Pod 进入宿主机内核网络栈,被内核里的 iptables / IPVS / nftables 规则拦截——这些规则是 kube-proxy 提前写好的。
- 内核按规则做 DNAT(目的地址转换),把目标 IP 从 ClusterIP 换成某个后端 Pod IP(比如
10.244.1.25)。 - 包被正常路由到目标 Pod。
所以「虚拟 IP 怎么到 Pod」的答案是:内核在 L4 做了一次地址转换,不是有人拿着 ClusterIP 去网络里寻址,而是先做 DNAT 再继续转发。
后端列表从哪来:EndpointSlice#
下一个问题:kube-proxy 凭什么知道一个 Service 背后有哪些 Pod、它们的 IP 是多少?
答案是 EndpointSlice。CoreDNS 和 kube-proxy 之间其实不直接通信,它们都是 Kubernetes control plane 的消费者:
(Pod IP + 端口列表)"] EPS -->|watch| KP["每个节点上的 kube-proxy"] KP -->|刷新 iptables/IPVS/nftables| K["节点内核规则"]
- 你创建带 selector 的 Service 时,
EndpointSlice Controller持续监听符合条件、且处于 Ready 状态的 Pod,把它们的 IP 和端口写进 EndpointSlice 对象。 - 每个节点上的 kube-proxy 通过 API Server watch Service 和 EndpointSlice 的变化。
- 一旦后端 Pod 扩容、缩容或挂掉,EndpointSlice 变化,kube-proxy 会尽快更新本节点内核里的规则,尽量让 ClusterIP 映射的 Pod IP 列表保持最新。
(早期用的是 Endpoints 对象,所有后端塞在一个对象里;EndpointSlice 是为了大规模场景把它切片,减少 watch 时的更新放大。)
负载均衡到底是谁做的#
既然一个 ClusterIP 后面可能挂着多个 Pod,那「这次请求落到哪个 Pod」就是负载均衡。至少对常规 ClusterIP 类型 Service 来说,负载均衡的策略通常由 kube-proxy 在每个节点上配置,再由内核执行。 不同代理模式策略差别很大:
- iptables 模式:iptables 本身没有「轮询」这种应用级算法,kube-proxy 用
statistic模块做基于概率的分发。比如 3 个后端,第一条规则 1/3 命中 Pod 1,没中进下一条;第二条在剩下流量里 1/2 命中 Pod 2;第三条 100% 命中 Pod 3。宏观上接近均匀,但它做不到真正的轮询,也感知不到后端连接数或负载。 - IPVS 模式:直接用内核的 IPVS(L4 负载均衡器),支持多种调度算法,可通过
--ipvs-scheduler配:rr(轮询,默认)、lc(最小连接)、dh(目的哈希)、sh(源哈希,常用于简单会话保持)、sed(最短预期延迟)等。
会话保持是 Service 层面的另一种策略,让同一客户端 IP 的请求总落到同一个 Pod:
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 保持 3 小时
开启后 kube-proxy 会在当前 proxy mode(iptables / IPVS / nftables 等)里加上对应的会话保持逻辑。
哪些负载均衡不归 kube-proxy 管#
值得划清边界:kube-proxy 只管到 L4(IP + 端口)。 下面这些决策不由 kube-proxy 做;底层流量是否还经过 ClusterIP,取决于具体实现:
- L7 路由 / 灰度:基于 HTTP header、cookie、URL path 转发,或蓝绿、金丝雀发布,是 Ingress Controller(Nginx Ingress、Traefik)或 Service Mesh(Istio、Linkerd)的活;它们可能转发到 Service,也可能直接使用后端 endpoints。
- Headless Service:见下一节,负载均衡交给客户端。
- eBPF 数据面(如开启 kube-proxy replacement 的 Cilium):直接在内核用 eBPF 完成 Service 转发,绕过 kube-proxy 的 ClusterIP 那套。
一个例外:Headless Service#
如果你把 Service 的 clusterIP 显式设成 None,它就变成 Headless Service,机制完全不同:
- 不分配 ClusterIP,kube-proxy 也不为它建转发规则。
- CoreDNS 不再返回一个虚拟 IP,而是直接返回该 Service 后端所有 Ready Pod 的 IP 列表(A / AAAA 记录)。
- 客户端拿到这组 Pod IP 后,自己决定连哪个——负载均衡主要交给客户端代码。
这在部署有状态集群(Kafka、Elasticsearch、ZooKeeper 这类需要稳定网络标识、客户端自己做分片/选主的场景)时非常常见。
dataplane 在换代:iptables → IPVS → nftables → eBPF#
前面反复出现「iptables / IPVS / nftables」,因为这层 dataplane 正好在换代。我写这篇时(2026-05-29)查到的最新稳定版已经到了 K8s 1.36「Haru」;具体版本状态和发布日期还是建议以官方 release note 为准。先上一张对比表:
| 维度 | iptables | IPVS | nftables | eBPF (Cilium) |
|---|---|---|---|---|
| 底层结构 | 线性链表 | hash 表 | verdict maps | eBPF map |
| 匹配复杂度 | O(n) | O(1) | O(1) | O(1) |
| 大规模表现 | 相对差,全量 reload 时 CPU 易飙 | 好 | 好,更轻量 | 通常最好 |
| 定位 | 通用防火墙 | 纯 L4 负载均衡器 | netfilter 新一代统一子系统 | 绕过 netfilter |
| K8s 社区状态 | 1.36 仍是默认 | v1.35 已弃用 | v1.33 GA | CNI 自带,非 kube-proxy |
几点展开:
- iptables:规则更新是非增量的,每次增删(Pod 扩缩容、Service 变更)都要锁住整张表、全量倒出再 reload。大集群里这会带来明显延迟和 CPU 抖动;线性匹配在 Service 上千时也拖慢每个包。但出于兼容性,至少在 1.36 这一版里,它仍是 kube-proxy 的默认模式。
- IPVS:O(1) 解决了 iptables 的查找瓶颈,但它只是个纯 L4 负载均衡器,撑不住 NetworkPolicy、NodePort 伪装、拓扑感知路由这些 K8s 原生需求。结果是 kube-proxy 在 IPVS 模式下底层还得再建一部分 iptables 规则打补丁,「IPVS + iptables」双系统混合,维护代价很高。这也正是它被弃用的原因——IPVS 模式在 v1.35 被标记为 deprecated。
- nftables:netfilter 的正统继任者,用 verdict maps 实现 O(1),且支持原子的增量更新,不再需要全量 reload,同时能在一套架构里统一处理负载均衡、NAT 和策略过滤。它 v1.33 GA(v1.29 引入 alpha,中间过了 beta),但有硬性前提:节点内核必须 ≥ 5.13,否则 kube-proxy 启动时会在版本检查阶段报错。
- eBPF:Cilium 这类 CNI 用 socket-level 负载均衡,在
connect/sendmsg等系统调用阶段就把 ClusterIP 换成 Pod IP,直接从 socket 层替掉 netfilter 那套。开启kubeProxyReplacement=true后,kube-proxy 可以直接从集群里删掉。
关于「1.36 已移除 IPVS」:不少二手文章写成「1.36 已彻底移除 IPVS」,但对照 kubernetes.io 文档,IPVS 在 v1.35 只是 deprecated(feature state 标的是 v1.35 [deprecated]),1.36 里仍然可用,只是会打印告警。按 K8s 的弃用惯例,GA 特性被弃用后还会再留数个版本才真正移除。
取舍:1.36 时代我会怎么选#
下面是结合我自己 homelab 经验的判断,至少在我这些场景下成立,不一定适配所有集群:
- 中小规模、求稳(Service 数量不大):我会留在默认的 iptables。它服役十多年,兼容性最好、经验也最丰富;小规模下那点开销通常无感,未必值得为一点性能收益去承担网络重构的风险。
- 原本就是 IPVS 的大规模 / 高并发集群:我会趁升级窗口评估迁到 nftables。先确认所有节点内核 ≥ 5.13,再改 kube-proxy 的
mode、滚动重启;这样能甩掉 IPVS 那套「双系统打补丁」的技术债,也避开它后续被移除的被动局面。 - 已经或准备上 Cilium:那这次 IPVS 弃用对你可能反而是个「架构瘦身」契机。不用纠结 nftables 怎么配,直接开
kubeProxyReplacement=true,把 kube-proxy 连同 netfilter 那套一起请出集群,走纯 eBPF 转发。我 homelab 和 Oracle 的两个 K3s 集群就是这条路,迁移踩坑记在 K3s CNI 从 Flannel 到 Cilium,跨集群服务发现在 Cilium ClusterMesh 实战。
一个容易踩的坑:如果用的是 Calico 的 eBPF 数据面,官方要求切换前先把 kube-proxy 设回 iptables 模式,再开 eBPF(Calico 需要基于 iptables 清理旧规则)。这点和 Cilium 不一样——Cilium 是直接不装 kube-proxy,别把两家的步骤记混了。
排查清单:下次 DNS / Service 出问题我按这个顺序看#
把上面整条链路压成一个排查顺序,方便下次照着走:
- 先看出问题 Pod 的
/etc/resolv.conf:确认nameserver指向的是不是你预期里的集群 DNS(很多集群是kube-dnsService,也有一些会先走 NodeLocal DNSCache)。 - 再同时解析
kubernetes.default和google.com,用前面那张四象限表圈范围;顺手看报错是No such host(DNS 名称不存在)还是Timeout(通常是根本没摸到 DNS)。 - DNS 没问题但连不上 Service:检查
kubectl get endpointslices,看后端 Pod 是否真的进了列表(Pod 没 Ready 就不会被收进去)。 - EndpointSlice 有后端但流量不通:去看节点上的 kube-proxy——它的
mode、日志有没有报错、规则有没有刷上(iptables-save/ipvsadm -Ln/nft list ruleset,取决于模式)。 - 怀疑负载不均:先确认是不是 iptables 模式(它只是概率分发,不是真轮询),再看有没有意外开了
sessionAffinity: ClientIP。 - 如果是 Cilium / eBPF 集群:上面 kube-proxy 那几步基本跳过,直接用 Cilium 自带的工具(
cilium status、Hubble)看 Service 和转发。
说到底,K8s 网络这条链路并不神秘:CoreDNS 翻译 DNS 名称、kube-proxy 写规则、内核做转发、EndpointSlice 提供后端,dataplane 只是把「内核怎么做转发」这一段从 iptables 一路换到了 eBPF。把角色分清楚,排查时就知道该去链路的哪一层看。
参考#
一手资料:
- KodeKloud, Pod DNS Not Working - Part 1(YouTube Shorts,本文引子;[need manual confirm])
- Kubernetes 官方文档:Virtual IPs and Service Proxies(iptables / IPVS / nftables 模式与 feature state)
- Kubernetes 官方博客:NFTables mode for kube-proxy(nftables 模式与 kernel 5.13+ 要求)
- Kubernetes enhancements:Deprecate ipvs mode in kube-proxy (#5495)(IPVS 弃用与移除时间线)
二手报道(属于报道 / 解读,不是一手证据,引用时建议回查上面的一手文档):
- Tigera:From IPVS to nftables: Kubernetes v1.35 migration guide with Calico(迁移步骤参考)
- OneUptime:How to Migrate from iptables to nftables in Kubernetes Clusters(迁移步骤参考)
- cloudmagazin:Kubernetes 1.36: User NS stable, IPVS gone, Ingress-NGINX retired 一文称「IPVS gone」,与一手文档的弃用时间点不符,见上文纠偏
本博客相关: