# 使用 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，示例如下：

```yaml
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：

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

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

```bash
$  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`。

## 标识工作负载

我们使用标签，把第 `0`、`2`、`4` 三个 Pod 的 `working` 标签设置为 `true`。

```bash
$ 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`：

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

## 验证结果

查看 Pod 状态：

```bash
$ 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
```

可以看到，`working` 为 `false` 的 Pod 已经被更新。

查询一下所用的镜像：

```bash
$ 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，看看会发生什么：

```bash
$ 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-0` 和 `sample-2` 还在使用 `3.18.2` 版本，其它副本都成功变更为 `3.18.0` 的镜像。

## 注

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