Kubernetes 基础体系 · 第 26/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes Service:ClusterIP、NodePort、LoadBalancer、EndpointSlice 和流量

Kubernetes Pod 是可替换的:Pod 重建后 IP 可能改变,副本扩缩容后后端集合也会变化。Service 的核心作用,是为一组后端 Pod 提供稳定的访问抽象:

客户端稳定的 Service 地址某个可用后端\text{客户端} \rightarrow \text{稳定的 Service 地址} \rightarrow \text{某个可用后端}

其中“稳定地址”通常是 DNS 名称和虚拟 IP,“后端集合”由 EndpointSlice 描述,“如何转发”则由 kube-proxy 或其他服务代理实现。

Service 不是应用层网关。它通常只根据目标 IP、端口和传输协议转发 TCP、UDP 或 SCTP 流量,不理解 HTTP URL、Host、TLS 证书或业务路由。需要七层路由时,应使用 Ingress 或 Gateway API。


一、先建立完整的对象模型

一个典型 Service 至少涉及四类信息:

  1. Service 对象:定义稳定名称、虚拟 IP、端口映射、流量策略等。
  2. Pod 标签:Service 通过 selector 选择后端 Pod。
  3. EndpointSlice 对象:记录当前后端 Pod 的地址、端口和就绪状态。
  4. 节点上的服务转发规则:通常由 kube-proxy 根据 Service 和 EndpointSlice 同步生成。

例如:

apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: default
spec:
  selector:
    app: web
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - name: http
              containerPort: 8080

这里有三个不同的端口概念:

  • port: 80:Service 对客户端暴露的端口。
  • targetPort: 8080:Service 转发到后端 Pod 的端口。
  • containerPort: 8080:容器声明的端口,主要用于描述和工具发现,不会单独创建转发规则。

如果 targetPort 使用字符串,例如:

ports:
  - name: http
    port: 80
    targetPort: http

Kubernetes 会根据 Pod 容器端口中名为 http 的端口解析目标端口。使用命名端口可以避免应用端口变更时同时修改多个对象,但名称必须真实存在且协议匹配。

Service selector 中的标签匹配是逻辑上的集合运算:

E={pp 的标签匹配 selector,且地址可用于服务转发}E = \{p \mid p \text{ 的标签匹配 selector,且地址可用于服务转发}\}

Service 本身不直接保存这个集合;控制器会根据 Pod 状态生成或更新 EndpointSlice。


二、Service 的稳定地址:DNS、ClusterIP 与端口

创建 Service 后,集群 DNS 通常提供:

web.default.svc.cluster.local

在同一命名空间中,客户端通常可以直接使用:

web

完整 DNS 名称的组成是:

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

集群内客户端解析 Service 名称时,通常得到一个或多个 Service IP,而不是某个 Pod IP。对于普通 Service,这个 IP 是 clusterIP,也称为虚拟 IP(virtual IP)。

Service 的虚拟 IP 通常不会作为某个网卡接口上的真实地址存在。节点上的转发规则会匹配发往这个 IP 和端口的连接,然后选择后端 Endpoint。可将其抽象为:

(SIP,SP)(EIPi,EPi)(SIP, SP) \longrightarrow (EIP_i, EP_i)

其中:

  • SIP 是 Service IP;
  • SP 是 Service port;
  • EIP_i 是第 ii 个后端地址;
  • EP_i 是后端端口。

例如:

10.96.20.10:80  ->  10.244.1.12:8080
                         或
                         10.244.2.7:8080
                         或
                         10.244.3.4:8080

客户端通常只感知 10.96.20.10:80,后端 Pod 的变化由控制器和节点转发规则处理。

clusterIP 的地址范围

集群控制面会从配置的 Service CIDR 中分配 ClusterIP。这个地址应被视为集群内部的虚拟地址,而不是可从互联网直接路由到的地址。

可以查看实际分配结果:

kubectl get svc web -o wide
kubectl get svc web -o jsonpath='{.spec.clusterIP}{"\n"}'

典型输出可能是:

NAME   TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE   SELECTOR
web    ClusterIP   10.96.20.10   <none>        80/TCP    20s   app=web

具体 IP、Service CIDR 和输出格式取决于集群配置。


三、ClusterIP:集群内部的虚拟服务

ClusterIP 是默认 Service 类型。它的语义是:

  • 分配一个集群内部可访问的虚拟 IP;
  • 通过 DNS 提供稳定名称;
  • 由服务转发组件将流量送往 EndpointSlice 中的后端;
  • 默认不承诺从集群外部直接访问。

配置可以显式写出:

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: ClusterIP
  selector:
    app: api
  ports:
    - name: http
      port: 80
      targetPort: 8080

客户端测试:

kubectl run curl --rm -it \
  --image=curlimages/curl:8.10.1 \
  --restart=Never -- \
  curl -v http://api.default.svc.cluster.local/

前置条件是 api Service 存在,并且后端容器确实在 8080 端口提供 HTTP 服务。命令中的临时 Pod 必须能够访问集群网络;如果网络策略、DNS 或镜像拉取受到限制,失败原因可能不在 Service 本身。

ClusterIP 并不等于“任意节点都监听这个地址”

常见误解是:每个节点都有一个真正监听 clusterIP:port 的进程。通常并非如此。kube-proxy 会在节点内核的数据路径中安装规则,数据包进入节点后被规则匹配和改写。

不同 kube-proxy 模式的细节不同:

  • iptables 模式通常使用 DNAT 规则和概率或统计匹配在多个 Endpoint 之间选择。
  • IPVS 模式使用内核 IPVS 虚拟服务和后端集合;具体可用性取决于集群版本和发行版配置。
  • nftables 模式使用 nftables 数据路径;它是较新的 kube-proxy 实现,启用条件和默认值依 Kubernetes 版本、发行版而变化。
  • 也存在替代 kube-proxy 的实现,例如基于 eBPF 的网络组件;它们可能不遵循 kube-proxy 的具体实现方式,但仍需实现 Kubernetes Service 语义。

因此,Service API 是规范保证;iptables、IPVS、nftables 和 eBPF 的规则形态是实现细节,不能把某一种节点命令输出当作所有集群的通用事实。


四、Service 的端口映射与协议

Service 的每个端口至少包含:

ports:
  - name: https
    protocol: TCP
    port: 443
    targetPort: 8443
  • name:在多端口 Service 中应使用唯一名称;命名端口还可供其他对象引用。
  • protocol:常见为 TCPUDPSCTP,默认是 TCP。
  • port:Service 对外提供的端口。
  • targetPort:后端端口,可以是数字或命名端口。

Service 不会自动把 HTTP 的 443 转成 TLS,也不会检查证书。若后端服务提供的是纯 HTTP,客户端访问 Service 的 443 仍然只是向某个 TCP 端口建立连接,是否能完成 TLS 取决于后端应用。

当一个 Service 同时暴露多个端口时,端口名称和协议尤其重要:

ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 8080
  - name: metrics
    protocol: TCP
    port: 9090
    targetPort: 9090

对 TCP 和 UDP 使用相同端口号并不表示它们冲突;传输层协议不同,Service 端口条目的匹配空间也不同。


五、EndpointSlice:Service 后端状态的实际载体

5.1 为什么需要 EndpointSlice

早期 Kubernetes 使用 Endpoints 对象描述 Service 后端。当一个 Service 拥有大量 Pod 时,单个对象会不断变大,每次 Pod 变化都可能导致整个对象更新和传输。

EndpointSlice 将后端拆成多个切片。一个 EndpointSlice 通常包含一组具有相同地址族、协议、端口描述和拓扑属性的 Endpoint。这样可以降低大规模集群中单个对象更新的成本。

查看 EndpointSlice:

kubectl get endpointslice \
  -l kubernetes.io/service-name=web

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

选择器 kubernetes.io/service-name=web 用于找到属于 web Service 的切片。不要假设只存在一个 EndpointSlice。

5.2 EndpointSlice 的关键字段

EndpointSlice 的常见结构可以简化为:

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: web-abcde
  labels:
    kubernetes.io/service-name: web
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses:
      - 10.244.1.12
    conditions:
      ready: true
      serving: true
      terminating: false
    nodeName: worker-a
    targetRef:
      kind: Pod
      name: web-7d9f...
  - addresses:
      - 10.244.2.7
    conditions:
      ready: false
      serving: true
      terminating: false
    nodeName: worker-b

关键字段的语义如下:

  • addressType:例如 IPv4IPv6FQDN
  • ports:该切片中 Endpoint 对应的端口定义。
  • addresses:后端地址。对普通 Pod Service,通常是 Pod IP。
  • conditions.ready:后端是否就绪,通常与 Pod readiness 相关。
  • conditions.serving:后端是否仍在提供服务。
  • conditions.terminating:后端是否正在终止。
  • nodeName:后端所在节点,可用于拓扑和本地流量选择。
  • targetRef:通常指向对应 Pod,便于追踪来源。

实际的服务转发通常优先选择可用、就绪的 Endpoint。终止中的 Endpoint 具有更细的状态组合,供控制器和实现处理优雅终止;不能简单地把所有出现在 EndpointSlice 中的地址都理解成“新连接一定会被发送到的后端”。

5.3 EndpointSlice 的生成过程

对带 selector 的 Service,控制器大致执行以下过程:

  1. 观察 Service 的 selector。
  2. 找到标签匹配的 Pod。
  3. 检查 Pod IP、容器端口和就绪状态。
  4. 按地址族、端口和其他兼容属性分组。
  5. 创建、更新或删除 EndpointSlice。
  6. kube-proxy 或其他服务实现观察 Service 与 EndpointSlice。
  7. 节点数据路径更新后,新的连接才使用新的后端集合。

这意味着 Pod 已经变为 Ready 与节点转发规则完成更新之间存在传播延迟。规范提供最终一致性,而不是每个状态变化都同步完成的瞬间一致性。

5.4 没有 selector 的 Service

Service 可以不写 selector:

apiVersion: v1
kind: Service
metadata:
  name: external-api
spec:
  ports:
    - name: https
      port: 443
      targetPort: 443

这时 Kubernetes 不会根据 Pod 自动生成后端。管理员或控制器可以手动创建 EndpointSlice,例如:

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: external-api-1
  labels:
    kubernetes.io/service-name: external-api
addressType: IPv4
ports:
  - name: https
    protocol: TCP
    port: 443
endpoints:
  - addresses:
      - 192.0.2.20
    conditions:
      ready: true

这种方式可把 Service 抽象到集群外部的固定地址,但需要自行维护地址的正确性、健康状态和安全边界。EndpointSlice 地址不能随意指向集群控制面或 Kubernetes API Server 等受限制目标;Admission 和服务实现可能拒绝或限制某些地址。

5.5 Headless Service

设置:

spec:
  clusterIP: None

得到 Headless Service。它没有虚拟 ClusterIP,DNS 通常直接返回后端 Pod 地址或相关记录:

apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  clusterIP: None
  selector:
    app: db
  ports:
    - name: client
      port: 5432
      targetPort: 5432

Headless Service 的含义不是“没有服务发现”,而是“不提供一个由 Service 虚拟 IP 汇聚的单一入口”。客户端或上层系统会得到多个后端地址,并自行决定连接哪个地址。

StatefulSet 常用 Headless Service 配合稳定的 Pod DNS 名称进行成员发现。它不会自动提供数据库主从选举、故障转移或连接池管理。


六、NodePort:在每个节点暴露一个端口

NodePort 在 ClusterIP 的基础上,为 Service 分配一个节点端口:

apiVersion: v1
kind: Service
metadata:
  name: web-nodeport
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080
      nodePort: 30080

此时通常存在两条访问路径:

节点 IP:30080 -> Service 转发逻辑 -> Pod:8080

以及:

节点 IP:30080 -> 对应的 ClusterIP:80 -> Pod:8080

NodePort 的默认分配范围由集群配置决定,常见默认范围是 30000-32767,但不能把这个范围当成所有集群的规范保证。显式指定 nodePort 时必须位于允许范围内,并且不能与同一协议下已有端口冲突。

查看分配结果:

kubectl get svc web-nodeport
kubectl describe svc web-nodeport

典型端口列可能显示:

80:30080/TCP

这表示 Service port 是 80,节点端口是 30080,而不是后端 Pod 必须监听 30080

NodePort 的真实边界

NodePort 需要满足两个条件:

  1. 访问者能够到达某个节点的节点 IP 和 NodePort。
  2. 节点防火墙、云安全组、网络 ACL 和 kube-proxy 数据路径允许该流量通过。

Kubernetes 创建 NodePort 不会自动替云环境开放安全组,也不会保证所有节点 IP 都能从互联网路由到。裸机环境中还要考虑节点上的其他进程是否占用端口、外部负载均衡器探活是否能到达节点,以及节点下线时如何摘除流量。

NodePort 常被外部负载均衡器作为后端目标:

外部 LB:443
    -> node-a:30080
    -> node-b:30080
    -> node-c:30080

但如果外部 LB 本身能够直接发现 Pod,并且 Kubernetes Service 实现支持绕过 NodePort,则不一定需要这层节点端口。


七、LoadBalancer:请求外部负载均衡能力,而不是 Kubernetes 自己实现公网入口

LoadBalancer Service 表达的是:

请基础设施或云控制器为这个 Service 提供一个外部可达的负载均衡入口。

示例:

apiVersion: v1
kind: Service
metadata:
  name: web-public
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080

创建过程通常是:

  1. API Server 接受 Service。
  2. 云控制器或其他 LoadBalancer 实现观察到 type: LoadBalancer
  3. 它创建或配置外部负载均衡器。
  4. 外部地址写入 status.loadBalancer.ingress
  5. 外部负载均衡器把流量送到节点端口或直接送到 Pod,取决于实现。
  6. 节点服务转发和 EndpointSlice 变化继续影响后端选择。

查看状态:

kubectl get svc web-public -w

EXTERNAL-IP 长时间处于 <pending>,通常意味着没有可用的 LoadBalancer Controller、云凭据错误、子网或配额问题,或者控制器无法创建外部资源。这不是应用容器监听错误的直接证据。

LoadBalancer 与 NodePort 的关系

历史上,大量 Kubernetes 实现通过 NodePort 实现 LoadBalancer:

外部地址:80
       -> 节点:NodePort
       -> Service ClusterIP/后端规则
       -> Pod

这也是为什么一个 LoadBalancer Service 常常同时显示:

80:31234/TCP

但当前 Service API 允许实现使用:

spec:
  allocateLoadBalancerNodePorts: false

前提是底层 LoadBalancer 实现支持不依赖 NodePort 的流量路径,例如直接把外部负载均衡器配置到 Pod 后端。设置该字段不会凭空让云平台获得“直达 Pod”的能力;不支持该语义的实现可能导致流量不可达。对于已有 Service,关闭 NodePort 分配也不会自动删除历史上已经分配的 nodePort,通常需要显式清理。

loadBalancerClass

集群中可能存在多个 LoadBalancer 实现。可以通过 loadBalancerClass 指定由哪一类实现处理:

apiVersion: v1
kind: Service
metadata:
  name: web-private
spec:
  type: LoadBalancer
  loadBalancerClass: example.com/internal-lb
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080

具体 class 名称必须由实际安装的控制器定义。不能任意写一个字符串后期待 Kubernetes 自动创建负载均衡器。

云厂商差异

以下行为没有统一的跨云保证:

  • 公网还是内网负载均衡器;
  • 是否自动分配公网地址;
  • 是否支持跨区域后端;
  • 健康检查探测哪个端口;
  • 是否保留客户端源 IP;
  • 是否支持 externalTrafficPolicy: Local
  • 是否支持直接 Pod 后端;
  • 注解和参数名称;
  • 删除 Service 后外部负载均衡器资源是否同步删除。

因此,LoadBalancer 是 Kubernetes 对控制器的请求,不是一个内置、跨平台完全同构的云产品 API。


八、从客户端到 Pod:一条 TCP 流量的完整路径

以同一集群内的客户端访问 ClusterIP 为例:

sequenceDiagram
    participant C as 客户端 Pod
    participant D as 集群 DNS
    participant K as 节点服务转发规则
    participant E as Endpoint Pod

    C->>D: 查询 web.default.svc.cluster.local
    D-->>C: 返回 ClusterIP
    C->>K: 连接 ClusterIP:80
    K->>K: 根据 Service/EndpointSlice 选择后端
    K->>E: DNAT 或等效转发到 PodIP:8080
    E-->>K: 返回响应
    K-->>C: 返回响应

具体执行并不是“每个请求都重新查询 DNS 并随机选 Pod”:

  1. DNS 解析结果通常会被客户端、语言运行时或本地 DNS 缓存。
  2. 对 TCP,客户端先建立连接;一旦连接选择了后端,后续数据通常沿同一连接到同一后端。
  3. kube-proxy 或替代实现使用 Service 和 EndpointSlice 状态更新节点数据路径。
  4. 连接是否能在 Endpoint 删除后继续存在,取决于连接状态、协议、终止处理和实现;删除 Endpoint 不等于立即强制关闭所有已建立连接。
  5. 新连接通常不会继续选择已经不满足就绪条件的后端,但状态传播存在短暂延迟。

对 UDP,连接状态不具备 TCP 的同样语义。实现可能使用流表、连接跟踪或其他机制保持一段时间的后端关联,因此“每个 UDP 数据报都严格重新随机选择”也不是可以泛化的保证。

后端选择不是应用层负载均衡

Service 通常在传输层做后端选择。它不会知道:

  • 某个 HTTP 请求是否是登录请求;
  • 某个用户是否已经登录到某个 Pod;
  • 某个后端的业务队列是否已满;
  • URL /orders/1 应该路由到哪个版本。

因此,Service 的“负载均衡”更准确地说是连接或流量转发层面的后端分配。需要基于请求内容的路由,应使用 Ingress、Gateway API 或应用层代理。


九、SNAT、源 IP 与返回路径

转发过程中可能发生两种地址转换:

  • DNAT:把目标从 Service IP 改为某个 Endpoint IP。
  • SNAT:把客户端源 IP 改为节点 IP 或其他地址。

理想情况下:

客户端源 IP -> Pod
Pod 返回客户端源 IP

但跨节点转发时,若 Pod 的返回路径无法正确回到原客户端,服务代理可能需要 SNAT:

客户端 IP -> 节点 A
节点 A DNAT -> 节点 B 上的 Pod
节点 B 看到的源地址可能是节点 A

SNAT 的直接代价是应用日志看到的源地址可能不再是原客户端 IP。是否发生、发生在哪个方向,取决于服务转发实现、网络插件、路由能力和流量策略。

externalTrafficPolicy

externalTrafficPolicy 主要影响从集群外部进入 Service 的流量:

spec:
  type: LoadBalancer
  externalTrafficPolicy: Local

两种主要语义:

Cluster

默认常见值。外部流量可以从接收流量的节点转发到集群中其他节点的后端。

优点是节点上的后端选择更灵活;代价可能是:

  • 发生跨节点转发;
  • 客户端源 IP 可能被 SNAT;
  • 外部负载均衡器不一定需要把流量精确送到有 Pod 的节点。

Local

只使用接收流量节点上的本地 Endpoint:

客户端 -> node-a:NodePort
                 |
                 +-- node-a 有本地 Pod:转发
                 |
                 +-- node-a 没有本地 Pod:通常丢弃或被健康检查摘除

它通常用于保留客户端源 IP,或减少跨节点转发。但它带来明确风险:如果外部负载均衡器把流量发送到没有本地后端的节点,而该节点没有被正确摘除,流量可能失败。

因此使用 Local 时,必须验证:

  • 外部 LB 是否支持并正确执行节点健康检查;
  • 健康检查端口和探测路径是否符合该实现;
  • 各节点后端分布是否均衡;
  • 节点排空和 Pod 滚动升级时是否会产生短暂无本地后端。

internalTrafficPolicy: Local 是另一个维度,用于集群内部访问:

spec:
  internalTrafficPolicy: Local

它要求集群内部客户端优先或仅使用本节点 Endpoint,具体无本地 Endpoint 时的行为应按当前 Kubernetes 版本和实现验证。不能用 externalTrafficPolicy 推断集群内部流量的行为。

sessionAffinity

默认情况下,Service 不保证同一个客户端总是访问同一个后端。可以配置基于客户端 IP 的会话亲和性:

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800

它的直觉是:

f(客户端源地址)=固定后端f(\text{客户端源地址}) = \text{固定后端}

但这个“客户端源地址”可能已经被 NAT,多个真实客户端可能共享一个出口 IP;反过来,同一客户端地址变化后也可能被分配到不同后端。会话亲和性通常依赖节点服务转发和连接跟踪状态,不是跨所有故障、节点重启和实现切换的持久会话数据库。

如果应用需要真正可靠的用户会话,应把会话放在共享存储,或使用应用层可验证的粘性机制,而不能把 Service 的 ClientIP 当成完整的会话存储方案。


十、Endpoint 就绪、探针与优雅终止

Service 是否把 Pod 当作可用后端,和容器进程是否存在不是一回事。

例如:

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5

当 readiness probe 失败时,Pod 仍可能处于 Running,但通常不再被作为普通新流量的就绪 Endpoint。这样可以把“进程活着”和“能够接收业务流量”区分开。

一个典型滚动更新过程是:

  1. 新 Pod 启动,但 readiness 失败。
  2. 新 Pod 出现在 Pod 列表中,但尚未作为就绪后端接收普通流量。
  3. 新 Pod readiness 成功,EndpointSlice 标记其可服务。
  4. Deployment 增加新 Pod 流量并减少旧 Pod。
  5. 旧 Pod 进入 terminating 状态。
  6. EndpointSlice 反映旧 Endpoint 的终止状态。
  7. kube-proxy 和外部负载均衡器逐步停止向旧 Pod 建立新连接。
  8. 旧 Pod 在 terminationGracePeriodSeconds 内处理已有连接并退出。

这不是绝对无损的协议保证。应用必须配合:

  • readiness 探针在真正能服务前保持失败;
  • 收到终止信号后停止接收新业务;
  • 正确处理已有连接;
  • 设置合理的优雅终止时间;
  • 外部 LB 的摘除延迟不能被忽略。

publishNotReadyAddresses: true 会改变服务发现行为,常用于需要发现尚未 Ready 成员的系统,例如某些有自己选举或启动协调过程的有状态系统:

spec:
  publishNotReadyAddresses: true

这意味着客户端可能得到尚未通过 readiness 的地址。它不是“让探针失效”,而是明确要求服务发现发布未就绪地址;如果普通 HTTP 服务误用,可能把启动中的 Pod 暴露给客户端。


十一、双栈与 IP 家族

当前 Kubernetes Service API 支持 IPv4、IPv6 和双栈场景,但是否能实际使用取决于集群是否启用对应网络能力。

可以查看:

kubectl get svc web -o yaml

关注:

spec:
  ipFamilies:
    - IPv4
  ipFamilyPolicy: SingleStack

常见策略包括:

  • SingleStack:使用单一地址族;
  • PreferDualStack:在支持时尽量使用双栈;
  • RequireDualStack:要求双栈,否则创建或配置不能满足。

EndpointSlice 也按 addressType 区分地址族。一个 IPv4 EndpointSlice 不应被当作 IPv6 后端使用。双栈环境中,客户端 DNS 结果、应用连接策略、网络插件和外部负载均衡器都必须共同支持双栈,不能只看 Service YAML 就假设 IPv6 流量可达。


十二、外部地址、ExternalName 与 Service 的边界

externalIPs

Service 可以声明:

spec:
  externalIPs:
    - 203.0.113.20

这不会自动购买、配置或宣告这个 IP。它只是允许服务实现把发往该地址的流量视作该 Service 的流量。集群网络必须真的能让该地址的流量到达正确节点,地址所有权和防火墙也必须由管理员负责。

误用 externalIPs 可能造成地址劫持或绕过网络边界,不应把它当作云 LoadBalancer 的替代品。

ExternalName

apiVersion: v1
kind: Service
metadata:
  name: payments
spec:
  type: ExternalName
  externalName: payments.example.net

ExternalName 主要创建 DNS CNAME 语义,不创建 ClusterIP,也不创建 EndpointSlice,不提供 kube-proxy 的四层转发。客户端解析到外部域名后,能否访问由外部 DNS、路由、TLS 和目标服务决定。

因此:

  • ClusterIP:集群内虚拟 IP 加服务转发;
  • 无 selector 的 Service 加 EndpointSlice:手工维护后端;
  • ExternalName:DNS 别名;
  • LoadBalancer:请求外部负载均衡实现。

这四者不能互换。


十三、从头创建一个可验证的 Service

下面的示例让容器明确监听 8080,避免把镜像默认端口和 Service 映射混淆:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-web
  template:
    metadata:
      labels:
        app: demo-web
    spec:
      containers:
        - name: app
          image: hashicorp/http-echo:1.0.0
          args:
            - "-listen=:8080"
            - "-text=hello from kubernetes"
          ports:
            - name: http
              containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: demo-web
spec:
  selector:
    app: demo-web
  ports:
    - name: http
      port: 80
      targetPort: http
  type: ClusterIP

应用并验证:

kubectl apply -f demo-web.yaml

kubectl rollout status deployment/demo-web
kubectl get svc demo-web
kubectl get endpointslice \
  -l kubernetes.io/service-name=demo-web \
  -o wide

然后从集群内部访问:

kubectl run test-client --rm -it \
  --image=curlimages/curl:8.10.1 \
  --restart=Never -- \
  curl -sS http://demo-web.default.svc.cluster.local/

预期会得到:

hello from kubernetes

这个结果依赖四个映射同时成立:

demo-web.default.svc.cluster.local
    -> demo-web 的 ClusterIP:80
    -> EndpointSlice 中的 PodIP:8080
    -> 容器中的 http-echo 监听 :8080

如果将 targetPort 错写为 80,Service 仍可能创建成功,DNS 也仍能解析,但连接会在 Pod 的 80 端口失败。这说明 Service API 校验“字段格式和引用关系”,通常不会探测应用是否真的在目标端口监听。

排查顺序可以是:

kubectl get svc demo-web -o yaml
kubectl get pods -l app=demo-web -o wide
kubectl describe pod -l app=demo-web
kubectl get endpointslice \
  -l kubernetes.io/service-name=demo-web -o yaml

重点检查:

  1. selector 是否与 Pod 标签完全匹配;
  2. Pod 是否 Ready;
  3. EndpointSlice 是否包含预期地址;
  4. targetPort 数字或命名端口是否正确;
  5. 容器是否监听正确地址和端口;
  6. NetworkPolicy 是否阻止客户端到 Pod 的流量;
  7. DNS 解析得到的地址是否属于预期 Service。

十四、如何判断问题发生在哪一层

一次访问失败可能发生在不同层,不能只执行 kubectl get svc 就下结论。

1. DNS 层

kubectl run dns-test --rm -it \
  --image=busybox:1.36 \
  --restart=Never -- \
  nslookup demo-web.default.svc.cluster.local

若名称解析失败,优先检查 CoreDNS、客户端 Pod 的 DNS 配置和网络连通性,而不是先检查 EndpointSlice。

2. Service 对象层

kubectl get svc demo-web -o yaml

检查:

  • spec.type
  • spec.clusterIP
  • spec.ports
  • spec.selector
  • spec.ipFamilies
  • spec.internalTrafficPolicyspec.externalTrafficPolicy
  • status.loadBalancer

3. 后端发现层

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

如果没有切片或没有就绪地址,常见原因包括 selector 不匹配、Pod 没有可用 IP、readiness 失败、端口定义错误或 Service 是无 selector 且无人手动维护后端。

4. Pod 应用层

直接绕过 Service 测试 Pod:

kubectl get pods -l app=demo-web -o wide
kubectl port-forward pod/<pod-name> 18080:8080
curl http://127.0.0.1:18080/

port-forward 主要用于诊断,不等价于生产流量路径。它能验证应用是否监听和响应,但不能证明 Service、节点路由或外部 LoadBalancer 正常。

5. 节点服务转发层

不同 kube-proxy 模式需要不同工具。例如:

kubectl -n kube-system get pods -l k8s-app=kube-proxy
kubectl -n kube-system logs <kube-proxy-pod>

在节点上还可能使用:

iptables-save
ipvsadm -Ln
nft list ruleset

这些命令必须在对应节点上执行,并且需要权限。没有看到 iptables 规则不代表 Service 一定没有生效,因为集群可能使用 IPVS、nftables、eBPF 或其他实现。

6. 外部负载均衡器层

对于 LoadBalancer:

kubectl describe svc web-public
kubectl get events --sort-by=.lastTimestamp

检查事件、外部地址、健康检查和云控制器日志。外部地址已分配只说明控制器写入了入口状态,不证明:

  • 入口端口已在安全组放行;
  • LB 后端健康;
  • 节点端口可达;
  • Pod 应用正在监听;
  • TLS 配置正确。

十五、常见反例与误解

反例一:Service selector 写成 Deployment 名称

spec:
  selector:
    app: demo-web

selector 匹配的是 Pod 的标签,不是 Deployment 名称、容器名称或 Service 名称。如果 Pod 模板没有 app: demo-web,Service 可以正常创建,但 EndpointSlice 为空。

反例二:把 containerPort 当成自动监听

ports:
  - containerPort: 8080

这不会让进程自动监听 8080。真正决定应用监听端口的是进程参数和应用配置。

反例三:把 NodePort 当成外部防火墙配置

NodePort 只在 Kubernetes 服务转发层声明了一个端口。云安全组、裸机防火墙、路由器和上游负载均衡器仍可能阻止它。

反例四:把 LoadBalancer 当成 HTTP 路由器

多个 HTTP 服务不能仅靠一个 Service 按 URL 路径分流。Service 只按其地址、端口和协议转发;HTTP 路由应由 Ingress 或 Gateway API Controller 完成。

反例五:认为删除 Endpoint 会立即断开所有连接

EndpointSlice 更新主要影响后续后端选择。已经建立的 TCP 连接是否继续、何时关闭,取决于连接本身、应用优雅终止、conntrack 和服务实现。需要强制切断连接时,应处理应用和节点连接状态,而不是只修改标签。

反例六:对外部客户端启用 externalTrafficPolicy: Local 却没有本地后端

这会形成一个可预测的失败路径:

外部 LB -> node-a
node-a 没有本地 Endpoint
externalTrafficPolicy=Local
结果:连接被丢弃,或节点被健康检查摘除后不再接收流量

它不是“更保留源 IP 但仍自动跨节点转发”的配置。


十六、生产取舍:选择哪种 Service 类型

可以用访问范围和转发责任来区分:

类型 稳定入口 主要可达范围 是否依赖外部实现
ClusterIP ClusterIP 与 DNS 集群内部 通常不依赖云 LB
NodePort 每节点 IP + 端口 能到达节点的网络 需要网络侧放通
LoadBalancer 外部入口地址 由基础设施决定 需要 LoadBalancer Controller
Headless DNS 返回后端地址 由客户端访问后端 不提供虚拟 IP 汇聚
ExternalName DNS 别名 由外部 DNS 和网络决定 不提供四层转发

对于内部微服务,ClusterIP 通常足够;需要将节点端口交给已有外部负载均衡器时使用 NodePort;需要由云或其他控制器创建外部入口时使用 LoadBalancer。如果需求是基于域名、路径、TLS 或策略进行 HTTP 路由,应增加 Ingress 或 Gateway API,而不是把多个业务端口堆到一个 Service 上。


十七、规范保证、实现差异与版本边界

需要明确区分以下三类事实:

Kubernetes API 语义

Service 类型、端口字段、selector、EndpointSlice API、externalTrafficPolicyinternalTrafficPolicysessionAffinity 等由 Kubernetes API 定义。对象状态和控制器行为遵循 Kubernetes 的兼容语义。

常见实现

kube-proxy 的 iptables、IPVS、nftables 模式,以及云厂商 LoadBalancer Controller 的具体规则,是实现选择。它们在后端选择、连接跟踪、健康检查、源地址保留和故障收敛方面可能存在差异。

环境经验

NodePort 默认端口范围、Service CIDR、云厂商注解、负载均衡器类型、跨区域能力和安全组行为,可能由集群安装方式或厂商决定。部署到新环境时应读取该环境的控制面、网络插件和云控制器文档,而不能只复制另一套集群的 YAML。

EndpointSlice 是当前服务发现与后端表示的主 API;旧的 Endpoints API 在大规模场景下存在扩展性限制,并已被 Kubernetes 文档标记为弃用方向。编写新控制器或诊断工具时,应优先使用 discovery.k8s.io/v1 的 EndpointSlice,而不是假设所有后端都集中在一个 Endpoints 对象中。

理解 Service 的关键,不是记住四个 type 名称,而是沿着这条状态链验证:

Service 配置
   -> DNS / ClusterIP / NodePort
   -> selector 或手工 EndpointSlice
   -> 就绪与终止状态
   -> 节点服务转发实现
   -> SNAT、流量策略和返回路径
   -> 外部 LoadBalancer(如果存在)
   -> 应用端口与应用协议

链条中任意一层不成立,最终都可能表现为“访问 Service 失败”;只有把每一层的对象、状态和数据流分别验证,才能区分是服务发现问题、转发问题、网络策略问题、外部负载均衡问题,还是应用自身没有正确监听端口。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。