关于阿里云:应用纳管和灰度发布谐云基于-KubeVela-的企业级云原生实践

39次阅读

共计 7997 个字符,预计需要花费 20 分钟才能阅读完成。

以下文章来源于谐云科技,作者陈炜舜

在 OAM 最早推出时,谐云就参加其中,并基于社区中 oam-kubernetes-runtime 我的项目二次开发,以满足容器云产品中 OAM 利用模型的性能需要。该性能是将利用划分为多个 Kubernetes 资源 —— 组件(Component)、配置清单(ApplicationConfiguration = Component + Trait),其目标是心愿将用户侧的开发、运维视角进行拆散,并可能借助社区的资源疾速上线一些开源组件和运维特色。

后续,KubeVela 我的项目在组件和配置清单两个资源上形象出利用资源(Application),并借助 cuelang 实现 KubeVela 的渲染引擎。谐云疾速集成了 KubeVela,并将原有的多种利用模型(基于 Helm、基于原生 Workload、基于 OAM 的利用模型)对立成基于 KubeVela 的利用模型。这样做既加强了谐云 Kubernetes 底座的扩展性和兼容性,同时又基于 Application 这一形象资源拆散的基础架构和平台研发,将许多业务性能下沉到底座基础设施,以便适应社区一直倒退的节奏,疾速接入多种解决方案。

除此之外,确定的、对立的利用模型可能帮忙谐云多产品间的交融,尤其是容器云产品和中间件产品的交融,将中间件产品中提供的多款中间件作为不同的组件类型疾速接入到容器云平台,用户在解决中间件个性时应用中间件平台的能力,在解决底层资源运维时,应用容器云平台的能力。

基于以上背景,谐云对社区版本治理、纳管性能进行了加强,并心愿可能分享至社区进行探讨,引发更多的思考后,对社区性能做出奉献。

利用版本治理

版本控制与回滚

在利用运维时,利用的版本控制是用户十分关怀的问题,KubeVela 社区中提供了 ApplicationRevision 资源进行版本治理,该资源在用户每次批改 Application 时将产生新的版本,记录用户的批改,实现用户对每次批改的审计和回滚。

而谐云的利用模型当中,因为组件可能会蕴含一些“不须要计入版本”的纯运维 Trait,例如版本升级的 Trait,手动指定实例数的 Trait 等,咱们在降级、回滚时,须要将这些 Trait 疏忽。

在社区晚期版本中,TraitDefinition 含有 skipRevisionAffect 字段,该字段在晚期社区版本中实现如下:

  • ApplicationRevision 中仍会记录 skipRevisionAffect 的 Trait
  • 若用户触发的批改范畴仅蕴含 skipRevisionAffect 的 Trait,将此次更新间接批改至以后记录的最新版本中
  • 若用户触发的批改范畴不仅蕴含 skipRevisionAffect 的 Trait,将此次更新作为新版本记录

这样实现的 skipRevisionAffect 无奈真正使 Trait 不计入版本,例如,咱们将 manualscaler 作为不计入版本运维特色,与 Deployment 的伸缩相似,当咱们仅批改 manualscaler,新的实例数量会被计入到最新版本,但当咱们的版本真正产生扭转产生新的版本后,再次手动批改了实例数量,最初因为某些起因回滚到上一个版本时,此时实例数量将产生回滚(如下图)。而通常状况下,决定利用实例数量的起因不在其处于什么版本,而在以后的资源使用率、流量等环境因素。且在 Deployment 的应用中,实例数量也不受版本的影响。

基于以上需要,谐云提出了一套另一种思路的 版本治理设计 [ 1],在记录版本时,将彻底疏忽 skipRevisionAffect 的 Trait,在进行版本回滚时,将以后 Application 中蕴含的 skipRevisionAffect 的 Trait 合并到指标版本中,这样便是的这些纯运维的 Trait 不会随着利用版本的扭转而扭转。

下图是该设计的版本治理过程:

  • testapp 利用中蕴含 nginx 组件,且镜像版本为 1.16.0,其中蕴含三个运维特色,manualreplicas 管制其实例数量,是 skipRevisionAffect 的 Trait,该利用公布后,版本治理控制器将记录该版本至自定义的 Revision 中,且将 manualreplicas 从组件的运维特色中删除;
  • 当批改 testapp 的镜像版本、实例数量及其他运维特色,产生降级时,将生成新版本的 Revision,且 manualreplicas 仍被删除;
  • 此时若产生回滚,新的 Application 将应用 v1 版本 Revision 记录的信息与回滚前版本(v2)进行合并,失去实例数量为 2 的 1.6.0 的 nginx 组件。

版本升级

版本治理除了版本控制和回滚之外,还须要关注利用的降级过程,社区目前较为风行的形式是应用 kruise-rollout 的 Rollout 资源对工作负载进行金丝雀降级。咱们在应用 kruise-rollout 时发现,在金丝雀降级过程中,利用新旧版本最多可能同时存在两倍实例数量的实例,在某些资源有余的环境中,可能呈现因为资源有余导致实例无奈启动,从而阻塞降级过程。

基于以上场景,咱们在 kruise-rollout 上进行了二次开发,增加了滚动降级的金丝雀策略,可能使得利用在降级过程中通过新版本滚动替换旧版本实例,管制实例数量总数最大不超过实例数量 + 滚动步长。

但这么做依然存在一些问题,例如 kruise-rollout 可能齐全兼容降级过程中的实例扩缩场景(无论是 hpa 触发还是手动扩缩),但带有滚动策略的降级过程一开始就须要确定总的降级实例数量,且在降级过程中,hpa 和手动扩缩容都将生效。

咱们认为带有滚动策略的金丝雀公布仍在某些资源有余的场景下是有用的,所有并没有更改社区 kruise-rollout 的策略,仅是在社区的版本上做了一些补充。

以下是社区版本的金丝雀降级过程与带有滚动策略的金丝雀降级的过程:

  • 社区金丝雀降级过程:
  • 带有滚动策略的金丝雀降级过程:

小结

咱们 在基于 KubeVela 的利用模型上对利用版本治理采纳了另外一种思路,次要为了满足上文中形容的场景,利用版本治理的整体架构如下:

  • 通过 vela-core 治理利用模型
  • 通过自研的 rollback-controller 进行利用版本控制和利用回滚
  • 通过二次开发的 kruise-rollout 进行利用降级

利用纳管

在接入 KubeVela 的同时,面对存量集群的利用模型纳管也是一个值得思考的话题。对于谐云而言,将 KubeVela 定义为新版本容器云的惟一利用模型,在平台降级过程中,纳管问题也是无奈防止的。

因为咱们定义的 ComponentDefinition 和存量集群中的工作负载在大部分状况下都存在差别,间接将原有的工作负载转换为 Component 将导致存量业务的重启。而平台降级后,KubeVela 作为咱们的惟一模型,咱们须要在业务上可能看到原有的利用,但不心愿它间接重启,而是在冀望的工夫窗口有打算地按需重启。

为了解决上述矛盾,咱们提出了以下纳管思路:

首先要做的是在平台降级过程中,尽可能地不去影响原有的利用,即 在首次纳管时咱们通过社区中提供的 ref-objects 组件对现有的工作负载进行纳管。因为容器云产品中面向的是 Application 资源,此时被纳管的组件在利用模型中无奈进行日常的运维操作(没有可用的运维特色),但仍能够通过工作负载资源间接运维(如间接操作 Deployment)。

咱们 将工作负载及其关联资源转换为 Component 的关键在于了解 Definition。在 KubeVela 中,工作负载及其关联资源是通过 cuelang 进行渲染生成的,这是一个凋谢的模型,咱们无奈假设 Definition 的内容,但咱们冀望编写 Definition 的人员能够同时编写 Decompile 资源领导程序如何将工作负载及其关联资源转换为 Component 或 Trait。

这就相似于咱们将 Definition 作为一次事务,而回滚时要执行的动作由 Decompile 决定,两者都是凋谢的,具体行为取决于开发者。

在首次纳管之后到下一次纳管指标组件进行版本升级之前,咱们将持续保持上述状态,等到该组件进行降级时,咱们将通过“反编译”将纳管指标工作负载及其关联资源转换为 Component + Trait,并将须要降级的局部合并到反编译的后果中,通过容器云平台更新到 Application 中,彻底实现利用模型的转换

该过程如下图所示:

示例

例如,咱们蕴含一个节点亲和类型的运维特色:

# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
apiVersion: core.oam.dev/v1beta1
kind: TraitDefinition
metadata:
  annotations:
    definition.oam.dev/description: Add nodeAffinity for your Workload
  name: hc.node-affinity
  namespace: vela-system
spec:
  appliesToWorkloads:
  - hc.deployment
  podDisruptive: true
  schematic:
    cue:
      template: |
        parameter: {
          isRequired: bool
          labels: [string]: string
        }

        patch: spec: template: spec: affinity: nodeAffinity: {
          if parameter.isRequired == true {
            // +patchKey=matchExpressions
            requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: [
              for k, v in parameter.labels {
                {
                  matchExpressions: [
                    {
                      key:      k
                      operator: "In"
                      values: [v]
                    },
                  ]
                }
              },
            ]
          }
          if parameter.isRequired == false {
            // +patchKey=preference
            preferredDuringSchedulingIgnoredDuringExecution: [
              for k, v in parameter.labels {
                {
                  weight: 50
                  preference: matchExpressions: [{
                    key:      k
                    operator: "In"
                    values: [v]
                  }]
                }
              },
            ]
          }
        }

同时咱们还蕴含一个从 Deployment 节点亲和到 Trait 转换的 Decompile 资源(它的 cuelang 模型与 Trait 相似,都是通过参数和输入局部组成,只是在正向过程中,output 输入的是 CR,而在本过程中,output 输入的是 component 或是 trait):

apiVersion: application.decompile.harmonycloud.cn/v1alpha1
kind: DecompileConfig
metadata:
  annotations:
    "application.decompile.harmonycloud.cn/description": "decompiling deployment node affinity to application"
  labels:
    "decompiling/apply": "true"
    "decompiling/type": "node-affinity"
  name: node-affinity-decompile
  namespace: vela-system
spec:
  targetResource:
    - deployment
  schematic:
    cue:
      template: |
        package decompile

        import (
          "k8s.io/api/apps/v1"
          core "k8s.io/api/core/v1"
        )

        parameter: {deployment: v1.#Deployment}

        #getLabels: {
          x="in": core.#NodeSelectorTerm
          out: [string]: string

          if x.matchExpressions != _|_ {
            for requirement in x.matchExpressions {
              key: requirement.key
              if requirement.operator == "In" {
                if requirement.values == _|_ {out: "(key)": ""
                }
                if requirement.values != _|_ {
                  for i, v in requirement.values {
                    if i == 0 {out: "(key)": v
                    }
                  }
                }
              }
            }
          }
          if x.matchFields != _|_ {
            for requirement in x.matchFields {
              if requirement.operator == "In" {
                if requirement.values == _|_ {out: "(requirement.key)": ""
                }
                if requirement.values != _|_ {
                  for i, v in requirement.values {
                    if i == 0 {out: "(requirement.key)": v
                    }
                  }
                }
              }
            }
          }
        }

        #outputParameter: {
          isRequired: bool
            labels: [string]: string
        }

        #decompiling: {
          x="in": v1.#Deployment
          out?: #outputParameter

          if x.spec != _|_ && x.spec.template.spec != _|_ && x.spec.template.spec.affinity != _|_ && x.spec.template.spec.affinity.nodeAffinity != _|_  {
            nodeAffinity: x.spec.template.spec.affinity.nodeAffinity
            if nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution != _|_ {
              out: isRequired: true
              for term in nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms {result: #getLabels & {in: term}
                if result.out != _|_ {
                  for k, v in result.out {
                    out: labels: {"(k)": "(v)"
                    }
                  }
                }
              }
            }
            if nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution != _|_ {
              out: isRequired: false
              for schedulingTerm in nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution {
                if schedulingTerm.preference != _|_ {result: #getLabels & {in: schedulingTerm.preference}
                  if result.out != _|_ {
                    for k, v in result.out {
                      out: labels: {"(k)": "(v)"
                      }
                    }
                  }
                }
              }
            }
          }
        }

        result: #decompiling & {in: parameter.deployment}

        output: traits: [
          if result.out != _|_ {
            {
              type: "hc.node-affinity"
              properties: #outputParameter & result.out
            }
          }
        ]

(因为长度起因,省略了 hc.deployment 的正反渲染)

咱们在集群中创立这样一个 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: area
                operator: In
                values:
                - east
      containers:
      - image: 10.120.1.233:8443/library/nginx:1.21
        name: nginx
        ports:
        - containerPort: 80
          protocol: TCP

通过调用 kubevela-decompile-controller 提供的 API,将 demo-app 进行转换,将失去如下后果,平台能够将 data 中的 component 替换掉 Application 中的 ref-objects 组件。

{
  "code": 0,
  "message": "success",
  "data": {
    "components": [
      {
        "name": "demo-app",
        "type": "hc.deployment",
        "properties": {"initContainers": [],
          "containers": [
            {
              "name": "nginx",
              "image": "10.120.1.233:8443/library/nginx:1.21",
              "imagePullPolicy": "IfNotPresent",
              "ports": [
                {
                  "port": 80,
                  "protocol": "TCP"
                }
              ]
            }
          ]
        },
        "traits": [
          {
            "type": "hc.dns",
            "properties": {"dnsPolicy": "ClusterFirst"}
          },
          {
            "type": "hc.node-affinity",
            "properties": {
              "isRequired": true,
              "labels": {"area": "east"}
            }
          },
          {
            "type": "hc.manualscaler",
            "properties": {"replicas": 1}
          }
        ]
      }
    ]
  }
}

小结

谐云通过类比事务的形式,将渲染过程分为正向和逆向,同时将首次纳管和真正的纳管动作进行了拆散,实现了平台降级的同时,给利用的纳管行为留下了肯定的可操作空间。这是一种利用纳管的思路,近期社区当中对于利用纳管的探讨也非常炽热,并且在 1.7 的版本更新中也推出了 利用纳管的能力 [ 2],同时同样反对“反向渲染”的性能,可能反对咱们将现有的纳管模式迁徙到社区的性能中。

结语

到此,内容分享完结,心愿可能引发更多思考,对社区性能做出奉献。

您能够通过如下资料理解更多对于 KubeVela 以及 OAM 我的项目的细节:

  • 我的项目代码库:
    github.com/oam-dev/kubevela
    欢送 Star/Watch/Fork!
  • 我的项目官方主页与文档:kubevela.io
    从 1.1 版本开始,已提供中文、英文文档,更多语言文档欢送开发者进行翻译。
  • 我的项目钉钉群:23310022;Slack:CNCF #kubevela Channel
  • 退出微信群:请先增加以下 maintainer 微信号,表明进入 KubeVela 用户群:

相干链接

[1] 版本治理能力

https://github.com/kubevela/k…

[2] 利用纳管的能力

https://kubevela.net/docs/nex…

点击此处查看 KubeVela 我的项目官网

正文完
 0