Kubernetes Service 类型和会话亲和性
2023-10-16 Cloud Native Kubernetes
Service 概述
Service 被抽象为一组或多组 Pod 的静态 IP 地址 (也可以将 Service 看作是一个代理),任何到达 Service 的请求都会被转发到属于该服务背后的某个 Pod 中的应用。
与 Service 紧密关联的两个概念是 Pod 和 EndPoint, 其中,Pod 是 Kubernetes 中应用运行的最小单位,而 EndPoint 是 Service 指向的实际地址,定义了 Service 所指向的具体 Pod 的 IP 地址和端口。
EndPoint 二元组 = Pod IP + Pod Port
- 创建一个 Service 时,Kubernetes 会自动创建关联的 EndPoint
- EndPoint 中包含了 Service 对应的所有 Pod 的 IP 地址和端口
- 当有 Pod 加入 Service 或者从 Service 的中删除时 (通过标签选择器),Kubernetes 会自动更新 EndPoint 相关信息
三者的关系如下图所示:

上面的图中定义了 2 个 Service, 每个 Service 有 1 个 EndPoint, 每个 EndPoint 包含 3 个 Pod 的网络位置。
EndpointSlice
每个 Service 可以关联一个或多个 Endpoint,随着集群规模的扩增,单个 EndPoint 对象可能会变得非常巨大,当 Service 关联很多服务 Pod 时, EndPoint 限制了关联的 Pod 数量,因为 etcd 中存储的对象默认大小限制为 1.5 MB。
此外,频繁地扩容和更新操作会引发性能和扩展限制,例如某个 Service 关联了 5000 个 Pod, 最终资源数据大小为 1.5 MB, 即使这 5000 个 Pod 中只有一个 Pod 发生了变化, 也需要将更新后的完整资源分发到集群中的每个 Node 节点,假设集群节点个数为 3000, 那么单次更新时,发送的数据量为:
3000 * 1.5 MB = 4.5 GB
即使节点全部位于同一数据中心,也是一笔不小的网络开销,而且只要有任一 Pod 更新时都会产生 4.5 GB 流量,想象一下,如果超过一半以上的 Pod 发生变化时, 仅仅是同步 EndPoint 产生的流量就是 TB 级别的,为了解决这个问题,官方为 EndPoint 新增了一个扩展: EndpointSlice。
EndpointSlice 采用分组的方式,将单个 EndPoint 中的多个 Pod 分成多个切片 (Slice), 每个 Slice 包含了一部分 Pod,并包含了这些 Pod 的 IP 地址和端口。:
下图中的 EndPoint 包含 15 个 Pod, 通过每 5 个 Pod 逐个分组,最终得到 3 个不同的 EndpointSlice。

EndpointSlice 通过索引和标签等元数据与 Service 关联起来,可以更高效地分发 Pod 的变更数据,降低 Endpoint 在大规模集群中扩展和更新操作时的负载, 同时也解决了 etcd 的默认大小限制问题。
最佳实践
中大规模集群中使用 EndpointSlice API 替换 Endpoints, Kubernetes 从 1.19 版本开始默认启用 EndpointSlice。
Service 示例
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: api-user
ports:
- protocol: TCP
port: 80
targetPort: 80
上面的声明式 yaml 代码将创建一个名称为 user-service 的 Service, 指向标签中包含 name:api-uesr 的所有 Pod 都在监听的 80 端口。
Service 可以随意将 spec.ports.port 端口映射到 spec.ports.targetPort, 不过实际开发中为了方便,两者的端口值会保持一致。
最佳实践
Pod 中的端口可以定义名字,所以 Service 的 spec.ports.targetPort 字段可以直接指向 Pod 的端口名字。
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: api-user
spec:
containers:
- name: nginx
image: nginx:stable
ports:
- containerPort: 80
name: http-web-svc # 定义 Pod 端口名字
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: api-user
ports:
- protocol: TCP
port: 80
targetPort: http-web-svc # 使用 Pod 的端口名字
通过使用 Pod 的端口名字而不是端口号,可以为 Service 提供更高的灵活性,例如可以将 Pod 的声明端口从 80 改为 443, 而 Service 的声明无需任何修改。
Service 类型
Service 的类型分为以下四种。
1. ClusterIP
默认的 Service 类型,自动分配一个仅在集群内部可以访问的虚拟 IP 地址,可以通过标签选择器关联对应的 Pod, 当不需要负载均衡时,也就不需要单独的虚拟 IP, 可以使用 Headless Services, 将 ClusterIP 的值设置为 “None” 即可。
# Headless Service 示例
apiVersion: v1
kind: Service
metadata:
name: test-headless-service
spec:
clusterIP: None
selector:
app: test-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
kube_proxy 会忽略 Headless Services 类型,DNS 会根据 Headless Services 是否定义了标签选择符来配置不同的解析行为:
- 对于定义了标签选择符 的 Headless Services, Kubernetes 控制面板会创建对应的 EndPoint, 并且 DNS 解析返回 Service 标签选择对应的 Pod 的 IP 地址集合
- 对于未定义标签选择符 的 Headless Services,
spec.port和spec.targetPort必须相等, Kubernetes 控制面板不会创建对应的 EndPoint, 并且 DNS 会根据 Service 类型执行不同的行为:- 如果 Service 类型等于 ExternalName, 查找和解析对应的 CNAME 记录
- 如果 Service 类型不等于 ExternalName, 针对 Service 已有的 EndPoint 的所有 IP 地址, 查找和配置 DNS A(IPV4), AAAA(IPV6) 记录
最佳实践
- Headless Services 配合 StatefulSet 有状态应用,StatefulSet 可以确保每个 Pod 都有唯一的标识符和稳定的网络标识,可以满足与特定 Pod 直接通信的业务场景
- 通过 Headless Services, 每个 Pod 都会获得一个 DNS 记录,格式为
<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local, 这样就可以通过这条 DNS 记录直接访问某个 Pod - 通过给 Headless Services 设置标签选择器,来决定需要暴露和不需要暴露的 Pod 集合
2. NodePort
NodePort 在 ClusterIP 的基础上增加了对集群外部访问的能力,默认情况下,通过在每个节点上随机选择一个端口 (范围 30000-32767) 映射到 Service, 并将流量转发到 Service 关联的 Pod 上面。
这样就可以通过 <ClusterIP>:<ServicePort> 或者 <NodeIP>:<NodePort> 来访问 Service, 例如现在有一个 Service 名称为 user-svc,类型为 NodePort,分配到的 ClusterIP 为 172.17.0.4,
同时集群有 3 个工作节点并将各自的 31234 端口映射到了 user-svc, 此时可以直接请求 172.17.0.4:31234 来访问,最终流量会转发到具体的 Pod 中。

注意: 如果要使用 <NodeIP>:<NodePort> 方式访问 Service, 需要确保 <NodeIP> 对应的节点上面有 Service 关联的 Pod 在运行。
最佳实践
- 可以在集群外搭建一个反向代理实现负载均衡,将流量分发到不同的节点上,但更好的方案是使用 Ingress 替代 NodePort Service
- 由于 NodePort Service 是直接暴露在公网上的,可以使用网络策略 NetworkPolicy 限制从集群外部访问 Service 的源 IP 地址和端口,增强 Service 安全性
- 指定 NodePort 映射端口避免端口冲突 (内核文件 net.ipv4.ip_local_port_range 默认端口范围 32768 - 60999, 表示任意进程可以使用的端口号)
# 指定 NodePort 映射端口示例
apiVersion: v1
kind: Service
metadata:
name: test-headless-service
spec:
type: NodePort
selector:
app: test-app
ports:
- port: 80
targetPort: 80
# 指定端口为 31234
nodePort: 31234
3. LoadBalancer
在 NodePort 的基础上,基于云服务提供商创建一个外部的负载均衡器,并将流量从负载均衡转发到集群中的 Pod, 具体的内部负载均衡和流量转发过程由云服务提供商实现。
最佳实践
因为 LoadBalancer 是由云计算服务商提供的,所以使用时要确保为负载均衡器分配了足够的资源。
4. ExternalName
将 Service 映射到集群外部的 CNAME 记录,通常用于将集群内部的服务映射到外部的 DNS 名称,隐藏集群内部的细节。
apiVersion: v1
kind: Service
metadata:
name: test-external-service
namespace: prod
spec:
type: ExternalName
externalName: dns.example.com
使用了上面的声明式服务定义之后,在集群内部访问服务 test-external-service 时,集群 DNS 服务返回 CNAME 记录 dns.example.com,
最佳实践
- 确保集群内 DNS 服务 (例如 CoreDNS) 配置和解析正确,同时确保外部 DNS 服务没有单点故障
- 避免过度使用,因为 ExternalName 不是常规的集群内部通信方式
会话亲和性
默认情况下,单个客户端每次请求都会通过服务随机转发到一个 Pod 中,多次请求可能会转发到多个 Pod, 这对 “用户专有数据” 业务场景非常不太友好, 本来单个用户的数据只需要在一个 Pod 中有缓存数据即可,现在的结果可能是单个用户的数据在大多数 Pod 中都有缓存数据,造成大量的资源浪费。
可以通过设置服务亲和性 (sessionAffinity) 来制定单个客户端的每次请求都转发到同一个 Pod, Kubernetes 仅支持两种亲和性类型:
- ClientIP
- None
apiVersion: v1
kind: Service
metadata:
name: test-service
spec:
selector:
app: user-api
sessionAffinity: ClientIP # 基于客户端 IP 设置亲和性
sessionAffinityConfig:
clientIP:
timeoutSeconds: 1800 # 设置亲和性 IP 有效时间为 1800 秒
注意: 使用亲和性规则可能会导致 Pod 负载不均衡,因为大部分业务场景中,流量的地域特征符合 “2/8 原则”。
如果希望使用 HTTP Header, Token, Cookie 等七层网络字段值作为服务亲和性类型,可以使用 Ingress Controller 提供的会话亲和性功能。
小结
| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ClusterIP | 默认类型,简单易用 | 集群外部无法访问 Service | 集群内部进行服务发现和负载均衡 |
| Headless | 支持 DNS 记录解析 | 不支持负载均衡,需要配合其他方式实现 (如反向代理转发) | StatefulSet 有状态服务 (例如数据库集群) |
| NodePort | 支持集群外部直接访问 | 暴露了端口号,可能引发安全问题 | 远程调试或与第三方进行集成测试 |
| LoadBalancer | 直接集成云计算服务商的负载均衡器 | 1. 费用 2. 负载均衡机制是黑盒,无法定制调度过程 | 公开服务,一般会使用 Ingress 替换 LoadBalancer Service 类型 |
| ExternalName | 将 Service 映射到外部 DNS 服务 | 集群内部无法直接访问 Service | 集群内部服务与外部 DNS 简单映射 |