Service 这个东西,刚接触 Kubernetes 的时候觉得它特别简单——不就是给 Pod 套一个固定入口嘛,写个 YAML 选一下标签就完事了。真正在生产里跑起来才发现,Service 的坑远比想象中多:ClusterIP 分配了但访问不通、Endpoints 列表莫名其妙是空的、想把外部数据库接进集群却不知道怎么配、Headless Service 和普通 Service 到底什么时候用哪个。这些问题在文档里往往一句话带过,但排查起来能耗掉你大半天。
这篇内容我打算把 Service 从“能跑”讲到“跑得稳”。前半段拆解 Service 的几种类型和它们背后的转发逻辑,重点讲清楚 kube-proxy 到底做了什么、为什么 ClusterIP 有时候 ping 不通却能用;后半段专门讲一个很多人绕不过去的场景——怎么通过手动维护 Endpoints 把集群外的服务(比如自建数据库、遗留系统、第三方接口)挂到 Service 后面,让集群内的应用像访问普通 Service 一样访问它们。中间会穿插我自己踩过的坑和排查思路,适合已经能部署 Pod、但对 Service 网络细节还比较模糊的同学,也适合正在做集群内外服务打通的运维和开发。
1. Service 存在的意义:为什么 Pod IP 靠不住
先把最根本的问题说清楚:Kubernetes 里每个 Pod 都有独立 IP,为什么不能直接用 Pod IP 通信?答案藏在 Pod 的生命周期里。Pod 是“一次性”的,重启、滚动更新、扩缩容、节点故障迁移,每一次都会产生新的 IP。你今天在配置文件里写死一个 Pod IP,明天这个 Pod 被调度到别的节点重建,IP 就变了,配置直接失效。
Service 解决的正是这个“寻址不稳定”的问题。它在 Pod 前面架了一层虚拟 IP(ClusterIP),这层 IP 是稳定的,只要 Service 对象不被删除,它就不会变。客户端只管连 ClusterIP,至于后面到底是哪个 Pod 在接请求,由 Service 通过标签选择器动态维护。这有点像公司前台的总机号码——你不需要知道具体哪个员工坐在哪个工位,拨总机就行,前台会帮你转接,员工换工位了总机号码也不变。
1.1 标签选择器才是 Service 和 Pod 之间的纽带
很多人以为 Service 是“绑定”到 Pod 上的,其实不是。Service 通过selector字段匹配 Pod 的标签,匹配上的 Pod 会被自动加入 Endpoints(新版是 EndpointSlice)列表。这个机制是松耦合的:Pod 只要带着正确的标签,不管它是什么时候创建的、在哪个节点,都会被自动纳入;反过来,标签一改,Pod 就自动从 Service 后端摘除。
这里有个容易忽略的点:selector 匹配的是标签,不是 Pod 名字,也不是 Deployment 名字。我见过有同学在 selector 里写 Deployment 的名字,结果 Endpoints 一直是空的。正确的做法是让 selector 的键值对和 Pod 模板里metadata.labels完全对应上。比如 Pod 模板里写了app: web,那 Service 的 selector 就得是app: web,多一个空格、大小写不一致都会导致匹配失败。
1.2 ClusterIP 为什么 ping 不通却能 curl 通
这是新手最容易困惑的现象之一。你ping一个 Service 的 ClusterIP,大概率是不通的,但curl或者telnet对应端口却是通的。原因在于 ClusterIP 是一个虚拟 IP,它并不真实存在于任何网卡上,也没有任何主机去响应 ICMP 请求。它的作用只是 iptables 或 IPVS 规则里的一个“匹配条件”——数据包到达节点后,内核根据规则把目标地址是 ClusterIP 的包改写成某个真实 Pod IP,然后转发出去。
所以 ping 不通是正常的,别拿 ping 的结果判断 Service 是否可用。判断 Service 是否正常,应该看kubectl get endpoints <service-name>有没有后端地址,以及从集群内另一个 Pod 里curl或nc测试端口连通性。这个认知差异坑过不少人,尤其是习惯了传统网络排查思路的运维。
2. 四种 Service 类型的适用边界
Service 的type字段决定了它对外暴露的方式,四种类型各有明确的适用场景,选错了要么访问不了,要么白白暴露了不该暴露的东西。
2.1 ClusterIP:集群内部的默认选项
不写type时默认就是 ClusterIP,它只在集群内部可达。适合内部微服务之间的调用,比如订单服务调库存服务。这类 Service 不需要对外暴露,安全性最好。需要注意的是,ClusterIP 的网段是在集群初始化时通过--service-cluster-ip-range指定的,后期改动非常麻烦,规划时要留足地址空间。
2.2 NodePort:在每個节点上开一个端口
NodePort 会在集群所有节点上开放一个端口(默认范围 30000-32767),外部通过节点IP:NodePort就能访问。它的实现是在 ClusterIP 的基础上,额外加了一层 DNAT 规则。优点是简单直接,不需要额外的负载均衡器;缺点是端口范围有限、每个 Service 占一个端口、而且暴露的是节点 IP,节点增减时外部配置要跟着改。
生产环境里我一般不建议直接用 NodePort 对外,它更适合临时调试或者内部测试环境。如果非要用,记得显式指定nodePort值,避免自动分配导致端口漂移,给防火墙和外部调用方带来麻烦。
2.3 LoadBalancer:云上的标准做法
LoadBalancer 类型在 NodePort 基础上,向云厂商的负载均衡服务申请一个外部 IP,把流量导到各节点的 NodePort 上。这是云环境对外暴露服务的标准做法,用户拿到的是一个稳定的外部地址。代价是每个 Service 通常对应一个负载均衡实例,成本不低,所以内部服务不要滥用这个类型。
2.4 ExternalName:把外部域名映射成集群内名字
ExternalName 比较特殊,它不分配 ClusterIP,也不做任何代理转发,只是在 DNS 层面返回一个 CNAME 记录。比如把db.internal.example.com映射成mysql.prod.svc.cluster.local,集群内的应用用后者访问时,DNS 解析会指向外部域名。它适合做“名字适配”——让集群内应用用统一的内部命名访问外部服务,但要注意它不做端口转换、不做健康检查,纯粹是 DNS 层面的别名。
| 类型 | 可达范围 | 是否分配 ClusterIP | 典型场景 |
|---|---|---|---|
| ClusterIP | 集群内部 | 是 | 内部微服务调用 |
| NodePort | 集群外(经节点IP) | 是 | 临时调试、内部测试 |
| LoadBalancer | 集群外(经云LB) | 是 | 生产环境对外暴露 |
| ExternalName | 集群内部(DNS别名) | 否 | 外部域名映射 |
3. 用 Endpoints 把集群外服务接进来
现在进入这篇的重点。前面说的四种类型解决的都是“集群内 Pod 怎么暴露”的问题,但现实中经常遇到另一种需求:服务根本不在集群里。比如公司自建的 MySQL 跑在物理机上、某个老系统只提供固定 IP 的接口、第三方 API 只能通过特定地址访问。这些服务没有 Pod、没有标签,Service 的 selector 机制对它们完全失效。
这时候就要用到手动维护 Endpoints 的办法。核心思路是:创建一个不带 selector的 Service,然后手动创建一个同名的 Endpoints 对象,把外部服务的 IP 和端口填进去。这样 Service 就有了稳定的 ClusterIP,集群内的应用通过这个 ClusterIP 访问时,流量会被转发到你指定的外部地址。
3.1 为什么不能直接写 ExternalName
有人会问,ExternalName 不是也能指向外部吗?区别在于:ExternalName 只做 DNS 别名,客户端拿到解析结果后直接连外部地址,不经过 kube-proxy 的转发。这意味着它不支持端口映射(外部是 3306,你想用 3307 访问就不行),也不参与 Service 的负载均衡。而手动 Endpoints 的方式,流量是真正经过 Service 转发的,支持端口重映射、支持多个外部地址做负载均衡,灵活性高得多。
3.2 完整操作步骤
假设集群外有一台 MySQL,地址是192.168.10.50:3306,我们想让集群内应用通过external-mysql这个名字访问它。
第一步,创建不带 selector 的 Service:
apiVersion: v1 kind: Service metadata: name: external-mysql namespace: default spec: type: ClusterIP ports: - name: mysql port: 3306 targetPort: 3306 protocol: TCP注意这里故意不写 selector。一旦写了 selector,Kubernetes 的 Endpoints 控制器就会接管这个 Service,自动根据标签生成 Endpoints,你手动创建的同名 Endpoints 会被覆盖掉。
第二步,手动创建同名的 Endpoints:
apiVersion: v1 kind: Endpoints metadata: name: external-mysql namespace: default subsets: - addresses: - ip: 192.168.10.50 ports: - name: mysql port: 3306 protocol: TCP关键点:Endpoints 的 name 必须和 Service 的 name 完全一致,namespace 也必须一致。这是两者关联的唯一依据,名字对不上就不会生效。
第三步,验证:
kubectl get endpoints external-mysql kubectl describe endpoints external-mysql如果ENDPOINTS列显示192.168.10.50:3306,说明配置成功。然后从集群内任意 Pod 里测试:
kubectl run test --rm -it --image=busybox --restart=Never -- sh # 在容器内 nc -zv external-mysql 33063.3 多个外部地址做负载均衡
如果外部服务有多台(比如主从或者多活),可以在 Endpoints 的addresses里填多个 IP,kube-proxy 会在这些地址之间做轮询转发:
subsets: - addresses: - ip: 192.168.10.50 - ip: 192.168.10.51 ports: - name: mysql port: 3306不过要注意,这种负载均衡是无健康检查的。kube-proxy 不知道哪个后端挂了,会继续往挂掉的地址转发,导致部分请求失败。所以多地址场景下,要么确保外部服务本身有高可用机制,要么在应用层做重试。这是手动 Endpoints 相比自动管理最大的短板。
4. 排查 Endpoints 为空的完整链路
Endpoints 为空是 Service 相关故障里最高频的问题。我按实际排查顺序整理了一条链路,照着走基本能定位到根因。
4.1 第一步:确认 Service 有没有 selector
kubectl get service <name> -o yaml | grep -A5 selector如果 selector 为空,说明这是个手动管理 Endpoints 的 Service,那就要检查你手动创建的 Endpoints 对象是否存在、名字是否匹配。如果 selector 不为空,继续往下查。
4.2 第二步:确认 Pod 标签是否匹配
kubectl get pods --show-labels把 Pod 的标签和 Service 的 selector 逐字对比。常见问题包括:标签键拼写错误、值的大小写不一致、Pod 模板更新了但 Service 没跟着改。还有一种隐蔽情况:Pod 处于 Running 状态但Readiness Probe 没通过,这种 Pod 不会被加入 Endpoints。用kubectl describe pod看 Conditions 里的 Ready 字段,如果是 False,先解决探针问题。
4.3 第三步:确认 Pod 和 Service 是否在同一 namespace
Service 的 selector 只在同一个 namespace 内匹配 Pod。跨 namespace 的 Pod 即使标签完全一致也不会被选中。这个限制经常被忽略,尤其是多团队共用集群时。
4.4 第四步:检查端口名称是否对应
如果 Service 的targetPort写的是端口名字(比如http),那 Pod 的容器定义里必须有同名端口。名字对不上,Endpoints 里就不会有对应的端口条目,表现为 Service 存在但访问不通。
排查完这四步,Endpoints 为空的问题基本都能定位。我个人的经验是,八成的空 Endpoints 都是标签不匹配或 Readiness 没通过,剩下两成是 namespace 或端口名的问题。
5. 几个容易翻车的细节
5.1 Endpoints 和 EndpointSlice 的关系
较新的 Kubernetes 版本用 EndpointSlice 替代了 Endpoints 作为底层数据结构,一个 Service 可能对应多个 EndpointSlice(按地址数量分片)。但为了兼容,手动创建 Endpoints 对象的方式依然有效,控制器会自动把它同步成 EndpointSlice。查看时用kubectl get endpointslice能看到分片后的结果。手动维护时还是操作 Endpoints 对象,不用直接碰 EndpointSlice。
5.2 外部服务地址变更怎么办
手动 Endpoints 不会自动感知外部 IP 变化。如果外部服务迁移了地址,必须手动更新 Endpoints 对象。建议把 Endpoints 的 YAML 纳入版本管理,变更时走正常的发布流程,避免有人直接kubectl edit改完就忘了。另外可以配合 ConfigMap 或者外部 DNS 做一层抽象,减少硬编码。
5.3 网络策略的影响
如果集群启用了 NetworkPolicy,默认可能禁止 Pod 访问集群外地址。手动 Endpoints 的流量虽然目标是外部 IP,但源是集群内 Pod,出方向策略要放行。排查时如果 Endpoints 配置正确但访问不通,记得检查 NetworkPolicy 的 egress 规则。
5.4 kube-proxy 模式差异
iptables 模式和 IPVS 模式在 Service 转发上的行为略有不同。IPVS 模式下,ClusterIP 会真实地出现在一个虚拟网卡(kube-ipvs0)上,所以有时候能 ping 通;iptables 模式下则完全 ping 不通。这不是故障,是模式差异,排查时不要被误导。用kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode可以确认当前模式。
6. 写在最后的实操体会
Service 这套机制设计得很优雅,但优雅的背后是把复杂度藏进了 kube-proxy 和 DNS 里。我的建议是:遇到 Service 问题,先看 Endpoints,再看网络策略,最后才怀疑 kube-proxy。绝大多数问题都出在前两层,真正需要动 kube-proxy 配置的场景少之又少。
手动 Endpoints 这个技巧,用好了能把集群内外的服务统一到一套访问模型里,应用代码完全不用关心后端到底在集群内还是集群外。但它终究是个“半自动”方案,没有健康检查、不会自动更新,适合外部服务相对稳定的场景。如果外部服务变动频繁,还是考虑用专门的 Operator 或者服务网格来做服务发现,别硬扛。