天天看點

諧雲釋出基于KubeVela增強的應用版本管理和線上更新

作者:諧雲

在 OAM 最早推出時,諧雲就參與其中,并基于社群中 oam-kubernetes-runtime 項目二次開發,以滿足容器雲産品中 OAM 應用模型的功能需求。該功能是将應用劃分為多個 Kubernetes 資源 —— 元件(Component)、配置清單(ApplicationConfiguration = Component + Trait),其目的是希望将使用者側的開發、運維視角進行分離,并能夠借助社群的資源快速上線一些開源元件和運維特征。

後續,KubeVela 項目在元件和配置清單兩個資源上抽象出應用資源(Application),并借助 cuelang 實作 KubeVela 的渲染引擎。諧雲快速內建了 KubeVela,并将原有的多種應用模型(基于 Helm、基于原生 Workload、基于 OAM 的應用模型)統一成基于 KubeVela 的應用模型。這樣做既增強了諧雲 Kubernetes 底座的擴充性和相容性,同時又基于 Application 這一抽象資源分離的基礎架構和平台研發,将許多業務功能下沉到底座基礎設施,以便适應社群不斷發展的節奏,快速接入多種解決方案。

除此之外,确定的、統一的應用模型能夠幫助諧雲多産品間的融合,尤其是容器雲産品和中間件産品的融合,将中間件産品中提供的多款中間件作為不同的元件類型快速接入到容器雲平台,使用者在進行中間件特性時使用中間件平台的能力,在處理底層資源運維時,使用容器雲平台的能力。

01

應用版本管理

版本控制與復原

在應用運維時,應用的版本控制是使用者非常關心的問題,KubeVela 社群中提供了 ApplicationRevision 資源進行版本管理,該資源在使用者每次修改 Application 時将産生新的版本,記錄使用者的修改,實作使用者對每次修改的審計和復原。

而諧雲的應用模型當中,由于元件可能會包含一些“不需要計入版本”的純運維 Trait,例如版本更新的 Trait,手動指定執行個體數的 Trait 等,我們在更新、復原時,需要将這些 Trait 忽略。在社群早期版本中,TraitDefinition 含有 skipRevisionAffect 字段,該字段在早期社群版本中實作如下:

  • ApplicationRevision 中仍會記錄 skipRevisionAffect 的 Trait
  • 若使用者觸發的修改範圍僅包含 skipRevisionAffect 的 Trait,将此次更新直接修改至目前記錄的最新版本中
  • 若使用者觸發的修改範圍不僅包含 skipRevisionAffect 的 Trait,将此次更新作為新版本記錄

這樣實作的 skipRevisionAffect 無法真正使 Trait 不計入版本,例如,我們将 manualscaler 作為不計入版本運維特征,與 Deployment 的伸縮類似,當我們僅修改 manualscaler ,新的執行個體數量會被計入到最新版本,但當我們的版本真正發生改變産生新的版本後,再次手動修改了執行個體數量,最後因為某些原因復原到上一個版本時,此時執行個體數量将發生復原(如下圖)。而通常情況下,決定應用執行個體數量的原因不在其處于什麼版本,而在目前的資源使用率、流量等環境因素。且在 Deployment 的使用中,執行個體數量也不受版本的影響。

諧雲釋出基于KubeVela增強的應用版本管理和線上更新

基于以上需求,諧雲提出了一套另一種思路的版本管理設計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 元件。
諧雲釋出基于KubeVela增強的應用版本管理和線上更新

版本更新

版本管理除了版本控制和復原之外,還需要關注應用的更新過程,社群目前較為流行的方式是使用 kruise-rollout 的 Rollout 資源對工作負載進行金絲雀更新。我們在使用 kruise-rollout 時發現,在金絲雀更新過程中,應用新舊版本最多可能同時存在兩倍執行個體數量的執行個體,在某些資源不足的環境中,可能出現由于資源不足導緻執行個體無法啟動,進而阻塞更新過程。

基于以上場景,我們在 kruise-rollout 上進行了二次開發,添加了滾動更新的金絲雀政策,能夠使得應用在更新過程中通過新版本滾動替換舊版本執行個體,控制執行個體數量總數最大不超過執行個體數量+滾動步長。但這麼做仍然存在一些問題,例如 kruise-rollout 能夠完全相容更新過程中的執行個體擴縮場景(無論是 hpa 觸發還是手動擴縮),但帶有滾動政策的更新過程一開始就需要确定總的更新執行個體數量,且在更新過程中,hpa 和手動擴縮容都将失效。我們認為帶有滾動政策的金絲雀釋出仍在某些資源不足的場景下是有用的,所有并沒有更改社群 kruise-rollout 的政策,僅是在社群的版本上做了一些補充。以下是:

  • 社群金絲雀更新過程:
諧雲釋出基于KubeVela增強的應用版本管理和線上更新
  • 帶有滾動政策的金絲雀更新過程:
諧雲釋出基于KubeVela增強的應用版本管理和線上更新

小結

我們在基于 KubeVela 的應用模型上對應用版本管理采用了另外一種思路,主要為了滿足上文中描述的場景,應用版本管理的整體架構如下:

諧雲釋出基于KubeVela增強的應用版本管理和線上更新
  • 通過 vela-core 管理應用模型
  • 通過自研的 rollback-controller 進行應用版本控制和應用復原
  • 通過二次開發的 kruise-rollout 進行應用更新

02

應用納管

在接入 KubeVela 的同時,面對存量叢集的應用模型納管也是一個值得思考的話題。對于諧雲而言,将 KubeVela 定義為新版本容器雲的唯一應用模型,在平台更新過程中,納管問題也是無法避免的。由于我們定義的 ComponentDefinition 和存量叢集中的工作負載在大部分情況下都存在差異,直接将原有的工作負載轉換為 Component 将導緻存量業務的重新開機。而平台更新後,KubeVela 作為我們的唯一模型,我們需要在業務上能夠看到原有的應用,但不希望它直接重新開機,而是在期望的時間視窗有計劃地按需重新開機。:首先要做的是在平台更新過程中,盡可能地不去影響原有的應用,即在首次納管時我們通過社群中提供的 ref-objects 元件對現有的工作負載進行納管。由于容器雲産品中面向的是 Application 資源,此時被納管的元件在應用模型中無法進行日常的運維操作(沒有可用的運維特征),但仍可以通過工作負載資源直接運維(如直接操作 Deployment)。

諧雲釋出基于KubeVela增強的應用版本管理和線上更新

我們将工作負載及其關聯資源轉換為 Component 的關鍵在于了解 Definition。在 KubeVela 中,工作負載及其關聯資源是通過 cuelang 進行渲染生成的,這是一個開放的模型,我們無法假定 Definition 的内容,但我們期望編寫 Definition 的人員可以同時編寫 Decompile 資源指導程式如何将工作負載及其關聯資源轉換為 Component 或 Trait。這就類似于我們将 Definition 作為一次事務,而復原時要執行的動作由 Decompile 決定,兩者都是開放的,具體行為取決于開發者。在首次納管之後到下一次納管目标元件進行版本更新之前,我們将持續保持上述狀态,等到該元件進行更新時,我們将通過“反編譯”将納管目标工作負載及其關聯資源轉換為 Component + Trait,并将需要更新的部分合并到反編譯的結果中,通過容器雲平台更新到 Application 中,徹底完成應用模型的轉換。該過程如下圖所示:

諧雲釋出基于KubeVela增強的應用版本管理和線上更新

示例

例如,我們包含一個節點親和類型的運維特征:

# 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 元件。

諧雲釋出基于KubeVela增強的應用版本管理和線上更新
{
"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,同時同樣支援“反向渲染”的功能,能夠支援我們将現有的納管模式遷移到社群的功能中。

到此,内容分享結束,希望能夠引發更多思考,對社群功能做出貢獻。如需咨詢交流,可添加下方官方交流号或在公衆号留言。

【1】https://github.com/kubevela/kubevela/issues/2168

【2】https://kubevela.net/docs/next/end-user/policies/resource-adoption#use-in-application

繼續閱讀