Kubernetes 新玩法:在 yaml 中编程
作者 |?悟鵬
引子
性能測試在日常的開發工作中是常規需求,用來摸底服務的性能。
那么如何做性能測試?要么是通過編碼的方式完成,寫一堆腳本,用完即棄;要么是基于平臺,在平臺定義的流程中進行。對于后者,通常由于目標場景的復雜性,如部署特定的 workload、觀測特定的性能項、網絡訪問問題等,往往導致性能測試平臺要以高成本才能滿足不斷變化的開發場景的需求。
在云原生的背景下,是否可以更好解決這種問題?
先看兩個 yaml 文件:
-
performance-test.yaml 描述了在 K8s 中的操作流程:
- 創建測試用的 Namespace
- 啟動針對 Deployment 創建效率和創建成功率的監控
- 下述動作重復 N 次:① 使用 workload 模板創建 Deployment;② 等待 Deployment 變為 Ready
- 刪除測試用的 Namespace
-
basic-1-pod-deployment.yaml 描述使用的 workload 模板
performance-test.yaml :
apiVersion: aliyun.com/v1alpha1 kind: Beidou metadata:name: performancenamespace: beidou spec:steps:- name: "Create Namespace If Not Exits"operations:- name: "create namespace"type: Taskop: CreateNamespaceargs:- name: NSvalue: beidou- name: "Monitor Deployment Creation Efficiency"operations:- name: "Begin To Monitor Deployment Creation Efficiency"type: Taskop: DeploymentCreationEfficiencyargs:- name: NSvalue: beidou- name: "Repeat 1 Times"type: Taskop: RepeatNTimesargs:- name: TIMESvalue: "1"- name: ACTIONreference:id: deployment-operation- name: "Delete namespace"operations:- name: "delete namespace"type: Taskop: DeleteNamespaceargs:- name: NSvalue: beidou- name: FORCEvalue: "false"references:- id: deployment-operationsteps:- name: "Prepare Deployment"operations:- name: "Prepare Deployment"type: Taskop: PrepareBatchDeploymentsargs:- name: NSvalue: beidou- name: NODE_TYPEvalue: ebm- name: BATCH_NUMvalue: "1"- name: TEMPLATEvalue: "./templates/basic-1-pod-deployment.yaml"- name: DEPLOYMENT_REPLICASvalue: "1"- name: DEPLOYMENT_PREFIXvalue: "ebm"- name: "Wait For Deployments To Be Ready"type: Taskop: WaitForBatchDeploymentsReadyargs:- name: NSvalue: beidou- name: TIMEOUTvalue: "3m"- name: CHECK_INTERVALvalue: "2s"basic-1-pod-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata:labels:app: basic-1-pod spec:selector:matchLabels:app: basic-1-podtemplate:metadata:labels:app: basic-1-podspec:containers:- name: nginximage: registry-vpc.cn-hangzhou.aliyuncs.com/xxx/nginx:1.17.9imagePullPolicy: Alwaysresources:limits:cpu: 2memory: 4Gi然后通過一個命令行工具執行 performance-test.yaml:
$ beidou server -c ~/.kube/config services/performance-test.yaml執行效果如下 (每個 Deployment 創建耗時,所有 Deployment 創建耗時的 TP95 值,每個 Deployment 是否創建成功):
這些 metrics 是按照 Prometheus 標準輸出,可以被 Prometheus server 收集走,再結合 Grafana 可以可視化展示性能測試數據。
通過在 yaml 中表達想法,編排對 K8s 資源的操作、監控,再也不用為性能測試的實現頭疼了 😄
為什么要在?yaml 中編程?
性能測試、回歸測試等對于服務質量保障有很大幫助,需要做,但常規的實現方法在初期需要投入較多的時間和精力,新增變更后維護成本比較高。
通常這個過程是以代碼的方式實現原子操作,如創建 Deployment、檢測 Pod 配置等,然后再組合原子操作來滿足需求,如 創建 Deployment -> 等待 Deployment ready -> 檢測 Pod 配置等。
有沒有辦法在實現的過程中既可以盡量低成本實現,又可以復用已有的經驗?
可以將原子操作封裝為原語,如 CreateDeployment、CheckPod,再通過 yaml 的結構表達流程,那么就可以通過 yaml 而非代碼的方式描述想法,又可以復用他人已經寫好的 yaml 文件來解決某類場景的需求。
即在 yaml 中編程,減少重復性代碼工作,通過 聲明式 的方式描述邏輯,并以 yaml 文件來滿足場景級別的復用。
業界有很多種類型的 聲明式操作 服務,如運維領域中的 Ansible、SaltStack,Kubernetes 中的Argo Workflow、clusterloader2。它們的思想整體比較類似,將高頻使用的操作封裝為原語,使用者通過原語來表述操作邏輯。
通過聲明式的方法,將面向 K8s 的操作抽象成 yaml 中的關鍵詞,在 yaml 中提供串行、并行等控制邏輯,那么就可以通過 yaml 文件完整描述想要進行的工作。
這種思想和 Argo Workflow?比較像,但粒度比 Argo 更細,關注在操作函數上:
下面簡單描述該服務的設計和實現。
設計和實現
1. 服務形態
- 使用者在 yaml 中,通過 聲明式 的方式描述操作邏輯;
- 以 all-in-one 的二進制工具或 Operator 的方式交付;
- 服務內置常見原語的實現,以關鍵字的方式在 yaml 中提供;
- 支持配置原生 K8s 資源。
2. 設計
該方案的核心在于配置管理的設計,將操作流程配置化,自上而下有如下概念:
-
Service:Modules 或 Tasks 的編排;
-
Module:一種任務場景,是操作單元的集合(其中包含 templates/ 目錄,表征模板文件的集合,可用來配置 K8s 原生資源);
-
Task:操作單元,使用 plugin 及參數執行操作;
-
Plugin:操作指令,類似開發語言中的函數。
抽象目標場景中的通用操作,這些通用操作即為可在 yaml 中使用的原語,對應上述 Plugin:
-
K8s 相關
- CreateNamespace
- DeleteNamespace
- PrepareSecret
- PrepareConfigMap
- PrepareBatchDeployments
- WaitForBatchDeploymentsReady
- etc.
-
觀測性相關
- DeploymentCreationEfficiency
- PodCreationEfficiency
- etc.
-
檢測項相關
- CheckPodAnnotations
- CheckPodObjectInfo
- CheckPodInnerStates
- etc.
-
控制語句相關
- RepeatNTimes
- etc.
上述 4 個概念的關系如下:
示例可參見文章開頭的 yaml 文件,對應形式二。
3. 核心實現
CRD 設計:
package v1alpha1import (corev1 "k8s.io/api/core/v1"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" )// BeidouType is the type related to Beidou execution. type BeidouType stringconst (// BeidouTask represents the Task execution type.BeidouTask BeidouType = "Task" )// +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// Beidou represents a crd used to describe serices. type Beidou struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`Spec BeidouSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`Status BeidouStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` }// BeidouSpec is the spec of a Beidou. type BeidouSpec struct {Steps []BeidouStep `json:"steps" protobuf:"bytes,1,opt,name=steps"`References []BeidouReference `json:"references" protobuf:"bytes,2,opt,name=references"` }// BeidouStep is the spec of step. type BeidouStep struct {Name string `json:"name" protobuf:"bytes,1,opt,name=name"`Operations []BeidouOperation `json:"operations" protobuf:"bytes,2,opt,name=operations"` }// BeidouOperation is the spec of operation. type BeidouOperation struct {Name string `json:"name" protobuf:"bytes,1,opt,name=name"`Type BeidouType `json:"type" protobuf:"bytes,2,opt,name=type"`Op string `json:"op" protobuf:"bytes,3,opt,name=op"`Args []BeidouArg `json:"args" protobuf:"bytes,4,opt,name=args"` }// BeidouArg is the spec of arg. type BeidouArg struct {Name string `json:"name" protobuf:"bytes,1,opt,name=name"`Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`Reference BeidouOperationReference `json:"reference,omitempty" protobuf:"bytes,3,opt,name=reference"`Tolerations []corev1.Toleration `json:"tolerations,omitempty" protobuf:"bytes,4,opt,name=tolerations"`Checking []string `json:"checking,omitempty" protobuf:"bytes,5,opt,name=checking"` }// BeidouOperationReference is the spec of operation reference. type BeidouOperationReference struct {ID string `json:"id" protobuf:"bytes,1,opt,name=id"` }// BeidouReference is the spec of reference. type BeidouReference struct {ID string `json:"id" protobuf:"bytes,1,opt,name=id"`Steps []BeidouStep `json:"steps" protobuf:"bytes,2,opt,name=steps"` }// BeidouStatus represents the current state of a Beidou. type BeidouStatus struct {Message string `json:"message" protobuf:"bytes,1,opt,name=message"` }// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// BeidouList is a collection of Beidou. type BeidouList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`Items []Beidou `json:"items" protobuf:"bytes,2,opt,name=items"` }核心流程:
// ExecSteps executes steps. func ExecSteps(ctx context.Context, steps []v1alpha1.BeidouStep, references []v1alpha1.BeidouReference) error {logger, _ := ctx.Value(CtxLogger).(*log.Entry)var hasMonitored boolfor i, step := range steps {for j, op := range step.Operations {switch op.Op {case "DeploymentCreationEfficiency":if !hasMonitored {defer func() {err := monitor.Output()if err != nil {logger.Errorf("Failed to output: %s", err)}}()}hasMonitored = true}err := ExecOperation(ctx, op, references)if err != nil {return fmt.Errorf("failed to run operation %s: %s", op.Name, err)}}}return nil }// ExecOperation executes operation. func ExecOperation(ctx context.Context, op v1alpha1.BeidouOperation, references []v1alpha1.BeidouReference) error {switch op.Type {case v1alpha1.BeidouTask:if !tasks.IsRegistered(op.Op) {return ErrNotRegistered}if !tasks.DoesSupportReference(op.Op) {return ExecTask(ctx, op.Op, op.Args)}return ExecTaskWithRefer(ctx, op.Op, op.Args, references)}return nil }// ExecTask executes a task. func ExecTask(ctx context.Context, opname string, args []v1alpha1.BeidouArg) error {switch opname {case tasks.CreateNamespace:var ns stringfor _, arg := range args {switch arg.Name {case "NS":ns = arg.Value}}return op.CreateNamespace(ctx, ns)// ...}// ... }// ExecTaskWithRefer executes a task with reference. func ExecTaskWithRefer(ctx context.Context, opname string, args []v1alpha1.BeidouArg, references []v1alpha1.BeidouReference) error {switch opname {case tasks.RepeatNTimes:var times intvar steps []v1alpha1.BeidouStepvar err errorfor _, arg := range args {switch arg.Name {case "TIMES":times, err = strconv.Atoi(arg.Value)if err != nil {return ErrParseArgs}case "ACTION":for _, refer := range references {if refer.ID == arg.Reference.ID {steps = refer.Stepsbreak}}}}return RepeatNTimes(ctx, times, steps)}return ErrNotImplemented }操作原語的實現示例:
// PodAnnotations is an operation used to check whether annotations of Pod are expected. func PodAnnotations(ctx context.Context, data PodAnnotationsData) error {kclient, ok := ctx.Value(tasks.KubernetesClient).(kubernetes.Interface)if !ok {return tasks.ErrNoKubernetesClient}pods, err := kclient.CoreV1().Pods(data.Namespace).List(metav1.ListOptions{})if err != nil {return fmt.Errorf("failed to list pods in ns %s: %s", data.Namespace, err)}for _, pod := range pods.Items {if pod.Annotations == nil {return fmt.Errorf("pod %s in ns %s has no annotations", pod.Name, data.Namespace)}for _, annotation := range data.Exists {if _, exists := pod.Annotations[annotation]; !exists {return fmt.Errorf("annotation %s does not exist in pod %s in ns %s", annotation, pod.Name, data.Namespace)}}for k, v := range data.Equal {if pod.Annotations[k] != v {return fmt.Errorf("value of annotation %s is not %s in pod %s in ns %s", k, v, pod.Name, data.Namespace)}}}return nil }后續
目前阿里云容器服務團隊內部已經實現了初版,已用于部分云產品的內部性能測試以及常規的回歸測試,很大程度上提升了我們的工作效率。
在 yaml 中編程,是對云原生場景下聲明式操作的體現,也是對聲明式服務的一種實踐。對于常規工作場景中重復編碼或重復操作,可考慮類似的方式進行滿足。
歡迎大家對這樣的服務形態和項目進行討論,探索這種模式的價值。
阿里云容器服務持續招聘,歡迎加入我們,一起在 K8s、邊緣計算、Serverless 等領域開拓,讓當前變得更美好,也為未來帶來可能性!聯系郵箱:flyer.zyf@alibaba-inc.com
Spring Cloud Alibaba 七天訓練營
七天時間了解微服務各模塊的實現原理,手把手教學如何獨立開發一個微服務應用,助力小白開發者從 0 到 1 建立系統化的知識體系。點擊鏈接即可報名體驗:https://developer.aliyun.com/learning/trainingcamp/spring/1
“阿里巴巴云原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦云原生流行技術趨勢、云原生大規模的落地實踐,做最懂云原生開發者的公眾號。”
總結
以上是生活随笔為你收集整理的Kubernetes 新玩法:在 yaml 中编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringCloud 应用在 Kube
- 下一篇: 从零入门 Serverless | Kn