

























很多团队做性能优化时,第一反应是改 SQL、加缓存、扩机器。结果接口还是慢,而且慢得不稳定。
这类问题里,有一部分根因并不在业务代码,而在请求进入业务之前就已经产生了: 中间件顺序、重复序列化、过重日志、异常处理位置不当,都会把每个请求的固定成本悄悄抬高。
这篇文章我们不讲抽象概念,直接从一个真实工程场景出发,拆开 ASP.NET Core 请求管线,回答三个问题:
先看一个常见现象:
像商场收银台排队: 收银员速度没变,库存系统也没卡,但每位顾客在真正结账前都要先填两张表、复印一次小票、走一段绕路。单人多花 10 秒,队伍就会在高峰时段整体失控。
在 Web 服务里,这段“真正结账前的绕路”就是请求管线上的固定开销。
典型问题包括:
ASP.NET Core 启动时,IApplicationBuilder 会把你注册的中间件构造成一个 RequestDelegate 链。
关键点只有两个,但经常被忽略:
next(),从而短路后续链路。一个简化模型如下:
RequestDelegate app = context => Task.CompletedTask;
app = MiddlewareC(app);
app = MiddlewareB(app);
app = MiddlewareA(app);
// 实际执行顺序: A -> B -> C -> Endpoint -> C -> B -> A
这意味着:
UseRouting() 之前做基于 Endpoint 元数据的判断: 信息还没解析出来下面先看一个“看起来没问题,但成本偏高”的写法。
using System.Diagnostics;
using Microsoft.AspNetCore.HttpLogging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields = HttpLoggingFields.All;
});
var app = builder.Build();
app.UseHttpLogging(); // 对所有请求做重日志,静态文件也不例外
app.Use(async (ctx, next) =>
{
var sw = Stopwatch.StartNew();
await next();
sw.Stop();
// 每请求都写详细日志,高并发下会有明显写放大
app.Logger.LogInformation("{Path} took {Elapsed}ms", ctx.Request.Path, sw.Elapsed.TotalMilliseconds);
});
app.UseRouting();
app.MapGet("/ping", () => Results.Ok("pong"));
app.Run();
再看一版更适合线上场景的写法。
using System.Diagnostics;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiter =>
{
limiter.Window = TimeSpan.FromSeconds(1);
limiter.PermitLimit = 200;
limiter.QueueLimit = 100;
limiter.AutoReplenishment = true;
});
});
var app = builder.Build();
app.UseExceptionHandler("/error");
app.UseRouting();
app.UseRateLimiter();
// 仅对 API 路径做轻量计时,并且避免记录敏感/大体积内容
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/api"),
branch =>
{
branch.Use(async (ctx, next) =>
{
var start = Stopwatch.GetTimestamp();
await next();
var elapsedMs = (Stopwatch.GetTimestamp() - start) * 1000d / Stopwatch.Frequency;
if (elapsedMs > 50)
{
app.Logger.LogWarning(
"slow request {Method} {Path} {StatusCode} {ElapsedMs:F2}ms",
ctx.Request.Method,
ctx.Request.Path,
ctx.Response.StatusCode,
elapsedMs);
}
});
});
app.MapGet("/error", () => Results.Problem("unexpected error"));
app.MapGroup("/api")
.RequireRateLimiting("api")
.MapGet("/orders/{id:int}", (int id) => Results.Ok(new { id, status = "Paid" }));
app.MapGet("/health", () => Results.Ok("ok"));
app.Run();
这版改动的核心不是“少写几个中间件”,而是:
建议按职责分为三层:
这样做的好处是顺序稳定,审查成本低,新人也不容易“插错位置”。
全量日志在中高流量场景会迅速放大 I/O 成本,最后变成“为了观测而损失性能”。
至少建立这套最小验证闭环:
bombardier 或 wrkdotnet-counters monitor --process-id <pid>先拿到基线,再改顺序,再对比 P95/P99 和吞吐,不要只看平均值。
每次新增中间件前,团队至少回答 4 个问题:
ASP.NET Core 请求管线的优化,本质上是控制“每个请求必须支付的固定成本”。
IApplicationBuilder 到 RequestDelegate 的构建机制决定了中间件顺序就是性能策略。把顺序理顺、把观测做轻、把入口治理做实,通常比“盲目微优化业务代码”更快见效。
如果你线上也出现过“CPU 不高但接口发抖”的情况,建议先做两件事:
很多时候,系统的稳定性拐点,就在这两步里。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。