文檔說明
該文檔主要記錄了我在使用OpenKruise的操作,官方的文檔相對來說比較簡潔,對使用姿勢和API的示例并不是很直覺,是以我把整體測試流程記錄了下來,如果已經熟練使用OpenKruise的大神,則可以跳過該文檔。
背景
這兩年,随着雲計算的發展,雲原生的概念應運而生。為了讓應用能擁有雲一樣的特性,每個公司也開始了各自的雲原生實踐之路。
從今年上半年,我們公司也開始了自己的雲原生之路。概括的來說,雲原生 = DevOps+持續傳遞+服務網格+容器化
DevOps和持續傳遞,我們已經擁有了對應的釋出和工單等系統,已經在公司内部平穩維護和使用,目前需要解決的就是服務網格和容器化。由于曆史原因和技術債務考慮,我們決定先進行應用的容器化改造,然後再進行服務網格的支援。
容器化的改造也并不是一蹴而就的。對于那些有一定的發展時間和規模的公司,都會存在自身的技術債務,業務可能會依賴固定IP,應用可能依舊是有狀态的等等。是以,對我們而言,在盡量不影響業務開發的前提下,我們選擇使用statefulset且固定IP的方式進行前期的容器化遷移。
前期容器化改造
大部分網際網路公司都會有三個環境:線下開發環境、預發環境和正式生産環境
最開始,我們打算先将線下環境進行容器化改造,接下來伴随這k8s的環境準備,釋出系統的容器化改造,我們已經實作了應用通過釋出系統釋出後,從部署在原先的雲伺服器到部署在k8s的節點上。這裡的細節就不描述了,由于我們公司所有的系統都是自研的,原理基本就是應用建立對應的statefulset進行pod的管理。
當線下環境容器化改造完成以後,在推行的過程中,業務開發也回報了一些容器化後出現的問題,主要為以下幾點:
- 釋出的時間變長,影響了業務開發的效率。
相較于原先雲伺服器的釋出方式,隻需要更新應用的建構物,然後重新啟動應用就完成了應用的釋出。而如果容器化以後,pod會經過Terminating銷毀後,再進行pending重新排程,最後到容器的running,相比較耗時長,另外如果線上上環境,pod數量較多,預計的pending時間會更長。
- 釋出方式單一
對于使用statefulset的方式,如果對于某個應用存在多個pod執行個體,并不支援多個pod的并行釋出,隻能夠依次順序釋出,如果前一個pod沒有釋出成功,後一個pod不會開始釋出,這種場景線上上大批量的分批灰階釋出場景肯定是不滿足要求的
為了優化以上問題,我們查閱了相關資料,也聽了線下雲原生相關的meetUp,最後打算使用阿裡開源的Openkruise,對整體釋出流程進行優化。
關于OpenKruise
OpenKruise 是阿裡雲開源的雲原生應用自動化管理套件,也是目前托管在 Cloud Native Computing Foundation (CNCF) 下的 Sandbox 項目。
官方位址:
https://openkruise.io
對于openKruise,我們比較關注Advanced StatefulSet的原地更新和并行、灰階分批釋出功能等。
使用和測試過程
安裝OpenKruise
https://openkruise.io/zh-cn/docs/installation.html
比較簡單,照着文檔來就行了
測試原地更新功能
首先需要建立Advanced StatefulSet
這裡需要說明兩點:
- 對目前已經建立了k8s原生的StatefulSet,不能通過修改yaml,轉換成Advanced StatefulSet,需要将原先的statefulset删除,然後重新建立
- 建立完成以後,通過kubectl get statefulset 名稱 -n xxx 的方式無法看到建立Advanced StatefulSet資訊,是以如果使用的是其他雲廠商的容器服務,則在控制台對應的workload中檢視不到對應的資源資訊。例如我們公司使用的是騰訊雲的TKE服務,在控制台上就看不到建立的Advanced StatefulSet。但是可以通過kubectl get sts.apps.kruise.io檢視
建立的yaml如下:
apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
metadata:
annotations:
description: xxxx
tke.cloud.tencent.com/enable-static-ip: "true"
labels:
appname: xxxx
name: xxxx
namespace: xxxx
spec:
podManagementPolicy: OrderedReady
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
appname: xxxx
template:
metadata:
annotations:
tke.cloud.tencent.com/vpc-ip-claim-delete-policy: Never
labels:
appname: xxxx
spec:
readinessGates:
- conditionType: InPlaceUpdateReady
containers:
- env:
- name: PATH
value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
image: xxxx
imagePullPolicy: IfNotPresent
name: xxxx
readinessProbe:
failureThreshold: 3
httpGet:
path: /status
port: 80
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 300
resources:
limits:
cpu: 2500m
memory: 4Gi
requests:
cpu: 1500m
memory: 2Gi
securityContext:
privileged: true
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c","/home/mapp/bin/stop-app.sh"]
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: offline-test
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
updateStrategy:
type: RollingUpdate
rollingUpdate:
podUpdatePolicy: InPlaceIfPossible
inPlaceUpdateStrategy:
gracePeriodSeconds: 10
說明:已經将yaml中的敏感資訊去除。
建立以後,通過以下指令檢視:
kubectl get sts.apps.kruise.io
原地更新的條件:
InPlaceIfPossible: 控制器會優先嘗試原地更新 Pod,如果不行再采用重建更新。目前,隻有修改 spec.template.metadata.* 和 spec.template.spec.containers[x].image 這些字段才可以走原地更新。
是以我重新設定了yaml中的鏡像版本,然後執行
kubectl apply -f .\statefulset.yaml
執行指令的同時,通過指令監聽:
kubectl get sts.apps.kruise.io -w
kubectl get pod pod的名稱 --watch
檢視pod的event事件
總體從替換鏡像後到容器正常啟動,總共耗時40s
進入pod檢視業務啟動狀态,鏡像版本已經更新
結果對比
原地更新的方式與之前釋出的方式進行對比,差異就在于原地更新不需要進行pod的銷毀和排程過程。原地更新從更新鏡像到容器啟動成功,花費40s,而原先的方式,容器的銷毀和排程花費的時間為70s左右。
另外在配置上,我設定了gracePeriodSeconds
是以我們又在原地更新中提供了 graceful period 選項,作為優雅原地更新的政策。使用者如果配置了 gracePeriodSeconds 這個字段,控制器在原地更新的過程中會先把 Pod status 改為 not-ready,然後等一段時間(gracePeriodSeconds),最後再去修改 Pod spec 中的鏡像版本。 這樣,就為 endpoints-controller 這些控制器留出了充足的時間來将 Pod 從 endpoints 端點清單中去除。
如果将gracePeriodSeconds配置去除,原地更新的時間能夠縮短為30s
測試MaxUnavailable 政策
maxUnavailable 政策來支援并行 Pod 釋出,它會保證釋出過程中最多有多少個 Pod 處于不可用狀态。注意,maxUnavailable 隻能配合 podManagementPolicy 為 Parallel 來使用。
說明: 經測試,不允許在之前的Advanced StatefulSet上更新釋出政策
Resource: "apps.kruise.io/v1beta1, Resource=statefulsets", GroupVersionKind: "apps.kruise.io/v1beta1, Kind=StatefulSet" Name: "xxxx", Namespace: "xxx" for: ".\\statefulset.yaml": admission webhook "vstatefulset.kb.io" denied the request: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', 'reserveOrdinals', 'lifecycle' and 'updateStrategy' are forbidden
是以将之前的Advanced StatefulSet進行删除,然後建立
yaml如下:
apiVersion: apps.kruise.io/v1beta1 kind: StatefulSet metadata: annotations: description: xxxx tke.cloud.tencent.com/enable-static-ip: "true" labels: appname: xxxx name: xxxx namespace: xxx spec: podManagementPolicy: Parallel replicas: 3 revisionHistoryLimit: 10 selector: matchLabels: appname: xxxx template: metadata: annotations: tke.cloud.tencent.com/vpc-ip-claim-delete-policy: Never labels: appname: xxxx spec: readinessGates: - conditionType: InPlaceUpdateReady containers: - env: - name: PATH value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin image: xxxx imagePullPolicy: Always name: flowplusdevhosttest readinessProbe: failureThreshold: 3 httpGet: path: /status port: 80 scheme: HTTP initialDelaySeconds: 60 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 300 resources: limits: cpu: 2500m memory: 4Gi requests: cpu: 1500m memory: 2Gi securityContext: privileged: true lifecycle: preStop: exec: command: ["/bin/sh","-c","/home/mapp/bin/stop-app.sh"] terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst imagePullSecrets: - name: offline-test restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 updateStrategy: type: RollingUpdate rollingUpdate: podUpdatePolicy: InPlaceIfPossible maxUnavailable: 100%
說明:
建立了Advanced StatefulSet,副本數設定了3個,同時更新政策設定成了并行,最大不可用數為100%,說明在更新時,可以進行批量同時更新。
測試并行釋出
此時如果修改了鏡像版本後,觸發了3個pod的原地更新,觀察3個pod是否同時進行更新
修改了鏡像版本後,Advanced StatefulSet的确同時從Ready狀态數值轉換成了0
最終釋出完成以後:
檢視對應三個pod的event
pod-0:
pod-1:
pod-2:
最終得出結論:能夠支援并行釋出,3個pod同時進行原地更新
測試MaxUnavailable
如果在并發釋出的過程中,還可以設定maxUnavailable的值。
比如說3個pod中,我需要保證服務不能同時挂,起碼有一個pod正常提供服務,那我可以設定maxUnavailable為2。
調整後進行釋出:
Ready狀态的pod會保留一個,不會導緻所有的pod都停止服務
灰階分批釋出
灰階釋出通過partition字段進行控制
如果在釋出過程中設定了 partition:
如果是數字,控制器會将 (replicas - partition) 數量的 Pod 更新到最新版本。
如果是百分比,控制器會将 (replicas * (100% - partition)) 數量的 Pod 更新到最新版本。
以之前建立的Advanced StatefulSet為例,如果我設定了partition為2,則更新時,隻會更新一台機器
apiVersion: apps.kruise.io/v1beta1 kind: StatefulSet metadata: annotations: description: xxxx tke.cloud.tencent.com/enable-static-ip: "true" labels: appname: xxxx name: xxxx namespace: xxx spec: podManagementPolicy: Parallel replicas: 3 revisionHistoryLimit: 10 selector: matchLabels: appname: xxx template: metadata: annotations: tke.cloud.tencent.com/vpc-ip-claim-delete-policy: Never labels: appname: xxx spec: readinessGates: - conditionType: InPlaceUpdateReady containers: - env: - name: PATH value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin image: xxxx imagePullPolicy: Always name: xxxx readinessProbe: failureThreshold: 3 httpGet: path: /status port: 80 scheme: HTTP initialDelaySeconds: 60 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 300 resources: limits: cpu: 2500m memory: 4Gi requests: cpu: 1500m memory: 2Gi securityContext: privileged: true lifecycle: preStop: exec: command: ["/bin/sh","-c","/home/mapp/bin/stop-app.sh"] terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst imagePullSecrets: - name: offline-test restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 updateStrategy: type: RollingUpdate rollingUpdate: podUpdatePolicy: InPlaceIfPossible maxUnavailable: 2 partition: 2
測試灰階釋出
接下來修改鏡像版本:
隻更新了一個pod的鏡像版本
檢視3個pod的資訊
隻有第三個 pod-2 的鏡像更新了,其他兩個鏡像還是原來的鏡像
如果此時partition設定為1,則會再更新一個pod的鏡像版本。測試結果如下:
這個時候會繼續更新一台,此時pod-1的鏡像已經更新。
如果需要更新所有的鏡像,則把partition設定為0,則會對所有pod進行更新。
控制釋出順序
控制釋出順序隻是将原先statefulset的順序釋出,可以改成權重和序号釋出。
weight: Pod 優先級是由所有 weights 清單中的 term 來計算 match selector 得出
order: Pod 優先級是由 orderKey 的 value 決定,這裡要求對應的 value 的結尾能解析為 int 值
是以這裡的釋出順序隻是将原先的順序釋出通過selector進行自定義的排序,并不滿足我們釋出過程中,可以随意指定一台進行釋出的操作。
是以略過
釋出暫停
觸發方式:
spec: # ... updateStrategy: rollingUpdate: paused: true
首先将maxUnavailable設定為2,則并行釋出時,會同時并行釋出兩個pod,等兩個pod釋出成功以後,再釋出另一台pod。測試在中途暫停釋出。
更改鏡像版本後,并行釋出了兩台,有一個pod依然保留,正常提供服務:
此時暫停釋出:
從UPDATES的數量可以看出,當最早并行的兩個pod已經釋出完成,但是剩餘的一個pod始終沒有釋出,是以釋出已經暫停了。
然後将暫停釋出關掉:
另一個pod也開始進行釋出更新
序号保留
通過在 reserveOrdinals 字段中寫入需要保留的序号,Advanced StatefulSet 會自動跳過建立這些序号的 Pod。如果 Pod 已經存在,則會被删除。 注意,spec.replicas 是期望運作的 Pod 數量
沒有使用場景,略過
對于這點,我也比較好奇這個功能的實際場景是什麼
總結
OpenKruise的确能夠對目前釋出系統的流程進行優化,提高開發使用的效率:
- 原地更新功能
通過原地更新的功能,能夠減少pod銷毀和排程的時間,能夠減少40s左右的釋出時間。另外線上上的節點數量規模,減少時間的效益應該更加明顯。
- 并行釋出能夠
目前對于部署場景下有多個pod進行釋出,使用openKruise,可以将原先的串行釋出方式改變為并行釋出,尤其是線上上大批量機器的釋出,并行釋出能夠節約大量時間
- 分批灰階釋出
目前openKruise可以通過partion的設定來進行機器的分批灰階釋出,開發可以指定目前灰階多少個pod,但是不能随意指定任意一台pod進行釋出
是以接下來,我們開始将原先的StatefulSet 轉換為Advanced StatefulSet,另外由于之前我釋出系統是Java + Python的應用,如果調用OpenKruise資源,有以下幾種方式:
- 通過官方提供的golang用戶端
- 通過官方提供的java api
- 通過python調用shell指令
最後我們選擇使用官方的golang api:
https://github.com/openkruise/kruise-api
Golang用戶端測試代碼
官網的執行個體代碼比較簡潔,下面貼下我的測試代碼:
go.mod:
module openKruiseApiTest
go 1.14
require github.com/openkruise/kruise-api v0.8.0-1.18
main.go:
package main
import (
"context"
"fmt"
kruiseclientset "github.com/openkruise/kruise-api/client/clientset/versioned"
"k8s.io/client-go/tools/clientcmd"
"log"
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func main() {
kubeconfig := filepath.Join("D:\\environment\\k8s\\", "xxxxx") //config的位址
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Fatal(err)
}
kruiseClient := kruiseclientset.NewForConfigOrDie(config)
statefulset, err := kruiseClient.AppsV1alpha1().StatefulSets("xxx").Get(context.TODO(),"xxxx",metav1.GetOptions{})
fmt.Printf("Result: %s \n", statefulset)
}