當 .NET API 在測試環境運作完美,卻在真實生產負載下崩潰時,問題幾乎從來不是單一災難性的錯誤。通常是許多微小的低效率所累積而成——這裡一個阻塞呼叫、那裡一個未追蹤的實體圖形、中介軟體管線配置錯誤——只有當並行請求量攀升至每分鐘數千次時才會顯現。ASP.NET Core 效能調校是系統性地找出並移除這些低效率的實踐,而不是等到故障發生後才反應。本文將逐一探討高流量 API 最重要的領域:非同步正確性、快取、資料存取、序列化、中介軟體排序、擴展,以及能告訴你實際該把時間花在哪裡的診斷工具。

正確使用非同步/等待

生產環境 ASP.NET Core 應用程式中最常見的效能殺手,就是在非同步程式碼上同步等待——使用 .Result.Wait() 呼叫任務而非等待它。這種模式在等待 I/O 繫結操作完成時會阻塞執行緒集區執行緒,而在負載下會觸發執行緒集區飢餓:執行緒集區必須緩慢擴張,新請求排隊等待可用執行緒,即使 CPU 使用率看起來很低,整個應用程式的延遲仍會大幅上升。原則上解決方法很簡單——在整個呼叫堆疊中全程等待——但這需要在整個程式碼庫中保持紀律,包括開發人員意想不到的地方,例如靜態建構函式、自訂中介軟體和背景服務迴圈。

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly IOrderRepository _orders;

    public OrdersController(IOrderRepository orders)
    {
        _orders = orders;
    }

    [HttpGet("{id:int}")]
    public async Task<ActionResult<OrderDto>> GetOrder(int id, CancellationToken cancellationToken)
    {
        var order = await _orders.GetByIdAsync(id, cancellationToken);
        if (order is null)
        {
            return NotFound();
        }

        return Ok(order.ToDto());
    }
}

Enter fullscreen mode Exit fullscreen mode

請注意 CancellationToken 參數,ASP.NET Core 會自動將其連接到請求的中止訊號。將它傳遞到資料存取和下游 HTTP 呼叫,意味著當用戶端中斷連線或請求逾時時,工作會立即停止,而不是繼續消耗執行緒集區和資料庫資源來產生沒人會收到的回應。另外要注意事件處理常式之外的 async void 方法——其中拋出的例外無法被呼叫者捕捉,並會導致處理程序當機,這是在負載下特別難以偵錯的失敗模式。

回應快取與輸出快取策略

不是每個請求都需要經過應用程式邏輯和資料庫。對於在短時間內向許多呼叫者傳回相同資料的端點——產品目錄、設定資料、公開內容——輸出快取可以完全消除大部分後端工作。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("Catalog", policy =>
        policy.Expire(TimeSpan.FromSeconds(60))
              .Tag("catalog"));
});

var app = builder.Build();
app.UseOutputCache();

app.MapGet("/api/products", async (ICatalogService catalog) =>
{
    var products = await catalog.GetAllAsync();
    return Results.Ok(products);
})
.CacheOutput("Catalog");

app.Run();

Enter fullscreen mode Exit fullscreen mode

上方顯示的基於標籤的失效機制很重要:當產品更新時,你可以只清除 catalog 標籤,而不是清空整個快取或等待固定到期時間,這能在保持資料合理新鮮的同時不犧牲命中率。即使你沒有使用伺服器端輸出快取,正確設定回應快取標頭(Cache-ControlETag)也值得,因為它們能讓瀏覽器、行動用戶端和 CDN 完全避免往返。

Minimal API 與控制器:負荷考量

Minimal API 的引入是為了減少傳統 MVC 控制器管線相關的繁瑣程式碼和每請求負荷。控制器會經過模型繫結、篩選器執行和動作呼叫機制,雖然靈活,但與將路由直接對應到委派的 Minimal API 端點相比,確實會產生可衡量的負荷。

話雖如此,對於典型的商業端點而言,差異遠小於人們的想像,因為資料庫呼叫或下游 HTTP 請求通常主導了總請求時間。更務實的方法是保留控制器提供價值的地方——模型繫結驗證、一致的篩選器管線、Swagger 整合、共享基底類別——並選擇性地對真正熱門路徑或新的輕量服務使用 Minimal API,此時減少的繁瑣程式碼才是淨收益。架構決策應由實際測量的瓶頸驅動,而不是基於某種風格總是較快的普遍信念。

連線集區與資料庫查詢最佳化

資料庫存取是大多數高流量 ASP.NET Core API 實際花費時間的地方。有兩個問題反覆出現:唯讀查詢上不必要的變更追蹤,以及單一邏輯請求悄然向資料庫發出數十或數百次往返的 N+1 查詢模式。

Entity Framework Core 預設會追蹤實體,以便偵測並將變更保存回資料庫。對於唯讀查詢——大多數 API 上的主要流量——這種追蹤負荷純屬浪費。呼叫 AsNoTracking() 會告知 EF Core 跳過建立變更追蹤快照。

public async Task<List<OrderSummaryDto>> GetRecentOrdersAsync(
    int customerId, CancellationToken cancellationToken)
{
    return await _dbContext.Orders
        .AsNoTracking()
        .Where(o => o.CustomerId == customerId).OrderByDescending(o => o.CreatedAt).Take(20).Select(o => new OrderSummaryDto
        {
            Id = o.Id,
            Total = o.Total,
            Status = o.Status
        })
        .ToListAsync(cancellationToken);
}

Enter fullscreen mode Exit fullscreen mode

如上所示,使用 Select() 直接投影到 DTO,除了避免追蹤負荷外還有額外好處:EF Core 可以將投影轉譯為僅選擇你實際需要的欄位的 SQL 查詢,而不是拉回整個實體列。N+1 問題更微妙,在本地使用小資料集測試時通常看不見——它發生在程式碼載入父實體集合,然後為每個實體惰性觸發個別查詢來載入相關子資料時。解決方法是使用 Include() 在單一查詢中預先載入相關資料,或者更好的是僅投影你需要的特定欄位。

在連線集區方面,請確保你的連線字串和 DbContext 生命週期已針對工作負載進行合理配置。AddDbContextPool 會跨請求重複使用 DbContext 執行個體,而不是每次都配置新的,這減少了高請求量下的配置壓力。

回應壓縮

對於傳回任何有意義大小 JSON 酬載的 API,啟用回應壓縮可減少透過網路傳送的位元組數。ASP.NET Core 的回應壓縮中介軟體支援 Gzip 和 Brotli,而對於典型的 JSON 內容,Brotli 通常能在相當或更好的壓縮速度下產生更小的酬載。

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = System.IO.Compression.CompressionLevel.Fastest;
});

Enter fullscreen mode Exit fullscreen mode

壓縮等級很重要:較高的壓縮等級會擠出更多位元組,但每個請求成本更多 CPU,在高並行度下可能成為瓶頸。如果你已經在反向代理或 CDN 層終止 TLS 並壓縮,請仔細檢查你是否重複壓縮。

有效率的 JSON 序列化

序列化基本上發生在 API 處理的每個請求上,這使其成為不成比例的重要調校目標。System.Text.Json 是 ASP.NET Core 中的預設序列化程式,開箱即用就很快,但其預設配置並不總是針對特定應用程式的酬載形狀進行調校。

builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
{
    options.SerializerOptions.DefaultIgnoreCondition =
        System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
    options.SerializerOptions.PropertyNamingPolicy =
        System.Text.Json.JsonNamingPolicy.CamelCase;
    options.SerializerOptions.NumberHandling =
        System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString;
});

Enter fullscreen mode Exit fullscreen mode

如上配置省略空值屬性,既縮小酬載大小,又減少具有許多選用欄位的物件序列化工作。對於非常高吞吐量的情境,請考慮透過 JsonSerializerContext 使用來源產生序列化,這會在編譯時產生序列化程式碼,而不是依賴執行階段反映。直接審核你的 DTO 而非序列化完整 EF Core 實體也值得:實體通常帶有導覽屬性和額外欄位,會膨脹酬載,並可能在序列化期間意外觸發惰性載入查詢。

中介軟體管線排序及其效能影響

ASP.NET Core 中的每個請求都會依註冊順序通過中介軟體管線,而該排序具有真正的效能後果,而不僅僅是正確性影響。在管線早期註冊的中介軟體會在每個請求上執行,包括最終會在更下游被拒絕或短路的請求,因此過早放置昂貴的中介軟體會浪費那些根本不需要它的請求的工作。驗證和授權中介軟體通常應在昂貴的商業邏輯中介軟體之前執行,以便未經驗證的請求能廉價且早早被拒絕。輸出快取通常應放置在足夠早的位置,以便在快取命中時完全短路管線。

健康檢查與負載平衡器考量

在水平擴展的部署中,你的負載平衡器或協調器依賴健康檢查端點來決定執行個體是否應繼續接收流量。

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database")
    .AddCheck<DependencyHealthCheck>("downstream-api");

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = _ => true
});

Enter fullscreen mode Exit fullscreen mode

如上所示,將存活檢查與就緒檢查分開在規模化時很重要:存活檢查應極為廉價,只需確認處理程序正在執行且有回應,因為協調器可能每隔幾秒就輪詢一次,以決定是否重新啟動執行個體。就緒檢查可以驗證下游相依性,但如果該檢查本身很慢,當在許多執行個體上頻繁輪詢時,它本身可能成為負荷來源。

水平擴展與無狀態設計

沒有任何處理程序內調校能取代流量成長時新增更多執行個體的能力,但水平擴展只有在應用程式實際上無狀態時才能順利運作。工作階段狀態、跨執行個體未同步的記憶體內快取,以及持有請求特定可變狀態的單體服務,都會破壞任何執行個體都能處理任何請求的假設。在擴展之前,請審核這些模式:工作階段資料應存放在 Redis 等分散式存放區中,而不是處理程序內記憶體中,而必須正好執行一次的背景工作應使用分散式鎖定或專用工作者,而不是依賴在每個執行個體上重複觸發的處理程序內計時器。

效能分析與診斷:找出瓶頸而非猜測

上述每種技術只有在應用於實際測量的瓶頸時才有用。當你需要了解 ASP.NET Core 處理程序在負載下的實際運作情況時,dotnet-trace 是值得首先使用的工具——它會擷取低負荷事件追蹤,特別擅長呈現執行緒集區飢餓、垃圾回收壓力,以及花費在特定方法呼叫上的時間。Application Insights 或同等 APM 工具扮演不同且互補的角色:跨完整請求生命週期的持續、低負荷監控,包括對資料庫和下游服務的相依性呼叫。對於微觀層級問題——此特定方法使用 AsNoTracking() 是否更快,此序列化配置是否真的減少配置——BenchmarkDotNet 是正確的工具,因為它正確處理了精確 .NET 基準測試的棘手機制:JIT 暖機、迭代之間的垃圾回收隔離,以及區分真實差異與雜訊的統計報告。

整合 ASP.NET Core 效能調校

這些技術都不是孤立存在的,一次將它們全部應用到尚未分析過的程式碼庫上並不是好策略。正確的方法是迭代式的:在代表性負載下使用真實診斷工具測量,找出實際瓶頸——這極有可能是不良的資料庫存取或同步等待非同步模式,而不是 JSON 序列化或中介軟體排序——修復該特定問題,然後再次測量。快取、壓縮和 Minimal API 採用是真正的槓桿,但只有在非同步正確性和有效率資料存取的基礎已到位後才最有價值;在 N+1 查詢問題之上疊加快取,只是意味著你在快取慢速回應而不是修復它。

如果你的團隊面臨擴展現有 API 的壓力,且內部沒有這些模式的深入經驗,通常更快且風險更低的方式是聘請有經驗的 ASP.NET Core 開發人員進行專注的效能調校合作,而不是透過生產事故來學習。