使用 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 可以使用整数和百分比两种方式
- 可以使用标签的方式指定升级时候的优先级。
如果这两个方法能够同时生效,这个需求就有望完成了,设计要点如下:
- 从仲裁服务中,获得工作和空闲副本的情况
- 刷新 Statefulset 中的 Pod 标签,用于指示当前工作状态。
- 设置 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"
这里的定义有几个需要关注的点:
- Pod 模版中加入了
working
标签,用于指示每个 Pod 当前的工作状态。 unorderedUpdate.priorityStrategy.weightPriority
:这里指定了非工作状态的 Pod 会有更高优先级。partition
设置为 3:这里我们假设工作中的副本有 3 个。- 这里镜像使用
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
。
标识工作负载
我们使用标签,把第 0
、2
、4
三个 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
可以看到,working
为 false
的 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-0
和 sample-2
还在使用 3.18.2
版本,其它副本都成功变更为 3.18.0
的镜像。
注
其实不难看出,要把上述功能实现到自动化流程里,还是需要编写一些控制逻辑的——以及引进一个三方软件的成本,活罪难逃不是?