ClusterIP底层原理与排障:iptables到conntrack

发布时间:2026/10/11 13:54:26
ClusterIP底层原理与排障:iptables到conntrack
干运维这些年我在 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这是新手最常问的问题之一。我创建了一个 Servicekubectl 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 到 PodEndpoints 与 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 所在的节点、区域。将来做拓扑感知路由、就近访问时这套数据结构就是基础。如果你在较新版本的集群里默认建议直接使用 EndpointSlicekube-proxy 已经默认从它取数据。3.3 Endpoints 为空的经典原因我遇到过很多次 Service 创建了但get endpoints结果为空的情况主要有这么几类Selector 写错了。Service 的 selector 和 Pod 的 label 必须完全匹配少一个标签都匹配不上。Pod 没有进入 Ready 状态。Endpoints 默认只包含 Ready 的 Pod如果 Pod 被健康检查卡住、容器启动失败或者没有通过 readinessProbe就不会出现在列表里。Service 类型本身不产生 Endpoints。ExternalName 类型不会生成 Endpointsheadless 模式下也不是传统意义的 Endpoints 聚合。Pod 设置了 publishNotReadyAddresses 参数来控制是否在未 Ready 时也加入 Endpoints。排查建议kubectl get pods --show-labels看看 Pod 标签再对照 Service 的 spec.selector。别嫌这一步基础大多数标签问题都出在“自以为匹配了”上。4. 服务发现DNS 如何把服务名变成 ClusterIP4.1 一条完整的 DNS 解析链路Service 创建后应用怎么用名字找到它答案是集群 DNS通常是 CoreDNS。集群里每个节点的 kubelet 在创建 Pod 时会把 DNS 配置写进容器的 /etc/resolv.confnameserver 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。它没有 ClusterIPDNS 解析返回的不是虚拟 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: DefaultPod 会使用节点上的 /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 --imagebusybox -it --rm --restartNever -- 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 SelectorEndpoints 正常但访问不通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-appkube-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-modeipvs。节点上安装 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 是 ClusterIPRemoteAddress 是各个 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 写着 labelsPod 却贴了一堆运行时附加标签。在 CI/CD 的部署流程里明确 label 和 selector 的归属能省掉大量手忙脚乱的排查时间。最后再分享一个小技巧排查跨命名空间服务调用时记得确认 Service 的 DNS 名称要带命名空间。default 命名空间里访问其他命名空间的 Service 时不写全名解析会失败或拿到错误地址。我自己就在这上面吃过亏写服务间调用代码时总是忘了带上命名空间后缀。ClusterIP 这座冰山从表面看只是 Service 的一个类型沉下去看却连着 kube-proxy、iptables/IPVS、conntrack、EndpointSlice、CoreDNS 一整条链路。把这个链路搞明白你再去看 NodePort、LoadBalancer、Ingress 甚至 Service Mesh 的流量劫持都会有一种豁然开朗的感觉。希望这篇内容能帮你省掉一些我当年踩过的坑。