雲栖号資訊:【 點選檢視更多行業資訊】
在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!
背景
OpenKruise 是阿裡雲開源的大規模應用自動化管理引擎,在功能上對标了 Kubernetes 原生的 Deployment / StatefulSet 等控制器,但 OpenKruise 提供了更多的增強功能如:優雅原地更新、釋出優先級/打散政策、多可用區workload抽象管理、統一 sidecar 容器注入管理等,都是經曆了阿裡巴巴超大規模應用場景打磨出的核心能力。這些 feature 幫助我們應對更加多樣化的部署環境和需求、為叢集維護者和應用開發者帶來更加靈活的部署釋出組合政策。
目前在阿裡巴巴内部雲原生環境中,絕大部分應用都統一使用 OpenKruise 的能力做 Pod 部署、釋出管理,而不少業界公司和阿裡雲上客戶由于 K8s 原生 Deployment 等負載不能完全滿足需求,也轉而采用 OpenKruise 作為應用部署載體。
今天的分享文章就從一個阿裡雲上客戶對接 OpenKruise 的疑問開始。這裡還原一下這位同學的用法(以下 YAML 資料僅為 demo):
準備一份 Advanced StatefulSet 的 YAML 檔案,并送出建立。如:
yaml apiVersion: apps.kruise.io/v1alpha1
kind: StatefulSet
metadata:
name: sample
spec:
# ...
template:
# ...
spec:
containers:
- name: main
image: nginx:alpine
updateStrategy:
type: RollingUpdate
rollingUpdate:
podUpdatePolicy: InPlaceIfPossible
然後,修改了 YAML 中的 image 鏡像版本,然後調用 K8s api 接口做更新。結果收到報錯如下:
shell metadata.resourceVersion: Invalid value: 0x0: must be specified for an update
而如果使用 kubectl apply 指令做更新,則傳回成功:
shell statefulset.apps.kruise.io/sample configured
問題在于,為什麼同一份修改後的 YAML 檔案,調用 api 接口更新是失敗的,而用 kubectl apply 更新是成功的呢?這其實并不是 OpenKruise 有什麼特殊校驗,而是由 K8s 自身的更新機制所決定的。
從我們的接觸來看,絕大多數使用者都有通過 kubectl 指令或是 sdk 來更新 K8s 資源的經驗,但真正了解這些更新操作背後原理的人卻并不多。本文将着重介紹 K8s 的資源更新機制,以及一些我們常用的更新方式是如何實作的。
更新原理
不知道你有沒有想過一個問題:對于一個 K8s 資源對象比如 Deployment,我們嘗試在修改其中 image 鏡像時,如果有其他人同時也在對這個 Deployment 做修改,會發生什麼?
當然,這裡還可以引申出兩個問題:
- 如果雙方修改的是同一個字段,比如 image 字段,結果會怎樣?
- 如果雙方修改的是不同字段,比如一個修改 image,另一個修改 replicas,又會怎麼樣?
其實,對一個 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 操作流程是:
- 首先,從 K8s 中拿到一個已經存在的對象(可以選擇直接從 K8s 中查詢;如果在用戶端做了 list watch,推薦從本地 informer 中擷取);
- 然後,基于這個取出來的對象做一些修改,比如将 Deployment 中的 replicas 做增減,或是将 image 字段修改為一個新版本的鏡像;
- 最後,将修改後的對象通過 update 請求送出給 K8s;
- 此時,kube-apiserver 會校驗使用者 update 請求送出對象中的 resourceVersion 一定要和目前 K8s 中這個對象最新的 resourceVersion 一緻,才能接受本次 update。否則,K8s 會拒絕請求,并告訴使用者發生了版本沖突(Conflict)。

上圖展示了多個使用者同時 update 某一個資源對象時會發生的事情。而如果如果發生了 Conflict 沖突,對于 User A 而言應該做的就是做一次重試,再次擷取到最新版本的對象,修改後重新送出 update。
是以,我們上面的兩個問題也都得到了解答:
- 使用者修改 YAML 後送出 update 失敗,是因為 YAML 檔案中沒有包含 resourceVersion 字段。對于 update 請求而言,應該取出目前 K8s 中的對象做修改後送出;
- 如果兩個使用者同時對一個資源對象做 update,不管操作的是對象中同一個字段還是不同字段,都存在版本控制的機制確定兩個使用者的 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):
篇幅限制這裡暫不對每個政策做詳細的介紹了,我們就以一個簡單的例子來看一下它們的差異性。如果針對一個已有的 Deployment 對象,假設 template 中已經有了一個名為 app 的容器:
- 如果要在其中新增一個 nginx 容器,如何 patch 更新?
- 如果要修改 app 容器的鏡像,如何 patch 更新?
json patch(RFC 6902)
新增容器:
bash kubectl patch deployment/foo --type='json' -p \
'[{"op":"add","path":"/spec/template/spec/containers/1","value":{"name":"nginx","image":"nginx:alpine"}}]
修改已有容器:
bash 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:
顯然,這個政策并不适合我們對一些清單深層的字段做更新,更适用于大片段的覆寫更新。
不過對于 labels/annotations 這些 map 類型的元素更新,merge patch 是可以單獨指定 key-value 操作的,相比于 json patch 友善一些,寫起來也更加直覺:
strategic merge patch
這種 patch 政策并沒有一個通用的 RFC 标準,而是 K8s 獨有的,不過相比前兩種而言卻更為強大的。
我們先從 K8s 源碼看起,在 K8s 原生資源的資料結構定義中額外定義了一些的政策注解。比如以下這個截取了 podSpec 中針對 containers 清單的定義,參考 Github:
可以看到其中有兩個關鍵資訊:
這就代表了,containers 清單使用 strategic merge patch 政策更新時,會把下面每個元素中的 name 字段看作 key。
簡單來說,在我們 patch 更新 containers 不再需要指定下标序号了,而是指定 name 來修改,K8s 會把 name 作為 key 來計算 merge。比如針對以下的 patch 操作:
如果 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 社群中,共同合作打造一款面向規模化場景、高性能的應用傳遞解決方案。
【雲栖号線上課堂】每天都有産品技術專家分享!
課程位址:
https://yqh.aliyun.com/live立即加入社群,與專家面對面,及時了解課程最新動态!
【雲栖号線上課堂 社群】
https://c.tb.cn/F3.Z8gvnK
原文釋出時間:2020-06-02
本文作者:酒祝 阿裡雲技術專家
本文來自:“
dockone”,了解相關資訊可以關注“dockone”