為什么這個API慢得離譜?從400ms到40ms的.NET 9性能優化實戰
和其他“簡單”的性能抱怨一樣,這個故事始于一個看似普通的性能問題。我們的.NET 9 Minimal API擁有所有時髦的特性——輕量級、快速啟動、簡潔的端點。但在生產環境中?平均延遲高達400毫秒。
這還發生在熱路徑上。一個GET請求。甚至沒有數據庫調用。
作為長期使用C#的開發者,我能感覺到事情不對勁。于是我打開性能分析器,準備深入調查。隨后便陷入了一系列“無害”中間件、不當使用的HttpClient和出乎意料的異步開銷的迷宮。
經過兩天殘酷的優化,我們將這個API的中位延遲降低到了約40毫秒。本文記錄了每一個關鍵的修復步驟、基準測試和代碼調整。
如果你在生產環境運行.NET 9 API,這可能是你這周最有價值的10分鐘閱讀。
第一步:先分析,別猜測
在深入代碼之前,重要提醒:不要盲目優化。我使用了以下工具:
? dotnet-trace:追蹤GC壓力和方法級性能
? dotnet-counters:監控CPU、分配率和請求吞吐量
? JetBrains Rider Profiler:深入分析調用棧和慢端點
這是我立即發現的問題:
[400ms總延遲]└── 120ms: 中間件(自定義日志、CORS、指標收集)└── 80ms: JSON序列化└── 60ms: HttpClient實例化(??)└── 40ms: GC暫停(分配密集型代碼)└── 100ms: 實際處理邏輯
現在我們來逐一解決。
第二步:無情削減中間件
我們喜歡可觀測性,但在Minimal API中,中間件的成本是真實存在的。
原來代碼:
app.Use(async (context, next) => {
var sw = Stopwatch.StartNew();
await next();
logger.LogInformation($"Request took {sw.ElapsedMilliseconds}ms");
});
app.UseCors(...);
app.Use(async (context, next) => {
metrics.Increment("api_requests");
await next();
});問題:每個Use都增加異步開銷,Stopwatch增加每次請求的分配
? 修復:
? 使用Middleware類替代內聯中間件(減少lambda捕獲)
? 通過OpenTelemetry將日志和指標推送到ActivityListener
? 內部API完全移除CORS
效果:節省約80ms
第三步:重用你的HttpClient
這個有點尷尬。在我們的處理程序中:
app.MapGet("/data", async () => {
using var client = new HttpClient();
var result = await client.GetStringAsync("https://internal-api/data");
return Results.Ok(result);
});經典新手錯誤:每次請求都銷毀HttpClient會殺死socket復用
? 修復:
var httpClient = new HttpClient(new SocketsHttpHandler {
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
});
app.MapGet("/data", async () => {
var result = await httpClient.GetStringAsync("https://internal-api/data");
return Results.Ok(result);
});或者更推薦使用IHttpClientFactory(如果需要策略)
效果:節省約60ms,負載下CPU降低12%
第四步:異步并不總是免費的
有個誤區:異步=快速。并非總是如此。
如果你的端點不需要等待I/O(比如從內存讀取),異步只會增加上下文切換和額外分配。
我們的“健康檢查”端點原來是這樣的:
app.MapGet("/health", async () => {
return Results.Ok("Healthy");
});? 修復:直接改為同步
app.MapGet("/health", () => Results.Ok("Healthy"));僅此一項就節省了約20ms(避免了異步狀態機)
第五步:精簡JSONSystem.Text.Json很快——但需要正確配置。
我們使用了默認設置,會序列化所有內容:包括null值和不需要的巨大DTO屬性。
? 修復:
? 使用[JsonIgnore]或創建精簡DTO
? 全局配置JSON:
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});? 序列化大型已知結構時使用源生成器
效果:節省約30ms,響應大小減少18%
第六步:啟用響應壓縮(但非必需)GZip有幫助——除非你大規模壓縮300字節的有效負載。
我們全局啟用了壓縮。這是個壞主意。對于小負載,這是CPU浪費。
? 修復:
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] {
"application/json"
});
});然后在邏輯中:
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/big"), builder =>
{
builder.UseResponseCompression();
});現在只壓縮大型JSON,跳過其他內容
效果:平均節省15-20ms
額外技巧:預熱JIT,緩存一切這在負載測試前并不明顯
? 添加[PreJIT]路由或在啟動時通過虛擬請求強制預熱
? 在單例或作用域服務中緩存查找和配置讀取
? 將靜態數據移入內存——如枚舉或只讀參考數據
app.Lifetime.ApplicationStarted.Register(() =>
{
_ = httpClient.GetStringAsync("https://internal-api/warmup");
});這些小調整又額外節省了10-15ms
最終基準測試:優化前后[圖片:優化前后性能對比圖表]
結語:性能不是偶然Minimal API很快——但前提是你要像對待F1賽車而不是家用轎車那樣對待它們。
每一層都很重要。每次分配都會累積。每個中間件、序列化器配置和異步調用都會引入摩擦。
事實上,這篇文章不是關于“巧妙技巧”,而是關于殘酷的專注和對抽象成本的尊重。
如果你正在大規模部署API,性能分析不是可選的。優化不是過早的。延遲就是一個功能特性。
TL;DR 檢查清單
? 移除或合并中間件
? 使用連接池重用HttpClient
? 在不需要的地方避免異步
? 精簡JSON輸出并使用源生成器
? 選擇性添加壓縮
? 預熱JIT并積極緩存
如果這對你有幫助,請點贊并與你的團隊分享。如果你在.NET Minimal API中遇到性能瓶頸,我很想知道你是如何解決的——或者你仍然卡在哪里。
































