ASP.NET Core 请求管线性能与可观测性实战,为,如何优化?
摘要:很多团队做性能优化时,第一反应是改 SQL、加缓存、扩机器。结果接口还是慢,而且慢得不稳定。 这类问题里,有一部分根因并不在业务代码,而在请求进入业务之前就已经产生了: 中间件顺序、重复序列化、过重日志、异常处理位置不当,都会把每个请求的固
很多团队做性能优化时,第一反应是改 SQL、加缓存、扩机器。结果接口还是慢,而且慢得不稳定。
这类问题里,有一部分根因并不在业务代码,而在请求进入业务之前就已经产生了: 中间件顺序、重复序列化、过重日志、异常处理位置不当,都会把每个请求的固定成本悄悄抬高。
这篇文章我们不讲抽象概念,直接从一个真实工程场景出发,拆开 ASP.NET Core 请求管线,回答三个问题:
请求管线到底是怎么执行的
哪些中间件写法会稳定拉低吞吐
如何在不牺牲可观测性的前提下,把链路成本控制住
1. 问题背景: 为什么明明 CPU 不高,RT 却在抖
先看一个常见现象:
峰值时段 P95 从 35ms 涨到 90ms
CPU 只到 45%
数据库监控正常
线程池没有明显爆满
像商场收银台排队: 收银员速度没变,库存系统也没卡,但每位顾客在真正结账前都要先填两张表、复印一次小票、走一段绕路。单人多花 10 秒,队伍就会在高峰时段整体失控。
在 Web 服务里,这段“真正结账前的绕路”就是请求管线上的固定开销。
典型问题包括:
将高成本日志中间件放在链路最前面,且对所有请求都做完整 Body 记录
鉴权、异常处理、路由等中间件顺序错误,导致重复执行或额外分支判断
在中间件中做同步阻塞 I/O
将一些本该按采样写出的指标,变成了每请求都完整打点
2. 原理解析: IApplicationBuilder 如何变成 RequestDelegate
ASP.NET Core 启动时,IApplicationBuilder 会把你注册的中间件构造成一个 RequestDelegate 链。
关键点只有两个,但经常被忽略:
中间件按“注册顺序”进入,按“逆序”包裹执行。每个中间件把后续链路作为自己的 next,形成嵌套闭包。
任意中间件都可以不调用 next(),从而短路后续链路。
一个简化模型如下:
RequestDelegate app = context => Task.CompletedTask;
app = MiddlewareC(app);
app = MiddlewareB(app);
app = MiddlewareA(app);
// 实际执行顺序: A -> B -> C -> Endpoint -> C -> B -> A
这意味着:
前置中间件越重,所有请求都要付出这笔成本
末端短路逻辑的位置决定了多少中间件能被跳过
可观测性埋点放在不同层,看到的是不同粒度与成本
常见顺序误区
在 UseRouting() 之前做基于 Endpoint 元数据的判断: 信息还没解析出来
在全局异常处理中间件之后再包一层局部 try/catch: 导致异常路径重复记录
在静态资源请求也走完整业务日志链路: 无效开销
3. 示例代码: 从“能跑”到“跑得稳”
下面先看一个“看起来没问题,但成本偏高”的写法。
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();
再看一版更适合线上场景的写法。
