您好,登錄后才能下訂單哦!
背景
OpenKruise 是阿里云開源的大規模應用自動化管理引擎,在功能上對標了 Kubernetes 原生的 Deployment / StatefulSet 等控制器,但 OpenKruise 提供了更多的增強功能如:優雅原地升級、發布優先級/打散策略、多可用區workload抽象管理、統一 sidecar 容器注入管理等,都是經歷了阿里巴巴超大規模應用場景打磨出的核心能力。這些 feature 幫助我們應對更加多樣化的部署環境和需求、為集群維護者和應用開發者帶來更加靈活的部署發布組合策略。
目前在阿里巴巴內部云原生環境中,絕大部分應用都統一使用 OpenKruise 的能力做 Pod 部署、發布管理,而不少業界公司和阿里云上客戶由于 K8s 原生 Deployment 等負載不能完全滿足需求,也轉而采用 OpenKruise 作為應用部署載體。
今天的分享文章就從一個阿里云上客戶對接 OpenKruise 的疑問開始。這里還原一下這位同學的用法(以下 YAML 數據僅為 demo):
準備一份 Advanced StatefulSet 的 YAML 文件,并提交創建。如:
apiVersion: apps.kruise.io/v1alpha1
kind: StatefulSet
metadata:
name: sample
spec:
# ...
template:
# ...spec: containers: - name: main image: nginx:alpine
updateStrategy:
type: RollingUpdaterollingUpdate: podUpdatePolicy: InPlaceIfPossible
然后,修改了 YAML 中的 image 鏡像版本,然后調用 K8s api 接口做更新。結果收到報錯如下:
metadata.resourceVersion: Invalid value: 0x0: must be specified for an update
而如果使用 kubectl apply 命令做更新,則返回成功:
statefulset.apps.kruise.io/sample configured
問題在于,為什么同一份修改后的 YAML 文件,調用 api 接口更新是失敗的,而用 kubectl apply 更新是成功的呢?這其實并不是 OpenKruise 有什么特殊校驗,而是由 K8s 自身的更新機制所決定的。
從我們的接觸來看,絕大多數用戶都有通過 kubectl 命令或是 sdk 來更新 K8s 資源的經驗,但真正理解這些更新操作背后原理的人卻并不多。本文將著重介紹 K8s 的資源更新機制,以及一些我們常用的更新方式是如何實現的。
更新原理
不知道你有沒有想過一個問題:對于一個 K8s 資源對象比如 Deployment,我們嘗試在修改其中 image 鏡像時,如果有其他人同時也在對這個 Deployment 做修改,會發生什么?
當然,這里還可以引申出兩個問題:
其實,對一個 Kubernetes 資源對象做“更新”操作,簡單來說就是通知 kube-apiserver 組件我們希望如何修改這個對象。而 K8s 為這類需求定義了兩種“通知”方式,分別是 update 和 patch。在 update 請求中,我們需要將整個修改后的對象提交給 K8s;而對于 patch 請求,我們只需要將對象中某些字段的修改提交給 K8s。
那么回到背景問題,為什么用戶提交修改后的 YAML 文件做 update 會失敗呢?這其實是被 K8s 對 update 請求的版本控制機制所限制的。
Update 機制
Kubernetes 中的所有資源對象,都有一個全局唯一的版本號(metadata.resourceVersion)。每個資源對象從創建開始就會有一個版本號,而后每次被修改(不管是 update 還是 patch 修改),版本號都會發生變化。
官方文檔告訴我們,這個版本號是一個 K8s 的內部機制,用戶不應該假設它是一個數字或者通過比較兩個版本號大小來確定資源對象的新舊,唯一能做的就是通過比較版本號相等來確定對象是否是同一個版本(即是否發生了變化)。而 resourceVersion 一個重要的用處,就是來做 update 請求的版本控制。
K8s 要求用戶 update 請求中提交的對象必須帶有 resourceVersion,也就是說我們提交 update 的數據必須先來源于 K8s 中已經存在的對象。因此,一次完整的 update 操作流程是:
上圖展示了多個用戶同時 update 某一個資源對象時會發生的事情。而如果如果發生了 Conflict 沖突,對于 User A 而言應該做的就是做一次重試,再次獲取到最新版本的對象,修改后重新提交 update。
因此,我們上面的兩個問題也都得到了解答:
Patch 機制
相比于 update 的版本控制,K8s 的 patch 機制則顯得更加簡單。
當用戶對某個資源對象提交一個 patch 請求時,kube-apiserver 不會考慮版本問題,而是“無腦”地接受用戶的請求(只要請求發送的 patch 內容合法),也就是將 patch 打到對象上、同時更新版本號。
不過,patch 的復雜點在于,目前 K8s 提供了 4 種 patch 策略:json patch、merge patch、strategic merge patch、apply patch(從 K8s 1.14 支持 server-side apply 開始)。通過 kubectl patch -h 命令我們也可以看到這個策略選項(默認采用 strategic):
$ kubectl patch -h
--type='strategic': The type of patch being provided; one of [json merge strategic]
篇幅限制這里暫不對每個策略做詳細的介紹了,我們就以一個簡單的例子來看一下它們的差異性。如果針對一個已有的 Deployment 對象,假設 template 中已經有了一個名為 app 的容器:
json patch([RFC 6902]())
新增容器:
kubectl patch deployment/foo --type='json' -p \
'[{"op":"add","path":"/spec/template/spec/containers/1","value":{"name":"nginx","image":"nginx:alpine"}}]'
修改已有容器 image:
kubectl patch deployment/foo --type='json' -p \
'[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"app-image:v2"}]'
可以看到,在 json patch 中我們要指定操作類型,比如 add 新增還是 replace 替換,另外在修改 containers 列表時要通過元素序號來指定容器。
這樣一來,如果我們 patch 之前這個對象已經被其他人修改了,那么我們的 patch 有可能產生非預期的后果。比如在執行 app 容器鏡像更新時,我們指定的序號是 0,但此時 containers 列表中第一個位置被插入了另一個容器,則更新的鏡像就被錯誤地插入到這個非預期的容器中。
merge patch(RFC 7386)
merge patch 無法單獨更新一個列表中的某個元素,因此不管我們是要在 containers 里新增容器、還是修改已有容器的 image、env 等字段,都要用整個 containers 列表來提交 patch:
kubectl patch deployment/foo --type='merge' -p \
'{"spec":{"template":{"spec":{"containers":[{"name":"app","image":"app-image:v2"},{"name":"nginx","image":"nginx:alpline"}]}}}}'
顯然,這個策略并不適合我們對一些列表深層的字段做更新,更適用于大片段的覆蓋更新。
不過對于 labels/annotations 這些 map 類型的元素更新,merge patch 是可以單獨指定 key-value 操作的,相比于 json patch 方便一些,寫起來也更加直觀:
kubectl patch deployment/foo --type='merge' -p '{"metadata":{"labels":{"test-key":"foo"}}}'
strategic merge patch
這種 patch 策略并沒有一個通用的 RFC 標準,而是 K8s 獨有的,不過相比前兩種而言卻更為強大的。
我們先從 K8s 源碼看起,在 K8s 原生資源的數據結構定義中額外定義了一些的策略注解。比如以下這個截取了 podSpec 中針對 containers 列表的定義,參考 Github:
// ...
// +patchMergeKey=name
// +patchStrategy=merge
Containers []Container
json:"containers" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=containers"
可以看到其中有兩個關鍵信息:patchStrategy:"merge" patchMergeKey:"name" 。這就代表了,containers 列表使用 strategic merge patch 策略更新時,會把下面每個元素中的 name 字段看作 key。
簡單來說,在我們 patch 更新 containers 不再需要指定下標序號了,而是指定 name 來修改,K8s 會把 name 作為 key 來計算 merge。比如針對以下的 patch 操作:
kubectl patch deployment/foo -p \
'{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:mainline"}]}}}}'
如果 K8s 發現當前 containers 中已經有名字為 nginx 的容器,則只會把 image 更新上去;而如果當前 containers 中沒有 nginx 容器,K8s 會把這個容器插入 containers 列表。
此外還要說明的是,目前 strategic 策略只能用于原生 K8s 資源以及 Aggregated API 方式的自定義資源,對于 CRD 定義的資源對象,是無法使用的。這很好理解,因為 kube-apiserver 無法得知 CRD 資源的結構和 merge 策略。如果用 kubectl patch 命令更新一個 CR,則默認會采用 merge patch 的策略來操作。
kubectl 封裝
了解完了 K8s 的基礎更新機制,我們再次回到最初的問題上。為什么用戶修改 YAML 文件后無法直接調用 update 接口更新,卻可以通過 kubectl apply 命令更新呢?
其實 kubectl 為了給命令行用戶提供良好的交互體感,設計了較為復雜的內部執行邏輯,諸如 apply、edit 這些常用操作其實背后并非對應一次簡單的 update 請求。畢竟 update 是有版本控制的,如果發生了更新沖突對于普通用戶并不友好。以下簡略介紹下 kubectl 幾種更新操作的邏輯,有興趣可以看一下 kubectl 封裝的源碼。
apply
在使用默認參數執行 apply 時,觸發的是 client-side apply。kubectl 邏輯如下:
首先解析用戶提交的數據(YAML/JSON)為一個對象 A;然后調用 Get 接口從 K8s 中查詢這個資源對象:
如果查詢結果不存在,kubectl 將本次用戶提交的數據記錄到對象 A 的 annotation 中(key 為 kubectl.kubernetes.io/last-applied-configuration),最后將對象 A提交給 K8s 創建;
如果查詢到 K8s 中已有這個資源,假設為對象 B:1. kubectl 嘗試從對象 B 的 annotation 中取出 kubectl.kubernetes.io/last-applied-configuration 的值(對應了上一次 apply 提交的內容);2. kubectl 根據前一次 apply 的內容和本次 apply 的內容計算出 diff(默認為 strategic merge patch 格式,如果非原生資源則采用 merge patch);3. 將 diff 中添加本次的 kubectl.kubernetes.io/last-applied-configuration annotation,最后用 patch 請求提交給 K8s 做更新。
這里只是一個大致的流程梳理,真實的邏輯會更復雜一些,而從 K8s 1.14 之后也支持了 server-side apply,有興趣的同學可以看一下源碼實現。
edit
kubectl edit 邏輯上更簡單一些。在用戶執行命令之后,kubectl 從 K8s 中查到當前的資源對象,并打開一個命令行編輯器(默認用 vi)為用戶提供編輯界面。
當用戶修改完成、保存退出時,kubectl 并非直接把修改后的對象提交 update(避免 Conflict,如果用戶修改的過程中資源對象又被更新),而是會把修改后的對象和初始拿到的對象計算 diff,最后將 diff 內容用 patch 請求提交給 K8s。
總結
看了上述的介紹,大家應該對 K8s 更新機制有了一個初步的了解了。接下來想一想,既然 K8s 提供了兩種更新方式,我們在不同的場景下怎么選擇 update 或 patch 來使用呢?這里我們的建議是:
如果要更新的字段只有我們自己會修改(比如我們有一些自定義標簽,并寫了 operator 來管理),則使用 patch 是最簡單的方式;
如果要更新的字段可能會被其他方修改(比如我們修改的 replicas 字段,可能有一些其他組件比如 HPA 也會做修改),則建議使用 update 來更新,避免出現互相覆蓋。
最終我們的客戶改為基于 get 到的對象做修改后提交 update,終于成功觸發了 Advanced StatefulSet 的原地升級。此外,我們也歡迎和鼓勵更多的同學參與到 OpenKruise 社區中,共同合作打造一款面向規模化場景、高性能的應用交付解決方案。(歡迎加入釘釘交流群:23330762)
課程推薦
為了更多開發者能夠享受到 Serverless 帶來的紅利,這一次,我們集結了 10+ 位阿里巴巴 Serverless 領域技術專家,打造出最適合開發者入門的 Serverless 公開課,讓你即學即用,輕松擁抱云計算的新范式——Serverless。
點擊即可免費觀看課程: https://developer.aliyun.com/...
“阿里巴巴云原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦云原生流行技術趨勢、云原生大規模的落地實踐,做最懂云原生開發者的公眾號。”
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。