
1. 为什么服务发现是 K8s 里最容易踩坑的一环刚接触 K8s 那会儿我对 Service 的理解就停留在“给 Pod 套个固定 IP”这个层面。直到有一次线上环境滚动更新前端服务突然大面积 502排查了半天才发现是 Service 的 Endpoints 列表在 Pod 重建期间出现了短暂的空窗。那次事故让我意识到Service 远不止“固定 IP”这么简单它背后是一整套服务发现与负载均衡的机制理解不透就等着半夜被叫起来处理故障。Service 解决的核心问题其实很朴素Pod 的生命周期是短暂的IP 会变副本数会变但服务之间的调用关系不应该跟着变。Service 就是在这层不稳定的 Pod 之上抽象出一个稳定的访问入口。它通过标签选择器Label Selector动态关联一组 Pod再借助kube-proxy在每个节点上维护转发规则把发往 Service 虚拟 IP 的流量分发到健康的 Pod 上。这套机制让服务调用方完全不需要关心后端有多少个副本、IP 是什么、哪个 Pod 挂了。这篇文章适合已经跑过几个 K8s 工作负载、但对 Service 的底层逻辑和实战细节还不够有把握的开发者。我会从设计思路讲到实操配置再到排查技巧把 ClusterIP、NodePort、LoadBalancer、Headless Service 这几种类型掰开揉碎讲清楚同时把 Endpoints、kube-proxy 转发模式、会话保持这些容易忽略的点一并带上。读完你至少能做到看到 Service 不通时知道从哪几个方向查配置 Service 时知道每个字段为什么这么填。2. Service 的整体设计与类型选型思路2.1 服务发现要解决的根本矛盾在传统部署里服务 A 调用服务 B通常是把 B 的 IP 写死在配置文件里。但在 K8s 里这套行不通原因有三Pod 重建后 IP 会变、扩缩容后副本数量会变、节点故障后 Pod 会被调度到别处。如果还靠写死 IP运维成本会高到无法接受。Service 的设计思路是引入一个**虚拟 IPVIP**作为稳定入口这个 VIP 不绑定任何真实网卡只存在于转发规则里。调用方访问 VIPkube-proxy 负责把流量转发到后端某个真实 Pod。同时Service 通过标签选择器持续监听 Pod 的变化自动更新后端列表。这样一来调用方只认 VIP后端怎么变都不影响。这里有个关键点容易被忽略Service 的 VIP 是虚拟的它不响应 ping也不在任何网卡上。你 ping 一个 ClusterIP 是 ping 不通的但这不代表 Service 有问题。很多新手在这里浪费大量时间以为网络不通其实是正常现象。2.2 四种 Service 类型的适用场景对比K8s 提供了四种 Service 类型选错了要么暴露不出去要么白白浪费资源。下面这张表是我在实际项目中总结的选型参考类型访问范围典型场景是否需要外部组件ClusterIP集群内部微服务间调用、数据库访问否NodePort集群外部通过节点 IP测试环境暴露、临时调试否LoadBalancer集群外部通过云负载均衡生产环境对外服务云厂商支持ExternalName集群内部映射外部域名迁移过渡、访问外部数据库否ClusterIP 是默认类型也是用得最多的。它只在集群内部可达适合服务之间的内部调用。NodePort 会在每个节点上开一个端口默认范围 30000-32767外部通过“节点 IP 端口”访问。LoadBalancer 通常依赖云厂商的负载均衡器会自动分配一个外部 IP。ExternalName 比较特殊它不做任何转发只是把 Service 名称映射成一个外部域名返回一个 CNAME 记录。提示NodePort 虽然方便但生产环境不建议直接用。端口范围有限、没有健康检查、节点 IP 暴露在外安全和可维护性都不理想。生产对外服务优先考虑 LoadBalancer 或 Ingress。2.3 为什么默认是 ClusterIP 而不是 NodePort很多人会问既然 NodePort 能对外暴露为什么默认不设成它原因在于最小暴露原则。集群内部的服务调用占绝大多数如果默认就对外暴露等于把内部服务全部暴露在节点端口上攻击面会大幅增加。ClusterIP 只在集群内部可达配合网络策略可以做到精细的访问控制。需要对外时再显式改成 NodePort 或 LoadBalancer这种“默认安全、按需暴露”的设计思路贯穿了整个 K8s 的安全模型。3. 核心字段与底层转发机制拆解3.1 selector、port、targetPort 三者的关系这是配置 Service 时最容易搞混的一组字段。我用一个具体例子来说明apiVersion: v1 kind: Service metadata: name: web-service spec: type: ClusterIP selector: app: web ports: - name: http protocol: TCP port: 80 targetPort: 8080selector标签选择器决定这个 Service 关联哪些 Pod。上面这个例子会选中所有带app: web标签的 Pod。portService 自身暴露的端口也就是调用方访问 VIP 时用的端口。targetPort后端 Pod 实际监听的端口。流量最终会转发到这个端口上。三者的关系可以这样理解调用方访问web-service:80kube-proxy 把流量转发到某个appweb的 Pod 的8080端口。port 和 targetPort 可以不同这给了我们灵活性——比如后端容器监听 8080但对外统一暴露 80调用方不需要知道后端的具体端口。注意targetPort 如果不写默认等于 port。但强烈建议显式写出来尤其是后端端口和 Service 端口不一致时漏写会导致流量转发到错误端口表现为连接被拒绝。3.2 Endpoints 是怎么自动维护的Service 本身不直接记录后端 Pod 的 IP真正干活的是Endpoints对象。当你创建一个带 selector 的 Service 时Endpoints 控制器会持续监听匹配的 Pod把它们的 IP 和端口写进 Endpoints 列表。kube-proxy 再根据这个列表生成转发规则。你可以用下面这条命令查看 Endpointskubectl get endpoints web-service -o wide正常情况应该能看到后端 Pod 的 IP 和端口列表。如果 Endpoints 是空的说明 selector 没匹配到任何 Pod或者匹配到的 Pod 没通过就绪探针。这是排查 Service 不通问题的第一站。这里有个细节值得说只有 Ready 状态的 Pod 才会被加入 Endpoints。如果 Pod 配置了就绪探针但一直没通过它不会被加入后端列表Service 自然转发不到它。很多人排查半天网络问题最后发现是就绪探针配置有误。3.3 kube-proxy 的三种转发模式kube-proxy 是 Service 机制的落地执行者它有三种工作模式性能和适用场景差别很大userspace 模式最早的实现流量在用户态和内核态之间来回拷贝性能最差现在基本不用了。iptables 模式目前最常用的模式。kube-proxy 在节点上写 iptables 规则流量在内核态直接转发性能比 userspace 好很多。但有个缺点规则是线性匹配的Service 数量多了之后规则链会很长匹配延迟增加。ipvs 模式基于 IPVSIP Virtual Server用哈希表存储转发规则匹配复杂度是 O(1)大规模集群下性能明显优于 iptables。同时支持更多负载均衡算法比如轮询、最少连接、源地址哈希等。查看当前模式kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode如果集群规模较大Service 数量超过 1000建议切换到 ipvs 模式。切换前要确认内核加载了 ipvs 相关模块否则 kube-proxy 会启动失败。3.4 会话保持与负载均衡策略默认情况下Service 是轮询转发同一个客户端的多次请求会打到不同 Pod 上。如果后端是有状态服务或者需要保持登录会话就得配置会话保持spec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800ClientIP模式会把同一个源 IP 的请求固定转发到同一个 Pod超时时间默认 10800 秒3 小时。这在某些场景下有用但要注意它可能导致负载不均——如果某个客户端请求量特别大它会一直打到同一个 Pod 上。实操心得会话保持能不用就不用。更好的做法是把状态外置到 Redis 或数据库让后端 Pod 保持无状态这样才能充分发挥 K8s 的弹性伸缩能力。会话保持只是过渡方案。4. 从零搭建一套可复现的 Service 实操环境4.1 部署后端工作负载先准备一个简单的后端应用用 Deployment 部署三个副本apiVersion: apps/v1 kind: Deployment metadata: name: web-deploy spec: replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 3 periodSeconds: 5这里特意加了就绪探针目的是演示 Endpoints 只收录 Ready Pod 的机制。应用之后确认三个 Pod 都处于 Running 且 Readykubectl get pods -l appweb4.2 创建 ClusterIP Service 并验证apiVersion: v1 kind: Service metadata: name: web-svc spec: type: ClusterIP selector: app: web ports: - port: 80 targetPort: 80应用后查看 Service 和 Endpointskubectl get svc web-svc kubectl get endpoints web-svcEndpoints 里应该有三个 Pod 的 IP。接着在集群内起一个临时 Pod 测试连通性kubectl run test --rm -it --imagebusybox -- sh wget -qO- http://web-svc能返回 nginx 欢迎页就说明 ClusterIP 工作正常。这里注意测试 Pod 必须和 Service 在同一个命名空间否则要用web-svc.default这种全限定名。4.3 切换到 NodePort 暴露服务把类型改成 NodePortspec: type: NodePort selector: app: web ports: - port: 80 targetPort: 80 nodePort: 30080nodePort不写的话 K8s 会自动分配一个。应用后从集群外部访问任意节点的 30080 端口curl http://节点IP:30080如果访问不通先检查节点防火墙是否放行了该端口再确认 kube-proxy 是否正常运行。4.4 Headless Service 与 StatefulSet 的配合Headless Service 就是clusterIP: None的 Service它不分配 VIPDNS 查询会直接返回所有后端 Pod 的 IP。这种 Service 主要用于 StatefulSet因为每个 Pod 需要稳定的网络标识。apiVersion: v1 kind: Service metadata: name: db-headless spec: clusterIP: None selector: app: db ports: - port: 5432配合 StatefulSet 使用时每个 Pod 会获得pod-name.service-name.namespace.svc.cluster.local这样的稳定域名。客户端可以通过这个域名直接访问特定 Pod这对数据库主从复制、分布式共识这类场景非常关键。提示Headless Service 的 DNS 返回的是 A 记录列表客户端拿到多个 IP 后需要自己决定连哪个。普通 Service 返回的是 VIP 的 A 记录由 kube-proxy 负责负载均衡。这是两者最本质的区别。5. 常见故障排查与避坑经验实录5.1 Service 不通的四步排查法遇到 Service 访问不通按下面这个顺序查基本能覆盖 90% 的情况第一步查 Endpoints。kubectl get endpoints svc-name如果是空的问题出在 selector 或就绪探针上。检查 Service 的 selector 和 Pod 的 labels 是否完全匹配注意大小写和拼写。第二步查 Pod 状态。kubectl get pods -l selector确认 Pod 是 Running 且 Ready。如果 Pod 没 Ready看就绪探针的配置和日志。第三步查 kube-proxy。kubectl get pods -n kube-system | grep kube-proxy确认每个节点上的 kube-proxy 都正常。kube-proxy 挂了转发规则就不会更新。第四步查网络策略。如果集群启用了 NetworkPolicy确认没有策略阻止了流量。kubectl get networkpolicy看看有没有相关规则。5.2 典型问题速查表现象可能原因排查命令Endpoints 为空selector 不匹配或 Pod 未 Readykubectl describe svc连接被拒绝targetPort 配错kubectl get svc -o yaml间歇性超时后端 Pod 部分不健康kubectl get pods外部访问不通防火墙或 nodePort 未放行检查节点安全组DNS 解析失败CoreDNS 异常kubectl get pods -n kube-system负载不均会话保持或连接复用检查 sessionAffinity5.3 几个我踩过的坑坑一targetPort 和 containerPort 不一致。有次后端容器监听 8080Service 的 targetPort 写成了 80结果流量全打到没监听的端口上表现为连接被拒绝。后来养成习惯containerPort、targetPort、应用实际监听端口三者必须对齐。坑二就绪探针路径配错。后端应用的健康检查路径是/healthz但探针配成了/返回 404 导致 Pod 一直不 ReadyEndpoints 始终为空。排查时先看 Pod 的 Ready 状态再看探针配置。坑三跨命名空间访问忘了全限定名。在 namespace A 里访问 namespace B 的 Service直接写svc-name是解析不到的必须写svc-name.namespace-b。这个错误很隐蔽因为 DNS 解析失败和网络不通的表现很像。坑四iptables 模式下规则过多导致延迟。集群 Service 数量涨到两千多之后新建立的连接偶尔出现几百毫秒延迟。后来切到 ipvs 模式延迟恢复到正常水平。大规模集群一定要提前规划好转发模式。5.4 调试 Service 的实用命令合集# 查看 Service 详情包括 Endpoints 和事件 kubectl describe svc web-svc # 查看 Endpoints 对应的 Pod kubectl get endpoints web-svc -o yaml # 在集群内测试 DNS 解析 kubectl run dns-test --rm -it --imagebusybox -- nslookup web-svc # 查看 kube-proxy 日志 kubectl logs -n kube-system -l k8s-appkube-proxy # 查看 iptables 规则iptables 模式 iptables -t nat -L KUBE-SERVICES -n | grep web-svc这些命令我在排查时基本都会走一遍尤其是describe svc和nslookup能快速定位问题在 Service 层还是 DNS 层。6. 生产环境下的进阶配置建议6.1 用 Ingress 替代 NodePort 做七层入口NodePort 是四层转发只能基于 IP 和端口做路由。生产环境对外暴露 HTTP 服务更推荐用 Ingress。Ingress 支持基于域名和路径的路由、TLS 终止、限流等七层能力配合 Ingress Controller 可以实现灰度发布、蓝绿部署等高级玩法。典型的 Ingress 配置apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-ingress spec: rules: - host: example.com http: paths: - path: / pathType: Prefix backend: service: name: web-svc port: number: 80这样外部流量先到 Ingress Controller再由它转发到 ClusterIP Service最后到 Pod。整个链路里 Service 依然是核心只是入口层换成了更强大的 Ingress。6.2 外部服务映射用 ExternalName迁移过程中经常遇到这种情况应用还在集群里但依赖的数据库还在集群外。这时候可以用 ExternalName 把外部数据库映射成一个集群内的 Service 名称apiVersion: v1 kind: Service metadata: name: external-db spec: type: ExternalName externalName: db.example.internal应用里直接连external-db就行等数据库迁进集群后把 ExternalName 改成 ClusterIP 指向内部 Pod应用代码完全不用动。这个技巧在迁移场景里特别好用。6.3 拓扑感知路由减少跨区流量多可用区集群里如果 Service 把流量转发到其他可用区的 Pod会产生跨区流量费用延迟也更高。K8s 支持拓扑感知路由优先把流量转发到同区的 Podspec: topologyKeys: - topology.kubernetes.io/zone - topology.kubernetes.io/region - *这个配置会优先选择同区的 Pod同区没有可用 Pod 时才跨区。对于跨区流量计费的云环境这个配置能省下不少成本。6.4 服务网格接管后的 Service 变化引入服务网格如 Istio后Service 的角色会发生一些变化。网格通常会用 Sidecar 代理接管 Pod 的进出流量Service 的负载均衡由 Sidecar 完成kube-proxy 的转发规则可能被绕过。这时候排查问题要看 Sidecar 的日志和网格的控制面配置而不是只看 kube-proxy。不过即使上了服务网格Service 依然是服务注册和发现的基础。网格的控制面会监听 Service 和 Endpoints 的变化把后端列表下发给 Sidecar。理解 Service 的机制对排查网格问题同样有帮助。7. 我个人在实际操作中的几点体会Service 这个东西看文档觉得简单真到线上出问题的时候才发现细节特别多。我最大的体会是Endpoints 是排查 Service 问题的核心线索。不管什么类型的 Service先看 Endpoints 对不对能排除掉一大半问题。Endpoints 正常但访问不通再去查 kube-proxy 和网络策略Endpoints 不正常就回头查 selector 和就绪探针。另一个体会是不要过度依赖会话保持。早期我为了省事给有状态服务配了 ClientIP 会话保持结果扩缩容时新 Pod 分不到流量老 Pod 压力山大。后来把状态外置后端改成无状态扩缩容才真正丝滑。会话保持应该是最后的手段而不是默认选项。最后分享一个小技巧调试 Service 时用kubectl run起一个带curl或wget的临时 Pod比在节点上折腾 iptables 规则高效得多。临时 Pod 和 Service 在同一网络平面内能直接验证 ClusterIP 和 DNS 是否正常排查完--rm自动清理不留痕迹。这个习惯帮我省下了大量排查时间。