作者 | 悟鹏
引子
性能测试在日常的开发工作中是惯例需要,用来摸底服务的性能。
那么如何做性能测试?要么是通过编码的形式实现,写一堆脚本,用完即弃;要么是基于平台,在平台定义的流程中进行。对于后者,通常因为指标场景的复杂性,如部署特定的 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 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 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 等畛域开辟,让以后变得更美妙,也为将来带来可能性!分割邮箱:flyer.zyf@alibaba-inc.com
Spring Cloud Alibaba 七天训练营
七天工夫理解微服务各模块的实现原理,手把手教学如何独立开发一个微服务利用,助力小白开发者从 0 到 1 建设系统化的常识体系。点击链接即可报名体验:https://developer.aliyun.com/learning/trainingcamp/spring/1
“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术畛域、聚焦云原生风行技术趋势、云原生大规模的落地实际,做最懂云原生开发者的公众号。”