jimyag's Blog

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 的关系可以简化为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Deployment / StatefulSet
          │ 创建 Pod
          ▼
Pods:标签、Pod IP、Ready 状态
          │ Service selector 匹配
          ▼
EndpointSlice:记录当前后端地址和状态
          │ 数据面监听变化
          ▼
Service ClusterIP:port
          │ 选择一个 Ready endpoint
          ▼
Pod IP:targetPort

这里有两条独立链路:

  • 控制链路:控制器根据 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 通过标签选择它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1 # Deployment API 版本。
kind: Deployment # 管理一组可互换的 Pod。
metadata:
  name: web
spec:
  replicas: 3 # 运行三个后端副本。
  selector:
    matchLabels:
      app.kubernetes.io/name: web # 必须与 Pod 标签一致。
  template:
    metadata:
      labels:
        app.kubernetes.io/name: web # Service 也使用这个标签选择 Pod。
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - name: http # 为容器端口命名,供 Service 的 targetPort 引用。
              containerPort: 80
          readinessProbe: # 未 Ready 的 Pod 默认不接收 Service 流量。
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 2
            periodSeconds: 5
---
apiVersion: v1 # Service 属于 core/v1 API。
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP # 默认类型,只在集群内部提供虚拟 IP。
  selector:
    app.kubernetes.io/name: web # 选择同一命名空间内匹配标签的 Pod。
  ports:
    - name: http
      protocol: TCP
      port: 80 # 客户端访问 Service 时使用的端口。
      targetPort: http # 转发到 Pod 中名为 http 的端口。

创建之后,可以看到三个不同层次的地址:

1
2
3
Service DNS:web.default.svc.cluster.local
Service VIP:10.96.120.10:80
Pod endpoint:10.244.1.8:80、10.244.2.5:80、10.244.3.7:80

客户端访问的是 DNS 名称或 ClusterIP。后端 Pod 被替换后,Service 地址不变,EndpointSlice 中的 Pod IP 会由控制器更新。

port、targetPort 和 nodePort

这三个字段分别处在不同位置:

字段谁使用含义
portService 客户端Service 暴露的端口
targetPortService 数据面后端 Pod 实际接收流量的端口或端口名
nodePort集群外客户端NodePort Service 在每台节点上暴露的端口

例如:

1
2
NodeIP:30080 -> ServiceIP:80 -> PodIP:8080
      nodePort        port       targetPort

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 查看 Service 的 ClusterIP、端口和 selector。
kubectl get service web -o wide
kubectl describe service web

# 查看 Service 对应的 EndpointSlice;不要只依赖旧 Endpoints 对象。
kubectl get endpointslice \
  -l kubernetes.io/service-name=web \
  -o wide

# 查看每个 endpoint 的地址和 ready 状态。
kubectl get endpointslice \
  -l kubernetes.io/service-name=web \
  -o yaml

EndpointSlice 从 Kubernetes v1.21 起稳定。默认情况下,已有 EndpointSlice 都达到至少 100 个 endpoint 后,控制器会在出现新 endpoint 时创建另一个 slice。旧的 Endpoints API 从 Kubernetes v1.33 起已弃用,而且单个对象最多保留 1000 个后端;新工具和控制器应读取 EndpointSlice。

publishNotReadyAddresses 的边界

有些集群应用需要在 Ready 之前发现彼此,例如数据库成员在启动阶段先完成选主或状态同步。这时可以设置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: peer-discovery
spec:
  clusterIP: None # 使用 Headless Service 暴露各个成员地址。
  publishNotReadyAddresses: true # DNS 也发布尚未 Ready 的成员。
  selector:
    app.kubernetes.io/name: database
  ports:
    - name: peer
      port: 7000
      targetPort: peer

这不是修复 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
ExternalNameDNS CNAME给外部域名提供集群内别名集群 DNS

ClusterIP

ClusterIP 是默认类型。它适合应用之间的集群内调用,也通常是 Ingress 或 Gateway 后端 Service 的类型。

如果不显式填写 clusterIP,API Server 会从 Service CIDR 分配地址。除非已有兼容性需求,不要手工指定 ClusterIP;地址必须位于集群配置的 Service CIDR 中,而且创建后通常不能随意修改。

NodePort

NodePort 在 ClusterIP 之上增加一个节点端口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: web-node-port
spec:
  type: NodePort # 在所有节点上开放同一个端口。
  selector:
    app.kubernetes.io/name: web
  ports:
    - name: http
      port: 80 # 集群内仍可通过 ClusterIP:80 访问。
      targetPort: http # 转发到 Pod 的命名端口。
      nodePort: 30080 # 外部通过任意 NodeIP:30080 访问。

默认 NodePort 范围是 30000-32767,但集群管理员可以修改。NodePort 不表示流量一定落到当前节点上的 Pod;默认 externalTrafficPolicy: Cluster 可以继续转发到其他节点的 Ready endpoint。

LoadBalancer

LoadBalancer 请求云控制器或其他负载均衡实现创建外部入口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: web-public
spec:
  type: LoadBalancer # 需要云厂商、MetalLB 或其他 LB controller 实现。
  selector:
    app.kubernetes.io/name: web
  ports:
    - name: http
      port: 80 # 外部负载均衡器暴露的 Service 端口。
      targetPort: http

Kubernetes 只定义 API,不自带一个适用于所有环境的外部负载均衡器。没有对应 controller 时,Service 可能长期没有外部地址。多数实现会同时分配 NodePort;只有在负载均衡器支持直接把流量送到 Pod 或其他后端时,才适合设置 allocateLoadBalancerNodePorts: false。

spec.loadBalancerIP 从 Kubernetes v1.24 起已弃用,因为不同实现对它的解释不一致,也无法完整表达双栈地址。需要固定地址时,应使用当前负载均衡实现提供的配置方式。

ExternalName

ExternalName 不选择 Pod,也不经过 Service 代理。集群 DNS 会为 Service 名称返回 CNAME:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
  name: external-database
  namespace: default
spec:
  type: ExternalName # 只创建 DNS 别名,不创建 ClusterIP 或 endpoint。
  externalName: database.example.com # 必须是 DNS 名称,不是 IP 地址。

查询 external-database.default.svc.cluster.local 会得到指向 database.example.com 的 CNAME。客户端随后直接解析和连接外部地址。

ExternalName 对 HTTP 和 TLS 需要额外注意:客户端最初使用的主机名可能仍是 Service 名称,HTTP Host、TLS SNI 和证书域名却未必与外部名称匹配。它适合明确支持这种别名行为的客户端,不能把它当成透明的四层代理。

Headless Service:把选择权交给客户端

Headless Service 的核心配置只有一项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  clusterIP: None # None 是特殊值,表示不分配 Service VIP。
  selector:
    app.kubernetes.io/name: mysql
  ports:
    - name: mysql
      port: 3306
      targetPort: mysql

它仍然是 Service,也仍然可以有 selector 和 EndpointSlice,但有三个关键差异:

  1. 不分配 ClusterIP;
  2. 不通过 Service VIP 或 kube-proxy 做平台侧负载均衡;
  3. Service DNS 返回 Ready endpoint 的 Pod IP 集合。

因此:

1
2
3
4
5
6
7
普通 Service DNS:
web.default.svc.cluster.local -> 10.96.120.10

Headless Service DNS:
mysql.default.svc.cluster.local -> 10.244.1.8
                                      10.244.2.5
                                      10.244.3.7

DNS 可能轮换返回顺序,但这不等于 Kubernetes 提供了和普通 Service 相同的连接代理。客户端、驱动或应用协议要自行选择地址,并处理重试、成员变化和连接池刷新。

StatefulSet 为什么常和 Headless Service 配合

StatefulSet 保证稳定的 Pod 名称,例如 mysql-0、mysql-1,Headless Service 则为这些 Pod 提供稳定 DNS 域:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql # 引用同名 Headless Service 作为网络域。
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: mysql
  template:
    metadata:
      labels:
        app.kubernetes.io/name: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.4
          ports:
            - name: mysql
              containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD # 示例使用 Secret,避免把密码写进 YAML。
              valueFrom:
                secretKeyRef:
                  name: mysql-credentials
                  key: root-password

对应 DNS 名称是:

1
2
3
mysql-0.mysql.default.svc.cluster.local
mysql-1.mysql.default.svc.cluster.local
mysql-2.mysql.default.svc.cluster.local

Pod 被重建后,名称和 DNS 仍可保持不变,但 DNS 记录指向的 Pod IP 可能变化。Headless Service 提供的是稳定名称,不是固定 Pod IP。

普通 Service 和 Headless Service 的 DNS

集群 DNS 为 Service 创建的完整名称通常是:

1
<service>.<namespace>.svc.<cluster-domain>

默认集群域常见为 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,需要显式维护:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: v1
kind: Service
metadata:
  name: legacy-api
spec:
  ports:
    - name: https
      protocol: TCP
      port: 443 # 客户端访问 Service 的端口。
      targetPort: 8443 # 转发到 EndpointSlice 声明的后端端口。
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: legacy-api-1
  labels:
    kubernetes.io/service-name: legacy-api # 把 EndpointSlice 关联到 Service。
    endpointslice.kubernetes.io/managed-by: staff # 标明由谁维护,避免控制器冲突。
addressType: IPv4
ports:
  - name: https # 必须与 Service 端口名称对应。
    protocol: TCP
    port: 8443
endpoints:
  - addresses:
      - 192.0.2.10 # 示例地址,实际环境替换为可路由的后端 IP。
    conditions:
      ready: true

手工 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 的新连接倾向同一个后端,可以配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
  name: web-sticky
spec:
  selector:
    app.kubernetes.io/name: web
  sessionAffinity: ClientIP # 根据数据面看到的客户端 IP 保持会话。
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 3600 # 一小时后允许重新选择后端。
  ports:
    - name: http
      port: 80
      targetPort: http

这不是基于 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四层和服务发现如何稳定找到一组后端
IngressHTTP/HTTPS如何按域名、路径把外部请求路由到 Service
Gateway API多协议、角色分离的流量入口如何表达更完整的网关、监听器和路由关系

Ingress 或 Gateway 通常仍把 Service 当作后端。创建 Ingress 不会让一个没有 endpoint 的 Service 自动恢复,创建 LoadBalancer Service 也不会自动获得 HTTP 路径路由和 TLS 证书管理。

一条可复用的排查路径

遇到「Service 访问不通」时,从声明到真实后端逐层检查,比先猜 kube-proxy 更快。

1. Service 是否存在,端口是否正确

1
kubectl get service web -o yaml

确认:

  • clusterIP 是否已经分配,Headless Service 是否为 None;
  • port、targetPort、协议是否与应用一致;
  • NodePort、LoadBalancer 是否已经获得预期的外部入口。

2. selector 是否匹配目标 Pod

1
2
3
# 先读取 Service 的 selector,再用同一组标签查询 Pod。
kubectl get service web -o jsonpath='{.spec.selector}{"\n"}'
kubectl get pod -l 'app.kubernetes.io/name=web' -o wide

如果查询不到 Pod,问题在标签或命名空间,不在转发数据面。Service selector 不能跨命名空间选择 Pod。

3. EndpointSlice 是否有 Ready endpoint

1
2
3
kubectl get endpointslice \
  -l kubernetes.io/service-name=web \
  -o yaml

常见结果:

  • 没有 EndpointSlice:Service 没有 selector,或控制器未正常工作;
  • EndpointSlice 没有地址:selector 没有匹配 Pod;
  • 地址存在但 ready: false:readiness probe 或 Pod 状态未满足;
  • endpoint 端口错误:检查命名 targetPort 和容器端口名称。

4. 应用是否真的监听目标端口

Service 和 EndpointSlice 正常,不代表容器内进程一定监听:

1
2
# 使用目标镜像已有的工具检查;极简镜像可改用 Ephemeral Container。
kubectl exec deploy/web -- wget -qO- http://127.0.0.1:80/

同时检查 NetworkPolicy、服务网格规则和应用日志。readiness probe 使用的端口与 Service targetPort 不一致时,Pod 可能 Ready,但业务端口仍不可用。

5. 从调用方验证 DNS 和连接

1
2
3
4
# 在真实调用方 Pod 中检查,避免只从节点网络推断 Pod 网络。
client_pod=client-pod-name
kubectl exec "$client_pod" -- nslookup web.default.svc.cluster.local
kubectl exec "$client_pod" -- wget -qO- http://web.default.svc.cluster.local/

DNS 失败时检查 CoreDNS、Pod 的 dnsPolicy 和 /etc/resolv.conf;DNS 正常而 ClusterIP 不通,再检查 kube-proxy 或替代数据面、NetworkPolicy 以及 Service 流量策略。

常见现象对应的检查点

现象优先检查
Service 存在但没有 endpointselector、Pod 标签、命名空间
endpoint 全部不是 Readyreadiness probe、Pod 状态
DNS 能解析但连接被拒绝targetPort、应用监听地址和端口
ClusterIP 可用,NodePort 不通节点防火墙、NodePort 范围、external traffic policy
LoadBalancer 一直没有地址云控制器或 LB controller、Service 事件
internalTrafficPolicy: Local 后部分节点不通这些节点是否存在本地 Ready endpoint
Headless DNS 返回多个 IP,但流量不均客户端 DNS 缓存、连接池和重试策略

如何选择

可以按调用方需要的入口选择:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
只需要集群内稳定入口
└── ClusterIP

需要调用方发现并直连每个成员
└── Headless Service

需要通过节点端口暴露
└── NodePort

需要基础设施创建外部负载均衡器
└── LoadBalancer

需要为外部域名提供集群内 DNS 别名
└── ExternalName

需要按 HTTP 域名或路径路由
└── ClusterIP Service + Ingress / Gateway

面对「Service 访问不通」,更完整的生命周期证据和 DNS/排空时间线在 生产实践篇 里展开。判断 Service 是否正确,最终要同时看四层对象:Service 声明、EndpointSlice 后端、DNS 记录和实际数据面。ClusterIP 提供稳定虚拟入口,Headless Service 提供后端发现;两者都不会替应用解决成员协议、长连接再均衡或错误重试。

参考资料

#Kubernetes #Service #EndpointSlice #网络