- 性能测试
- 测试
- 后端
- 开发者工具
【免费下载链接】FrameworkBenchmarks
Source for the TechEmpower Framework Benchmarks project
导读
本文以 FrameworkBenchmarks 仓库中aspnetcore框架下的 Minimal APIs 实现为研究对象,讲解这套"符合惯用写法(idiomatic)"的 ASP.NET Core 基准应用是如何仅用约 60 行Program.cs完成 plaintext、json、db、queries、fortunes、updates 六个 TechEmpower 场景的。读完本文,你将掌握:如何用 Minimal APIs 路由映射六个基准端点、如何在基准场景下"关闭日志"与调整 Kestrel 参数、如何用 Dapper + Npgsql 实现单行查询/多行查询/批量更新,以及如何通过 RazorSlices 渲染 Fortunes 表格页面,并理解benchmark_config.json与 Dockerfile 中的运行环境配置。
说明:该实现目录的原始说明非常精简,本文的扩充内容均来自同一仓库内的源码、配置与构建脚本(相对路径以
frameworks/CSharp/aspnetcore/为前缀),可作为深入研读该实现的索引。
目录结构与项目定位
src/Minimal目录是一个独立的 ASP.NET Core 项目(Minimal.csproj),它被 benchmark_config.json 中的minimal测试项引用,作为框架aspnetcore下"Micro"分类的实现与默认的default(Platform)测试做对照。目录内文件分工如下:
| 文件 | 职责 |
|---|---|
| Program.cs | 应用入口:配置 Kestrel、关闭日志、注册Db服务、映射六个路由 |
| Database/Db.cs | Dapper 数据访问:单行查询、多行查询、批量更新、Fortunes 列表 |
| Database/BatchUpdateString.cs | 按批次大小惰性生成并缓存的批量 UPDATE SQL |
| Models/ | World结构体、Fortunerecord、ViewModel视图模型 |
| Slices/ | RazorSlices 模板(Fortunes 页面) |
| AppSettings.cs | 绑定配置中的连接字符串 |
| appsettings.json | 默认配置(连接字符串为占位符) |
| minimal.benchmarks.yml | Crank 压测场景定义(可选使用) |
项目以net10.0为目标框架(见 Minimal.csproj),引用了Npgsql 10.0.0、Dapper 2.1.66与RazorSlices 0.9.5三个包。从源码结构看,这是一套刻意保持"小而快"的实现:无 Controller、无 EF Core,数据库访问全部通过 Dapper 的轻量 API 完成。
应用入口:关闭日志与 Kestrel 配置
原始 README 提到两点实现细节:Logging is disabled(日志被关闭)与Database access uses Dapper(数据库访问使用 Dapper)。前者在 Program.cs 中对应两段关键代码:
var builder = WebApplication.CreateBuilder(args); // Disable logging as this is not required for the benchmark builder.Logging.ClearProviders(); builder.WebHost.ConfigureKestrel(options => { options.AllowSynchronousIO = true; });builder.Logging.ClearProviders()清空默认注册的日志 Provider,避免基准测试期间产生无关 I/O 开销;这正是 README 中 "Logging is disabled" 的实现证据。options.AllowSynchronousIO = true允许同步 I/O,因为部分请求处理路径会使用同步 Dapper 调用(见下文 Dapper 部分)。- 随后
new AppSettings()与builder.Configuration.Bind(appSettings)将配置中的ConnectionString绑定到强类型设置对象,并注册为单例:
var appSettings = new AppSettings(); builder.Configuration.Bind(appSettings); builder.Services.AddSingleton(new Db(appSettings));AppSettings只有单一属性ConnectionString(见 AppSettings.cs),默认的 appsettings.json 中连接字符串是占位提示:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionString": "This should be set in user secrets or environment config" }在基准运行环境中,这个占位值会被真实配置覆盖:构建镜像时 Dockerfile 会把 PostgreSQL 连接串配置复制为应用的appsettings.json(详见下文"镜像构建"一节)。由于ClearProviders()已关闭日志,即使配置文件里仍保留LogLevel节点,实际运行也不会输出日志。
六个基准场景的路由映射
Program.cs 以 Minimal APIs 的app.MapGet方式定义了所有端点,与 benchmark_config.json 中minimal测试项的plaintext_url、json_url、db_url、query_url、fortune_url、update_url一一对应:
app.MapGet("/plaintext", () => "Hello, World!"); app.MapGet("/plaintext/result", () => Results.Text("Hello, World!")); app.MapGet("/json", () => new { message = "Hello, World!" }); app.MapGet("/db", async (Db db) => await db.LoadSingleQueryRow()); app.MapGet("/fortunes", async (HttpContext context, Db db) => { var fortunes = await db.LoadFortunesRows(); var result = Results.Extensions.RazorSlice<Minimal.Slices.Fortunes, List<Fortune>>(fortunes); result.HtmlEncoder = htmlEncoder; return result; }); app.MapGet("/queries/{count?}", async (Db db, string? count) => await db.LoadMultipleQueriesRows(count)); app.MapGet("/updates/{count?}", async (Db db, string? count) => await db.LoadMultipleUpdatesRows(count));各端点的行为与配置对应关系如下:
| 端点 | 返回内容 | 对应配置项(minimal测试) |
|---|---|---|
/plaintext | 纯文本Hello, World! | plaintext_url |
/json | {"message":"Hello, World!"} | json_url |
/db | 单行 world 记录(JSON) | db_url |
/queries/{count?} | N 行 world 记录(JSON) | query_url(压测时请求/queries/20) |
/updates/{count?} | 更新后的 N 行 world 记录(JSON) | update_url(压测时请求/updates/20) |
/fortunes | Fortune 列表 HTML 页面 | fortune_url |
几个值得注意的设计点:
Db通过 DI 直接注入路由处理器,无需构造函数;这是 Minimal APIs 天然支持的模式。/queries/{count?}与/updates/{count?}的可选路由参数count会被传入Db方法进行解析与钳制。/fortunes使用RazorSlice渲染页面,并显式设置自定义的HtmlEncoder(编码器允许 BasicLatin、片假名、平假名及 EM DASH 字符通过,见 Program.cs),确保基准测试的 HTML 输出符合预期字符集。- 还有
/plaintext/result、/json/result等"result"变体端点,是供 minimal.benchmarks.yml 这类 Crank 场景对比Results.Text等不同写法时使用的,TechEmpower 主基准并不访问它们。
Dapper 数据访问层:连接、查询与更新
README 强调 "Database access uses Dapper"。核心实现在 Database/Db.cs,其构造器固定使用NpgsqlFactory.Instance创建连接,并通过ArgumentException.ThrowIfNullOrEmpty强制要求连接字符串非空:
public Db(AppSettings appSettings) { ArgumentException.ThrowIfNullOrEmpty(appSettings.ConnectionString); _dbProviderFactory = Npgsql.NpgsqlFactory.Instance; _connectionString = appSettings.ConnectionString; }单行查询/db
LoadSingleQueryRow创建连接后直接交给 Dapper 执行,注释明确指出"如果只做一件事就不需要显式 Open 连接,让 Dapper 处理":
public async Task<World> LoadSingleQueryRow() { await using var db = _dbProviderFactory.CreateConnection(); db!.ConnectionString = _connectionString; return await ReadSingleRow(db); } static Task<World> ReadSingleRow(DbConnection db) { return db.QueryFirstOrDefaultAsync<World>( "SELECT id, randomnumber FROM world WHERE id = @Id", new { Id = Random.Shared.Next(1, 10001) }); }- 查询使用参数化 SQL(
@Id),id 在[1, 10000]范围内随机生成,符合 TechEmpower 单查询场景规则。 - 结果映射到 World.cs 这个只含
Id与RandomNumber两个属性的struct。
多行查询/queries/{count?}
LoadMultipleQueriesRows先解析count,然后在一个连接上循环执行ReadSingleRow:
public async Task<World[]> LoadMultipleQueriesRows(string? parameter) { var count = ParseQueries(parameter); var results = new World[count]; await using var db = _dbProviderFactory.CreateConnection(); db!.ConnectionString = _connectionString; await db.OpenAsync(); for (var i = 0; i < count; i++) { results[i] = await ReadSingleRow(db); } return results; }count的解析与钳制逻辑在ParseQueries中:解析失败时回退为 1,成功时用Math.Clamp(queries, 1, 500)钳制到[1, 500],这与 TechEmpower 基准对查询数参数的要求一致:
private static int ParseQueries(string? parameter) { if (!int.TryParse(parameter, out int queries)) { queries = 1; } else { queries = Math.Clamp(queries, 1, 500); } return queries; }注意这里每个连接打开后复用执行多次查询(一次连接往返只服务一个应用请求),而非为每条查询新建连接。
批量更新/updates/{count?}
LoadMultipleUpdatesRows是六个场景中逻辑最复杂的部分,展示了如何用"单条 UPDATE + unnest 数组"实现批量更新:
for (var i = 0; i < count; i++) { results[i] = await ReadSingleRow(db); ids[i] = results[i].Id; } Array.Sort(ids); // Ensure unique ids by incrementing duplicates for (var i = 1; i < count; i++) if (ids[i] == ids[i - 1]) ids[i] = (ids[i] % 10000) + 1; for (var i = 0; i < count; i++) { var randomNumber = Random.Shared.Next(1, 10001); if (results[i].RandomNumber == randomNumber) { randomNumber = (randomNumber % 10000) + 1; } results[i].RandomNumber = randomNumber; numbers[i] = randomNumber; } var update = "UPDATE world w SET randomnumber = u.new_val FROM (SELECT unnest(@ids) as id, unnest(@newValues) as new_val) u WHERE w.id = u.id"; parameters["ids"] = ids; parameters["newValues"] = numbers; await db.ExecuteAsync(update, parameters); return results;实现要点:
- 先随机查询
count行并收集Id; - 对
Id排序后,把重复 id 通过(ids[i] % 10000) + 1修正为唯一值,避免同一次更新中同一行被更新多次(TechEmpower 规定更新场景必须使用唯一 id); - 为每行生成新的随机数;若新随机数恰好与旧值相同,则同样
(randomNumber % 10000) + 1调整,保证"更新"语义成立; - 最终用单条 SQL通过 PostgreSQL 的
unnest(@ids)/unnest(@newValues)数组展开完成批量 UPDATE,减少语句往返次数。
仓库中还有另一个批更新 SQL 生成器 BatchUpdateString.cs:它用VALUES (@Id_0, @Rn_0), ...方式按batchSize惰性生成 SQL 并缓存到string[](MaxBatch = 500),可作为对比实现参考。当前Db.cs采用 unnest 方案,两种都属于"减少往返、批量提交"的优化思路。
Fortunes 列表与 HTML 渲染
LoadFortunesRows读取全部 fortune 记录后,在应用内存中追加一条固定文案的额外记录,再按"Message 的序号比较"(string.CompareOrdinal,即 ordinal 比较)排序:
result = (await db.QueryAsync<Fortune>("SELECT id, message FROM fortune")).AsList(); result.Add(new Fortune(0, "Additional fortune added at request time.")); result.Sort(FortuneSortComparison);这与 TechEmpower fortunes 场景的固定规则一致(新增一条 fortune、按 message 排序、渲染 HTML 表格)。模板位于 Slices/Fortunes.cshtml,继承RazorSliceHttpResult<List<Fortune>>,用@foreach输出表格行,并通过@WriteNumber(item.Id, default, CultureInfo.InvariantCulture, false)以不变文化格式输出 id,保证输出稳定。Fortune是带位置参数的 record(见 Models/Fortune.cs):
public record Fortune(int Id, string Message);基准环境配置:连接字符串、端口与镜像构建
连接字符串
TechEmpower 容器化运行环境下,连接字符串来自 appsettings.postgresql.json(位于 aspnetcore 根目录,构建时会被复制为应用内的appsettings.json):
{ "ConnectionString": "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;SSL Mode=Disable;Maximum Pool Size=18;Enlist=false;Max Auto Prepare=4;Multiplexing=true;Write Coalescing Buffer Threshold Bytes=1000;", "Database": "postgresql" }其中几个参数对性能有直接影响:
Maximum Pool Size=18:连接池上限,与压测并发规模匹配;Enlist=false:不参与分布式事务登记,减少开销;Max Auto Prepare=4:Npgsql 自动预编译语句上限;Multiplexing=true:启用 Npgsql 多路复用,提升连接利用率;Write Coalescing Buffer Threshold Bytes=1000:写合并缓冲区阈值,合并小写入减少系统调用。
端口与环境变量
aspnetcore-minimal.dockerfile 展示了完整的两阶段构建:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /app COPY src/Minimal . RUN dotnet publish -c Release -o out FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime ENV URLS http://+:8080 ENV DOTNET_GCDynamicAdaptationMode=0 ENV DOTNET_ReadyToRun=0 ENV DOTNET_HillClimbing_Disable=1 WORKDIR /app COPY --from=build /app/out ./ COPY appsettings.postgresql.json ./appsettings.json EXPOSE 8080 ENTRYPOINT ["dotnet", "Minimal.dll"]- 监听端口
http://+:8080,与 benchmark_config.json 中minimal测试项的"port": 8080一致; DOTNET_GCDynamicAdaptationMode=0、DOTNET_ReadyToRun=0、DOTNET_HillClimbing_Disable=1用于稳定 JIT/GC 行为,避免动态自适应影响基准结果的一致性;COPY appsettings.postgresql.json ./appsettings.json将基准专用连接串覆盖进镜像,这就是上文"占位符被真实配置替换"的落地方式。
在 benchmark_config.json 中,minimal测试项被标记为:
"minimal": { "plaintext_url": "/plaintext", "json_url": "/json", "db_url": "/db", "query_url": "/queries/", "fortune_url": "/fortunes", "update_url": "/updates/", "port": 8080, "approach": "Realistic", "classification": "Micro", "database": "Postgres", "framework": "ASP.NET Core [Minimal APIs]", "language": "C#", "orm": "Micro", "platform": ".NET", "flavor": "CoreCLR", "webserver": "Kestrel", "os": "Linux", "database_os": "Linux", "display_name": "ASP.NET Core [Minimal APIs]", "notes": "", "versus": "aspnetcore" }classification = "Micro"、orm = "Micro"说明该实现属于轻量级 Minimal API 方案,且versus: "aspnetcore"表明其作为default(Platform、Raw ORM)实现的对照,用于在同一框架下对比不同写法/不同抽象层带来的性能差异。同目录下的 config.toml 也提供了等价的[minimal]段配置。
可选的 Crank 压测配置
minimal.benchmarks.yml 提供了基于 Crank 的本地/远程压测定义,其中包含了与 TechEmpower 一致的场景拆分(plaintext、json、fortunes、single_query、multiple_queries、updates及其_result变体),并声明:
variables: serverPort: 5000 jobs: minimal: source: repository: https://github.com/aspnet/benchmarks.git branchOrCommit: main project: src/BenchmarksApps/TechEmpower/Minimal/Minimal.csproj readyStateText: Application started. arguments: "--urls {{serverScheme}}://{{serverAddress}}:{{serverPort}}"readyStateText: Application started.对应 Program.cs 中应用启动时打印的Application started. Press Ctrl+C to shut down.;- 文件底部
sourceprofile 使用localFolder: .引用本地目录,便于在本地对当前源码直接跑压测。
结语
这份 README 虽然只有两行实现说明,但其背后的代码展示了一套完整的"惯用 Minimal APIs + Dapper"基准实现:通过ClearProviders()关闭日志、调整 Kestrel 同步 I/O 开关,用六个MapGet覆盖 TechEmpower 全部核心场景,在数据访问层以参数化查询、Math.Clamp参数钳制、unnest批量更新和 ordinal 排序满足基准规则,最终通过两阶段 Dockerfile 以固定的端口、连接池参数与运行时环境变量完成可复现的基准运行。对于希望在 ASP.NET Core 项目中实践"轻量路由 + 轻量 ORM"架构的开发者,该目录的源码是值得对照阅读的样板。
- 性能测试
- 测试
- 后端
- 开发者工具
【免费下载链接】FrameworkBenchmarks
Source for the TechEmpower Framework Benchmarks project
相关推荐
ASP.NET Core Kestrel 微基准测试(Microbenchmarks)运行指南:编译、筛选与配置详解
ASP.NET Core Kestrel 微基准测试(Microbenchmarks)运行指南:编译、筛选与配置详解 本篇指南以 src/Servers/Kes
后端Web框架bert-base-multilingual-cased:华为昇腾NPU优化的104语言BERT模型全面解析
bert base multilingual cased:华为昇腾NPU优化的104语言BERT模型全面解析 在当今人工智能飞速发展的时代, bert base
Minimal APIs 深度指南:在 ASP.NET Core 中构建轻量级高性能 HTTP 服务
Minimal APIs 深度指南:在 ASP.NET Core 中构建轻量级高性能 HTTP 服务 Minimal APIs 是 ASP.NET Core 提
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考