kubernetes-kep-1880-多服务CIDR

Posted by 梁远鹏 on 2023-12-27 | 阅读 |,阅读约 5 分钟

TOC

KEP-1880 多服务 CIDR 是什么

KEP-1880 允许通过 API 对象配置分配给 Kubernetes 集群的 ServiceCIDR,能够动态的扩展 kubernetes 集群的服务的 IP,该功能发布 kubernetes v1.29.0 中,功能版本时 alpha,因此需要开放对应 featuregate 才能使用.

Proposal

这个提议实现了一个新的IP分配器逻辑,其中主要使用了两个 API 对象,ServiceCIDR 和 IPAddress,并且允许用户可以通过创建一个新的 ServiceCIDR 来达到动态增加可用 service IP 的效果.

这个分配器可以”自动”的使用任何可用的 ServiceCIDR 中的 IP,kubernetes 社区考虑添加这种模型,就像给存储系统添加更多的磁盘以增加可用的磁盘空间一样.

为了简化 API 模型以及使其可以向后兼容,并且避免它演变成不同的东西并且与其他的 API(例如 Gateway API)发生冲突,这个提议添加了以下约束:

  • ServiceCIDR 在创建之后是不可变的
  • 只有当 ServiceCIDR 没有被任何 service IP 引用时才能够删除 ServiceCIDR 对象
  • 可能存在重叠的 ServiceCIDR
  • apiserver 会定期的确保存在一个默认的ServiceCIDR
  • 集群中存在的任何 IP 地址都必须属于 ServiceCIDR 定义的 service CIDR 内
  • 任何配合了 ClusterIP 的服务都应该有一个关联的 IPAddress 对象
  • 处于正在删除状态的 ServiceCIDR 不能分配新的 IP

这样的话 Service 和 IPAddress 之间就是一个 1:1 的关系,以及 IPAddress 与对应的 ServiceCIDR 是 N:1 的关系.有一个值得关注的是重叠的 ServiceCIDR 会合并在内存中, IPAddress 不是存在一个特定的 ServiceCIDR 对象,而是包含了这个 IP 段的任何 ServiceCIDR.(也就是说多个 ServiceCIDR 可以分配相同的 IP 段)

新的分配器逻辑还可以被其他 API 使用,例如 Gateway API.

演示

接下来会使用 Kind 来创建一个 kubernetes 来演示多服务 CIDR 的效果.

准备 kubernetes 集群

准备好一个 kubernetes 集群,这里使用 Kind 快速部署一个 kubernetes.

kind.yaml

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  ipFamily: ipv4
  kubeProxyMode: iptables
  serviceSubnet: "10.96.0.0/28"
  dnsSearch: []
nodes:
- role: control-plane
  image: kindest/node:v1.29.0@sha256:eaa1450915475849a73a9227b8f201df25e55e268e5d619312131292e324d570
  extraPortMappings:
  - containerPort: 32080
    hostPort: 32080
    listenAddress: "0.0.0.0"
- role: worker
  image: kindest/node:v1.29.0@sha256:eaa1450915475849a73a9227b8f201df25e55e268e5d619312131292e324d570
- role: worker
  image: kindest/node:v1.29.0@sha256:eaa1450915475849a73a9227b8f201df25e55e268e5d619312131292e324d570
featureGates: {"AllAlpha":true}
runtimeConfig: {"api/alpha":"true"}
kubeadmConfigPatches:
- |
  kind: ClusterConfiguration
  metadata:
    name: config
  apiServer:
    extraArgs:
      "v": "4"
  controllerManager:
    extraArgs:
      "v": "4"
  scheduler:
    extraArgs:
      "v": "4"
  ---
  kind: InitConfiguration
  nodeRegistration:
    kubeletExtraArgs:
      "v": "4"
  ---
  kind: JoinConfiguration
  nodeRegistration:
    kubeletExtraArgs:
      "v": "4"

运行以下命令使用上述的 Kind 配置文件创建一个 1 master 2 node 的 kubernetes 集群:

kind create cluster --config kind.yaml -v9 --name servicecidr  

等待一会出现以下提示则表示命令执行成功了:

...
I1228 14:39:34.531417     139 round_trippers.go:553] PATCH https://servicecidr-control-plane:6443/api/v1/nodes/servicecidr-worker2?timeout=10s 200 OK in 3 milliseconds

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-servicecidr"
You can now use your cluster with:

kubectl cluster-info --context kind-servicecidr

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/

观察默认情况的一些数据

kubernetes 集群准备好后可以观察一下默认情况 kubernetes 的一些关于 ServiceCIDR 的资源对象情况.

  1. 有一个名为 kubernetes 的默认 ServiceCIDR 对象,根据上面 Kind 配置文件中的 serviceSubnet 来创建的.
lank8s@lank8s:~/cidrs$ kubectl get servicecidrs
NAME         CIDRS          AGE
kubernetes   10.96.0.0/28   3m57s
  1. 有一个名为 kubernetes 的默认 service 对象,是默认 ServiceCIDR 对象分配的第一个 IP.
lank8s@lank8s:~/cidrs$ kubectl get service
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   5m7s
  1. 默认 Service 会有一个对应的 IPAddress 对象
lank8s@lank8s:~/cidrs$ kubectl get ipaddress
NAME         PARENTREF
10.96.0.1    services/default/kubernetes
10.96.0.10   services/kube-system/kube-dns

可以看到实际上会有两个 IPAddress,另一个是由 coreDNS 产生的.

ServiceCIDR 使用 finalizers 机制来保证资源的联级删除,只有当另一个 ServiceCIDR 包含了对应的 IP 段或者是没有相关的 IP 段时 ServiceCIDR 才会被删除.

用例

IP耗尽

在某些情况下, ServiceCIDR IP 段被耗尽了,这时候想要增加 service 的话是一种破坏性的操作,甚至可能会造成数据丢失. 而当有了 Multiple-Service-CIDRs 之后,只需要新增一个 ServiceCIDR 对象就可以完成这个操作了.

接下来简单演示一下,首先遍历创建 service,把集群的 service IP 都耗尽:

lank8s@lank8s:~/cidrs$ for i in $(seq 1 13); do kubectl create service clusterip "test-$i" --tcp 80 -o json | jq -r .spec.clusterIP; done
10.96.0.14
10.96.0.12
10.96.0.11
10.96.0.9
10.96.0.13
10.96.0.5
10.96.0.8
10.96.0.7
10.96.0.2
10.96.0.3
10.96.0.4
10.96.0.6
error: failed to create ClusterIP service: Internal error occurred: failed to allocate a serviceIP: range is full

可以看到最后提示无法创建新的 Service 了,这个时候我们创建一个 ServiceCIDR 对象:

cidr.yaml

apiVersion: networking.k8s.io/v1alpha1
kind: ServiceCIDR
metadata:
  name: newcidr1
spec:
  cidrs: 
  - 192.96.0.0/24
lank8s@lank8s:~/cidrs$ kubectl apply -f cidrs.yaml 
servicecidr.networking.k8s.io/newcidr1 created

然后继续创建新的 Service 看看结果如何:

lank8s@lank8s:~/cidrs$ for i in $(seq 13 16); do kubectl create service clusterip "test-$i" --tcp 80 -o json | jq -r .spec.clusterIP; done
192.96.0.43
192.96.0.172
192.96.0.146
192.96.0.112

可以看到这时候已经可以继续创建 Service 了,并且新的 Service 对应的 IP 都是和刚创建的 ServiceCIDR 资源所配置的网段里面.

接下来试着删除一下这个 ServiceCIDR 对象:

lank8s@lank8s:~/cidrs$ kubectl delete serviceCIDR newcidr1
servicecidr.networking.k8s.io "newcidr1" deleted

由于集群中有关联的 service,所以会无法删除这个 ServiceCIDR,看一下这个时候 ServiceCIDR 对象的状态:

lank8s@lank8s:~/cidrs$  kubectl get servicecidr newcidr1 -o yaml
apiVersion: networking.k8s.io/v1alpha1
kind: ServiceCIDR
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"networking.k8s.io/v1alpha1","kind":"ServiceCIDR","metadata":{"annotations":{},"name":"newcidr1"},"spec":{"cidrs":["192.96.0.0/24"]}}
  creationTimestamp: "2023-12-28T14:53:28Z"
  deletionGracePeriodSeconds: 0
  deletionTimestamp: "2023-12-28T14:55:36Z"
  finalizers:
  - networking.k8s.io/service-cidr-finalizer
  name: newcidr1
  resourceVersion: "2363"
  uid: 38496a54-f718-470f-935f-f6db63873869
spec:
  cidrs:
  - 192.96.0.0/24
status:
  conditions:
  - lastTransitionTime: "2023-12-28T14:55:36Z"
    message: There are still IPAddresses referencing the ServiceCIDR, please remove
      them or create a new ServiceCIDR
    reason: Terminating
    status: "False"
    type: Ready

可以看到这时候它的 status 是 false,finalizers 标志上有 networking.k8s.io/service-cidr-finalizer 所以无法删除.

现在我们将对应的 service 都删掉:

lank8s@lank8s:~/cidrs$ for i in $(seq 13 16); do kubectl delete service "test-$i" ; done
service "test-13" deleted
service "test-14" deleted
service "test-15" deleted
service "test-16" deleted

这个时候再查询一下刚才的 ServiceCIDR:

lank8s@lank8s:~/cidrs$  kubectl get servicecidr newcidr1
Error from server (NotFound): servicecidrs.networking.k8s.io "newcidr1" not found

可以看到这个 ServiceCIDR 已经不存在了,因为没有与其关联的 service 所以它被成功删除了.

给 kubernetes 集群的 service 切换一个新的 IP 段

另一个常见的用例是当用户想要将集群中现有的 service IP 都切换到一个新的 IP 段,例如想要将 10.96.0.0/28 切换到 192.168.7.0/24.

我们可以按照以下的步骤操作:

  1. 使用 192.168.7.0/24 临时创建一个新的 ServiceCIDR 对象
  2. 删除默认的 ServiceCIDR,让它处于一个被删除的状态,这样有新的 service 创建时就不会使用这个 ServiceCIDR 分配新的 IP 地址了
  3. 这个时候只有默认的 kubernetes.default service 是一定要在默认的 ServiceCIDR 里面的
  4. 重新创建集群内的其他所有 service,让它们可以从新的 ServiceCIDR 分配 IP 地址
  5. 这个时候可以启动一个新的 apiserver,然后指定它的 serviceSubnet 与新创建的 ServiceCIDR 网段相匹配,在这个示例的话就是 192.168.7.0/24
  6. 当新的 apiserver 成功启动后可以关闭就的 apiserver 了
  7. 删除默认的 kubernetes.default 服务,这样就可以让集群内原有的 ServiceCIDR 被删除了,然后默认的 kubernetes.default 服务会由新的 apiserver 创建出来,并且 IP 网段是由新的 ServiceCIDR 分配的.
  8. 这个时候可以删除刚才临时创建的 ServiceCIDR 了,因为新启动的 apiserver 已经指定了 serviceSubnet 与这个 ServiceCIDR 是同一个网段,这时候临时创建的 ServiceCIDR 已经不需要了.

集群 IP 迁移

与集群切换 IP 网段是一样的.

最后

如果 ServiceCIDR 处于被删除的状态,这时候管理的 Service 是不受影响的,可以继续访问.

文章演示部分翻译于[2],并且更新了其被淘汰的部分(字段名是旧的).

参考:

微信公众号

扫描下面的二维码关注我们的微信公众号,第一时间查看最新内容。同时也可以关注我的Github,看看我都在了解什么技术,在页面底部可以找到我的Github。

wechat-qrcode