前段时间,我主导推动组里实现了一套基于 Locust+boomer 的通用的压测平台,主要目的是满足我们组内的各种压测场景,比如 grpc、websocket、webrtc、http 等协议的压测场景。正好我们公司的技术栈以 go 为主,我们可以轻松地使用 go 编写脚本,通过公司的部署平台编译打包后横向扩缩施压集群,可以说解决了各种压测需求。但是我们发现,尽管自己编写脚本非常自由,但是对不了解平台、不了解 Go 的同学来说,使用成本是比较大的,尤其是首次接触,因此我开始思考如何简化脚本的编写和部署。
从 http 开始
来源于 httprunner 和公司其他 Group 的工具的灵感,我想到用 json 的方式去定义 http 压测场景,然后用 go 去解析执行,可以预见的是这种方式的压测性能不如直接写代码,但是如果可以通过可接受的性能损耗来换取更简单的接入方式,更统一的使用方式,也是极好的,毕竟我们不缺机器。针对 http 协议,以下大概梳理了一下需要实现的能力。文章源自玩技e族-https://www.playezu.com/184208.html
文章源自玩技e族-https://www.playezu.com/184208.html简单说明下:文章源自玩技e族-https://www.playezu.com/184208.html
- 多接口
很好理解,压测的时候需要满足多个接口按一定的比例同时压测。在某些特殊场景下,可能还存在接口依赖的问题,这也要考虑到。文章源自玩技e族-https://www.playezu.com/184208.html
- 登录态
压测的接口可能有登录校验,那么压测的时候需要带上登录态,如果能打通账号平台自动批量生成登录态就方便很多了。文章源自玩技e族-https://www.playezu.com/184208.html
- 参数化
定义的脚本需要提供参数化能力,总不能所有参数写死,比如动态生成时间戳,ID,变长字符串等等,如果简单的参数生成无法满足,用户自己上传也是挺好的。文章源自玩技e族-https://www.playezu.com/184208.html
- 校验
响应内容校验是接口测试很重要的部分,在压测场景也是一样的。文章源自玩技e族-https://www.playezu.com/184208.html
定义 Json 结构
接下来,定义 Json 结构,尽量去满足上面所描述的需求。http 协议,无非就是三个部分,body、header、url,因此每一个接口需要包含这个三个字段,当然,名字是必不可少的,还有一个非常重要的字段,就是校验字段 validator,下面就来看看这个 Json 应该是什么样子。文章源自玩技e族-https://www.playezu.com/184208.html
{
"debug": true,
"domain": "https://postman-echo.com",
"header": {},
"declare": [
"{{ $sessionId := getSid }}"
],
"init_variables": {
"roomId": 1001,
"sessionId": "{{ $sessionId }}",
"ids": "{{ $sessionId }}"
},
"running_variables": {
"tid": "{{ getRandomId 5000 }}"
},
"func_set": [
{
"key": "getTest",
"method": "GET",
"url": "/get?name=gannicus&roomId={{ .roomId }}&age=10&tid={{ .tid }}",
"body": "{"timeout":10000}",
"validator": "{{ and (eq .http_status_code 200) (eq .args.age (10 | toString )) }}"
},
{
"key": "postTest",
"method": "POST",
"header": {
"Cookie": "{{ .tid }}",
"Content-Type": "application/json"
},
"url": "/post?name=gannicus",
"body": "{"timeout":{{ .tid }}, "retry":true}",
"validator": "{{ and (eq .http_status_code 200) (eq .data.timeout (.tid | toFloat64 ) ) (eq .data.retry false) }}"
}
]
}
func_set 应该挺好理解的,这里解释一下 declare、init_variables、running_variables:文章源自玩技e族-https://www.playezu.com/184208.html
- declare 这个字段是为了声明变量的,比如在 init 或 running 变量中都可以引用这个变量,声明方式如:
["{{ $sessionId := getSid }}","{{ $id := 100100 }}"]
- init_variables
初始化变量,只初始化一次,可以是常量,也可以从模板函数中获取,如:文章源自玩技e族-https://www.playezu.com/184208.html
{"roomId":1001,"sessionId":"{{ $sessionId }}","ids":"{{ now }}"}
- running_variables
运行时变量,每一个请求发起前都会去构造参数,因此不建议常量定义在这里。
{"tid":"{{ getRandomId 5000 }}"}
解析流程
想要利用 boomer,那就需要想办法生成 boomer.Task,它的结构如下:
type Task struct {
// The weight is used to distribute goroutines over multiple tasks.
Weight int
// Fn is called by the goroutines allocated to this task, in a loop.
Fn func()
Name string
}
核心就是得到这个执行函数 Fn,思路就是分别根据 init 和 running 变量定义,为 func_set 中声明的每个请求分别定义一个匿名函数,函数中去动态生成变量,然后发起真实请求,最后根据每个请求声明的 validator 进行断言。整个执行流程如下:
实现原理
go 的原生库中就有模板相关的库 text/template,我直接使用模板库实现了这套解析逻辑,包括参数的生成,模板方法、断言,整个 json 脚本的语法都是基于 go 的模板库的。感兴趣的朋友可以查看:
- 官方模板库
- Go 语言标准库 text/template 包深入浅出
如何断言
因为断言这部分非常重要,所以单独讲。上面已经说过断言是通过模板来实现的,所以要使用断言就要掌握基本的模板语法。
模板库内置了比较和逻辑方法所以可以直接使用,比如比较 http 状态码:
"validator": "{{ eq .http_status_code 200 }}"
再比如多个比较:
"validator": "{{ and (eq .http_status_code 200) (eq .data.timeout (.tid | toFloat64 ) ) (eq .data.retry false) }}"
可能你也已经注意到了有一个 toFloat64, 它是一个模板自定义函数,这里是为了做类型转换。
此外,你也可以看到基于 go 模板库,访问变量变得非常简单,比如上面的.data.timeout,它对应响应中的内容类似如下:
{"data":{"timeout":1000}}
这样我们就可以比较响应 json 中的任何字段了。
写在最后
简单做了一下压力测试,对比不使用模板解析和使用模板解析的情况,模板解析在 CPU 密集型的场景下性能大概是直接写脚本编译的三分之一,如果不是 CPU 密集型应该可以去到二分之一,因此我暂时不优化了。令人惊喜的是这个模板解析也可以扩展成为接口测试的编写方式,类似 httprunner。
目前只是做了一个 Demo,还没正真集成到我们的压测平台,还是挺令人期待呀。
未知地区 1F
很棒!我本来也有一个这样的想法,没想到你们先实现了哈哈,我将持续更新,欢迎交流探讨 已经 star,期待更加完整的功能和文档挺好的分享,字符串模板形式的。
boomer 刚出时玩了一会,mq 那层后的确比 locust 本身好。
set 那层测试任务,安利一种注册的写法。
func NewCatLocust() *boomer.Task {
task := &boomer.Task{
Weight: 1,
Fn: TaskList,
Name: “TestType+游戏名称”,
}
return task
}
TaskList 内部通过 registerFunc(taskName string, f func() error) 注册到一个支持权重范围的 WeightedFuncs() Map 对象里面后,进行压测。
func registerFunc(taskName string, fc func() error) {
st := time.Now()
err := fc()
if err != nil {
boomer.RecordFailure(“xxGame”, taskName , e.Milliseconds(), err)
} else {
end := time.Since(st)
boomer.RecordSuccess(“xxGame”, taskName , e.Milliseconds(), 0)
}
}
大佬你好,想请问一下,我分别在两个 ide 中运行 master 与 slave 的代码。但是 master 那边控制台提示:You are running in distributed mode but have no worker servers connected. Please connect workers prior to swarming. 实际上我已经把 slave 的代码运行起来了。请问这个是什么问题呢?最好能把 worker(slave) 那边的图贴出来,你是运行 go 版本的吧,默认是找本地 5557 端口的 master,你可以看看端口情况上述问题已经解决了,目前是 boomer 包:Version 3.0 received does match expected version 3.1 的这个异常。我看 myzhan 的 issue 里面。也没说有啥解决方案。然后我更新 github.com/zeromq/goczmq 时会出现异常:fatal error: czmq.h: No such file or directory。 网上的解决方案说,这个包没有更新了,作者已经换成了 cppzmq 啥的。 额,目前遇到的情况就是这样。谢谢大佬的回复。
仅楼主可见娃刚出生,比较忙,以后有机会哈