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: 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 且 Ready:
kubectl get pods -l app=web4.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-svcEndpoints 里应该有三个 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: 30080nodePort不写的话 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 未 Ready | kubectl 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自动清理,不留痕迹。这个习惯帮我省下了大量排查时间。