Pod IP 漂移那天我们才懂 Service:一次 iptables 规则把流量导错,排查到凌晨
Kubernetes 里最容易被以为懂了的概念就是 Service。新人常以为 Service 是个负载均衡器、是个固定的 IP 入口但真到了 Pod 重建、IP 漂移、流量被导到错误后端的半夜才发现底层的网络模型没吃透。这篇文章从我们一次 Pod IP 变化引发的故障出发把 Service 的几种类型和背后的 iptables/ipvs 机制讲清楚并给出几段能直接拿来排查的 Java 代码。我的判断先放前面理解 Service 的关键是接受Pod 是临时的Service 才是稳定的抽象以及Service 背后只是一堆节点上的转发规则不是一个真正运行的代理进程。先说结论Pod IP 不是稳定的Service 才是Kubernetes 里每个 Pod 都有独立 IP但 Pod 一旦被重建滚动发布、节点驱逐、OOM 重启IP 就会变。如果你的代码或配置里写死了某个 Pod IP那就是埋雷。我们那次故障就是一个老服务把上游地址配置成了http://10.244.3.17:8080某个 Pod 的 IPPod 重建后 IP 变了调用全部失败告警在凌晨两点炸了。正确做法是永远通过 Service 名字访问由 kube-proxy 把虚 IP 转成实际 Pod IP。Service 的几种类型我们实际用过ClusterIP集群内访问、NodePort节点端口暴露、LoadBalancer云厂商 LB、Headless无 ClusterIP直接返回 Pod 列表用于有状态服务。一个典型 ClusterIP 定义apiVersion: v1 kind: Service metadata: { name: order-service } spec: selector: { app: order } # ① 按 label 选中后端 Pod ports: - port: 80 # ② Service 暴露的端口 targetPort: 8080 # ③ 转发到 Pod 的容器端口 type: ClusterIP① 的selector是关键Service 不是绑定Pod而是通过 label 动态匹配。Pod 重建后只要 label 不变就自动被纳管。理解这一点很多Service 不通的问题就清楚了——先kubectl get endpoints order-service看有没有健康的 Pod IP 挂上来十次有六次是 label 对不上或 readiness 探针没过。Service 背后的转发iptables 还是 ipvsClusterIP 是个虚 IP本身不监听任何端口流量靠节点上的 kube-proxy 维护的 iptables或 ipvs规则转发。用 iptables 模式时规则大概是这样访问10.96.x.x:80会被DNAT成某个 Pod IP。问题来了iptables 是线性规则链Service 和 Pod 数量上千后每条包都要遍历很长的规则转发延迟上升。我们集群扩到 800 个 Service 时P99 网络延迟从 0.3ms 涨到 2ms切到 ipvs 模式哈希表查找O(1)才降回去。那次流量导错的事故根因是 ipvs 模式下 conntrack连接跟踪表被打满。部分新建连接被丢弃表现就是偶发超时、重试就好了。排查时我们看的是节点 conntrack 计数# 查看 conntrack 表使用率接近上限就会丢包 cat /proc/sys/net/netfilter/nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max这个不是 Java 代码但它是定位诡异网络超时的第一现场。我们最后把nf_conntrack_max调大并在 ipvs 下启用conn_reuse_mode1缓解 TIME_WAIT 复用问题超时率从 0.5% 降到 0.01%。顺便说一句这类问题在压测时是看不见的只有真实流量混合长短连接时才暴露所以容量演练要覆盖长连接场景。用 Java 去发现 Service 背后的 Pod很多时候我们需要在应用里动态感知后端实例。如果用 Spring Cloud Kubernetes可以直接用DiscoveryClient拿到 Service 下的 PodAutowired DiscoveryClient discoveryClient; // ① Spring 注入 K8s 服务发现 public ListString getOrderInstances() { // ② 返回的是 Pod 的 IP:port 列表而非 Service 虚 IP return discoveryClient.getInstances(order-service) .stream() .map(i - i.getHost() : i.getPort()) // ③ 拿到真实 Pod 地址做自定义负载 .collect(Collectors.toList()); }注意 ②拿到的host是 Pod IP不是 ClusterIP。如果你自己写负载均衡要小心 Pod 随时可能下线——拿到的列表需要带健康检查否则会打到已终止的 Pod。我们曾因为缓存了这份列表 5 分钟不刷新把请求发给了已经 Terminating 的 Pod触发一堆连接拒绝。所以动态发现 短 TTL 缓存 主动探活三者缺一不可。另一个排查手段在容器里用 Java 直接解析 Service 的 DNSK8s 给每个 Service 生成order-service.namespace.svc.cluster.local的 A 记录Headless Service 还会返回所有 Pod IPInetAddress[] addrs InetAddress.getAllByName( order-service.default.svc.cluster.local); // ① 解析 Service DNS for (InetAddress a : addrs) { System.out.println(a.getHostAddress()); // ② 打印得到的 Pod IP 列表 }这段代码在我们排查Service 解析不到 Pod时非常有用——如果这里返回空或返回的是旧 IP说明 CoreDNS 或 endpoints 有问题而不是应用代码的问题。把它做成启动时的一次自检日志能少走很多弯路。我们后来在每个服务启动时都打一条解析关键依赖 Service 得到 N 个实例的日志发布后第一件事就是看这条日志对不对。第三个常用场景用 Java 客户端以编程方式创建/校验 Service而不是手写 YAML。比如用 fabric8 KubernetesClienttry (KubernetesClient client new DefaultKubernetesClient()) { Service svc new ServiceBuilder() .withNewMetadata().withName(order-service).endMetadata() // ① 指定 Service 名 .withNewSpec() .withSelector(Collections.singletonMap(app, order)) // ② 等价于 YAML 的 selector .addNewPort().withPort(80).withTargetPort(new IntOrString(8080)).endPort() .endSpec().build(); client.services().inNamespace(default).createOrReplace(svc); // ③ 声明式创建/更新 }① 到 ③ 把 YAML 里的东西用代码表达好处是可以把 Service 的创建纳入应用的初始化逻辑比如按需注册也方便在集成测试里用代码起一个临时 Service。我们主要拿它在测试环境做服务编排生产还是用 YAML GitOps二者不冲突。别忽视的两块Ingress 和 NetworkPolicyService 解决的是集群内怎么访问 Pod但外部流量进来要靠 IngressPod 之间能不能互访要靠 NetworkPolicy。我们曾因为没配 NetworkPolicy一个被入侵的测试 Pod 能直连生产 Redis同集群惊出一身汗。NetworkPolicy 是白名单模型默认拒绝需要显式放行比如只允许frontend命名空间访问order-service的 80 端口。Ingress 则是把外部域名/路径映射到内部 Service它自己也是个 Service通常是 LoadBalancer再叠加一层路由规则。我的取舍如果流量规模不大几百个 Service 以内iptables 模式够用且最稳别急着上 ipvs但当 Service/Pod 数量上到千级ipvs 的转发性能优势明显值得切。Headless Service 适合有状态、需要客户端自己感知每个实例的场景如 ZooKeeper、Kafka 集群无状态 Web 服务用普通 ClusterIP 就好。还有一句大实话新人最容易犯的错是把 Pod IP 当稳定地址用以及以为 Service 是个真正的负载均衡器进程——它只是节点上的一堆转发规则理解这点网络故障排查的思路会清晰很多。另外conntrack、CoreDNS、NetworkPolicy 这三块是 Service 之外最容易被忽略却最常出事的地方排障顺序我建议是先看 endpoints 有没有 Pod再看 DNS 能不能解析最后看网络策略和 conntrack。再补一个我们后来加的实战习惯每次发布后除了看启动日志里的 DNS 解析实例数还会用kubectl run起一个临时调试 Pod在里面 curl 一下目标 Service确认 ClusterIP 真能通。这个动作 30 秒却帮我们抓到过两次Service 建了但 endpoints 空的发布问题——一次是 readiness 探针路径写错Pod 一直没进就绪状态所以永远不在 endpoints 里一次是 selector 的 label 多打了一个空格匹配不上任何 Pod。这类问题靠应用日志是看不出来的必须从网络侧验证。另外调试 Pod 记得用完就删我们曾留了一堆curl-debug-xxxPod 占着资源被运维同学提了工单。所以排查 Service 的口诀我一直记着先看 endpoints再 curl 验证最后才怀疑 kube-proxy 和 conntrack——绝大多数问题在前两步就解决了。思考题你们集群用的是 iptables 还是 ipvs有没有遇到过Service 通但偶发超时的 conntrack 类问题欢迎评论区聊聊你的 K8s 网络踩坑。