本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

Kubernetes 1.27:避免在为 NodePort 服务分配端口时发生冲突

在 Kubernetes 中,Service 可以用于为在一组 Pods 上运行的应用程序提供统一的流量端点。客户端可以使用 Service 提供的虚拟 IP 地址(或称 **VIP**)进行访问,Kubernetes 为访问不同后端 Pod 的流量提供负载均衡,但 ClusterIP 类型的 Service 仅限于为集群内的节点提供访问,而来自集群外部的流量无法被路由。解决这个问题的一种方法是使用 `type: NodePort` 的 Service,它会为集群中所有节点的特定端口设置一个映射,从而将外部流量重定向到集群内部。

Kubernetes 如何为 Service 分配节点端口?

当创建 `type: NodePort` 的 Service 时,其对应的端口会通过以下两种方式之一进行分配:

  • 动态分配:如果 Service 的类型是 `NodePort`,并且你没有在 Service 的 `spec` 中明确设置 `nodePort` 值,那么 Kubernetes 控制平面会在创建时自动为其分配一个未使用的端口。

  • 静态分配:除了上述的动态自动分配外,你还可以明确指定一个在 NodePort 端口范围配置内的端口。

你手动分配的 `nodePort` 值在整个集群中必须是唯一的。如果尝试创建一个 `type: NodePort` 的 Service,并明确指定一个已经被分配的节点端口,将会导致错误。

为什么需要为 NodePort Service 预留端口?

有时,你可能希望让一个 NodePort Service 运行在众所周知的端口上,以便集群内外的其他组件和用户可以使用它们。

在一些复杂的集群部署中,Kubernetes 节点和其他服务器混合在同一网络中,可能需要使用一些预定义的端口进行通信。特别是,一些基础组件不能依赖于支持 `type: LoadBalancer` 服务的 VIP,因为该集群的虚拟 IP 地址映射实现也依赖于这些基础组件。

现在假设你需要将 Kubernetes 上的 Minio 对象存储服务暴露给运行在 Kubernetes 集群外的客户端,并且约定的端口是 `30009`,我们需要创建一个如下所示的 Service:

apiVersion: v1
kind: Service
metadata:
  name: minio
spec:
  ports:
  - name: api
    nodePort: 30009
    port: 9000
    protocol: TCP
    targetPort: 9000
  selector:
    app: minio
  type: NodePort

然而,如前所述,如果 `minio` Service 所需的端口 (30009) 没有被预留,并且在 `minio` Service 创建之前或同时,另一个 `type: NodePort`(或可能是 `type: LoadBalancer`)的 Service 被创建并动态分配,那么 TCP 端口 30009 可能会被分配给那个其他的 Service;如果发生这种情况,`minio` Service 的创建将会因为节点端口冲突而失败。

如何避免 NodePort Service 端口冲突?

Kubernetes 1.24 为 `type: ClusterIP` 的 Service 引入了变更,将集群 IP 地址的 CIDR 范围划分为两个块,采用不同的分配策略以减少冲突风险。在 Kubernetes 1.27 中,作为一个 alpha 功能,你可以为 `type: NodePort` 的 Service 采用类似的策略。你可以启用一个新的特性门控 `ServiceNodePortStaticSubrange`。开启此功能后,你可以为 `type: NodePort` 的 Service 使用一种不同的端口分配策略,并降低冲突的风险。

NodePort 的端口范围将根据公式 `min(max(16, nodeport-size / 32), 128)` 进行划分。该公式的结果将是一个介于 16 和 128 之间的数字,步长会随着 NodePort 范围的增大而增加。公式的结果决定了静态端口范围的大小。当端口范围小于 16 时,静态端口范围的大小将设置为 0,这意味着所有端口都将动态分配。

动态端口分配默认将使用上段范围,一旦该范围用尽,将使用下段范围。这将允许用户在下段范围使用静态分配,且冲突风险较低。

示例

默认范围:30000-32767

范围属性
service-node-port-range30000-32767
范围偏移量min(max(16, 2768/32), 128)
= `min(max(16, 86), 128)`
= `min(86, 128)`
= 86
静态范围起始30000
静态范围结束30085
动态范围起始30086
动态范围结束32767
饼图显示数据 标题 30000-32767 “静态” : 86 “动态” : 2682

非常小的范围:30000-30015

范围属性
service-node-port-range30000-30015
范围偏移量0
静态范围起始-
静态范围结束-
动态范围起始30000
动态范围结束30015
饼图显示数据 标题 30000-30015 “静态” : 0 “动态” : 16

小范围(下边界):30000-30127

范围属性
service-node-port-range30000-30127
范围偏移量min(max(16, 128/32), 128)
= `min(max(16, 4), 128)`
= `min(16, 128)`
= 16
静态范围起始30000
静态范围结束30015
动态范围起始30016
动态范围结束30127
饼图显示数据 标题 30000-30127 “静态” : 16 “动态” : 112

大范围(上边界):30000-34095

范围属性
service-node-port-range30000-34095
范围偏移量min(max(16, 4096/32), 128)
= `min(max(16, 128), 128)`
= `min(128, 128)`
= 128
静态范围起始30000
静态范围结束30127
动态范围起始30128
动态范围结束34095
饼图显示数据 标题 30000-34095 “静态” : 128 “动态” : 3968

非常大的范围:30000-38191

范围属性
service-node-port-range30000-38191
范围偏移量min(max(16, 8192/32), 128)
= `min(max(16, 256), 128)`
= `min(256, 128)`
= 128
静态范围起始30000
静态范围结束30127
动态范围起始30128
动态范围结束38191
饼图显示数据 标题 30000-38191 “静态” : 128 “动态” : 8064