
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 的核心语义相同。

<!--more-->

## Service 解决的是地址变化，不是 Pod 生命周期

Deployment、StatefulSet、DaemonSet 等工作负载控制器负责创建和维持 Pod。Service 不创建 Pod，也不决定 Pod 应该运行几个副本；它只根据标签选择后端，并为这些后端提供稳定入口。

一个普通 Service 的关系可以简化为：

```text
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 通过标签选择它们：

```yaml
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 的端口。
```

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

```text
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`

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

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

例如：

```text
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：

```sh
# 查看 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 之前发现彼此，例如数据库成员在启动阶段先完成选主或状态同步。这时可以设置：

```yaml
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 |
| `ExternalName` | DNS CNAME | 给外部域名提供集群内别名 | 集群 DNS |

### ClusterIP

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

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

### NodePort

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

```yaml
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` 请求云控制器或其他负载均衡实现创建外部入口：

```yaml
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：

```yaml
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 的核心配置只有一项：

```yaml
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 集合。

因此：

```text
普通 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 域：

```yaml
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 名称是：

```text
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 创建的完整名称通常是：

```text
<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，需要显式维护：

```yaml
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 的新连接倾向同一个后端，可以配置：

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

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

## 一条可复用的排查路径

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

### 1. Service 是否存在，端口是否正确

```sh
kubectl get service web -o yaml
```

确认：

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

### 2. selector 是否匹配目标 Pod

```sh
# 先读取 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

```sh
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 正常，不代表容器内进程一定监听：

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

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

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

```sh
# 在真实调用方 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 存在但没有 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 缓存、连接池和重试策略 |

## 如何选择

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

```text
只需要集群内稳定入口
└── ClusterIP

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

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

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

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

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

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

## 参考资料

- [Service](https://kubernetes.io/docs/concepts/services-networking/service/)
- [EndpointSlice](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/)
- [Virtual IPs and Service Proxies](https://kubernetes.io/docs/reference/networking/virtual-ips/)
- [DNS for Services and Pods](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/)
- [Service Internal Traffic Policy](https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/)
- [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
- [Gateway API](https://gateway-api.sigs.k8s.io/)

