ch10 Minimal API 性能评估与基准
本章主要目的是介绍为啥使用 Minimal API.
本章会提供一些数据和测量方法, 来比较传统方式创建 ASP.NET 6 项目和 ASP.NET 6 Minimal API 项目的性能.
性能是任何应用的关键, 但常常被方才次要位置.
应用的性能和可扩展性不仅仅依赖于我们的代码, 还依赖于开发用的技术栈本身. .NET 技术栈日新月异, 不仅底层完全重写, 并且各个性能都得到了提升.
本章会介绍如何评估 web 应用的性能, 并两个使用相同代码开发, 一个采用 MinimalAPI, 而另一个采用传统方式开发的 web 应用进行对比. 我们会充分利用 BenchmarkDotNet 框架来进行测试. 该框架也同样适用于其他应用场景.
Minimal API 是一个简单的框架, 其中不包含那些在传统框架中, 看起来理当然的组件. 因而其性能会有所改善.
本章重点:
- 改善 Minimal API
- 利用负载测试来探究性能
- 使用 BenchmarkDotNet 来标记 minimal API
技术要求
很多系统可以帮助我们测试框架性能.
我们会记录应用每秒可以处理多少个请求, 并将其与其他应用在相同负载下的测量结果进行比较. 这里案例中我们讨论的便是负载测试.
要测试 minimal API, 我们需要安装 k6 框架, 我们会使用它来执行测试.
我们会在仅执行 .NET 应用程序的 win 机器上进行负载测试.
这里了解一个打开, 没办法完全模拟.
安装 k6 可使用下列其中一个方法:
如果你使用 Chocolatey package manager (https://chocolatey.org), 你可以使用下面的命令来安装非官方的 k6 包.
choco install k6
如果你使用 WIndows Package Manager (https://github.com/microsoft/winget-cli), 使用下面命令安装官方 k6
winget install k6
也可以像我一样, 在 win 上安装, 然后通过命令行启动所有应用. 可以从下面地址来下载 k6
在本章的最后, 我们还会测量调用 API 过程中 HTTP 方法的持续时间.
我们会站在系统的终端, 将 API 视为一个黑盒, 来测量响应的时间.
我们会使用到一个名为 BenchmarkDotNet 的工具, 将其加入到项目中, 我们需要基于 nuget 来安装
dotnet add package BenchmarkDotNet
同样所有的内容都可以在 GitHub 仓库中找到源代码.
Minimal API 的改进
Minimal API 的设计不仅仅是对性能的提升, 也是为了让编写代码更方便, 与其他语言更接近, 从而降低开发平台与其他平台的差异. 从框架角度上看, 每一个版本都有性能的提高; 从简化应用程序管道的角度看也是如此. 下面从细节上看哪些部分没变化, 哪些部分提升了性能.
Minimal API 执行管道省略了下面的特性, 使得框架更轻量级:
- 过滤器. 例如:
IAsyncAuthorizationFilter
,IAsyncActionFilter
,IAsyncExceptionFilter
,IAsyncResultFilter
, 以及IAsyncResourceFilter
- 模型绑定
- 从表单绑定, 例如
IFormFile
. - 内置校验
- 格式化
- 内容协商 (Content negotiation)
- 一些中间件
- 视图渲染
- JsonPatch
- OData
- API 版本
.NET 6 中性能的改善
对于性能的改善具体内容可以参考: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/
jk: 这个文章看起来需要深入一下.
使用负载测试来探索性能
如果评估 Minimal API 的性能呢? 有需要方面需要考虑, 本章会尽可能从负载的角度来一一说明. 首先从 k6 开始, 它会告诉我们在 Minimal API 中每秒可以处理多少个请求.
如 k6 的作者而言, k6 是一个开源的负载工具, 它让测试变得简单高效. 它是开源的, 以开发者为中心的, 可扩展的. 使用该工具可以对性能与可靠性进行测试, 并能尽早的发现性能退化的原因, 为创建高性能的可扩展应用提供帮助.
在我们的示例中, 我们将其作为性能评估的工具, 而不是仅用于负载测试. 在负载测试中需要考虑很多的参数, 但是我们仅仅考虑 http_reqs 这一项, 它表示每秒钟可以正确处理的请求数.
作者比较赞同 k6 作者对性能评估的看法, 即性能与综合监控
使用 case
k6 的使用者多半是开发者, QA 工程师, SDET, 以及 SRE. 他们使用 k6 来测试 API, 微服务, 以及网站的性能与稳定性. 通常 k6 的 case 包括:
- 负载测试 (Load testing). k6 对资源进行了优化, 用于进行高负载的测试 (spike, stress 以及 soak test).
- 性能与综合监控 (Performance and synthetic monitoring). 使用 k6, 可以在小负载的情况下运行测试, 来持续验证生产环境的稳定性和性能.
- 混乱与可靠性测试 (Chaos and reliability testting). k6 提供了一个可扩展的架构. 你可以使用 k6 来模拟混沌运行环境, 并在其中使用 k6 进行测试.
jk: 有些术语似乎不太明晰
然而, 如果需要基于某些方面进行应用程序评估, 我们依旧需要一些假设. 实际上的运行环境, 比起本章介绍的要复杂的多. 当应用被请轰炸时, 并非所有的请求都会成功. 如果只有少部分的响应失败, 我们就可以说测试通过. 通常会考虑 95% ~ 98% 的统计结果.
基于此背景, 我们可以逐步的进行下面的测试: 逐步的, 系统会接受虚拟用户 (virtual user, VU) 从 0 到 50 的访问量, 并持续 15 秒钟. 然后保持该用户数 60 秒, 最后再逐步将用户数将至 0, 并持续 15 秒.
每一个新的测试阶段都写在 JavaScript 文件中的阶段节点中. 因此, 测试在简单的经验评价下进行.
不太懂, 原文是: Each newly written stage of the test is expressed in the JavaScript file in the stages section. Testing is therefore conducted under a simple empirical evaluation.
首先, 我们同时在 ASP.NET WebAPI 和 minimal API 中创建三个类型的响应:
- 纯文本
- 非常小的 JSON 数据, 并保证数据是静态的, 而且每次请求都返回相同数据.
- 第三个响应, 我们向 API 发送 POST 请求, 并携带 JSON 数据. 在 Web API 中, 我们检查对象的校验结果, 在 minimal API 中不进行校验, 然后返回接收到的数据.
下面的代码会用于在 minimal API 和 传统 API 间进行性能比较:
Minimal API
app.MapGet("text-plain", () => Results.Ok("response"))
.WithName("GetTextPlain");
app.MapPost("Validations", (ValidationData validation) => Results.Ok(validation))
.WithName("PostValidationData");
app.MapGet("jsons", () => {
var response = new [] {
new PersonData { Name = "Andrea", Surname = "Tosato", Birthdate = new DateTime(2022, 01, 01) },
new PersonData { Name = "Emamnele", Surname = "Bartolesi", Birthdate = new DateTime(2022, 01, 01) },
new PersonData { Name = "Marco", Surname = "Minerva", Birthdate = new DateTime(2022, 01, 01) }
};
return Results.Ok(response);
})
.WithName("GetJsonData");
传统方法
传统方法中, 设计三个不同的控制器:
[Route("text-plain")]
[ApiController]
public class TextPlainController: ControllerBase {
[HttpGet]
public IActionResult Get() {
return Content("response");
}
}
[Route("validations")]
[ApiController]
public class ValidationsController: ControllerBase {
[HttpPost]
public IActionResult Post(ValidationData data) {
return Ok(data);
}
}
[Route("jsons")]
[ApiController]
public class JsonsController: ControllerBase {
[HttpGet]
public IActionResult Get() {
var response = new [] {
new PersonData { Name = "Andrea", Surname = "Tosato", Birthdate = new DateTime(2022, 01, 01) },
new PersonData { Name = "Emamnele", Surname = "Bartolesi", Birthdate = new DateTime(2022, 01, 01) },
new PersonData { Name = "Marco", Surname = "Minerva", Birthdate = new DateTime(2022, 01, 01) }
};
return Ok(response);
}
}
其中模型类型
public class ValidationData {
[Required]
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Description { get; set; }
}
public class PersonData {
public string Name { get; set; }
public string Surname { get; set; }
public DateTime Birthdate { get; set; }
}
下一节我们会定义 options
对象, 在该对象中定义执行的坡度描述. 我们所定义的所有条款都考虑了测试是否合格. 最后, 我们编写真正的测试, 它什么也不做, 只是在端点调用 GET 与 POST 请求.
编写 K6 测试
import http from 'k6/http'
import { check } from 'k6'
export let options = {
summaryTrendStats: [ 'avg', 'p(95)' ],
stages: [
// 10 秒内, 用户数从 1 到 50 线性增长
{ target: 50, duration: '10s' },
// 将用户数保持在 50, 持续 1 分钟
{ target: 50, duration: '1m' },
// 在最后 15 秒钟, 将用户数线性的降为 0
{ target: 0, duration: '15s' }
],
thresholds: {
// 我们希望 95% 的请求响应时间低于 500ms
'http_req_duration': ['p(95)<500'],
// 阈值基于自定义尺度, 我定义并使用它来跟踪应用程序的失败情况
'check_failure_rate': [
// 全局失败率应该低于 1%
'rate<0.01',
// 如果失败率超过了 5%, 则提前结束测试
{ threshold: 'rate<=0.05', abortOnFail: true },
]
}
}
export default function () {
// 执行 http get 请求
let response = http.get('http://localhost:7060/jsons');
// 若任何一个条件失败, 则 check() 返回 false
check(response, {
'status is 200': r => r.status === 200
})
}
然后作者对该代码进行了简单介绍.
运行 k6 来进行测试性能
下面运行测试并生成测试统计
首先运行项目 (两个)
dotnet .\MinimalAPI.Sample\bin\Release\net6.0\MinimalAPI.Sample.dll --urls=https://localhost:7059/;http://localhost:7060/ dotnet .\ControllerAPI.Sample\bin\Release\net6.0\ControllerAPI.Sample.dll --urls=https://localhost:7149/;http://localhost:7150/
然后运行 k6 (也是两个)
k6 run .\K6\Minimal\json.js --summary-export=.\K6\results\minimal-json.json k6 run .\K6\Controllers\json.js --summary-export=.\K6\results\controller-json.json
然后等待结果
补充: 编译执行命令
dotnet build dotnet publish -c Release
也许是版本问题, 也许有更新, 当下 (2023年12月28日)
check_failure_rate
无法使用, 然后修改为http_req_failed
再运行.然后运行的是 minimal api 项目进行的截图, 没有使用 https 协议, 端口号设置为 12345
上述截图中描述, 这个 API 每秒处理请求 30112 次.
这个操作细节可以参考 k6 官网: https://k6.io/
作者书中记录的统计数据为:
- 传统项目,
plain-text
接口每秒处理 1547 次. - 传统项目,
jsons
接口每秒处理 1614 次. - 传动项目,
json
类型带模型验证, 每秒处理 1602 次. - minimal API,
plain-text
接口每秒处理 2285 次. - minimal API,
jsons
接口每秒处理 2030 次. - minimal API,
json
类型带模型验证, 每秒处理 2070 次.
作者给出一个统计图:
性能差距在 30% 左右. Minimal API 减少的组件性能拉开差距的可能是模型校验. 这里有效的模型校验负载很小, 所以差异不大. 在复杂的数据校验条件下, 性能差异会更加明显.
使用 BenchmarkDotNet 来标记 Minimal API
BenchmarkDotNet 是一个框架, 它可以度量编写的代码, 以及比较用不同版本库编译的项目, 或比较用不同框架编译的项目的性能.
该工具使用执行任务的时间, 使用的内存, 以及一些其他参数来进行度量.
我们的实例是非常简单的场景. 只需要比较使用相同的框架编写的两个应用响应的时间.
那么如何执行比较? 使用 HttpClient
对象调用为负载测试的方法即可.
BenchmarkDotNet 帮助你将执行的方法转换为测量基准, 并跟踪性能. 总之该库被大量使用, 很厉害就对了.
参考网站: https://benchmarkdotnet.org/
运行 BenchmarkDotNet
编写一个类, 其中的方法用于发送对应的请求. 如果有函数被标记为 [GlobalSetup]
则不进行计时, 以保证测试在稳定环境下运行.
操作步骤:
在
Program.cs
文件中注册所有的类.BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
这段代码会注册
Program
程序集, 该程序集需要实现所有需要评估的方法. 凡是被标记[Benchmark]
的方法会被一次次执行, 以建立平均执行时间.应用必须编译为 release 版本, 或在生产环境中运行
namespace DotNetBenchmarkRunners { [SimpleJob(RuntimeMoniker.Net60, baseline: true)] [JsonExporter] public class Performances { private readonly HttpClient clientMinimal = new HttpClient(); private readonly HttpClient clientControllers = new HttlClient(); private readonly ValidationData data = new ValidationData() { Id = 1, Description = "Performance" }; [GlobalSetup] public void Setup() { clientMinimal.BaseAddress = new Uri("https://localhost:7059"); clientCOntrollers.BaseAddress = new Uri("https://localhost:7149"); } [Benchmark] public async Task Minimal_Json_Get() => await clientMinimal.GetAsync("/jsons"); [Benchmark] public async Task Controller_Json_Get() => await clientCOntrollers.GetAsync("/jsons"); [Benchmark] public async Task Minimal_TextPlain_Get() => await clientMinimal.GetAsync("/plain-text"); [Benchmark] public async Task Controller_TextPlain_Get() => await clientControllers.GetAsync("/plain-text"); [Benchmark] public async Task Minimal_Validation_Post() => await clientMinimal.PostAsync("/validations", data); [Benchmark] public async Task Controller_Validation_Post() => await clientControllers.PostAsync("/validations", data); } public class ValidationData { public int Id { get; set; } public string Decription { get; set; } } }
然后运行程序
Minimal API
同上一个运行脚本
基于控制器的应用
同上一个运行脚本
运行测试用程序
dotnet .\DotNetBenchmarkRunners\bin\Release\net6.0\DotNetBenchmarkRunners.dll --filter *
然后获得执行报告
上述表格中, 误差 (Error) 表示测量误差, 标准差 (StdDev) 表示平均值的偏差