Kubernetes Service:从稳定入口到服务发现
Pod 会因为发布、扩缩容、驱逐和节点故障不断被创建、删除,Pod IP 也可能随之变化。如果调用方直接保存 Pod IP,每次 Pod 重建都可能让地址失效;如果调用方自己监听 Pod 列表,又要重复处理健康状态、并发更新和负载分配。
Service 在调用方和 Pod 之间提供一个稳定的服务发现入口。普通 Service 通常有固定的 ClusterIP 和 DNS 名称,数据面再把连接转发给当前可用的后端;Headless Service 则不分配 ClusterIP,而是通过 DNS 暴露后端地址,让客户端自己选择具体 Pod。
本文以 Kubernetes v1.37 官方文档为基准,说明 Service、EndpointSlice、DNS 和流量转发之间的关系,并解释 ClusterIP、NodePort、LoadBalancer、ExternalName 与 Headless Service 分别适合什么场景。不同集群可能使用 kube-proxy、eBPF 数据面或云厂商实现,但 Service API 的核心语义相同。
Service 解决的是地址变化,不是 Pod 生命周期
Deployment、StatefulSet、DaemonSet 等工作负载控制器负责创建和维持 Pod。Service 不创建 Pod,也不决定 Pod 应该运行几个副本;它只根据标签选择后端,并为这些后端提供稳定入口。
一个普通 Service 的关系可以简化为:
| |
这里有两条独立链路:
- 控制链路:控制器根据 Service selector 和 Pod 状态维护 EndpointSlice。
- 数据链路:客户端访问 Service IP,kube-proxy 或替代数据面根据 EndpointSlice 选择后端。
Service 的 ClusterIP 是虚拟 IP,通常不是某台节点或某个容器网卡上真实监听的地址。以 kube-proxy 为例,它会监听 Service 和 EndpointSlice 的变化,再通过 iptables、IPVS、nftables 或 Windows 内核规则实现转发;有些 CNI 会用 eBPF 替代 kube-proxy。排查时应先确认集群实际使用的数据面,不能看到 Service 就假定一定经过 iptables。
一个最小的 ClusterIP Service
下面的 Deployment 创建三个可互换的 Web Pod,Service 通过标签选择它们:
| |
创建之后,可以看到三个不同层次的地址:
| |
客户端访问的是 DNS 名称或 ClusterIP。后端 Pod 被替换后,Service 地址不变,EndpointSlice 中的 Pod IP 会由控制器更新。
port、targetPort 和 nodePort
这三个字段分别处在不同位置:
| 字段 | 谁使用 | 含义 |
|---|---|---|
port | Service 客户端 | Service 暴露的端口 |
targetPort | Service 数据面 | 后端 Pod 实际接收流量的端口或端口名 |
nodePort | 集群外客户端 | NodePort Service 在每台节点上暴露的端口 |
例如:
| |
targetPort 可以写端口号,也可以引用 Pod 中命名的 containerPort。使用名称可以让 Service 配置保持不变,由不同版本的 Pod 把同一个端口名映射到各自端口号。
containerPort 本身不会让进程开始监听端口,也不是 Service 转发的强制前提。真正决定连接能否建立的是容器内进程是否监听 targetPort,以及网络策略和数据面是否允许这条流量。
EndpointSlice 才是当前后端清单
Service selector 匹配的是标签,不是 Deployment 名称,也不是 owner reference。只要同一命名空间内的 Pod 标签匹配,它就可能进入 Service 的后端集合,即使这个 Pod 由另一个控制器创建。
EndpointSlice 控制器会为带 selector 的 Service 自动创建 EndpointSlice。每个 endpoint 可以记录地址、节点、可用区以及以下状态:
ready:是否应作为普通 Service 流量的可用后端;serving:即使 endpoint 正在终止,它是否仍能提供服务;terminating:对应 Pod 是否正在终止。
正常情况下,readiness probe 未通过的 Pod 不会成为普通 Service 的 Ready 后端。检查后端时应直接查看 EndpointSlice:
| |
EndpointSlice 从 Kubernetes v1.21 起稳定。默认情况下,已有 EndpointSlice 都达到至少 100 个 endpoint 后,控制器会在出现新 endpoint 时创建另一个 slice。旧的 Endpoints API 从 Kubernetes v1.33 起已弃用,而且单个对象最多保留 1000 个后端;新工具和控制器应读取 EndpointSlice。
publishNotReadyAddresses 的边界
有些集群应用需要在 Ready 之前发现彼此,例如数据库成员在启动阶段先完成选主或状态同步。这时可以设置:
| |
这不是修复 readiness probe 的捷径。启用后,调用方可能连接到仍在初始化、尚不能处理业务请求的 Pod,应用必须能区分「可发现」和「可服务」。
Service type 决定入口
Service 的 type 只有四种稳定取值:ClusterIP、NodePort、LoadBalancer 和 ExternalName。Headless Service 不是第五种 type,而是把 ClusterIP Service 的 clusterIP 显式设置为 None。
| 类型 | 入口 | 主要用途 | 依赖条件 |
|---|---|---|---|
ClusterIP | 集群内部虚拟 IP | 集群内服务调用 | 集群 Service 数据面 |
NodePort | 每个节点的 IP 和固定端口 | 简单外部访问、外部 LB 后端 | 节点端口可达 |
LoadBalancer | 外部负载均衡器地址 | 云环境或 MetalLB 中暴露服务 | 对应 LB controller |
ExternalName | DNS CNAME | 给外部域名提供集群内别名 | 集群 DNS |
ClusterIP
ClusterIP 是默认类型。它适合应用之间的集群内调用,也通常是 Ingress 或 Gateway 后端 Service 的类型。
如果不显式填写 clusterIP,API Server 会从 Service CIDR 分配地址。除非已有兼容性需求,不要手工指定 ClusterIP;地址必须位于集群配置的 Service CIDR 中,而且创建后通常不能随意修改。
NodePort
NodePort 在 ClusterIP 之上增加一个节点端口:
| |
默认 NodePort 范围是 30000-32767,但集群管理员可以修改。NodePort 不表示流量一定落到当前节点上的 Pod;默认 externalTrafficPolicy: Cluster 可以继续转发到其他节点的 Ready endpoint。
LoadBalancer
LoadBalancer 请求云控制器或其他负载均衡实现创建外部入口:
| |
Kubernetes 只定义 API,不自带一个适用于所有环境的外部负载均衡器。没有对应 controller 时,Service 可能长期没有外部地址。多数实现会同时分配 NodePort;只有在负载均衡器支持直接把流量送到 Pod 或其他后端时,才适合设置 allocateLoadBalancerNodePorts: false。
spec.loadBalancerIP 从 Kubernetes v1.24 起已弃用,因为不同实现对它的解释不一致,也无法完整表达双栈地址。需要固定地址时,应使用当前负载均衡实现提供的配置方式。
ExternalName
ExternalName 不选择 Pod,也不经过 Service 代理。集群 DNS 会为 Service 名称返回 CNAME:
| |
查询 external-database.default.svc.cluster.local 会得到指向 database.example.com 的 CNAME。客户端随后直接解析和连接外部地址。
ExternalName 对 HTTP 和 TLS 需要额外注意:客户端最初使用的主机名可能仍是 Service 名称,HTTP Host、TLS SNI 和证书域名却未必与外部名称匹配。它适合明确支持这种别名行为的客户端,不能把它当成透明的四层代理。
Headless Service:把选择权交给客户端
Headless Service 的核心配置只有一项:
| |
它仍然是 Service,也仍然可以有 selector 和 EndpointSlice,但有三个关键差异:
- 不分配 ClusterIP;
- 不通过 Service VIP 或 kube-proxy 做平台侧负载均衡;
- Service DNS 返回 Ready endpoint 的 Pod IP 集合。
因此:
| |
DNS 可能轮换返回顺序,但这不等于 Kubernetes 提供了和普通 Service 相同的连接代理。客户端、驱动或应用协议要自行选择地址,并处理重试、成员变化和连接池刷新。
StatefulSet 为什么常和 Headless Service 配合
StatefulSet 保证稳定的 Pod 名称,例如 mysql-0、mysql-1,Headless Service 则为这些 Pod 提供稳定 DNS 域:
| |
对应 DNS 名称是:
| |
Pod 被重建后,名称和 DNS 仍可保持不变,但 DNS 记录指向的 Pod IP 可能变化。Headless Service 提供的是稳定名称,不是固定 Pod IP。
普通 Service 和 Headless Service 的 DNS
集群 DNS 为 Service 创建的完整名称通常是:
| |
默认集群域常见为 cluster.local,但它可以被修改。Pod 的 /etc/resolv.conf 通常包含当前命名空间和 svc 搜索域,因此同一命名空间中可以只访问 web;跨命名空间应至少使用 web.production,需要避免搜索域歧义时使用完整域名。
普通 Service 的 A/AAAA 记录指向 Service ClusterIP,Headless Service 的记录指向后端 endpoint。命名端口还可以生成 SRV 记录,让客户端同时发现主机和端口。
DNS 不是强一致配置接口。新 Pod 或 Service 刚创建时,客户端可能受到 CoreDNS TTL 和本地负缓存影响,短时间内继续看到旧结果或 NXDOMAIN。需要立即感知成员变化的控制器应监听 Kubernetes API,而不是靠高频 DNS 查询模拟 watch。
没有 selector 的 Service
Service 也可以不写 selector,用来表示 Kubernetes 集群外、另一个集群或由其他控制器管理的后端。此时 Kubernetes 不会自动创建 EndpointSlice,需要显式维护:
| |
手工 endpoint 不能使用环回地址、链路本地地址,也不能指向另一个 Service 的 ClusterIP。还要注意一个安全边界:API Server 不允许通过 kubectl port-forward service/... 代理到没有映射到 Pod 的 endpoint,因此 selectorless Service 的普通转发可以工作,kubectl port-forward service 却可能失败。
如果目标本身就是一个域名,优先判断 ExternalName 是否足够;只有需要明确端口映射、多个 IP 或普通 Service 转发语义时,才维护 selectorless Service 和 EndpointSlice。
Service 的负载分配边界
Service 是四层抽象,通常按连接选择 endpoint。一个已经建立的 TCP 连接不会因为出现新 Pod 就自动迁移,HTTP keep-alive、WebSocket 和 gRPC 长连接可能长时间停留在同一个后端。因此,副本数均衡不等于每个时刻的请求量一定均匀。
Session Affinity
默认 sessionAffinity: None。如果需要让相同来源 IP 的新连接倾向同一个后端,可以配置:
| |
这不是基于 Cookie 的应用层会话。经过 NAT 或外部代理后,多个真实用户可能表现为同一个来源 IP;需要可靠用户会话时,仍应使用共享状态或应用层机制。
Cluster 和 Local 流量策略
internalTrafficPolicy 和 externalTrafficPolicy 都支持 Cluster、Local:
Cluster:可以转发到集群内任意 Ready endpoint,是默认行为;Local:只使用当前节点上的 Ready endpoint,没有本地 endpoint 时不转发。
externalTrafficPolicy: Local 常用于减少跨节点转发并保留来源 IP,但它会让各节点承担的流量取决于本地 Pod 分布。使用前必须确认外部负载均衡器的健康检查、Pod 拓扑和节点缩容过程。
trafficDistribution 表达的是偏好,例如 PreferSameZone 或 PreferSameNode;Local 则是严格约束。PreferSameZone 需要集群启用 ServiceTrafficDistribution 特性并由 EndpointSlice 控制器为每个 endpoint 写出 hints,它只在 hints 可用时生效,不能把它当成「没有同区后端就丢弃流量」。
Service、Ingress 和 Gateway 的分工
这三个对象处理的层次不同:
| 对象 | 主要层次 | 解决的问题 |
|---|---|---|
| Service | 四层和服务发现 | 如何稳定找到一组后端 |
| Ingress | HTTP/HTTPS | 如何按域名、路径把外部请求路由到 Service |
| Gateway API | 多协议、角色分离的流量入口 | 如何表达更完整的网关、监听器和路由关系 |
Ingress 或 Gateway 通常仍把 Service 当作后端。创建 Ingress 不会让一个没有 endpoint 的 Service 自动恢复,创建 LoadBalancer Service 也不会自动获得 HTTP 路径路由和 TLS 证书管理。
一条可复用的排查路径
遇到「Service 访问不通」时,从声明到真实后端逐层检查,比先猜 kube-proxy 更快。
1. Service 是否存在,端口是否正确
| |
确认:
clusterIP是否已经分配,Headless Service 是否为None;port、targetPort、协议是否与应用一致;- NodePort、LoadBalancer 是否已经获得预期的外部入口。
2. selector 是否匹配目标 Pod
| |
如果查询不到 Pod,问题在标签或命名空间,不在转发数据面。Service selector 不能跨命名空间选择 Pod。
3. EndpointSlice 是否有 Ready endpoint
| |
常见结果:
- 没有 EndpointSlice:Service 没有 selector,或控制器未正常工作;
- EndpointSlice 没有地址:selector 没有匹配 Pod;
- 地址存在但
ready: false:readiness probe 或 Pod 状态未满足; - endpoint 端口错误:检查命名
targetPort和容器端口名称。
4. 应用是否真的监听目标端口
Service 和 EndpointSlice 正常,不代表容器内进程一定监听:
| |
同时检查 NetworkPolicy、服务网格规则和应用日志。readiness probe 使用的端口与 Service targetPort 不一致时,Pod 可能 Ready,但业务端口仍不可用。
5. 从调用方验证 DNS 和连接
| |
DNS 失败时检查 CoreDNS、Pod 的 dnsPolicy 和 /etc/resolv.conf;DNS 正常而 ClusterIP 不通,再检查 kube-proxy 或替代数据面、NetworkPolicy 以及 Service 流量策略。
常见现象对应的检查点
| 现象 | 优先检查 |
|---|---|
| Service 存在但没有 endpoint | selector、Pod 标签、命名空间 |
| endpoint 全部不是 Ready | readiness probe、Pod 状态 |
| DNS 能解析但连接被拒绝 | targetPort、应用监听地址和端口 |
| ClusterIP 可用,NodePort 不通 | 节点防火墙、NodePort 范围、external traffic policy |
| LoadBalancer 一直没有地址 | 云控制器或 LB controller、Service 事件 |
internalTrafficPolicy: Local 后部分节点不通 | 这些节点是否存在本地 Ready endpoint |
| Headless DNS 返回多个 IP,但流量不均 | 客户端 DNS 缓存、连接池和重试策略 |
如何选择
可以按调用方需要的入口选择:
| |
面对「Service 访问不通」,更完整的生命周期证据和 DNS/排空时间线在 生产实践篇 里展开。判断 Service 是否正确,最终要同时看四层对象:Service 声明、EndpointSlice 后端、DNS 记录和实际数据面。ClusterIP 提供稳定虚拟入口,Headless Service 提供后端发现;两者都不会替应用解决成员协议、长连接再均衡或错误重试。