作者 | 趙明山(立衡)
來源 | 阿里巴巴雲原生公眾號
前言
OpenKruise 是阿里雲開源的雲原生應用自動化管理套件,也是當前托管在 Cloud Native Computing Foundation (CNCF) 下的 Sandbox 項目。它來自阿里巴巴多年來容器化、雲原生的技術沉淀,是阿里內部生產環境大規模應用的基於 Kubernetes 之上的標准擴展組件,也是緊貼上游社區標准、適應互聯網規模化場景的技術理念與最佳實踐。
OpenKruise 在 2021 年 3 月 4 日發布了最新的 v0.8.0版本(ChangeLog),其中增強了 SidecarSet 的能力,特別是對日志管理類 Sidecar 有了更加完善的支持。
背景
Sidecar 是雲原生中一種非常重要的容器設計模式,它將輔助能力從主容器中剝離出來成為單獨的 sidecar 容器。在微服務架構中,通常也使用 sidecar 模式將微服務中的配置管理、服務發現、路由、熔斷等通用能力從主程序中剝離出來,從而極大降低了微服務架構中的復雜性。隨着 Service Mesh 的逐步風靡,sidecar 模式也日益深入人心,在阿里巴巴集團內部也大量使用 sidecar 模式來管理諸如運維、安全、消息中間件等通用組件。
在 Kubernetes 集群中,Pod 不僅可以實現主容器與 sidecar 容器的構建,同時提供了許多功能強大的 workload(例如:deployment、statefulset)來對 Pod 進行管理、升級。但是隨着 Kubernetes 集群上的業務日益增多,sidecar 容器的種類與規模也隨之日益龐大,對線上 sidecar 容器的管理和升級成為了愈發繁雜的工作:
-
業務 Pod 里面包含了運維、安全、代理等多個 sidecar 容器,業務線同學不僅要完成自身主容器的配置,而且還需要熟悉這些 sidecar 容器的配置,這不僅增加了業務同學的工作量,同時也無形增加了 sidecar 容器配置的風險。
-
sidecar 容器的升級需要連同業務主容器一起重啟(deployment、statefulset 等 workload 基於 Pod 銷毀、重建的模式,來實現 Pod 的滾動升級),推動和升級支撐着線上數百款業務的 sidecar 容器,必然存在着極大的業務阻力。
-
作為 sidecar 容器的提供者對線上諸多各種配置以及版本的 sidecar 容器沒有直接有效的升級手段,這對 sidecar 容器的管理意味着極大的潛在風險。
阿里巴巴集團內部擁有着百萬級的容器數量連同上面承載的上千個業務,因此,sidecar 容器的管理與升級也就成為了亟待完善的主題。因此,我們總結了內部許多 sidecar 容器的通用化需求,並將其沉淀到 OpenKruise 上面,最終抽象為 SidecarSet 作為管理和升級種類繁多 sidecar 容器的利器。
OpenKruise SidecarSet
SidecarSet 是 OpenKruise 中針對 sidecar 抽象出來的概念,負責注入和升級 Kubernetes 集群中的 sidecar 容器,是 OpenKruise 的核心 workload 之一。它提供了非常豐富的功能,用戶使用 SidecarSet 可以非常方便實現 sidecar 容器的管理。主要特性如下:
-
配置單獨管理:為每一個 sidecar 容器配置單獨的 SidecarSet 配置,方便管理。
-
自動注入:在新建、擴容、重建 pod 的場景中,實現 sidecar 容器的自動注入。
-
原地升級:支持不重建 pod 的方式完成 sidecar 容器的原地升級,不影響業務主容器,並包含豐富的灰度發布策略
注意:針對 Pod 中包含多個容器的模式,其中對外提供主要業務邏輯能力的容器稱之為 主容器,其它一些如日志采集、安全、代理等輔助能力的容器稱之為 Sidecar 容器。例如:一個對外提供 web 能力的 pod,nginx 容器提供主要的 web server 能力即為主容器,logtail 容器負責采集、上報 nginx 日志即為 Sidecar 容器。本文中的 SidecarSet 資源抽象也是為解決 Sidecar 容器的一些問題。
1. Sidecar logging architectures
應用日志可以讓你了解應用內部的運行狀況,日志對調試問題和監控集群活動非常有用。應用容器化后,最簡單且最廣泛采用的日志記錄方式就是寫入標准輸出和標准錯誤。
但是,在當前分布式系統、大規模集群的時代下,上述方案還不足以達到生產環境的標准。首先,對於分布式系統而言,日志都是分散在單個容器里面,沒有一個統一匯總的地方。其次,如果發生容器崩潰、Pod 被驅逐等場景,會出現日志丟失的情況。因此,需要一種更加可靠,獨立於容器生命周期的日志解決方案。
Sidecar logging architectures 是將 logging agent 放到一個獨立的 sidecar 容器中,通過共享日志目錄的方式,實現容器日志的采集,然后存儲到日志平台的后端存儲。
阿里巴巴以及螞蟻集團內部同樣也是基於這種架構實現了容器的日志采集,下面我將介 紹OpenKruise SidecarSet 如何助力 Sidecar 日志架構在 Kubernetes 集群中的大規模落地實踐。
2. 自動注入
OpenKruise SidecarSet 基於 Kubernetes AdmissionWebhook 機制實現了 sidecar 容器的自動注入,因此只要將 sidecar 配置到 SidecarSet 中,不管用戶用 CloneSet、Deployment、StatefulSet 等任何方式部署,擴出來的 Pod 中都會注入定義好的 sidecar 容器。
Sidecar 容器的所有者只需要配置自身的 SidecarSet,就可以在業務無感知的情況下完成 sidecar 容器的注入,這種方式極大的降低了 sidecar 容器使用的門檻,也方便了 sidecar 所有者的管理工作。為了滿足 sidecar 注入的多種場景,SidecarSet 除 containers 之外還擴展了如下字段:
# sidecarset.yaml
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: test-sidecarset
spec:
# 通過selector選擇pod
selector:
matchLabels:
app: web-server
# 指定 namespace 生效
namespace: ns-1
# container definition
containers:
- name: logtail
image: logtail:1.0.0
# 共享指定卷
volumeMounts:
- name: web-log
mountPath: /var/log/web
# 共享所有卷
shareVolumePolicy: disabled
# 環境變量共享
transferEnv:
- sourceContainerName: web-server
# TZ代表時區,例如:web-server容器中存在環境變量 TZ=Asia/Shanghai
envName: TZ
volumes:
- name: web-log
emptyDir: {}
-
Pod 選擇器
- 支持 selector 來選擇要注入的 Pod,如示例中將選擇 labels[app] = web-server 的 pod,將 logtail 容器注入進去,也可以在所有的 pod 中添加一個 labels[inject/logtail] = true 的方式,來實現全局性的 sidecar 注入。
- namespace:sidecarSet 默認是全局生效的,如果只想對某一個 namespace 生效,則配置該參數。
-
數據卷共享
- 共享指定卷:通過 volumeMounts 和 volumes 可以完成與主容器的特定卷的共享,如示例中通過共享 web-log volume 來達到日志采集的效果。
- 共享所有卷:通過 shareVolumePolicy = enabled | disabled 來控制是否掛載 pod 主容器的所有卷卷,常用於日志收集等 sidecar,配置為 enabled 后會把應用容器中所有掛載點注入 sidecar 同一路經下(sidecar 中本身就有聲明的數據卷和掛載點除外)。
-
環境變量共享:可以通過 transferEnv 從其它容器中獲取環境變量,會把名為 sourceContainerName 容器中名為 envName 的環境變量拷貝到本 sidecar 容器,如示例中日志 sidecar 容器共享了主容器的時區 TZ,這在海外環境中尤其常見。
注意:Kubernetes 社區對於已經創建的 Pod 不允許修改 container 數量,所以上述注入能力只能發生在 Pod 創建階段,對於已經創建的 Pod 需要通過重建的方式來注入。
3. 原地升級
SidecarSet 不僅實現 sidecar 容器的注入,而且復用了 OpenKruise 中原地升級的特性,實現了在不重啟 Pod 和主容器的前提下單獨升級 sidecar 容器的能力。由於這種升級方式基本上能做到業務方無感知的程度,所以 sidecar 容器的升級已不再是上下交困的難題,從而極大解放了 sidecar 的所有者,提升了 sidecar 版本迭代的速度。
注意:Kubernetes 社區對於已經創建的 Pod 只允許修改 container.image 字段,因此對於 sidecar 容器的修改包含除 container.image 的其它字段,則需要通過 Pod 重建的方式,不能直接原地升級。
為了滿足一些復雜的 sidecar 升級場景,SidecarSet 除了原地升級以外,還提供了非常豐富的灰度發布策略。
4. 灰度發布
灰度發布應該算是日常發布中最常見的一種手段,它能夠比較平滑的完成 sidecar 容器的發布,尤其是在大規模集群的場景下,強烈建議使用這種方式。下面是首批暫停,后續基於最大不可用滾動發布的例子,假設一個有 1000 個 pod 需要發布:
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
partition: 980
maxUnavailable: 10%
上述配置首先發布(1000 - 980)= 20 個 pod 之后就會暫停發布,業務可以觀察一段時間發現 sidecar 容器正常后,調整重新 update SidecarSet 配置:
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
maxUnavailable: 10%
這樣調整后,對於余下的 980 個 pod,將會按照最大不可用的數量(10% * 1000 = 100)的順序進行發布,直到所有的 pod 都發布完成。
Partition 的語義是保留舊版本 Pod 的數量或百分比,默認為 0
。這里的 partition
不表示任何 order
序號。如果在發布過程中設置了 partition
:
-
如果是數字,控制器會將
(replicas - partition)
數量的 Pod 更新到最新版本。 -
如果是百分比,控制器會將
(replicas * (100% - partition))
數量的 Pod 更新到最新版本。
MaxUnavailable 是發布過程中保證的,同一時間下最大不可用的 Pod 數量,默認值為 1。用戶可以將其設置為絕對值或百分比(百分比會被控制器按照 selected pod 做基數來計算出一個背后的絕對值)。
注意:maxUnavailable 和 partition 兩個值是沒有必然關聯。舉例:
-
當 {matched pod}=100,partition=50,maxUnavailable=10,控制器會發布 50 個 Pod 到新版本,但是發布窗口為 10,即同一時間只會發布 10 個 Pod,每發布好一個 Pod 才會再找一個發布,直到 50 個發布完成。
-
當 {matched pod}=100,partition=80,maxUnavailable=30,控制器會發布 20 個 Pod 到新版本,因為滿足 maxUnavailable 數量,所以這 20 個 Pod 會同時發布。
5. 金絲雀發布
對於有金絲雀發布需求的業務,可以通過 strategy.selector 來實現。方式:對於需要率先金絲雀灰度的 pod 打上固定的 labels[canary.release] = true,再通過 strategy.selector.matchLabels 來選中該 pod。
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
- canary.release: true
maxUnavailable: 10%
上述配置只會發布打上金絲雀 labels 的容器,在完成金絲雀驗證之后,通過將 updateStrategy.selector 配置去掉,就會繼續通過最大不可用來滾動發布。
6. 打散發布
SidecarSet 對於 pod 的升級順序,默認按照如下規則:
-
對升級的 pod 集合,保證多次升級的順序一致。
-
選擇優先順序是(越小優先級越高):unscheduled < scheduled, pending < unknown < running, not-ready < ready, newer pods < older pods。
除了上述默認發布順序之外,scatter 打散策略允許用戶自定義將符合某些標簽的 Pod 打散到整個發布過程中。比如,對於像 logtail 這種全局性的 sidecar container,一個集群當中很可能注入了幾十個業務 pod,因此可以使用基於 應用名 的方式來打散 logtail 的方式進行發布,實現不同應用間打散灰度發布的效果,並且這種方式可以同最大不可用一起使用。
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
# 配置pod labels,假設所有的pod都包含labels[app_name]
scatterStrategy:
- key: app_name
value: nginx
- key: app_name
value: web-server
- key: app_name
value: api-gateway
maxUnavailable: 10%
注意:當前版本必須要列舉所有的應用名稱,我們將在下個版本支持只配置 label key 的智能打散方式。
7. 實踐
阿里巴巴以及螞蟻集團內部已經大規模的使用 SidecarSet 來管理 sidecar 容器,下面我將通過日志采集 Logtail sidecar 來作為一個示例。
- 基於 sidecarSet.yaml 配置文件創建 SidecarSet 資源。
# sidecarset.yaml
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: logtail-sidecarset
spec:
selector:
matchLabels:
app: nginx
updateStrategy:
type: RollingUpdate
maxUnavailable: 10%
containers:
- name: logtail
image: log-service/logtail:0.16.16
# when recevie sigterm, logtail will delay 10 seconds and then stop
command:
- sh
- -c
- /usr/local/ilogtail/run_logtail.sh 10
livenessProbe:
exec:
command:
- /etc/init.d/ilogtaild
- status
resources:
limits:
memory: 512Mi
requests:
cpu: 10m
memory: 30Mi
##### share this volume
volumeMounts:
- name: nginx-log
mountPath: /var/log/nginx
transferEnv:
- sourceContainerName: nginx
envName: TZ
volumes:
- name: nginx-log
emptyDir: {}
- 基於 pod.yaml 創建 Pod。
apiVersion: v1
kind: Pod
metadata:
labels:
# matches the SidecarSet's selector
app: nginx
name: test-pod
spec:
containers:
- name: nginx
image: log-service/docker-log-test:latest
command: ["/bin/mock_log"]
args: ["--log-type=nginx", "--stdout=false", "--stderr=true", "--path=/var/log/nginx/access.log", "--total-count=1000000000", "--logs-per-sec=100"]
volumeMounts:
- name: nginx-log
mountPath: /var/log/nginx
envs:
- name: TZ
value: Asia/Shanghai
volumes:
- name: nginx-log
emptyDir: {}
- 創建這個 Pod,你會發現其中被注入了 logtail 容器:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
test-pod 2/2 Running 0 118s
$ kubectl get pods test-pod -o yaml |grep 'logtail:0.16.16'
image: log-service/logtail:0.16.16
- 此時,SidecarSet status 被更新為:
$ kubectl get sidecarset logtail-sidecarset -o yaml | grep -A4 status
status:
matchedPods: 1
observedGeneration: 1
readyPods: 1
updatedPods: 1
- 更新 sidecarSet 中 sidecar container 的 image logtail:0.16.18。
$ kubectl edit sidecarsets logtail-sidecarset
# sidecarset.yaml
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: logtail-sidecarset
spec:
containers:
- name: logtail
image: log-service/logtail:0.16.18
- 此時,發現 pod 中的 logtail 容器已經被更新為了 logtail:0.16.18 版本,並且 pod 以及其它的容器沒有重啟。
$ kubectl get pods |grep test-pod
test-pod 2/2 Running 1 7m34s
$ kubectl get pods test-pod -o yaml |grep 'image: logtail:0.16.18'
image: log-service/logtail:0.16.18
$ kubectl describe pods test-pod
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Killing 5m47s kubelet Container logtail definition changed, will be restarted
Normal Pulling 5m17s kubelet Pulling image "log-service/logtail:0.16.18"
Normal Created 5m5s (x2 over 12m) kubelet Created container logtail
Normal Started 5m5s (x2 over 12m) kubelet Started container logtail
Normal Pulled 5m5s kubelet Successfully pulled image "log-service/logtail:0.16.18"
總結
本次 OpenKruise v0.8.0 版本的升級,SidecarSet 特性主要是完善了日志管理類 Sidecar 場景的能力,后續我們在持續深耕 SidecarSet 穩定性、性能的同時,也將覆蓋更多的場景,比如下一個版本將會增加針對 Service Mesh 場景的支持。同時,我們也歡迎更多的同學參與到 OpenKruise 社區來,共同建設一個場景更加豐富、完善的 K8s 應用管理、交付擴展能力,能夠面向更加規模化、復雜化、極致性能的場景。
如果大家對 OpenKruise 項目感興趣,有任何希望交流的話題,歡迎大家訪問 OpenKruise 官網、GitHub