1. 對於可變基礎設施的思考
1.1 kubernetes中的可變與不可變基礎設施
在雲原生逐漸盛行的如今,不可變基礎設施的理念已經逐漸深刻人心。不可變基礎設施最先是由Chad Fowler於2013年提出的,其核心思想爲任何基礎設施的實例一旦建立以後變成爲只讀狀態,如須要修改和升級,則使用新的實例進行替換。這一理念的指導下,實現了運行實例的一致,所以在提高發布效率、彈性伸縮、升級回滾方面體現出了無與倫比的優點。git
kubernetes是不可變基礎設施理念的一個極佳實踐平臺。Pod做爲k8s的最小單元,承擔了應用實例這一角色。經過ReplicaSet從而對Pod的副本數進行控制,從而實現Pod的彈性伸縮。而進行更新時,Deployment經過控制兩個ReplicaSet的副本數此消彼長,從而進行實例的總體替換,實現升級和回滾操做。github
咱們進一步思考,咱們是否須要將Pod做爲一個徹底不可變的基礎設施實例呢?其實在kubernetes自己,已經提供了一個替換image的功能,來實現Pod不變的狀況下,經過更換image字段,實現Container的替換。這樣的優點在於無需從新建立Pod,便可實現升級,直接的優點在於免去了從新調度等的時間,使得容器能夠快速啓動。web
從這個思路延伸開來,那麼咱們其實能夠將Pod和Container分爲兩層來看。將Container做爲不可變的基礎設施,確保應用實例的完整替換;而將Pod看爲可變的基礎設施,能夠進行動態的改變,亦便可變層。swift
1.2 關於升級變化的分析
對於應用的升級變化種類,咱們來進行一下分類討論,將其分爲如下幾類:後端
升級變化類型 | 說明 |
---|---|
規格的變化 | cpu、內存等資源使用量的修改 |
配置的變化 | 環境變量、配置文件等的修改 |
鏡像的變化 | 代碼修改後鏡像更新 |
健康檢查的變化 | readinessProbe、livenessProbe配置的修改 |
其餘變化 | 調度域、標籤修改等其餘修改 |
(滑動查看完整表格)api
針對不一樣的變化類型,咱們作過一次抽樣調查統計,能夠看到下圖的一個統計結果。微信
在一次升級變化中若是含有多個變化,則統計爲屢次。網絡
能夠看到支持鏡像的替換能夠覆蓋一半左右的的升級變化,可是仍然有至關多的狀況下致使不得不從新建立Pod。這點來講,不是特別友好。因此咱們作了一個設計,將對於Pod的變化分爲了三種Dynamic,Rebuild,Static三種。架構
修改類型 | 修改類型說明 | 修改舉例 | 對應變化類型 |
---|---|---|---|
Dynamic 動態修改 | Pod不變,容器無需重建 | 修改了健康檢查端口 | 健康檢查的變化 |
Rebuild 原地更新 | Pod不變,容器須要從新建立 | 更新了鏡像、配置文件或者環境變量 | 鏡像的變化,配置的變化 |
Static 靜態修改 | Pod須要從新建立 | 修改了容器規格 | 規格的變化 |
(滑動查看完整表格)app
這樣動態修改和原地更新的方式能夠覆蓋90%以上的升級變化。在Pod不變的狀況下帶來的收益也是顯而易見的。
減小了調度、網絡建立等的時間。
因爲同一個應用的鏡像大部分層都是複用的,大大縮短了鏡像拉取的時間。
資源鎖定,防止在集羣資源緊缺時因爲出讓資源從新建立進入調度後,致使資源被其餘業務搶佔而沒法運行。
IP不變,對於不少有狀態的服務十分友好。
2. Kubernetes與OpenKruise的定製
2.1 kubernetes的定製
那麼如何來實現Dynamic和Rebuild更新呢?這裏須要對kubernetes進行一下定製。
動態修改定製
liveness和readiness的動態修改支持相對來講較爲簡單,主要修改點在與prober_manager中增長了UpdatePod函數,用以判斷當liveness或者readiness的配置改變時,中止原先的worker,從新啓動新的worker。然後將UpdatePod嵌入到kubelet的HandlePodUpdates的流程中便可。
func (m *manager) UpdatePod(pod *v1.Pod) { m.workerLock.Lock() defer m.workerLock.Unlock()
key := probeKey{podUID: pod.UID} for _, c := range pod.Spec.Containers { key.containerName = c.Name { key.probeType = readiness worker, ok := m.workers[key] if ok { if c.ReadinessProbe == nil { //readiness置空了,原worker中止 worker.stop() } else if !reflect.DeepEqual(*worker.spec, *c.ReadinessProbe) { //readiness配置改變了,原worker中止 worker.stop() } } if c.ReadinessProbe != nil { if !ok || (ok && !reflect.DeepEqual(*worker.spec, *c.ReadinessProbe)) { //readiness配置改變了,啓動新的worker w := newWorker(m, readiness, pod, c) m.workers[key] = w go w.run() } } } { //liveness與readiness類似 ...... } }}
原地更新定製
kubernetes原生支持了image的修改,對於env和volume的修改是未作支持的。所以咱們對env和volume也支持了修改功能,以便其能夠進行環境變量和配置文件的替換。這裏利用了一個小技巧,就是咱們在增長了一個ExcludedHash,用於計算Container內,包含env,volume在內的各項配置。
func HashContainerExcluded(container *v1.Container) uint64 { copyContainer := container.DeepCopy() copyContainer.Resources = v1.ResourceRequirements{} copyContainer.LivenessProbe = &v1.Probe{} copyContainer.ReadinessProbe = &v1.Probe{} hash := fnv.New32a() hashutil.DeepHashObject(hash, copyContainer) return uint64(hash.Sum32())}
這樣當env,volume或者image發生變化時,就能夠直接感知到。在SyncPod時,用於在計算computePodActions時,發現容器的相關配置發生了變化,則將該容器進行Rebuild。
func (m *kubeGenericRuntimeManager) computePodActions(pod *v1.Pod, podStatus *kubecontainer.PodStatus) podActions { ...... for idx, container := range pod.Spec.Containers { ...... if expectedHash, actualHash, changed := containerExcludedChanged(&container, containerStatus); changed { // 當env,volume或者image更換時,則重建該容器。 reason = fmt.Sprintf("Container spec exclude resources hash changed (%d vs %d).", actualHash, expectedHash) restart = true } ...... message := reason if restart { //將該容器加入到重建的列表中 message = fmt.Sprintf("%s. Container will be killed and recreated.", message) changes.ContainersToStart = append(changes.ContainersToStart, idx) }...... return changes}
Pod的生命週期
在Pod從調度完成到建立Running中,會有一個ContaienrCreating的狀態用以標識容器在建立中。而原生中當image替換時,先前的一個容器銷燬,後一個容器建立過程當中,Pod狀態會一直處於Running,容易有錯誤流量導入,用戶也沒法識別此時容器的狀態。
所以咱們爲原地更新,在ContainerStatus裏增長了ContaienrRebuilding的狀態,同時在容器建立成功前Pod的Ready Condition置爲False,以便表達容器整在重建中,應用在此期間不可用。利用此標識,能夠在此期間方便識別Pod狀態、隔斷流量。
2.2 OpenKruise的定製
OpenKruise(https://openkruise.io/)是阿里開源的一個項目,提供了一套在Kubernetes核心控制器以外的擴展 workload 管理和實現。其中Advanced StatefulSet,基於原生 StatefulSet 之上的加強版本,默認行爲與原生徹底一致,在此以外提供了原地升級、並行發佈(最大不可用)、發佈暫停等功能。
Advanced StatefulSet中的原地升級即與本文中的Rebuild一致,可是原生只支持替換鏡像。所以咱們在OpenKruise的基礎上進行了定製,使其不只能夠支持image的原地更新,也能夠支持當env、volume的原地更新以及livenessProbe、readinessProbe的動態更新。這個主要在shouldDoInPlaceUpdate
函數中進行一下判斷便可。這裏就再也不作代碼展現了。
還在生產運行中還發現了一個基礎庫的小bug,咱們也順帶向社區作了提交修復。https://github.com/openkruise/kruise/pull/154。
另外,還有個小坑,就是在pod裏爲了標識不一樣的版本,加入了controller-revision-hash值。
[root@xxx ~]# kubectl get pod -n predictor -o yaml predictor-0 apiVersion: v1kind: Podmetadata: labels: controller-revision-hash: predictor-85f9455f6...
通常來講,該值應該只使用hash值做爲value就能夠了,可是OpenKruise中採用了{sts-name}+{hash}的方式,這帶來的一個小問題就是sts-name
就要由於label value的長度受到限制了。
3. 寫在最後
定製後的OpenKruise和kubernetes已經大規模在各個集羣上上線,普遍應用在多個業務的後端運行服務中。經統計,經過原地更新覆蓋了87%左右的升級部署需求,基本達到預期。
特別鳴謝阿里貢獻的開源項目OpenKruise。
☆ END ☆
OPPO互聯網雲平臺團隊招聘一大波崗位,涵蓋Java、容器、Linux內核開發、產品經理、項目經理等多個方向,請在公衆號後臺回覆關鍵詞「雲招聘」查看查詳細信息。
更多技術乾貨
掃碼關注
OPPO互聯網技術
本文分享自微信公衆號 - OPPO互聯網技術(OPPO_tech)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。