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 从连接建立到请求进入应用层的完整链路,以及各层可调的关键参数。

Kestrel 请求链路与关键调优参数 客户端 TCP / QUIC 连接队列 backlog / 等待 accept MaxConcurrentConnections 传输层 Socket / QUIC TLS 握手 / 缓冲区 协议层 HTTP/1.1 · HTTP/2 · HTTP/3 MaxRequestBodySize 应用层:中间件 / Endpoint / MVC 线程池调度 · 异步 await 不阻塞 I/O 线程 关键参数与作用 MaxConcurrentConnections 限制同时打开的连接总数 MaxRequestBodySize 限制单个请求体字节数 HTTP/2 · HTTP/3 多路复用 / QUIC 降低握手延迟 调优原则:连接队列与并发上限匹配负载,请求体上限按业务实际设置,协议版本按客户端能力协商。 异步 I/O 保证连接数增长不导致线程数线性增长,是 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,由客户端协商选择。

HTTP/2 的 MaxStreamsPerConnection 默认 100,表示单连接最多并行 100 个流。若客户端并发请求数很高,可适当提高,但要注意每个流都会占用内存。

五、调优后的观察与判断

调优不是一次性的,需要结合指标验证。建议关注以下信号:连接队列是否持续积压(可通过 netstat 的 Recv-Q 观察)、线程池队列长度是否增长、请求延迟 P99 是否随并发上升而陡增、内存是否随连接数线性增长。若线程池队列持续非空,说明异步链路中存在同步阻塞调用,应优先排查业务代码而非继续调大参数。

参数默认值调优方向观察指标
MaxConcurrentConnectionsnull(不限)设为稳定并发 1.5~2 倍连接数、内存
MaxRequestBodySize约 30MB按业务下调至 1~10MB请求体拒绝率
RequestHeadersTimeout30 秒缩短至 10~15 秒慢连接占比
KeepAliveTimeout130 秒短连接缩短,长连接延长空闲连接数
ProtocolsHttp1AndHttp2按需加入 Http3协议协商分布

最后要强调:Kestrel 调优的收益有上限,它解决的是传输层和连接层的容量问题。如果应用层存在同步阻塞、数据库慢查询或锁竞争,再大的连接上限也无法提升吞吐。正确的顺序是先用异步 I/O 保证链路不阻塞,再用连接与协议参数匹配负载,最后回到业务代码消除真正的瓶颈。