ASP.NET Core 源码性能基准测试:用 Mvc/perf/benchmarkapps 套件对自定义 MVC 分支进行压测
【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore
导读
在 dotnet/aspnetcore 仓库的src/Mvc/perf/benchmarkapps目录下,维护着一组专门用于 MVC 组件性能基准测试(Benchmarking MVC)的独立 Web 应用。这些项目本身不是普通示例,而是被设计为配合 BenchmarksDriver 中三个被测应用的内部结构、benchmarks.json场景描述文件的字段语义,以及如何把自己分支上的改动通过一条基准测试命令在真实负载下验证性能回归。这对需要在本地 MVC 分支上做性能实验、又不愿等待官方 Benchmarks 仓库同步的开发者尤其有用。
benchmarkapps 的定位:为什么仓库里要"自带"压测应用
按照 src/Mvc/perf/benchmarkapps/README.md 的说明,这套项目的核心目的非常明确:
These projects assist in Benchmarking MVC. They make it easier to test local changes than having the App in the Benchmarks repo by letting us make changes in MVC branches and use the example commandline below to run the benchmarks against our branches.
也就是说,官方 Benchmarks 仓库与当前开发仓库之间存在"发布滞后"。如果被测应用只存在于 Benchmarks 仓库中,那么在 MVC 分支上做的修改就难以立刻用该应用验证。把被测应用直接放进 aspnetcore 仓库后,开发者可以在自己的 MVC 特性分支里同时修改框架代码与基准测试应用,随后直接用 BenchmarksDriver 对该分支发起压测——这实现了"改动即测、分支即基准"的开发闭环。
在目前的仓库布局中,这些应用位于 src/Mvc/perf/benchmarkapps,包含三个子项目(每个都是独立可运行的Microsoft.NET.Sdk.Web应用):
| 子项目 | 关注点 | 压测流量类型 |
|---|---|---|
| BasicApi | API/JSON 序列化、JWT 认证授权、EF Core 数据访问 | JSON 请求/响应 |
| BasicViews | Razor 视图引擎、HTML Helper/Tag Helper、静态文件 | HTML 请求/响应 + 表单 POST |
| RazorRendering | Razor Pages 视图渲染、组件渲染、依赖注入作用域数据 | 纯渲染页面 |
三者共享同一套 Directory.Build.props / Directory.Build.targets 与独立的 NuGet.config。
从源码看三个被测应用的内部设计
1. BasicApi:带 JWT 授权的 JSON API 压测目标
BasicApi 是一个模拟 PetStore 风格 REST API 的轻量应用,覆盖了 ASP.NET Core MVC 最核心的"Web API 热路径":模型绑定、JSON 序列化、路由、认证与授权、EF Core 查询与写入。
从 Controllers/PetController.cs 可以看到完整的接口与授权矩阵:
- 控制器级
[Route("/pet")]、[ApiController]、[Authorize("pet-store-reader")],默认所有操作都需要读取者令牌; GET /pet/{id}(FindById)——按主键查询并.Include()Category、Images、Tags 三个导航属性后返回Pet;GET /pet/anonymous/{id}(FindByIdWithoutToken)——标记[AllowAnonymous],用于对照无授权开销基线;GET /pet/findByStatus?status=available、GET /pet/findByCategory/{categoryId}、GET /pet/findByTags?tags=...——覆盖 QueryString 与路由值两种取值方式;POST /pet(AddPet)——[Authorize("pet-store-writer")],[FromBody]绑定并把实体写入数据库,返回CreatedAtRouteResult;POST /pet/add-pet(AddPetWithoutDb)——同样走认证授权与模型绑定但不落库,用于分离"框架开销"与"数据库开销";PUT、上传图片、删除等操作用throw new NotImplementedException()占位,从源码结构看是为了保证路由/授权规则完整而不必真正实现。
权限策略在 Startup.cs 中通过services.AddAuthorization定义:pet-store-reader与pet-store-writer两个策略均绑定JwtBearerDefaults.AuthenticationScheme、要求已认证用户并校验scope声明;AddMvcCore().AddAuthorization().AddNewtonsoftJson(...).AddDataAnnotations()提供了最小 MVC 核心管线(AddMvcCore而非AddMvc,避免引入视图等无关功能,保证 API 基线纯净)。
Controllers/TokenController.cs 是压测里的"发令牌"端点:内置reader@example.com(scope 含pet-store-reader)与writer@example.com(额外含pet-store-writer)两套内存中的 ClaimsIdentity,GET /token?username=writer@example.com会签发出一个 JWT 并原样返回,供压测脚本在后续请求中携带。
2. BasicViews:HTML Helper 与 Tag Helper 的视图渲染对照
BasicViews 走的是传统 MVC + Razor 视图路线。控制器 HomeController.cs 成对暴露了四个动作,恰好构成2×2 对照实验:
GET /Home/Index(Tag Helper 表单)与GET /Home/HtmlHelpers(HTML Helper 表单):两种不同视图技术栈的 GET 渲染;POST /Home/Index带[ValidateAntiForgeryToken],而POST /Home/IndexWithoutToken标[IgnoreAntiforgeryToken]:用于量化防伪令牌验证本身的性能成本。
视图分别位于 Views/Home/Index.cshtml(Tag Helper 版)与 Views/Home/HtmlHelpers.cshtml(HTML Helper 版)。模型 Person.cs 带有数据注解([StringLength(27, MinimumLength = 2)]、[Range(10, 54)]),确保 POST 路径在成功提交时走真实的数据注解校验;wwwroot下还提供了 site.css/site.js 及压缩版,配合 Startup.cs 的UseStaticFiles,可对静态文件中间件一并压测。
3. RazorRendering:纯渲染吞吐的极端场景
RazorRendering 是三者中最"单点"的:只为验证 Razor Pages/视图的纯渲染吞吐。从 Startup.cs 可见其把List<DataA>与List<DataB>注册为AddScoped服务,每个作用域会为页面组装约 100 条数据(DataA与DataB在 Data 中定义,含HtmlString、数值、DateTimeOffset等多样类型),从而覆盖 ViewData/ViewComponent 序列化、HtmlString 输出等渲染细节;页面入口在 Pages/Category/PageA.cshtml,其 Readme.md 只记录了一个 URL——/Category/PageA,即基准测试要命中的端点。
benchmarks.json:场景描述文件的字段语义
每个应用根目录下的benchmarks.json是 BenchmarksDriver 消费的场景定义。下面以 BasicApi/benchmarks.json 为例拆解:
| 字段 | 含义 | BasicApi 示例取值 |
|---|---|---|
Default | 所有场景共享的公共默认配置 | Client: "Wrk"、PresetHeaders: "Json"、ReadyStateText: "Application started." |
Default.Source | 被测源码来源:仓库、分支/提交、要启动的项目 | Repository=dotnet/aspnetcore.git、BranchOrCommit=main、Project=src/Mvc/perf/benchmarkapps/BasicApi/BasicApi.csproj |
BasicApi.GetToken | 无负载脚本场景:先打/token | Path: "/token"、Query: "?username=reader@example.com" |
BasicApi.GetUsingQueryString | 带 lua 脚本场景 | ClientProperties.Scripts指向getWithToken.lua,Path: "/pet/findByStatus" |
BasicApi.Post/PostWithoutDb | POST 场景 | 脚本换为postJsonWithToken.lua,路径分别落到/pet与/pet/add-pet |
注意ReadyStateText与 Startup.cs 中 Kestrel 启动输出 "Application started." 相对应——BenchmarksDriver 会等应用输出该文本后才认为"就绪并开始压测"。BasicViews/benchmarks.json 结构相同,只是把PresetHeaders换成Html、场景切换为 HtmlHelpers/TagHelpers 的 GET 与三种 POST 组合(含 token、忽略 token、无 token),可见字段设计完全一致,掌握一个即通全部。
用 BenchmarksDriver 对自己的 MVC 分支跑压测
官方使用步骤
按 src/Mvc/perf/benchmarkapps/README.md 的 Usage 说明,完整流程是:
- 把要测试的改动推送到一个 GitHub 分支(benchmarkapps 的设计前提是"被测应用与框架改动在同一个分支上");
- 准备 BenchmarksDriver:在本机克隆 aspnet/benchmarks 仓库,或安装全局 BenchmarksDriver 工具(该工具以 NuGet 包形式发布,包名为
BenchmarksDriver);若采用克隆方式,进入 BenchmarksDriver 项目目录; - 执行下面的命令行发起压测(命令中
{your branch}需替换为第 1 步的分支名):
benchmarks --server <server-endpoint> --client <client-endpoint> \ -j https://raw.githubusercontent.com/aspnet/MVC/{your branch}/benchmarkaps/BasicApi/BasicApi.json其中<server-endpoint>是运行被测应用(server)的机器端点,<client-endpoint>是运行压测负载客户端(client)的机器端点,两者可以是同一台或多台机器,由 BenchmarksDriver 统一调度;-j(job)参数给出benchmarks.json的raw 地址。驱动会读取该 JSON:拉取Source指定的仓库与分支、构建并启动Project指定的应用、就绪后按每个场景定义用 wrk(Client: "Wrk")发起请求。
对照当前仓库的修正说明
原文档写作时仓库还位于aspnet/MVC、路径写作benchmarkaps(拼写笔误)。而在当前仓库中,被测项目真实路径为仓库根下的src/Mvc/perf/benchmarkapps/...(与benchmarks.json的Source.Project字段一致),仓库即当前这个dotnet/aspnetcore。若你要引用自己 fork 的 raw 地址,请按真实路径与真实分支名替换{your branch}部分,并核对路径拼写。
压测脚本(.lua)与令牌流程:请求如何"带鉴权"打进来
benchmarks.json里ClientProperties.Scripts引用的 .lua 是 wrk 的扩展脚本。分析 getWithToken.lua 可以看到一套**"先取令牌、再发业务请求"**的握手逻辑:
init(args):构造对/token?username=writer@example.com的第一个空请求;maxRequests与username均可通过脚本参数覆盖(args[1]/args[2]);response(...):当首个请求返回200时,把响应体当作令牌写入wrk.headers["Authorization"] = "Bearer " .. token,随后wrk.format()生成真正要压的业务请求;若首个请求失败则打印错误并wrk.thread:stop();counter计数器让脚本可以用同一令牌持续请求到maxRequests(默认 -1,即整个测试全程)。
默认取writer@example.com是有意的:该身份同时具备pet-store-reader与pet-store-writer两个 scope(见 TokenController.cs 的静态构造),因此同一个脚本既能打[Authorize("pet-store-reader")]的 GET 接口,也能打[Authorize("pet-store-writer")]的 POST 接口。POST 变体 postJsonWithToken.lua 在此之上把wrk.method切到 POST、Content-Type设为application/json,并内置一段含category、images、tags、age、name、status的完整 Pet JSON 作为请求体——这正是 BasicApi 模型绑定与序列化路径要消化的真实负载。
BasicViews 一侧则配套了 post.lua(无令牌 POST)与 postWithToken.lua(携带防伪令牌的 POST),与控制器中Index/IndexWithoutToken的[ValidateAntiForgeryToken]/[IgnoreAntiforgeryToken]开关一一对应,从而单独测量表单验证码校验的成本。
数据层与运行时细节:为公平基准而做的设计
从源码看,三个应用在"被测环境"的公平性上做了不少刻意设计:
- 数据库类型可配置:BasicApi/Startup.cs 通过环境变量/命令行参数(
Configuration同时接了AddEnvironmentVariables与AddCommandLine)读取Database与ConnectionString,支持SQLite、PostgreSQL、SQL Server(以及非 .NET Framework 条件下的MySql);未指定时默认回落 SQLite(注释说明当 bench 用户指定 "None" 时该值并不会传给 Web 应用,因此应用侧按 SQLite 处理),从而无需外部数据库即可本地跑通,压测时才换用真实数据库; - 数据库生命周期自动清理:
Configure阶段CreateDatabaseTables先dbContext.Database.Migrate();若用 SQLite,注册ApplicationStopping时EnsureDeleted()删除.db文件以免残留,其他数据库则回滚到Migration.InitialDatabase清空表——保证每一轮压测起点一致; - Npgsql 连接串强制约束:使用 PostgreSQL 时若连接串缺少
No Reset On Close=true或未设Enlist=false,启动会直接抛出ArgumentException,这是为了避免连接重置/事务登记引入不可控开销; - DbContext 池化:统一使用
AddDbContextPool<T>,减少对象分配; - 服务器 GC 显式开启:BasicApi/runtimeconfig.template.json 与 BasicViews 的对应文件都设置了
"System.GC.Server": true,与服务端压测负载匹配; - 统一监听与主机配置:三个应用的
CreateHost都用UseKestrel().UseUrls("http://+:5000"),固定 5000 端口、以当前目录为内容根目录——既方便 BenchmarksDriver 定位,也便于本地dotnet run人工验证; - csproj 双态引用:BasicApi.csproj 与 BasicViews.csproj 在设置
BenchmarksTargetFramework时引用预编译的框架二进制(Benchmarks 侧构建用),未设置时引用仓库内源码项目(本地/CI 构建用),并注明 Pomelo MySQL 提供程序非强命名故SignAssembly=false;GenerateSqlScripts=true时还会通过条件编译输出建表/删表 SQL 脚本。
三种本地验证方式小结
- 直接本地运行:在任一应用目录执行
dotnet run(如src/Mvc/perf/benchmarkapps/BasicApi),应用默认监听http://+:5000,无需数据库即可体验,适合先确认路由与页面可用; - BenchmarksDriver 驱动:按上文 Usage 步骤,用
benchmarks --server ... --client ... -j <raw json 地址>对自己的分支发起标准化压测,场景由各应用的benchmarks.json定义; - 阅读对照源码:压测场景与代码路径一一对应(例如 BasicViews 的四组动作、BasicApi 的带/不带 DB 的 POST),可以从 benchmarks.json 反查 PetController.cs 等实现,理解每一条压测结果背后的框架开销来源。
需要说明的是:BenchmarksDriver 本身不在本仓库内(README 指明它由 aspnet/benchmarks 仓库或全局工具提供),本仓库提供的是"被测应用 + 场景描述 + 压测脚本"。若你希望做与官方一致的 MVC 性能回归测试,这套 benchmarkapps 就是推荐的起点——把改动与示例应用放进同一分支,再交给 BenchmarksDriver 去压即可。
【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考