☰
K8s Service 服务发现与负载均衡实战:从 ClusterIP 到 Headless 的避坑指南
2026/10/21 21:28:09 网站建设 项目流程

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 的设计思路是引入一个**虚拟 IP(VIP)**作为稳定入口,这个 VIP 不绑定任何真实网卡,只存在于转发规则里。调用方访问 VIP,kube-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: 8080
  • selector:标签选择器,决定这个 Service 关联哪些 Pod。上面这个例子会选中所有带app: web标签的 Pod。
  • port:Service 自身暴露的端口,也就是调用方访问 VIP 时用的端口。
  • targetPort:后端 Pod 实际监听的端口。流量最终会转发到这个端口上。

三者的关系可以这样理解:调用方访问web-service:80,kube-proxy 把流量转发到某个app=web的 Pod 的8080端口。port 和 targetPort 可以不同,这给了我们灵活性——比如后端容器监听 8080,但对外统一暴露 80,调用方不需要知道后端的具体端口。

注意:targetPort 如果不写,默认等于 port。但强烈建议显式写出来,尤其是后端端口和 Service 端口不一致时,漏写会导致流量转发到错误端口,表现为连接被拒绝。

3.2 Endpoints 是怎么自动维护的

Service 本身不直接记录后端 Pod 的 IP,真正干活的是Endpoints对象。当你创建一个带 selector 的 Service 时,Endpoints 控制器会持续监听匹配的 Pod,把它们的 IP 和端口写进 Endpoints 列表。kube-proxy 再根据这个列表生成转发规则。

你可以用下面这条命令查看 Endpoints:

kubectl 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 模式:基于 IPVS(IP 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: 10800

ClientIP模式会把同一个源 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 且 Ready:

kubectl get pods -l app=web

4.2 创建 ClusterIP Service 并验证

apiVersion: v1 kind: Service metadata: name: web-svc spec: type: ClusterIP selector: app: web ports: - port: 80 targetPort: 80

应用后查看 Service 和 Endpoints:

kubectl get svc web-svc kubectl get endpoints web-svc

Endpoints 里应该有三个 Pod 的 IP。接着在集群内起一个临时 Pod 测试连通性:

kubectl run test --rm -it --image=busybox -- sh wget -qO- http://web-svc

能返回 nginx 欢迎页就说明 ClusterIP 工作正常。这里注意,测试 Pod 必须和 Service 在同一个命名空间,否则要用web-svc.default这种全限定名。

4.3 切换到 NodePort 暴露服务

把类型改成 NodePort:

spec: type: NodePort selector: app: web ports: - port: 80 targetPort: 80 nodePort: 30080

nodePort不写的话 K8s 会自动分配一个。应用后从集群外部访问任意节点的 30080 端口:

curl http://<节点IP>:30080

如果访问不通,先检查节点防火墙是否放行了该端口,再确认 kube-proxy 是否正常运行。

4.4 Headless Service 与 StatefulSet 的配合

Headless Service 就是clusterIP: None的 Service,它不分配 VIP,DNS 查询会直接返回所有后端 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
负载不均会话保持或连接复用检查 sessionAffinity

5.3 几个我踩过的坑

坑一:targetPort 和 containerPort 不一致。有次后端容器监听 8080,Service 的 targetPort 写成了 80,结果流量全打到没监听的端口上,表现为连接被拒绝。后来养成习惯,containerPort、targetPort、应用实际监听端口三者必须对齐。

坑二:就绪探针路径配错。后端应用的健康检查路径是/healthz,但探针配成了/,返回 404 导致 Pod 一直不 Ready,Endpoints 始终为空。排查时先看 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 --image=busybox -- nslookup web-svc # 查看 kube-proxy 日志 kubectl logs -n kube-system -l k8s-app=kube-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 支持拓扑感知路由,优先把流量转发到同区的 Pod:

spec: 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自动清理,不留痕迹。这个习惯帮我省下了大量排查时间。

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

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

立即咨询