使用 OpenKruise 升级指定 Pod

最近在和同事讨论一个非典型的云原生应用更新场景。目标应用是一系列有状态的 Statefulset,其中的实例用类似投票的机制对外提供服务,这意味着始终有一部分实例是处于待命状态的。由此情况,这个应用在虚拟化时期,会提供一个仲裁服务,每次对实例进行更新时,首先要从这个仲裁服务查出可以更新的摸鱼实例,然后仅仅对这些空闲实例进行升级。

这种行为在虚拟化场景是行之有效的,到了 Kubernetes 环境就难搞了,Statefulset 的 Partition 能力官网说明如下:

RollingUpdate更新策略可以通过指定.spec.updateStrategy.rollingUpdate.partition来进行分区。如果指定了分区,当StatefulSet的.spec.template被更新时,所有序数大于或等于分区的Pod将被更新。所有序号小于分区的Pod将不会被更新,即使它们被删除,也会以以前的版本重新创建。如果一个 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas,对其 .spec.template 的更新将不会被传播到其 Pods。在大多数情况下,你不需要使用分区,但如果你想进行阶段性更新、推出金丝雀或执行分阶段推出,它们是有用的。

如此看来,分区更新能力会保护分区序号以内的连续的 Pod 保持原样,和需求中提到的随时分配情况还是颇有不同的。在我看来,更好的做法是效仿 KubeDB 系列的产品,自行控制更新行为——当然,对甲方提出这种非分要求是不合适的。既然这个需求如此的不云原生,那么先看看 Open Kruise 总不会错的。经过对其文档的阅读,有两个发现:

  • Partition:和 Statefulset 不同,它的 Partition 可以使用整数和百分比两种方式
  • 可以使用标签的方式指定升级时候的优先级。

如果这两个方法能够同时生效,这个需求就有望完成了,设计要点如下:

  1. 从仲裁服务中,获得工作和空闲副本的情况
  2. 刷新 Statefulset 中的 Pod 标签,用于指示当前工作状态。
  3. 设置 Partition,将 Partition 数量设置为等于工作中的副本数量进行保护。

由此可以编写一个 Advanced Statefulset,示例如下:

apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
metadata:
  name: sample
spec:
  replicas: 5
  serviceName: fake-service
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
        working: "false"
    spec:
      readinessGates:
      - conditionType: InPlaceUpdateReady
      containers:
      - name: main
        image: alpine:3.18.2
        command: ['sleep', '3600']
  podManagementPolicy: Parallel
  updateStrategy:
    rollingUpdate:
      partition: 3
      unorderedUpdate:
        priorityStrategy:
          weightPriority:
          - weight: 30
            matchSelector:
              matchLabels:
                working: "true"
          - weight: 50
            matchSelector:
              matchLabels:
                working: "false"

这里的定义有几个需要关注的点:

  1. Pod 模版中加入了 working 标签,用于指示每个 Pod 当前的工作状态。
  2. unorderedUpdate.priorityStrategy.weightPriority:这里指定了非工作状态的 Pod 会有更高优先级。
  3. partition 设置为 3:这里我们假设工作中的副本有 3 个。
  4. 这里镜像使用 alpine:3.18.2,作为我们的工作负载。

安装 Open Kruise

传统的 Helm 三部曲:

  • helm repo add openkruise https://openkruise.github.io/charts/
  • helm update
  • helm install kruise openkruise/kruise --version 1.4.0

部署应用

部署前面的 Advanced Statefulset:

$ kubectl apply -f statefulset.yaml
statefulset.apps.kruise.io/sample created

查看当前工作负载的镜像:

$  kubectl images
[Summary]: 1 namespaces, 5 pods, 5 containers and 1 different images
+----------+-----------+---------------+
|   Pod    | Container |     Image     |
+----------+-----------+---------------+
| sample-0 | main      | alpine:3.18.2 |
+----------+           +               +
| sample-1 |           |               |
+----------+           +               +
| sample-2 |           |               |
+----------+           +               +
| sample-3 |           |               |
+----------+           +               +
| sample-4 |           |               |
+----------+-----------+---------------+

可以看到五个副本用的都是 alpine:3.18.2

标识工作负载

我们使用标签,把第 024 三个 Pod 的 working 标签设置为 true

$ kubectl label pods sample-0 working=true --overwrite
pod/sample-0 labeled
$ kubectl label pods sample-2 working=true --overwrite
pod/sample-2 labeled
$ kubectl label pods sample-4 working=true --overwrite
pod/sample-4 labeled

更新镜像

替换镜像为 alpine:3.18.0,然后 apply

$ kubectl apply -f statefulset.yaml
statefulset.apps.kruise.io/sample configured

验证结果

查看 Pod 状态:

$ kubectl get pods
NAME       READY   STATUS    RESTARTS   AGE
sample-0   1/1     Running   0          9m27s
sample-1   1/1     Running   0          5s
sample-2   1/1     Running   0          9m27s
sample-3   1/1     Running   0          39s
sample-4   1/1     Running   0          9m27s

可以看到,workingfalse 的 Pod 已经被更新。

查询一下所用的镜像:

$ 1 namespaces, 5 pods, 5 containers and 2 different images
+----------+-----------+---------------+
|   Pod    | Container |     Image     |
+----------+-----------+---------------+
| sample-0 | main      | alpine:3.18.2 |
+----------+           +---------------+
| sample-1 |           | alpine:3.18.0 |
+----------+           +---------------+
| sample-2 |           | alpine:3.18.2 |
+----------+           +---------------+
| sample-3 |           | alpine:3.18.0 |
+----------+           +---------------+
| sample-4 |           | alpine:3.18.2 |
+----------+-----------+---------------+

这里就看得出,工作状态的 Pod 保持原样,而非工作状态的 Pod 已经被更新。

继续推动

如果此时再次更新,应该让更新后的实例进入工作状态,把原有工作实例设为空闲,并减小 Partition 数量,所以把 1 号 Pod 设置为工作状态,同时把 4 号 Pod 设置为空闲状态,最后把分区数量缩减为 2,看看会发生什么:

$ kubectl images
[Summary]: 1 namespaces, 5 pods, 5 containers and 2 different images
+----------+-----------+---------------+
|   Pod    | Container |     Image     |
+----------+-----------+---------------+
| sample-0 | main      | alpine:3.18.2 |
+----------+           +---------------+
| sample-1 |           | alpine:3.18.0 |
+----------+           +---------------+
| sample-2 |           | alpine:3.18.2 |
+----------+           +---------------+
| sample-3 |           | alpine:3.18.0 |
+----------+           +               +
| sample-4 |           |               |
+----------+-----------+---------------+

这里看到,只有 sample-0sample-2 还在使用 3.18.2 版本,其它副本都成功变更为 3.18.0 的镜像。

其实不难看出,要把上述功能实现到自动化流程里,还是需要编写一些控制逻辑的——以及引进一个三方软件的成本,活罪难逃不是?

Avatar
崔秀龙

简单,是大师的责任;我们凡夫俗子,能做到清楚就很不容易了。

comments powered by Disqus
下一页
上一页