2. Kestrel服务器调优
Kestrel 是 ASP.NET Core 的默认跨平台 Web 服务器,它直接承载 HTTP/1.1、HTTP/2 与 HTTP/3 的传输层实现。很多团队在高并发场景下把性能问题归因于业务代码,但实际瓶颈往往出现在 Kestrel 的连接接收、请求解析和传输层参数上。理解 Kestrel 的异步 I/O 模型、连接队列和请求管道,是后续所有调优动作的前提。
一、异步 I/O 模型:为什么 Kestrel 能扛住高并发
Kestrel 基于 .NET 的 System.IO.Pipelines 和 SocketAsyncEventArgs 构建,采用全异步、事件驱动的 I/O 模型。每个连接不会独占一个线程,而是由少量 I/O 线程通过 epoll(Linux)或 IOCP(Windows)感知就绪事件,再把数据交给线程池处理。这意味着:连接数增长时,线程数不会线性增长,内存占用主要来自每个连接的缓冲区,而不是线程栈。
Kestrel 的请求管道大致分为四层:传输层(Socket/QUIC)→ 连接层(连接生命周期、TLS 握手)→ 协议层(HTTP/1.1、HTTP/2、HTTP/3 解析)→ 应用层(中间件与 MVC/Endpoint)。调优的本质,就是让这四层的参数与真实负载匹配。下图展示了 Kestrel 从连接建立到请求进入应用层的完整链路,以及各层可调的关键参数。
二、连接队列与并发上限
当客户端发起 TCP 连接时,操作系统会先把连接放入 accept 队列,Kestrel 再逐个取出并建立连接对象。如果 Kestrel 处理速度跟不上连接到达速度,队列会积压,超过 backlog 后新连接会被拒绝。Kestrel 的 Limits.MaxConcurrentConnections 控制的是已建立连接的总数上限,默认值为 null(不限制)。
在真实高并发场景中,不建议把 MaxConcurrentConnections 设得过小,否则大量连接会在队列中等待甚至被拒;也不建议完全不设,因为一旦遭遇连接洪泛,内存会被连接缓冲区耗尽。一个合理的做法是:先通过压测观察稳定状态下的并发连接数,再将其设置为该值的 1.5~2 倍作为保护上限。例如压测中稳定并发为 8000,可设置为 12000~16000。
builder.WebHost.ConfigureKestrel(options =>
{
// 限制同时打开的连接总数,防止连接洪泛耗尽内存
options.Limits.MaxConcurrentConnections = 12000;
// 限制单个请求体大小,默认约 30MB
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
// 连接建立后等待首个请求头的超时
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(15);
// 保持连接空闲超时
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
});
三、请求体大小与超时参数
MaxRequestBodySize 默认约为 30MB(30,000,000 字节)。对于只接收 JSON 小请求的 API,把它降到 1~10MB 可以显著减少恶意大包攻击面;对于文件上传接口,则应按业务上限单独放宽,而不是全局放大。注意该参数对 HTTP/2 和 HTTP/3 同样生效,且可以在单个 Endpoint 上通过 IHttpMaxRequestBodySizeFeature 覆盖。
与请求体相关的还有 RequestHeadersTimeout 和 KeepAliveTimeout。前者防止慢速攻击(Slowloris)长期占用连接,后者决定空闲连接何时被回收。高并发短连接场景可适当缩短 KeepAliveTimeout,让连接更快释放;长连接推送场景则应延长。
四、HTTP/2 与 HTTP/3 的传输层配置
HTTP/2 的核心优势是多路复用:一个 TCP 连接上可并行处理多个请求,避免了 HTTP/1.1 的队头阻塞(应用层)。启用方式是在监听端点声明协议:
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000, listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2;
});
// HTTP/3 基于 QUIC,需要同时启用 HTTP/1.1 和 HTTP/2 作为回退
options.ListenAnyIP(5001, listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listen.UseHttps();
});
});
HTTP/3 使用 QUIC(基于 UDP),把 TLS 握手和传输握手合并,显著降低首连接延迟,并且天然解决 TCP 层的队头阻塞。但它对网络环境有要求:UDP 端口需放行,部分企业防火墙会拦截 UDP。因此生产环境通常同时开启 HTTP/1.1、HTTP/2 和 HTTP/3,由客户端协商选择。
MaxStreamsPerConnection 默认 100,表示单连接最多并行 100 个流。若客户端并发请求数很高,可适当提高,但要注意每个流都会占用内存。五、调优后的观察与判断
调优不是一次性的,需要结合指标验证。建议关注以下信号:连接队列是否持续积压(可通过 netstat 的 Recv-Q 观察)、线程池队列长度是否增长、请求延迟 P99 是否随并发上升而陡增、内存是否随连接数线性增长。若线程池队列持续非空,说明异步链路中存在同步阻塞调用,应优先排查业务代码而非继续调大参数。
| 参数 | 默认值 | 调优方向 | 观察指标 |
|---|---|---|---|
| MaxConcurrentConnections | null(不限) | 设为稳定并发 1.5~2 倍 | 连接数、内存 |
| MaxRequestBodySize | 约 30MB | 按业务下调至 1~10MB | 请求体拒绝率 |
| RequestHeadersTimeout | 30 秒 | 缩短至 10~15 秒 | 慢连接占比 |
| KeepAliveTimeout | 130 秒 | 短连接缩短,长连接延长 | 空闲连接数 |
| Protocols | Http1AndHttp2 | 按需加入 Http3 | 协议协商分布 |
最后要强调:Kestrel 调优的收益有上限,它解决的是传输层和连接层的容量问题。如果应用层存在同步阻塞、数据库慢查询或锁竞争,再大的连接上限也无法提升吞吐。正确的顺序是先用异步 I/O 保证链路不阻塞,再用连接与协议参数匹配负载,最后回到业务代码消除真正的瓶颈。