OpenKruise:解放 DaemonSet 運維之路

1.png

作者 | 王思宇(酒祝)

前言

OpenKruise 是阿里雲開源的大規模應用自動化管理引擎,在功能上對標了 Kubernetes 原生的 Deployment/StatefulSet 等控制器,但 OpenKruise 提供了更多的增強功能,如:優雅原地升級、發佈優先級/打散策略、多可用區 workload 抽象管理、統一 sidecar 容器注入管理等,都是經歷了阿里巴巴超大規模應用場景打磨出的核心能力。這些 feature 幫助我們應對更加多樣化的部署環境和需求、爲集羣維護者和應用開發者帶來更加靈活的部署發佈組合策略。

目前在阿里巴巴內部雲原生環境中,應用全部統一使用 OpenKruise 的能力做 Pod 部署、發佈管理,而不少業界公司和阿里雲上的客戶由於 K8s 原生 Deployment 等負載不能完全滿足需求,也轉而採用 OpenKruise 作爲應用部署載體。我們希望 OpenKruise 讓每一位 Kubernetes 開發者和阿里雲上的用戶都能便捷地使用上阿里巴巴內部雲原生應用所統一使用的部署發佈能力!

背景

如何在 Kubernetes 集羣中部署節點組件呢?相信大家對 DaemonSet 並不陌生,它能夠幫助我們將定義好的 Pod 部署到所有符合條件的 Node 上,這大大減輕了過去我們維護節點上各類守護進程的痛苦。

在阿里巴巴內部的雲原生環境中,存在不少網絡、存儲、GPU、監控等等相關的節點組件都是通過 DaemonSet 部署管理的。但是隨着近兩年 Kubernetes 集羣規模越來越大,所有核心業務逐漸全量上雲原生之後,我們越發感受到原生 DaemonSet 很難滿足大規模、高可用的複雜場景需求。

大家可以理解爲原生的 DaemonSet 確實解決了 0 -> 1 的問題,避免了直接管理 Node 上各類軟件包和守護進程的難題,能做到用一致化的 Pod 來部署節點組件。但是在部署之後呢?我們面臨的是 1 -> N 的不斷迭代升級的問題了,而在升級能力方面,原生 DaemonSet 做的實在有些敷衍了事的感覺。

apiVersion: apps/v1
kind: DaemonSet
spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 2
  # ...
apiVersion: apps/v1
kind: DaemonSet
spec:
  updateStrategy:
    type: OnDelete
  # ...

以上是原生 DaemonSet 支持的兩種升級方式。相信多數人使用 DaemonSet 基本都是默認的 RollingUpdate 滾動升級,這本身是沒問題的,問題就在於滾動升級時只支持了 maxUnavailable 一個策略,這就讓我們很難接受了。目前阿里巴巴內的 Kubernetes 不少已經做到單集羣上萬節點,這些節點可能有不同的機型、拓撲、核心程度、內核版本等等,而 DaemonSet 升級也覆蓋到這上萬節點上的 daemon Pod、涉及所有節點上的應用 Pod。

面對如此複雜和規模化的環境,原生 DaemonSet 沒有灰度、沒有分批、沒有暫停、沒有優先級,僅僅用一個 maxUnavailable 策略顯然是無法滿足的。要知道 daemon Pod 即使配置了 readinessProbe 往往也只能檢查容器內進程是否啓動運行,而對於進程的運行情況很難考量。

因此,即使 DaemonSet 發佈了一個代碼有 bug 的版本,只要進程能正常啓動則 maxUnavailable 策略就無法保護,DaemonSet 會一直髮佈下去;如果升級開始了一段時間後才發現問題,那此時很可能故障範圍就已經覆蓋到整個集羣了。

爲了避免這個問題,我們曾經一度改爲使用 OnDelete 策略、在發佈平臺上控制發佈順序和分批,但終態上我們還是希望將 workload 的能力下沉歸還到 workload,形成閉環,避免將完整的能力分散到多個模塊。因此隨着 OpenKruise 的成熟和在阿里內外的鋪開,我們總結了內部對 DaemonSet 的通用化發佈需求、將其沉澱到 OpenKruise 中,稱之爲 Advanced DaemonSet。

目前阿里巴巴和螞蟻集團內部的大部分 DaemonSet 都已經統一到 Advanced DaemonSet 部署管理,並且隨着 OpenKruise v0.6.0 版本的推出之後,外部一些公司如位於以色列的 Bringg 都已經開始對接使用。

能力解析

Advanced DaemonSet 中主要增加的 API 字段如下:

const (
+    // StandardRollingUpdateType replace the old daemons by new ones using rolling update i.e replace them on each node one after the other.
+    // this is the default type for RollingUpdate.
+    StandardRollingUpdateType RollingUpdateType = "Standard"

+    // SurgingRollingUpdateType replaces the old daemons by new ones using rolling update i.e replace them on each node one
+    // after the other, creating the new pod and then killing the old one.
+    SurgingRollingUpdateType RollingUpdateType = "Surging"
)

// Spec to control the desired behavior of daemon set rolling update.
type RollingUpdateDaemonSet struct {
+    // Type is to specify which kind of rollingUpdate.
+    Type RollingUpdateType `json:"rollingUpdateType,omitempty" protobuf:"bytes,1,opt,name=rollingUpdateType"`

    // ...
    MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty" protobuf:"bytes,2,opt,name=maxUnavailable"`

+    // A label query over nodes that are managed by the daemon set RollingUpdate.
+    // Must match in order to be controlled.
+    // It must match the node's labels.
+    Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,3,opt,name=selector"`

+    // The number of DaemonSet pods remained to be old version.
+    // Default value is 0.
+    // Maximum value is status.DesiredNumberScheduled, which means no pod will be updated.
+    // +optional
+    Partition *int32 `json:"partition,omitempty" protobuf:"varint,4,opt,name=partition"`

+    // Indicates that the daemon set is paused and will not be processed by the
+    // daemon set controller.
+    // +optional
+    Paused *bool `json:"paused,omitempty" protobuf:"varint,5,opt,name=paused"`

+    // Only when type=SurgingRollingUpdateType, it works.
+    // The maximum number of DaemonSet pods that can be scheduled above the desired number of pods
+    // during the update. Value can be an absolute number (ex: 5) or a percentage of the total number
+    // of DaemonSet pods at the start of the update (ex: 10%). The absolute number is calculated from
+    // the percentage by rounding up. This cannot be 0. The default value is 1. Example: when this is
+    // set to 30%, at most 30% of the total number of nodes that should be running the daemon pod
+    // (i.e. status.desiredNumberScheduled) can have 2 pods running at any given time. The update
+    // starts by starting replacements for at most 30% of those DaemonSet pods. Once the new pods are
+    // available it then stops the existing pods before proceeding onto other DaemonSet pods, thus
+    // ensuring that at most 130% of the desired final number of DaemonSet  pods are running at all
+    // times during the update.
+    // +optional
+    MaxSurge *intstr.IntOrString `json:"maxSurge,omitempty" protobuf:"bytes,7,opt,name=maxSurge"`
}

type DaemonSetSpec struct {
    // ...

+    // BurstReplicas is a rate limiter for booting pods on a lot of pods.
+    // The default value is 250
+    BurstReplicas *intstr.IntOrString `json:"burstReplicas,omitempty" protobuf:"bytes,5,opt,name=burstReplicas"`
}

按節點灰度

在一個大規模 Kubernetes 集羣中往往存在很多種差異化的節點類型,比如機型、拓撲、核心程度、內核版本等,因此在 DaemonSet 發佈的時候我們支持根據 Node 的標籤來匹配發布哪些 Node 上的 Pod。

apiVersion: apps.kruise.io/v1alpha1
kind: DaemonSet
spec:
  # ...
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      selector:
        matchLabels:
          nodeType: canary

比如上述配置了滾動升級下的 selector 策略,則 DaemonSet 只會在符合 selector 條件的 Node 上把 Pod 做滾動升級。如果 selector 改變,則 DaemonSet 會按照新的 selector 做升級,對已經是最新版本的 Pod 不會做變動。

因此,用戶可以通過多次修改 selector,來實現不同類型 Node 的前後發佈順序。這個優先順序可以是特定一批用於灰度的非核心節點,也可以是一些邏輯資源池等。

按數量灰度

如果說你不關心節點類型,Advanced DaemonSet 同樣提供了按數量灰度的能力:

apiVersion: apps.kruise.io/v1alpha1
kind: DaemonSet
spec:
  # ...
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 100

這裏的 partition 和 OpenKruise 中其他 CloneSet、Advanced StatefulSet 類似,都表示了維持舊版本的數量,也就是說 Kruise 控制器會選擇 status.DesiredNumberScheduled - partition 數量的 Pod 滾動升級爲新版本。

比如當前集羣中 DaemonSet 部署的節點數量是 120 個,當滾動升級時如果設置了 partition 爲 100,則 DaemonSet 只會選擇 20 個 Pod 滾動到新版本。只有當用戶再次下調 partition,DaemonSet 纔會繼續按要求數量來繼續升級。

多維度灰度

上述兩種灰度策略相信都不難理解,那麼如果同時配置了按節點和按數量兩種灰度策略,會怎麼樣呢?

apiVersion: apps.kruise.io/v1alpha1
kind: DaemonSet
spec:
  # ...
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 5
      partition: 100
      selector:
        matchLabels:
          nodeType: canary

想搞清楚這個問題,其實看懂 Advanced DaemonSet 的發佈策略計算邏輯就很好理解了,有興趣的同學可以跳去看一下:https://github.com/openkruise/kruise/blob/master/pkg/controller/daemonset/update.go#L459

參考上面這個 YAML,如果用戶同時配置了 partition 和 selector,那麼控制器在發佈的時候會先按照 selector 匹配符合條件的 Node,再按照 partition 計算其中能夠發佈的數量。當然,如果你還配置了原生 DaemonSet 就支持的 maxUnavailable,那麼最後還會按照 unavailable 的數量再次限制實際能滾動升級的數量。

簡單來說,最終真正執行滾動升級的 Pod,一定是要同時滿足所有配置的灰度策略。

熱升級

標準的 DaemonSet 滾動升級過程,是通過先刪除舊 Pod、再創建新 Pod 的方式來做的。在絕大部分場景下這樣的方式都是可以滿足的,然而如果這個 daemon Pod 的作用還需要對外提供服務,那麼滾動的時候可能對應 Node 上的服務就不可用了。

爲了提供高可用能力,我們對 DaemonSet 也提供了 surging 發佈策略。(回顧一下原生 Deployment 或者 OpenKruise 的 CloneSet,在這些面向無狀態服務的 workload 中如果配置了 maxSurging,則發佈時會先多擴出來 maxSurging 數量的 Pod,再逐漸刪掉舊版本的 Pod。)

apiVersion: apps.kruise.io/v1alpha1
kind: DaemonSet
spec:
  # ...
  updateStrategy:
    rollingUpdate:
      type: Surging  # defaults to Standard
      maxSurge: 30%

首先,在滾動升級中配置 type: Surging,這個類型默認是 Standard -- 也就是先刪再擴,而一旦設置爲 Surging 則變爲先擴再縮。也就是在滾動升級時,DaemonSet 會先在要發佈的 Node 上新建一個 Pod,等這個新版本 Pod 變爲 ready 之後再把舊版本 Pod 刪除掉。

另外在流式的策略上,maxUnavailable 是用於 Standard 類型的,對應了在滾動升級時最多在多少個 Node 上刪除 Pod。而 maxSurge 策略是用於 Surging 類型的,對應了在滾動升級時最多在多少個 Node 上多擴出一個 Pod。

發佈暫停

此外,Advanced DaemonSet 還支持了 paused 一鍵暫停發佈。這個比較好理解,就不細表述了。

apiVersion: apps.kruise.io/v1alpha1
kind: DaemonSet
spec:
  # ...
  updateStrategy:
    rollingUpdate:
      paused: true

總結

總的來看,OpenKruise 在原生 DaemonSet 基礎上增加了一系列面向生產場景的發佈策略,讓 DaemonSet 的升級過程更加安全、可控、自動化。

2.png

後續 OpenKruise 還會持續在應用部署/發佈能力上做出更深的優化,我們也歡迎每一位雲原生愛好者來共同參與 OpenKruise 的建設。與其他一些開源項目不同,OpenKruise 並不是阿里內部代碼的復刻;恰恰相反,OpenKruise Github 倉庫是阿里內部代碼庫的 upstream。因此,每一行你貢獻的代碼,都將運行在阿里內部的所有 Kubernetes 集羣中、都將共同支撐了阿里巴巴全球頂尖規模的雲原生應用場景!

阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公衆號。”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章