文档说明
该文档主要记录了我在使用OpenKruise的操作,官方的文档相对来说比较简洁,对使用姿势和API的示例并不是很直观,因此我把整体测试流程记录了下来,如果已经熟练使用OpenKruise的大神,则可以跳过该文档。
背景
这两年,随着云计算的发展,云原生的概念应运而生。为了让应用能拥有云一样的特性,每个公司也开始了各自的云原生实践之路。
从今年上半年,我们公司也开始了自己的云原生之路。概括的来说,云原生 = DevOps+持续交付+服务网格+容器化
DevOps和持续交付,我们已经拥有了对应的发布和工单等系统,已经在公司内部平稳维护和使用,当前需要解决的就是服务网格和容器化。由于历史原因和技术债务考虑,我们决定先进行应用的容器化改造,然后再进行服务网格的支持。
容器化的改造也并不是一蹴而就的。对于那些有一定的发展时间和规模的公司,都会存在自身的技术债务,业务可能会依赖固定IP,应用可能依旧是有状态的等等。因此,对我们而言,在尽量不影响业务开发的前提下,我们选择使用statefulset且固定IP的方式进行前期的容器化迁移。
前期容器化改造
大部分互联网公司都会有三个环境:线下开发环境、预发环境和正式生产环境
最开始,我们打算先将线下环境进行容器化改造,接下来伴随这k8s的环境准备,发布系统的容器化改造,我们已经实现了应用通过发布系统发布后,从部署在原先的云服务器到部署在k8s的节点上。这里的细节就不描述了,由于我们公司所有的系统都是自研的,原理基本就是应用创建对应的statefulset进行pod的管理。
当线下环境容器化改造完成以后,在推行的过程中,业务开发也反馈了一些容器化后出现的问题,主要为以下几点:
- 发布的时间变长,影响了业务开发的效率。
相较于原先云服务器的发布方式,只需要更新应用的构建物,然后重新启动应用就完成了应用的发布。而如果容器化以后,pod会经过Terminating销毁后,再进行pending重新调度,最后到容器的running,相比较耗时长,另外如果在线上环境,pod数量较多,预计的pending时间会更长。
- 发布方式单一
对于使用statefulset的方式,如果对于某个应用存在多个pod实例,并不支持多个pod的并行发布,只能够依次顺序发布,如果前一个pod没有发布成功,后一个pod不会开始发布,这种场景在线上大批量的分批灰度发布场景肯定是不满足要求的
为了优化以上问题,我们查阅了相关资料,也听了线下云原生相关的meetUp,最后打算使用阿里开源的Openkruise,对整体发布流程进行优化。
关于OpenKruise
OpenKruise 是阿里云开源的云原生应用自动化管理套件,也是当前托管在 Cloud Native Computing Foundation (CNCF) 下的 Sandbox 项目。
官方地址:
https://openkruise.io
对于openKruise,我们比较关注Advanced StatefulSet的原地升级和并行、灰度分批发布功能等。
使用和测试过程
安装OpenKruise
https://openkruise.io/zh-cn/docs/installation.html
比较简单,照着文档来就行了
测试原地升级功能
首先需要创建Advanced StatefulSet
这里需要说明两点:
- 对当前已经创建了k8s原生的StatefulSet,不能通过修改yaml,转换成Advanced StatefulSet,需要将原先的statefulset删除,然后重新创建
- 创建完成以后,通过kubectl get statefulset 名称 -n xxx 的方式无法看到创建Advanced StatefulSet信息,因此如果使用的是其他云厂商的容器服务,则在控制台对应的workload中查看不到对应的资源信息。例如我们公司使用的是腾讯云的TKE服务,在控制台上就看不到创建的Advanced StatefulSet。但是可以通过kubectl get sts.apps.kruise.io查看
创建的yaml如下:
apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
metadata:
annotations:
description: xxxx
tke.cloud.tencent.com/enable-static-ip: "true"
labels:
appname: xxxx
name: xxxx
namespace: xxxx
spec:
podManagementPolicy: OrderedReady
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
appname: xxxx
template:
metadata:
annotations:
tke.cloud.tencent.com/vpc-ip-claim-delete-policy: Never
labels:
appname: xxxx
spec:
readinessGates:
- conditionType: InPlaceUpdateReady
containers:
- env:
- name: PATH
value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
image: xxxx
imagePullPolicy: IfNotPresent
name: xxxx
readinessProbe:
failureThreshold: 3
httpGet:
path: /status
port: 80
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 300
resources:
limits:
cpu: 2500m
memory: 4Gi
requests:
cpu: 1500m
memory: 2Gi
securityContext:
privileged: true
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c","/home/mapp/bin/stop-app.sh"]
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: offline-test
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
updateStrategy:
type: RollingUpdate
rollingUpdate:
podUpdatePolicy: InPlaceIfPossible
inPlaceUpdateStrategy:
gracePeriodSeconds: 10
说明:已经将yaml中的敏感信息去除。
创建以后,通过以下命令查看:
kubectl get sts.apps.kruise.io
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLzcjN3MTMwAjMzETNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
原地升级的条件:
InPlaceIfPossible: 控制器会优先尝试原地升级 Pod,如果不行再采用重建升级。目前,只有修改 spec.template.metadata.* 和 spec.template.spec.containers[x].image 这些字段才可以走原地升级。
因此我重新设置了yaml中的镜像版本,然后执行
kubectl apply -f .\statefulset.yaml
执行命令的同时,通过命令监听:
kubectl get sts.apps.kruise.io -w
kubectl get pod pod的名称 --watch
查看pod的event事件
总体从替换镜像后到容器正常启动,总共耗时40s
进入pod查看业务启动状态,镜像版本已经更新
结果对比
原地升级的方式与之前发布的方式进行对比,差异就在于原地升级不需要进行pod的销毁和调度过程。原地升级从更新镜像到容器启动成功,花费40s,而原先的方式,容器的销毁和调度花费的时间为70s左右。
另外在配置上,我设置了gracePeriodSeconds
因此我们又在原地升级中提供了 graceful period 选项,作为优雅原地升级的策略。用户如果配置了 gracePeriodSeconds 这个字段,控制器在原地升级的过程中会先把 Pod status 改为 not-ready,然后等一段时间(gracePeriodSeconds),最后再去修改 Pod spec 中的镜像版本。 这样,就为 endpoints-controller 这些控制器留出了充足的时间来将 Pod 从 endpoints 端点列表中去除。
如果将gracePeriodSeconds配置去除,原地升级的时间能够缩短为30s
测试MaxUnavailable 策略
maxUnavailable 策略来支持并行 Pod 发布,它会保证发布过程中最多有多少个 Pod 处于不可用状态。注意,maxUnavailable 只能配合 podManagementPolicy 为 Parallel 来使用。
说明: 经测试,不允许在之前的Advanced StatefulSet上更新发布策略
Resource: "apps.kruise.io/v1beta1, Resource=statefulsets", GroupVersionKind: "apps.kruise.io/v1beta1, Kind=StatefulSet" Name: "xxxx", Namespace: "xxx" for: ".\\statefulset.yaml": admission webhook "vstatefulset.kb.io" denied the request: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', 'reserveOrdinals', 'lifecycle' and 'updateStrategy' are forbidden
因此将之前的Advanced StatefulSet进行删除,然后创建
yaml如下:
apiVersion: apps.kruise.io/v1beta1 kind: StatefulSet metadata: annotations: description: xxxx tke.cloud.tencent.com/enable-static-ip: "true" labels: appname: xxxx name: xxxx namespace: xxx spec: podManagementPolicy: Parallel replicas: 3 revisionHistoryLimit: 10 selector: matchLabels: appname: xxxx template: metadata: annotations: tke.cloud.tencent.com/vpc-ip-claim-delete-policy: Never labels: appname: xxxx spec: readinessGates: - conditionType: InPlaceUpdateReady containers: - env: - name: PATH value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin image: xxxx imagePullPolicy: Always name: flowplusdevhosttest readinessProbe: failureThreshold: 3 httpGet: path: /status port: 80 scheme: HTTP initialDelaySeconds: 60 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 300 resources: limits: cpu: 2500m memory: 4Gi requests: cpu: 1500m memory: 2Gi securityContext: privileged: true lifecycle: preStop: exec: command: ["/bin/sh","-c","/home/mapp/bin/stop-app.sh"] terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst imagePullSecrets: - name: offline-test restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 updateStrategy: type: RollingUpdate rollingUpdate: podUpdatePolicy: InPlaceIfPossible maxUnavailable: 100%
说明:
创建了Advanced StatefulSet,副本数设置了3个,同时更新策略设置成了并行,最大不可用数为100%,说明在更新时,可以进行批量同时更新。
测试并行发布
此时如果修改了镜像版本后,触发了3个pod的原地升级,观察3个pod是否同时进行升级
修改了镜像版本后,Advanced StatefulSet的确同时从Ready状态数值转换成了0
最终发布完成以后:
查看对应三个pod的event
pod-0:
pod-1:
pod-2:
最终得出结论:能够支持并行发布,3个pod同时进行原地升级
测试MaxUnavailable
如果在并发发布的过程中,还可以设置maxUnavailable的值。
比如说3个pod中,我需要保证服务不能同时挂,起码有一个pod正常提供服务,那我可以设置maxUnavailable为2。
调整后进行发布:
Ready状态的pod会保留一个,不会导致所有的pod都停止服务
灰度分批发布
灰度发布通过partition字段进行控制
如果在发布过程中设置了 partition:
如果是数字,控制器会将 (replicas - partition) 数量的 Pod 更新到最新版本。
如果是百分比,控制器会将 (replicas * (100% - partition)) 数量的 Pod 更新到最新版本。
以之前创建的Advanced StatefulSet为例,如果我设置了partition为2,则更新时,只会更新一台机器
apiVersion: apps.kruise.io/v1beta1 kind: StatefulSet metadata: annotations: description: xxxx tke.cloud.tencent.com/enable-static-ip: "true" labels: appname: xxxx name: xxxx namespace: xxx spec: podManagementPolicy: Parallel replicas: 3 revisionHistoryLimit: 10 selector: matchLabels: appname: xxx template: metadata: annotations: tke.cloud.tencent.com/vpc-ip-claim-delete-policy: Never labels: appname: xxx spec: readinessGates: - conditionType: InPlaceUpdateReady containers: - env: - name: PATH value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin image: xxxx imagePullPolicy: Always name: xxxx readinessProbe: failureThreshold: 3 httpGet: path: /status port: 80 scheme: HTTP initialDelaySeconds: 60 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 300 resources: limits: cpu: 2500m memory: 4Gi requests: cpu: 1500m memory: 2Gi securityContext: privileged: true lifecycle: preStop: exec: command: ["/bin/sh","-c","/home/mapp/bin/stop-app.sh"] terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst imagePullSecrets: - name: offline-test restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 updateStrategy: type: RollingUpdate rollingUpdate: podUpdatePolicy: InPlaceIfPossible maxUnavailable: 2 partition: 2
测试灰度发布
接下来修改镜像版本:
只更新了一个pod的镜像版本
查看3个pod的信息
只有第三个 pod-2 的镜像更新了,其他两个镜像还是原来的镜像
如果此时partition设置为1,则会再更新一个pod的镜像版本。测试结果如下:
这个时候会继续更新一台,此时pod-1的镜像已经更新。
如果需要更新所有的镜像,则把partition设置为0,则会对所有pod进行更新。
控制发布顺序
控制发布顺序只是将原先statefulset的顺序发布,可以改成权重和序号发布。
weight: Pod 优先级是由所有 weights 列表中的 term 来计算 match selector 得出
order: Pod 优先级是由 orderKey 的 value 决定,这里要求对应的 value 的结尾能解析为 int 值
因此这里的发布顺序只是将原先的顺序发布通过selector进行自定义的排序,并不满足我们发布过程中,可以随意指定一台进行发布的操作。
因此略过
发布暂停
触发方式:
spec: # ... updateStrategy: rollingUpdate: paused: true
首先将maxUnavailable设置为2,则并行发布时,会同时并行发布两个pod,等两个pod发布成功以后,再发布另一台pod。测试在中途暂停发布。
更改镜像版本后,并行发布了两台,有一个pod依然保留,正常提供服务:
此时暂停发布:
从UPDATES的数量可以看出,当最早并行的两个pod已经发布完成,但是剩余的一个pod始终没有发布,因此发布已经暂停了。
然后将暂停发布关掉:
另一个pod也开始进行发布更新
序号保留
通过在 reserveOrdinals 字段中写入需要保留的序号,Advanced StatefulSet 会自动跳过创建这些序号的 Pod。如果 Pod 已经存在,则会被删除。 注意,spec.replicas 是期望运行的 Pod 数量
没有使用场景,略过
对于这点,我也比较好奇这个功能的实际场景是什么
总结
OpenKruise的确能够对当前发布系统的流程进行优化,提高开发使用的效率:
- 原地升级功能
通过原地升级的功能,能够减少pod销毁和调度的时间,能够减少40s左右的发布时间。另外在线上的节点数量规模,减少时间的效益应该更加明显。
- 并行发布能够
当前对于部署场景下有多个pod进行发布,使用openKruise,可以将原先的串行发布方式改变为并行发布,尤其是在线上大批量机器的发布,并行发布能够节约大量时间
- 分批灰度发布
当前openKruise可以通过partion的设置来进行机器的分批灰度发布,开发可以指定当前灰度多少个pod,但是不能随意指定任意一台pod进行发布
因此接下来,我们开始将原先的StatefulSet 转换为Advanced StatefulSet,另外由于之前我发布系统是Java + Python的应用,如果调用OpenKruise资源,有以下几种方式:
- 通过官方提供的golang客户端
- 通过官方提供的java api
- 通过python调用shell命令
最后我们选择使用官方的golang api:
https://github.com/openkruise/kruise-api
Golang客户端测试代码
官网的实例代码比较简洁,下面贴下我的测试代码:
go.mod:
module openKruiseApiTest
go 1.14
require github.com/openkruise/kruise-api v0.8.0-1.18
main.go:
package main
import (
"context"
"fmt"
kruiseclientset "github.com/openkruise/kruise-api/client/clientset/versioned"
"k8s.io/client-go/tools/clientcmd"
"log"
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func main() {
kubeconfig := filepath.Join("D:\\environment\\k8s\\", "xxxxx") //config的地址
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Fatal(err)
}
kruiseClient := kruiseclientset.NewForConfigOrDie(config)
statefulset, err := kruiseClient.AppsV1alpha1().StatefulSets("xxx").Get(context.TODO(),"xxxx",metav1.GetOptions{})
fmt.Printf("Result: %s \n", statefulset)
}