☰
ClusterIP底层原理与排障:iptables到conntrack
2026/10/11 13:54:18 网站建设 项目流程

干运维这些年,我在 K8s 集群上排查过不少网络问题,最折腾的一类往往不是应用本身出错,而是 Service 通不通、域名解析灵不灵这种基础能力翻车。有一次同事反馈“服务 A 调用服务 B 超时”,我kubectl get svc一看 ClusterIP 正常,get endpoints也全在,但请求就是不通。折腾了一下午,最后把矛头指向 conntrack 才解决。从那以后我开始认真对待 ClusterIP 背后的每一层实现,不再把它当黑盒。

如果你是在用 Kubernetes 的开发者或运维,你一定听说过 ClusterIP,但未必知道它既不是一台真实的机器,也不是某个进程在监听。这篇文章就把 ClusterIP 的来龙去脉、底层实现、服务发现链路以及常见坑全部摊开来讲。无论你是刚接触 K8s 的新人,还是被网络问题折磨过几次的实战派,看完应该都能建立一套完整的排查思路。

1. ClusterIP 是什么:先破除两个常见误解

1.1 不是“集群里的虚拟服务器”

很多人第一次接触 Service 时,会把它理解成“负载均衡器”,认为 ClusterIP 是集群内部的一台代理节点。这个理解在结果层面说得通:它的确把请求分发给一组 Pod。但在实现层面,绝大多数 Kubernetes 集群里根本没有任何一个进程绑定在 ClusterIP 上监听端口。

ClusterIP 的实际载体是节点内核里的 netfilter 规则。kube-proxy 只是把这些规则写入内核,数据包在网络栈里经过规则匹配时,目标 IP 被改写,流量才被送到真正的 Pod。你可以把 ClusterIP 想成前台的一个分机号:你拨这个号码,电话系统直接把呼叫转接给某个员工,但这个号码本身不挂在任何一台实体话机上。

这个认知差异直接影响排查思路。如果你把 ClusterIP 当成一台“机器”,遇到问题时会去检查有没有进程监听、IP 是否可达、路由是否正常;但正确思路应该是检查内核里的 NAT 规则是否正确、Endpoints 是否有内容、kube-proxy 与 API Server 的同步是否正常。方向错了,后面全是白忙。

1.2 ClusterIP 与其它 Service 类型的关系

Kubernetes 的 Service 有四种类型。ClusterIP 是默认类型,NodePort 是在 ClusterIP 基础上额外在每台节点上开一个端口做映射,LoadBalancer 则是这层级联之上的云负载均衡器。因此不管用哪种类型,ClusterIP 的机制始终都在,NodePort 和 LoadBalancer 的最终目的地址都会被改写成 Pod IP。

所以当你排查 NodePort 不通时,最后大概率也会落到 ClusterIP 的规则上。这也是为什么我把 ClusterIP 单独拿出来讲——它是 K8s 网络流量的共同底座。

提示:任何时候在集群里创建一个 Service,节点上的 kube-proxy 就会把对应规则写入内核。规则不生效,NodePort、LoadBalancer 也都跟着歇菜。

1.3 kube-proxy 的三种工作模式

kube-proxy 支持多种实现模式,不同模式对规则的组织方式完全不同:

  • userspace 模式:最早期的实现,所有流量先进 kube-proxy 进程,由用户态程序转发。性能差、延迟高,现在已经很少使用。
  • iptables 模式:最常见的默认模式。规则全部走内核 iptables,性能好,但规则量随 Service 和 Pod 数量线性增长,几千条规则时规则下发和匹配都会变慢。
  • IPVS 模式:内核中的 IP Virtual Server,使用哈希表和连接调度算法,支持 rr、wrr、lc 等策略,适合集群规模较大的场景。

iptables 模式因为默认、通用、排查工具成熟,是绝大多数人的第一选择;但如果你管理的是几百上千个 Service 的集群,我会建议认真评估 IPVS。后面会专门对比这两种模式,这里先记住一个结论:不管哪种模式,Service 的语义不变,变的只是底层规则的实现和性能特征。

2. iptables 模式下 ClusterIP 的实现拆解

2.1 一个 Service 对应三条链

iptables 模式下,每个 Service 不是简单的一条规则,而是一组链条。我在节点上运行iptables -t nat -L -n经常会看到这样的结构:

Chain KUBE-SERVICES (2 references) target prot opt source destination KUBE-SVC-XXX tcp -- 0.0.0.0/0 10.96.0.10/32 tcp dpt:53 KUBE-SVC-YYY tcp -- 0.0.0.0/0 10.96.32.15/32 tcp dpt:8080 Chain KUBE-SVC-YYY (1 references) target prot opt source destination KUBE-SEP-ZZZ all -- 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.5000000000 KUBE-SEP-WWW all -- 0.0.0.0/0 0.0.0.0/0 Chain KUBE-SEP-ZZZ (1 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp to:10.244.1.20:8080

这三层链分别负责什么?

  • KUBE-SERVICES 是总入口。数据包进入节点网络栈后,经过 PREROUTING 链被引导进这里。这一层根据目标 IP 判断这个数据包是否匹配某个 Service。
  • KUBE-SVC- 是每个 Service 专属的链。如果一个 Service 有多个后端 Pod,这一层通过 statistic 模块按概率分配到不同的 KUBE-SEP 链,实现简单的连接级负载均衡。
  • KUBE-SEP- 是每个 Endpoint 专属的链。最终在这里执行 DNAT,把数据包的目标 IP 和端口改写成某个 Pod 的 IP 和端口(targetPort)。

2.2 DNAT 之后流量去向哪里

DNAT 完成后,数据包的目标地址已经变成了某个具体 Pod 的 IP。后续由 CNI 插件建立的网络路由决定这个包怎么到达 Pod。在常见的 flannel、calico 等方案下,这个包会按节点的转发规则进入对应的网络命名空间,最终被 Pod 内的进程收到。

整个过程对 Pod 里的应用是透明的。应用只知道自己发往 ClusterIP:Port,内核在发出去之前就把目标地址替换成了 Pod 的地址。所以你抓包的时候,在应用侧看目标地址是 ClusterIP,但在网络节点侧看已经是 Pod IP。

2.3 回程流量:conntrack 的隐藏角色

DNAT 只改写请求方向,回程怎么办?Pod 收到请求后,回包的目标地址是客户端 Pod 的 IP,源地址是自己 Pod 的 IP。如果源地址直接是 Pod IP,客户端会“一脸懵”:我明明连接的是 ClusterIP。这个问题的解决者是 conntrack(连接跟踪)机制。

当请求被 DNAT 时,内核在 conntrack 表里记录了一条映射关系:原始五元组(客户端 IP、客户端端口、ClusterIP、端口)→ 转换后的五元组(客户端 IP、客户端端口、Pod IP、端口)。当回包经过节点时,内核查询 conntrack 表,把源地址重新改回 ClusterIP,客户端看到的就是一个完整的、对称的连接。

这就是为什么 conntrack 表非常关键。如果 conntrack 表满了,新连接无法记录,会导致服务间歇性超时或者完全不可用。我在生产集群里不止一次遇到这种问题:表现为“一会儿通一会儿不通”,看 iptables 规则完全正常,最后用 dmesg 或conntrack -L才发现表项满了。

注意:conntrack 表的容量是节点级的 sysctl 参数,受 nf_conntrack_max 控制。集群里长连接多、Pod 频繁创建销毁时,这个表会被快速占满,建议规划时预留足够空间。

2.4 为什么 ping 不通 ClusterIP

这是新手最常问的问题之一。我创建了一个 Service,kubectl get svc看到的 ClusterIP 是 10.96.32.15,为什么ping 10.96.32.15永远没响应?

原因很简单:iptables 的 NAT 规则只匹配 TCP、UDP、SCTP 等指定协议。ICMP 数据包不会被 KUBE-SERVICES 链里的 DNAT 规则匹配,因此报文没有转给后端 Pod,而节点上也没有任何进程监听 10.96.32.15。ICMP 到了节点网络栈之后找不到对应程序,直接被丢掉。

所以记住:ClusterIP 不能作为“可用的测试目标”来 ping,要测试 Service 是否可用,应当使用 nc、curl、wget 或 telnet 连接 ClusterIP 的具体端口。很多人第一步用 ping 发现不通,就误以为 Service 坏了,这一条能帮你节省不少排查时间。

3. 从 Service 到 Pod:Endpoints 与 EndpointSlice 的幕后机制

3.1 谁在维护 Endpoints 资源

Service 只是一个声明,真正决定“流量分给谁”的对象是 Endpoints 或 EndpointSlice。当你创建了一个带 Selector 的 Service 之后,kube-controller-manager 内部的 EndpointSlice Controller 开始工作:它持续 watch 集群里的 Pod,凡是标签匹配 Selector 的 Pod,只要状态满足条件,就把 IP 和端口写入 EndpointSlice。

kube-proxy 监听的是 EndpointSlice 的变化,而不是直接监听 Pod。这是一个非常重要的架构细节:即使你的 Pod 存在且标签正确匹配,如果 EndpointSlice 没有更新,kube-proxy 也不会更新内核规则。所以我排查 Service 不通时,第一件事永远是:

kubectl get endpoints <service-name> -n <namespace>

端到端的关系是:Service 定义 → controller 生成 EndpointSlice → kube-proxy 读取 EndpointSlice → 内核规则更新。任何一个环节断了,Service 都无法正常工作。

3.2 EndpointSlice 为什么更好

在早期的 Kubernetes 版本里,Service 的后端列表维护在单个 Endpoints 对象里。后端 Pod 数量一多,这个对象变得又大又频繁更新,给 API Server 和 kube-proxy 带来明显压力。EndpointSlice 把这些后端按 100 个为一段切成多个对象,分开存储和同步,大幅减少了每次变更的影响范围。

EndpointSlice 还有一个附加优势:它带 topology 字段,记录了后端 Pod 所在的节点、区域。将来做拓扑感知路由、就近访问时,这套数据结构就是基础。如果你在较新版本的集群里,默认建议直接使用 EndpointSlice,kube-proxy 已经默认从它取数据。

3.3 Endpoints 为空的经典原因

我遇到过很多次 Service 创建了,但get endpoints结果为空的情况,主要有这么几类:

  • Selector 写错了。Service 的 selector 和 Pod 的 label 必须完全匹配,少一个标签都匹配不上。
  • Pod 没有进入 Ready 状态。Endpoints 默认只包含 Ready 的 Pod;如果 Pod 被健康检查卡住、容器启动失败或者没有通过 readinessProbe,就不会出现在列表里。
  • Service 类型本身不产生 Endpoints。ExternalName 类型不会生成 Endpoints,headless 模式下也不是传统意义的 Endpoints 聚合。
  • Pod 设置了 publishNotReadyAddresses 参数来控制是否在未 Ready 时也加入 Endpoints。

排查建议:kubectl get pods --show-labels看看 Pod 标签,再对照 Service 的 spec.selector。别嫌这一步基础,大多数标签问题都出在“自以为匹配了”上。

4. 服务发现:DNS 如何把服务名变成 ClusterIP

4.1 一条完整的 DNS 解析链路

Service 创建后,应用怎么用名字找到它?答案是集群 DNS,通常是 CoreDNS。集群里每个节点的 kubelet 在创建 Pod 时,会把 DNS 配置写进容器的 /etc/resolv.conf:

nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5

当应用请求 my-service 时,由于名字里没有足够多的点,解析器会依次尝试加上 search domain 的组合:my-service.default.svc.cluster.local、my-service.svc.cluster.local、my-service.cluster.local……最终命中第一个 A 记录,拿到 ClusterIP。

CoreDNS 本身也是以一个普通 Deployment 的方式运行在集群里,它的 Service 也是 ClusterIP 类型。也就是说,DNS 解析过程用到的 10.96.0.10 本身就是 ClusterIP 机制的典型使用者。说到这就不得不感叹 Kubernetes 的自举设计:连服务发现自己都建立在服务发现的底座上。

4.2 headless Service 的解析行为

当你把 Service 的 clusterIP 显式设为 None,得到的就是 headless Service。它没有 ClusterIP,DNS 解析返回的不是虚拟 IP,而是后端 Pod 的 IP 列表。客户端拿到列表后自己去选择连哪个。

headless 模式主要用于 StatefulSet:每个 Pod 都有稳定网络标识,比如 mysql-0.mysql.default.svc.cluster.local,解析为对应 Pod 的 IP。其他需要直连 Pod 的场景(比如某些数据库集群、消息队列)也常用 headless。

这里有一个实用技巧:如果你只是想知道一个 Service 现在对应哪些 Pod IP,可以临时用 dig 来解析 headless 服务的记录名,比反复get endpoints查看更方便。不过要小心 DNS 缓存,不是每次解析都实时。

4.3 DNS 服务发现中的常见坑

  • ndots 的陷阱:默认 ndots:5 意味着如果应用访问的名字包含的点数少于 5,系统会先尝试拼接 search domain。如果应用直接访问完整 FQDN(带 .svc.cluster.local 后缀),就能少做几次无效 DNS 查询。某些 Java、Node.js 应用因为使用系统解析器,遇到这种情况会表现为首次调用特别慢。
  • resolv.conf 被覆盖:如果你在 Pod 的 yaml 里设置了 dnsPolicy: Default,Pod 会使用节点上的 /etc/resolv.conf。节点 DNS 未必能解析集群内部的 Service 名,这会导致服务名解析失败。正确的做法是用 ClusterFirst。
  • 应用内自定义 DNS:很多中间件的客户端框架(比如某些 RPC 框架)会自己维护服务端地址,不一定走容器 DNS。遇到“服务名能解析但还是不通”时,要确认访问的是哪个端口、是否有自定义注册中心。

5. 实战排查:ClusterIP 不通时的系统化方法

5.1 从外到内逐层定位

我把排查思路固定成一套流程,遇到 Service 不通时依次执行:

# 1. 确认 Service 本身 kubectl get svc <name> -n <namespace> # 2. 确认后端 Pod 就绪 kubectl get pods -l <selector> -n <namespace> -o wide # 3. 确认 Endpoints 状态 kubectl get endpoints <name> -n <namespace> # 4. 在集群内用临时 Pod 访问 ClusterIP kubectl run test --image=busybox -it --rm --restart=Never -- wget -qO- http://10.96.32.15:8080/health # 5. 查看节点上的 NAT 规则 iptables -t nat -L -n | grep -E "10.96.32.15|KUBE-SVC"

这套流程的好处是从声明到实际逐层验证,哪一步卡住,问题就锁定了哪一层。

5.2 常见问题速查表

现象可能原因处理方式
get endpoints 为空Selector 不匹配或 Pod 未 Ready检查 Pod 标签和 Service Selector
Endpoints 正常但访问不通kube-proxy 规则未更新查看 kube-proxy 日志,确认是否 watch 到 EndpointSlice
同节点 Pod 间偶尔不通conntrack 表满检查 nf_conntrack_max,清理无用表项
能 curl 访问但 ping 不通正常现象用 nc/curl 验证,不要用 ping 测试 ClusterIP
只有集群外访问不通使用了 ClusterIP 类型改用 NodePort 或 LoadBalancer
特定端口不通targetPort 写错检查 Service spec.targetPort 与 Pod 暴露端口

5.3 深入内核层的三板斧

如果常规手段都查不出问题,就要下沉到内核层。这里分享三个我最常用的诊断命令:

# 查看 conntrack 表当前条目数 conntrack -L | wc -l # 或者直接看计数 cat /proc/sys/net/netfilter/nf_conntrack_count # 查看一个 Service 的 DNAT 规则细节 iptables -t nat -nvL KUBE-SVC-XXX # 抓包确认 DNAT 是否发生 tcpdump -i any host 10.96.32.15 and port 8080

抓包能直接看到数据包到达节点后是否被改写。如果在节点上能看到发往 ClusterIP 的包,但 tcpdump 看不到任何发往 Pod IP 的流量,那 DNAT 规则或 conntrack 多半出了毛病;如果能看到发往 Pod IP 的包但 Pod 内没收到,那问题就在 CNI 路由那一层了。

5.4 kube-proxy 日志怎么看

kube-proxy 是 DaemonSet 部署的,日志默认输出到标准输出。排查时可以用:

kubectl logs -n kube-system -l k8s-app=kube-proxy | grep -i error

常见错误信息包括:无法连接 API Server、EndpointSlice 同步失败、规则更新失败。如果 kube-proxy 与 API Server 之间存在网络分区,它会反复报连接错误,服务规则停止更新,表现就是“新增 Service 不通,老 Service 正常”。这种情况先把 kube-proxy 和 API Server 之间的连接恢复再说。

6. IPVS 模式:大规模集群的另一种答案

6.1 IPVS 和 iptables 的本质差异

iptables 模式的规则是链式的,每增加一个 Service,规则链变长,匹配时需要线性遍历多条规则。IPVS 则不同,它是在内核里维护一组虚拟服务表,使用哈希查找,匹配时间复杂度接近常数,与 Service 数量无关。

IPVS 同时支持更丰富的负载均衡算法:round-robin、weighted round-robin、least-connection、source-hashing 等等。iptables 只有概率分配这一种方式,虽然简单但不灵活。如果你的集群单节点上 Service 数量超过几百个,我更推荐切到 IPVS。

6.2 启用 IPVS 的注意事项

启用 IPVS 需要满足几个条件,否则 kube-proxy 会回退到 iptables:

  • 宿主机内核加载了 ip_vs、ip_vs_rr、ip_vs_wrr 等模块。大部分发行版默认有,但有些精简内核需要手动 modprobe。
  • kube-proxy 启动参数里显式设置 --proxy-mode=ipvs。
  • 节点上安装 ipset,用于管理 IPVS 的集合规则。

启用后可以用 ipvsadm 查看虚拟服务:

ipvsadm -ln IP Virtual Server version 1.2.1 Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.96.32.15:8080 rr -> 10.244.1.20:8080 Masq 1 0 0 -> 10.244.1.21:8080 Masq 1 0 0

看到 LocalAddress 是 ClusterIP,RemoteAddress 是各个 Pod IP,说明 IPVS 规则已经生效。

6.3 切到 IPVS 后容易踩的坑

我在某一次从 iptables 切 IPVS 时踩过这样一个坑:切换后部分 Service 莫名其妙不可用,重启 kube-proxy 恢复。排查半天发现是节点上 ip_vs 模块没加载完整,kube-proxy 虽然以 IPVS 模式启动,但有部分内核功能缺失。切回 iptables 后又正常。

另一个坑是 IPVS 模式下的 NodePort 访问。有时会因为 ipset 规则没建好,导致从节点外部访问 NodePort 失败,但从集群内部访问 ClusterIP 正常。这种问题排查起来很费时间,因为表象和配置都对不上。我的经验是先把ipset list拿出来对比一下,看 KUBE-NODE-PORT 等集合是否存在、内容是否完整。

7. 一些值得长期坚持的实践习惯

排查 ClusterIP 问题这几年,我积累了几条写代码和配置之外的心得。

第一个经验是不要过度相信仪表盘。虽然配置了完善的监控告警,但很多网络问题的根因发生在内核和节点层面,监控只是告诉你“服务坏了”,不会告诉你哪一条规则丢了。所以每次遇到网络类问题,我都要手动跑一遍 iptables 或 ipvsadm 去确认规则状态,这个习惯已经救了我很多次。

第二个经验是主动管理 conntrack。在长连接密集的集群里,nf_conntrack_max 不能默认放任不管。我会在部署前根据节点的内存规划这个值,同时监控 nf_conntrack_count 的涨势,设定告警。否则“服务间歇性超时”会是长期困扰。

第三个经验是给 Service 和 Pod 命名、标签做好规范。很多 Endpoints 对不上的问题,根源其实是命名混乱,selector 写着 labels,Pod 却贴了一堆运行时附加标签。在 CI/CD 的部署流程里明确 label 和 selector 的归属,能省掉大量手忙脚乱的排查时间。

最后再分享一个小技巧:排查跨命名空间服务调用时,记得确认 Service 的 DNS 名称要带命名空间。default 命名空间里访问其他命名空间的 Service 时不写全名,解析会失败或拿到错误地址。我自己就在这上面吃过亏,写服务间调用代码时总是忘了带上命名空间后缀。

ClusterIP 这座冰山,从表面看只是 Service 的一个类型,沉下去看却连着 kube-proxy、iptables/IPVS、conntrack、EndpointSlice、CoreDNS 一整条链路。把这个链路搞明白,你再去看 NodePort、LoadBalancer、Ingress 甚至 Service Mesh 的流量劫持,都会有一种豁然开朗的感觉。希望这篇内容能帮你省掉一些我当年踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询