聊一聊如何使用Crank给我们的类库做基准测试
Posted Catcher Wong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊一聊如何使用Crank给我们的类库做基准测试相关的知识,希望对你有一定的参考价值。
背景
当我们写了一个类库提供给别人使用时,我们可能会对它做一些基准测试来测试一下它的性能指标,好比内存分配等。
在 .NET 的世界中,用 BenchmarkDotNet 来做这件事是非常不错的选择,我们只要写少量的代码就可以在本地运行基准测试然后得到结果。
这个在修改代码的时候,效果可能会更加明显,因为我们想知道我们的修改会不会使这段代码跑的更快,占用的资源更少。
作一个简单的假设,根据测试用例,代码变更之前,某方法在基准测试的分配的内存是 1M,修改之后变成 500K,那么我们可以认为这次的代码变更是有性能提升的,占用的资源更少了,当然这个得在单元测试通过的前提下。
试想一下,如果遇到下面的情况
- 想在多个不同配置的机器上面运行基准测试,好比 4c8g 的windows, 4c16g 的 linux
- Pull Request/Merge Request 做代码变更时,如何较好的做变更前后的基准测试比较
这个时候就会复杂一点了,要对一份代码在多个环境下面运行,做一些重复性的工作。
那么我们有没有办法让这个变得简单呢?答案是肯定的。
我们可以用 Crank 这个工具来完成这些内容。
什么是 Crank
Crank 是.NET团队用于运行基准测试的基础设施,包括(但不限于)TechEmpower Web Framework基准测试中的场景。 Crank 第一次出现在公众的视野应该是在 .NET Conf 2021, @sebastienros 演讲的 Benchmarking ASP.NET Applications with .NET Crank。
Crank 是 client-server (C/S) 的架构,主要有一个控制器 (Controller) 和一个或多个代理 (Agent) 组成。 其中控制器就是 client,负责发送指令;代理就是 server,负责执行 client 发送的指令,也就是执行具体的测试内容。
下面是它的架构图。
可以看到,控制器和代理之间的交互是通过 HTTP 请求来驱动的。然后代理可以执行多个不同类型的作业类型。
我们这篇博客主要讲的是图中的 .NET project Job。
先来看看官方仓库一个比较简单的入门示例。
入门示例
首先要安装 crank 相关的两个工具,一个是控制器,一个是代理。
dotnet tool update Microsoft.Crank.Controller --version "0.2.0-*" --global
dotnet tool update Microsoft.Crank.Agent --version "0.2.0-*" --global
然后运行官方仓库上面的 micro 示例,是一个 Md5 和 SHA 256 对比的例子。
public class Md5VsSha256
[Params(100, 500)]
public int N get; set;
private readonly byte[] data;
private readonly SHA256 sha256 = SHA256.Create();
private readonly MD5 md5 = MD5.Create();
public Md5VsSha256()
data = new byte[N];
new Random(42).NextBytes(data);
[Benchmark]
public byte[] Sha256() => sha256.ComputeHash(data);
[Benchmark]
public byte[] Md5() => md5.ComputeHash(data);
要注意的是 Main 方法,要用 BenchmarkSwitcher 来运行,因为 Crank 是用命令行来执行的,会附加一些参数,也就是代码中的 args。
public static void Main(string[] args)
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
然后是控制器要用到的配置文件,里面就是要执行的基准测试的内容,要告诉代理怎么执行。
# 作业
jobs:
# 作业名,自定义
benchmarks:
# 源相关内容
source:
# 这里是本地文件夹,也可以配置远程 repository 和分支
localFolder: .
# 这个是具体的 csproj
project: micro.csproj
# 一些变量
variables:
filterArg: "*"
jobArg: short
# 参数
arguments: --job jobArg --filter filterArg --memory
options:
# 使用 BenchmarkDotNet
benchmarkDotNet: true
# 场景
scenarios:
# 场景名,自定义
Md5VsSha256:
application:
# 与前面的定义作业名一致
job: benchmarks
# 档案
profiles:
# 档案名,自定义
local:
jobs:
application:
# 代理的地址
endpoints:
- http://localhost:5010
下面先来启动代理,直接运行下面的命令即可。
crank-agent
会看到下面的输出:
[11:42:30 INF] Created temp directory \'C:\\Users\\catcherwong\\AppData\\Local\\Temp\\2\\benchmarks-agent\\benchmarks-server-8952\\2mmqc00i.3b1\'
[11:42:30 INF] Agent ready, waiting for jobs...
默认端口是 5010,可以通过
-u|--url
来指定其他的;如果运行代理的电脑已经安装好 SDK 了,可以指定--dotnethome
避免因网络问题导致无法正常下载 SDK。
然后是通过控制器向代理发送指令。
crank --config C:\\code\\crank\\samples\\micro\\micro.benchmarks.yml --scenario Md5VsSha256 --profile local
上面的命令指定了我们上面的配置文件,同时还指定了 scenario 和 profile。因为配置文件中可以有多个 scenario 和 profile,所以在单次执行是需要指定具体的一个。
如果需要执行多个 scenario 则需要执行多次命令。
在执行命令后,代理里面就可以看到日志输出了:
最开始的是收到作业请求,然后安装对应的 SDK。安装之后就会对指定的项目进行 release 发布。
发布成功后就会执行 BenchmarkDotNet 相关的内容。
运行完成后会输出结果,最后清理这次基准测试的内容。
代理执行完成后,可以在控制器侧看到对应的结果:
一般来说,我们会把控制器得到的结果保存在 JSON 文件里面,便于后续作对比或者要出趋势图。
这里可以加上 --json 文件名.json
。
crank --config C:\\code\\crank\\samples\\micro\\micro.benchmarks.yml --scenario Md5VsSha256 --profile local --json base.json
运行多次,将结果存在不同的 JSON 文件里,尤其代码变更前后的结果。
crank --config C:\\code\\crank\\samples\\micro\\micro.benchmarks.yml --scenario Md5VsSha256 --profile local --json head.json
最后是把这两个结果做一个对比,就可以比较清楚的看到代码变更是否有带来提升。
crank compare base.json head.json
上面提到的还是在本地执行,如果要在不同的机器上面执行要怎么配置呢?
我们要做的是在配置文件中的 profiles 节点增加机器的代理地址即可。
下面是简单的示例:
profiles:
local:
jobs:
application:
endpoints:
- http://localhost:5010
remote-win:
jobs:
application:
endpoints:
- http://192.168.1.100:9090
remote-lin:
jobs:
application:
endpoints:
- http://192.168.1.102:9090
这个时候,如果指定 --profile remote-win
就是在 192.168.1.100
这台服务器执行基准测试,如果是 --profile remote-lin
就是在 192.168.1.102
。
这样就可以很轻松的在不同的机器上面执行基准测试了。
Crank 还有一个比较有用的功能是可以针对 Pull Request 进行基准测试,这对一些需要基准测试的开源项目来说是十分有帮助的。
接下来老黄就着重讲讲这一块。
Pull Request
正常来说,代码变更的肯定是某个小模块,比较少出现多个模块同时更新的情况,如果是有,估计也会被打回拆分!
所以我们不会选择运行所有模块的基准测试,而是运行变更的那个模块的基准测试。
思路上就是有人提交 PR 后,由项目组成员在 PR 上面进行评论来触发基准测试的执行,非项目组成员的话不能触发执行。
下面就用这个 Crank 提供的 Pull Request Bot 来完成后面的演示。
要想用这个 Bot 需要先执行下面的安装命令:
dotnet tool update Microsoft.Crank.PullRequestBot --version "0.2.0-*" --global
安装后会得到一个 crank-pr
的文件,然后执行 crank-pr
的命令就可以了。
可以看到它提供了很多配置选项。
下面是一个简单的例子
crank-pr \\
--benchmarks lib-dosomething \\
--components lib \\
--config ./benchmark/pr-benchmark.yml\\
--profiles local \\
--pull-request 1 \\
--repository "https://github.com/catcherwong/library_with_crank" \\
--access-token "$ secrets.GITHUB_TOKEN " \\
--publish-results true
这个命令是什么意思呢?
它会对 catcherwong/library_with_crank 这个仓库的 Id 为 1 的 Pull Request 进行两次基准测试,一次是主分支的代码,一次是 PR 合并后的代码;基准测试的内容由 benchmarks,components 和 profiles 三个选项共同决定;最后两个基准测试的结果对比会在 PR 的评论上面。
其中 catcherwong/library_with_crank 是老黄提前准备好的示例仓库。
下面来看看 pr-benchmark.yml 的具体内容
components:
lib:
script: |
echo lib
arguments:
# crank arguments
"--application.selfContained false"
# default arguments that are always used on crank commands
defaults: ""
# the first value is the default if none is specified
profiles:
local:
description: Local
arguments: --profile local
remote-win:
description: windows
arguments: --profile remote-win
remote-lin:
description: linux
arguments: --profile remote-lin
benchmarks:
lib-dosomething:
description: DoSomething
arguments: --config ./benchmark/library.benchmark.yml --scenario dosomething
lib-getsomething:
description: GetSomething
arguments: --config ./benchmark/library.benchmark.yml --scenario getsomething
lib-another:
description: Another
arguments: --config ./benchmark/library.benchmark.yml --scenario another
基本上可以说是把 crank
的参数拆分了到了不同的配置选项上面去了,运行的时候就是把这些进行组合。
再来看看 library.benchmark.yml
jobs:
lib:
source:
localFolder: ../src
project: BenchmarkLibrary/BenchmarkLibrary.csproj
variables:
filter: "*"
jobArg: short
arguments: --job jobArg --filter filter --memory
options:
benchmarkDotNet: true
scenarios:
dosomething:
application:
job: lib
variables:
filter: "*DoSomething*"
getsomething:
application:
job: lib
variables:
filter: "*GetSomething*"
another:
application:
job: lib
variables:
filter: "*Method*"
profiles:
local:
jobs:
application:
endpoints:
- http://localhost:9999
remote-lin:
jobs:
application:
endpoints:
- http://remote-lin.com
remote-win:
jobs:
application:
endpoints:
- http://remote-win.com
和前面入门的例子有点不一样,我们在 scenarios 节点 里面加了一个 variables,这个和 jobs 里面定义的 variables 和 arguments 是相对应的。
如果指定 --scenario dosomething
,那么最后得到的 arguments 就是
--job short --filter *DoSomething* --memory
后面就是来看看效果了。
这里省略了评论内容的解析,也就是评论什么内容的时候会触发执行,因为这一块不是重点,有兴趣可以看 workflow 的脚本即可。
具体的执行过程可以参考
https://github.com/catcherwong/library_with_crank/actions/runs/4598397510/jobs/8122376959
当然,如果条件允许的话,也可以用自己的服务器资源来跑基准测试,不用 Github Action 提供的资源。
这样的好处是相对稳定,可以自己根据场景指定不同配置的服务器。不过对一些没那么复杂类库,用 Github Action 的资源也是无伤大雅的。
下面这个截图就是在提交到外部服务器上面执行的。
如果仓库不是在 Github,是在自建 Gitlab 或者其他的,就可以根据这个思路来自定义流水线从而去完成这些基准测试的操作。
总结
Crank 还是一个挺不错的工具,可以结合 BenchmarkDotNet 来做类库的基准测试,也可以结合 wrk/wrk2/bombardier/h2load 等压测工具进行 api/grpc 框架和应用的测试。
这里只介绍了其中一个小块的内容,还有挺多内容可以挖掘一下的。
最后是本文的示例代码:
https://github.com/catcherwong/library_with_crank
参考资料
聊一聊Jmeter多用户token使用问题
背景
在测试的时候,经常会有模拟用户登录,拿到用户 token 后再去请求接口的场景。
这个模拟用户登录就会分为两种,一种是单用户,另一种是多用户。
日常自动化测试的时候可能一个用户对应 n 个用例就可以满足大多数场景;
如果是在压力测试的场景下面,可能就会略显单调,也无法满足一些真实业务场景。
对于单用户的情况下,和我们常规的多接口有依赖的测试其实没什么太大的差别。
所以这里主要讲的是多用户产生多个 token 的情况。
下面来看一个具体的例子来了解一下。
场景接口
在这里的话,只有两个接口,一个是登录拿 token,一个是有 token 才能请求的。
下面是各接口定义
登录接口
请求:
POST http://localhost:8532/MultiToken/Login
Content-Type: application/json
{
"UserName":"catcherwong-1",
"Password":"123"
}
响应:
{"code":0,"msg":"ok","data":"catcherwong-1-token"}
业务接口
请求:
GET http://localhost:8532//MultiToken/do?account=xxx
Content-Type: application/json
token: catcherwong-1-token
响应:
{"code":0,"msg":"ok","data":"catcherwong-1-token"}
登录接口处理
登录接口属于预请求,所以我们一般会选择把它放在 setUp 线程组里面。
我们需要准备一个 csv 文件,里面用来存放需要登录的用户名和密码。
接下来就是把这个 csv 配置好,定义了两个变量 account
和 pwd
然后是把登录的 HTTP 请求配置好:
由于后面要用到 token,所以要先把 token 提取出来,这里用的是 JSON Extractor。
到这里就要开始注意了!!!!
由于我们会有多个用户进行登录,但是这一个提取操作每次都会把 token 赋值到 access_token 这个变量上面,是覆盖的操作。
换句话就是说,每登录一个用户,这个 access_token 的值就会是最后一个登录的用户的 token,。
换个思路,每次它会覆盖,那么把这些 token 存到一个地方,然后业务接口去这个地方取就可以了。
如果没有用户登录这一步,给的直接是 token,那么我们也是直接把这个 token 放到 csv 文件里面,然后让 jmeter 去循环使用里面的 token。
那么要做的东西其实就很确定了,就是在提取到 token 后,把这个 token 写到一个 csv 文件里面。
要想做到这一步,需要在登录接口后面加一个后置的处理。
String p1 = System.getProperty("user.dir");
String p2 = System.getProperty("file.separator");
String p3 = "user_token.csv";
String path = p1 + p2 + p3;
FileWriter fileWriter = new FileWriter(new File(path), true);
BufferedWriter writer = new BufferedWriter(fileWriter);
writer.append(vars.get("accout")+","+vars.get("access_token")+"\\n");
writer.close();
fileWriter.close();
这段代码的意思是,把用户名和提取到的 access_token 写进到 csv 文件里面,这个文件在的位置是 jmeter 的目录。
这里是对文件路径做了处理,可以适配所有操作系统的。不会出现说指定了一个 windows 系统的路径,然后放到 linux 系统下面就跑不了了。
还有最重要的一个是,要修改 setUp 线程组的属性,把循环次数改成 3 。因为前面的 csv 文件里面有 3 个用户,这样它才会触发三次登录。
业务接口处理
业务接口要放到正常的线程组里面,独立于 setUp 线程组。
前面提到,登录后会有一个 csv 文件,所以这里第一个要做的是把 csv 配置好。
上面的截图用的是 ${__P(user.dir,)}${__P(file.separator,)}user_token.csv
这个文件路径,这个在本地环境的 Jmeter 是可以通过的,不过在一些云服务上面是不行的,如阿里云 PTS 。
这里可以忽略前面的路径,直接填写 user_token.csv
即可,填这两个,得到的文件路径是一样的,一个是绝对路径一个是相对路径。
然后就是配置 HTTP 请求了
PS:不要忘记把请求头也配置了,这里就不截图了。
这里试跑两次,可以发现业务请求的接口,它的 token 请求头每次都是不一样的,在交替变化,这个是符合预期的。
但是会发现生成 csv 文件里面的数据会重复,没有自动清理掉上一次产生的数据。如果上一次产生的 token 过期了,那么用了这些过期的 token === 凉凉。
所以这里还有必要加一步 tearDown 线程组,每次跑完脚本把这个文件删除掉。
String p1 = System.getProperty("user.dir");
String p2 = System.getProperty("file.separator");
String p3 = "user_token.csv";
String path = p1 + p2 + p3;
log.info("准备删除文件:" + path);
File file = new File(path);
if (!file.exists()) {
log.info("删除文件失败:" + path + "不存在!");
} else {
file.delete();
}
这个时候跑脚本就基本没什么问题了。
写在最后
多用户获取多 token 再使用的场景其实挺多的,这篇内容简单讲解了老黄正在用的一个方案,如果您有更好的建议,也欢迎一起沟通交流。
老黄把 JMeter 系列的内容都放在 github 了,方便大家查阅和测试。
以上是关于聊一聊如何使用Crank给我们的类库做基准测试的主要内容,如果未能解决你的问题,请参考以下文章