天天看点

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

文档说明

 该文档主要记录了我在使用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 
           
OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

原地升级的条件:

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
           
OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程
OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

查看pod的event事件

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

总体从替换镜像后到容器正常启动,总共耗时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

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

最终发布完成以后:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

查看对应三个pod的event

pod-0:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

pod-1:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

pod-2:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

最终得出结论:能够支持并行发布,3个pod同时进行原地升级

测试MaxUnavailable

如果在并发发布的过程中,还可以设置maxUnavailable的值。

比如说3个pod中,我需要保证服务不能同时挂,起码有一个pod正常提供服务,那我可以设置maxUnavailable为2。

调整后进行发布:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

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的镜像版本

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

查看3个pod的信息

只有第三个 pod-2 的镜像更新了,其他两个镜像还是原来的镜像

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

如果此时partition设置为1,则会再更新一个pod的镜像版本。测试结果如下:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

这个时候会继续更新一台,此时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依然保留,正常提供服务:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

此时暂停发布:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

从UPDATES的数量可以看出,当最早并行的两个pod已经发布完成,但是剩余的一个pod始终没有发布,因此发布已经暂停了。

然后将暂停发布关掉:

OpenKruise使用方法与测试文档说明背景前期容器化改造关于OpenKruise使用和测试过程

另一个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)

}