基於Kubernetes和OpenKruise的可變基礎設施實踐

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不變的狀況下帶來的收益也是顯而易見的。

  1. 減小了調度、網絡建立等的時間。

  2. 因爲同一個應用的鏡像大部分層都是複用的,大大縮短了鏡像拉取的時間。

  3. 資源鎖定,防止在集羣資源緊缺時因爲出讓資源從新建立進入調度後,致使資源被其餘業務搶佔而沒法運行。

  4. 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內核開發、產品經理、項目經理等多個方向,請在公衆號後臺回覆關鍵詞「雲招聘」查看查詳細信息。


你可能還喜歡

如何用 CI (持續集成) 保證研發質量

如何設計並實現存儲QoS?

雲原生Service Mesh探索與實踐

如何進行 kubernetes 問題的排障

OPPO自研ESA DataFlow架構與實踐


更多技術乾貨

掃碼關注

OPPO互聯網技術

 

我就知道你「在看」

本文分享自微信公衆號 - OPPO互聯網技術(OPPO_tech)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索