在 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 的使用中,實例數量也不受版本的影響。
基于以上需求,諧云提出了一套另一種思路的版本管理設計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 進行應用升級
02
應用納管
在接入 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,同時同樣支持“反向渲染”的功能,能夠支持我們將現有的納管模式遷移到社區的功能中。