本文首發在OPPO網際網路公眾號,歡迎點選轉載 https://mp.weixin.qq.com/s/hRvZz_bZfchmP0tkF6M2OA
對於可變基礎設施的思考
kubernetes中的可變與不可變基礎設施
在雲原生逐漸盛行的現在,不可變基礎設施的理念已經逐漸深入人心。不可變基礎設施最早是由Chad Fowler於2013年提出的,其核心思想為任何基礎設施的例項一旦建立之後變成為只讀狀態,如需要修改和升級,則使用新的例項進行替換。這一理念的指導下,實現了執行例項的一致,因此在提升釋出效率、彈性伸縮、升級回滾方面體現出了無與倫比的優勢。
kubernetes是不可變基礎設施理念的一個極佳實踐平臺。Pod作為k8s的最小單元,承擔了應用例項這一角色。通過ReplicaSet從而對Pod的副本數進行控制,從而實現Pod的彈性伸縮。而進行更新時,Deployment通過控制兩個ReplicaSet的副本數此消彼長,從而進行例項的整體替換,實現升級和回滾操作。
我們進一步思考,我們是否需要將Pod作為一個完全不可變的基礎設施例項呢?其實在kubernetes本身,已經提供了一個替換image的功能,來實現Pod不變的情況下,通過更換image欄位,實現Container的替換。這樣的優勢在於無需重新建立Pod,即可實現升級,直接的優勢在於免去了重新排程等的時間,使得容器可以快速啟動。
從這個思路延伸開來,那麼我們其實可以將Pod和Container分為兩層來看。將Container作為不可變的基礎設施,確保應用例項的完整替換;而將Pod看為可變的基礎設施,可以進行動態的改變,亦即可變層。
關於升級變化的分析
對於應用的升級變化種類,我們來進行一下分類討論,將其分為以下幾類:
升級變化型別 | 說明 |
---|---|
規格的變化 | cpu、記憶體等資源使用量的修改 |
配置的變化 | 環境變數、配置檔案等的修改 |
映象的變化 | 程式碼修改後映象更新 |
健康檢查的變化 | readinessProbe、livenessProbe配置的修改 |
其他變化 | 排程域、標籤修改等其他修改 |
針對不同的變化型別,我們做過一次抽樣調查統計,可以看到下圖的一個統計結果。
在一次升級變化中如果含有多個變化,則統計為多次。
可以看到支援映象的替換可以覆蓋一半左右的的升級變化,但是仍然有相當多的情況下導致不得不重新建立Pod。這點來說,不是特別友好。所以我們做了一個設計,將對於Pod的變化分為了三種Dynamic,Rebuild,Static三種。
修改型別 | 修改型別說明 | 修改舉例 | 對應變化型別 |
---|---|---|---|
Dynamic 動態修改 | Pod不變,容器無需重建 | 修改了健康檢查埠 | 健康檢查的變化 |
Rebuild 原地更新 | Pod不變,容器需要重新建立 | 更新了映象、配置檔案或者環境變數 | 映象的變化,配置的變化 |
Static 靜態修改 | Pod需要重新建立 | 修改了容器規格 | 規格的變化 |
這樣動態修改和原地更新的方式可以覆蓋90%以上的升級變化。在Pod不變的情況下帶來的收益也是顯而易見的。
- 減少了排程、網路建立等的時間。
- 由於同一個應用的映象大部分層都是複用的,大大縮短了映象拉取的時間。
- 資源鎖定,防止在叢集資源緊缺時由於出讓資源重新建立進入排程後,導致資源被其他業務搶佔而無法執行。
- IP不變,對於很多有狀態的服務十分友好。
Kubernetes與OpenKruise的定製
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狀態、隔斷流量。
OpenKruise的定製
OpenKruise(https://openkruise.io/)是阿里開源的一個專案,提供了一套在Kubernetes核心控制器之外的擴充套件 workload 管理和實現。其中Advanced StatefulSet,基於原生 StatefulSet 之上的增強版本,預設行為與原生完全一致,在此之外提供了原地升級、並行釋出(最大不可用)、釋出暫停等功能。
Advanced StatefulSet中的原地升級即與本文中的Redbuild一致,但是原生只支援替換映象。因此我們在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: v1
kind: Pod
metadata:
labels:
controller-revision-hash: predictor-85f9455f6
...
一般來說,該值應該只使用hash值作為value就可以了,但是OpenKruise中採用了{sts-name}+{hash}的方式,這帶來的一個小問題就是sts-name
就要因為label value的長度受到限制了。
寫在最後
定製後的OpenKruise和kubernetes已經大規模在各個叢集上上線,廣泛應用在多個業務的後端執行服務中。經統計,通過原地更新覆蓋了87%左右的升級部署需求,基本達到預期。
特別鳴謝阿里貢獻的開源專案OpenKruise。