
作者 | 悟鵬
引子
性能測試在日常的開發工作中是正常需求,用來摸底服務的性能。
那麼如何做性能測試?要麼是通過編碼的方式完成,寫一堆腳本,用完即棄;要麼是基于平台,在平台定義的流程中進行。對于後者,通常由于目标場景的複雜性,如部署特定的 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: performance
namespace: beidou
spec:
steps:
- name: "Create Namespace If Not Exits"
operations:
- name: "create namespace"
type: Task
op: CreateNamespace
args:
- name: NS
value: beidou
- name: "Monitor Deployment Creation Efficiency"
operations:
- name: "Begin To Monitor Deployment Creation Efficiency"
type: Task
op: DeploymentCreationEfficiency
args:
- name: NS
value: beidou
- name: "Repeat 1 Times"
type: Task
op: RepeatNTimes
args:
- name: TIMES
value: "1"
- name: ACTION
reference:
id: deployment-operation
- name: "Delete namespace"
operations:
- name: "delete namespace"
type: Task
op: DeleteNamespace
args:
- name: NS
value: beidou
- name: FORCE
value: "false"
references:
- id: deployment-operation
steps:
- name: "Prepare Deployment"
operations:
- name: "Prepare Deployment"
type: Task
op: PrepareBatchDeployments
args:
- name: NS
value: beidou
- name: NODE_TYPE
value: ebm
- name: BATCH_NUM
value: "1"
- name: TEMPLATE
value: "./templates/basic-1-pod-deployment.yaml"
- name: DEPLOYMENT_REPLICAS
value: "1"
- name: DEPLOYMENT_PREFIX
value: "ebm"
- name: "Wait For Deployments To Be Ready"
type: Task
op: WaitForBatchDeploymentsReady
args:
- name: NS
value: beidou
- name: TIMEOUT
value: "3m"
- name: CHECK_INTERVAL
value: "2s"
basic-1-pod-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: basic-1-pod
spec:
selector:
matchLabels:
app: basic-1-pod
template:
metadata:
labels:
app: basic-1-pod
spec:
containers:
- name: nginx
image: registry-vpc.cn-hangzhou.aliyuncs.com/xxx/nginx:1.17.9
imagePullPolicy: Always
resources:
limits:
cpu: 2
memory: 4Gi
然後通過一個指令行工具執行 performance-test.yaml:
$ beidou server -c ~/.kube/config services/performance-test.yaml
執行效果如下 (每個 Deployment 建立耗時,所有 Deployment 建立耗時的 TP95 值,每個 Deployment 是否建立成功):
這些 metrics 是按照 Prometheus 标準輸出,可以被 Prometheus server 收集走,再結合 Grafana 可以可視化展示性能測試資料。
通過在 yaml 中表達想法,編排對 K8s 資源的操作、監控,再也不用為性能測試的實作頭疼了 :D
為什麼要在 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 更細,關注在操作函數上:
下面簡單描述該服務的設計和實作。
設計和實作
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
- 檢測項相關
- CheckPodAnnotations
- CheckPodObjectInfo
- CheckPodInnerStates
- 控制語句相關
- RepeatNTimes
上述 4 個概念的關系如下:
示例可參見文章開頭的 yaml 檔案,對應形式二。
3. 核心實作
CRD 設計:
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BeidouType is the type related to Beidou execution.
type BeidouType string
const (
// 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 bool
for 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 string
for _, 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 int
var steps []v1alpha1.BeidouStep
var err error
for _, 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.Steps
break
}
}
}
}
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 等領域開拓,讓目前變得更美好,也為未來帶來可能性!聯系郵箱:[email protected]
Spring Cloud Alibaba 七天訓練營
七天時間了解微服務各子產品的實作原理,手把手教學如何獨立開發一個微服務應用,助力小白開發者從 0 到 1 建立系統化的知識體系。點選連結即可報名體驗:
https://developer.aliyun.com/learning/trainingcamp/spring/1“ 阿裡巴巴雲原生 關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公衆号。”