news 2026/10/15 15:27:46

ASP.NET Core Minimal APIs 的 TechEmpower 基准测试实现:Kestrel 配置与 Dapper 数据访问深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ASP.NET Core Minimal APIs 的 TechEmpower 基准测试实现:Kestrel 配置与 Dapper 数据访问深度解析
  • 性能测试
  • 测试
  • 后端
  • 开发者工具

【免费下载链接】FrameworkBenchmarks

Source for the TechEmpower Framework Benchmarks project

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载

导读

本文以 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.csDapper 数据访问:单行查询、多行查询、批量更新、Fortunes 列表
Database/BatchUpdateString.cs按批次大小惰性生成并缓存的批量 UPDATE SQL
Models/World结构体、Fortunerecord、ViewModel视图模型
Slices/RazorSlices 模板(Fortunes 页面)
AppSettings.cs绑定配置中的连接字符串
appsettings.json默认配置(连接字符串为占位符)
minimal.benchmarks.ymlCrank 压测场景定义(可选使用)

项目以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)
/fortunesFortune 列表 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;

实现要点:

  1. 先随机查询count行并收集Id;
  2. 对Id排序后,把重复 id 通过(ids[i] % 10000) + 1修正为唯一值,避免同一次更新中同一行被更新多次(TechEmpower 规定更新场景必须使用唯一 id);
  3. 为每行生成新的随机数;若新随机数恰好与旧值相同,则同样(randomNumber % 10000) + 1调整,保证"更新"语义成立;
  4. 最终用单条 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

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 15:27:46

异步Rust中的Sink:从trait协议到反压与实现详解

写异步 Rust 写久了你会发现&#xff0c;真正折磨人的往往不是 borrow checker&#xff0c;而是那些一眼看到底、用起来却到处是暗坑的 stream 概念。Stream 是读&#xff0c;Sink 是写&#xff0c;二者一进一出&#xff0c;组成了异步 IO 世界里最基础的骨架。futures-rs 里的…

作者头像 李华
网站建设 2026/10/15 15:26:57

优先级任务调度实战:PriorityBlockingQueue结合任务老化,高优低优两不误

“苍生我要救&#xff0c;钟离权我也要救”——这句话初看像一句仙侠剧情台词&#xff0c;但放在后端系统里&#xff0c;它其实就是我们经常遇到的两难局面&#xff1a;一边是海量普通任务在不断积压&#xff0c;一边是某个核心高优任务必须立即处理。很多同学在处理这种需求时…

作者头像 李华
网站建设 2026/10/15 15:26:01

Redis高可用架构:主从复制与哨兵模式深度实战

这里结合我这些年实际运维和搭建 Redis 高可用架构的经验&#xff0c;把主从复制和哨兵模式这套东西从头到尾捋一遍。标题听起来是基础内容&#xff0c;但真要在生产环境里把它玩明白&#xff0c;里面还是有不少容易被忽视的细节。我尽量把原理、配置、实战和排障都讲透&#x…

作者头像 李华
网站建设 2026/10/15 15:25:03

JMP统计软件操作指南:数据表、图表与质量分析工具详解

简介&#xff1a;JMP是由SAS公司开发的交互式统计分析软件&#xff0c;凭借图形化界面与丰富统计功能&#xff0c;常被视为比Minitab更强大的工具。这份培训讲义课件面向质量工程师、数据分析师及统计初学者&#xff0c;系统介绍JMP的数据处理、统计分析及质量改进方法。资源仅…

作者头像 李华
网站建设 2026/10/15 15:25:01

OPENMV数码管识别数据集与实时部署指南

简介&#xff1a;本资源是面向嵌入式视觉开发初学者与计算机视觉实践者的LED数码管数字识别专用数据集&#xff0c;专为OPENMV等低功耗微控制器平台设计&#xff0c;解决0–9数字图像采集、标注与模型训练的实际需求&#xff0c;适用于智能仪表读数、工业设备状态识别及教学实验…

作者头像 李华
网站建设 2026/10/15 15:24:51

注意力架构的KV压缩-从MHA到GQA与MQA的工程账

摘要 KV Cache 是长上下文推理显存的主要消耗者。运行时可以用分页、换出、压缩来管理它&#xff0c;更根本的办法是从架构源头减少 KV 的产生&#xff1a;标准多头注意力每个头都有独立键值&#xff0c;而 GQA 和 MQA 让多个查询头共享一份键值&#xff0c;KV 体量直接从根上…

作者头像 李华