ASP.NET Core高性能服务器HTTP.SYS

如果我们只需要将ASP.NET CORE应用部署到Windows环境下,并且希望获得更好的性能,那么我们选择的服务器类型应该是HTTP.SYS。Windows环境下任何针对HTTP的网络监听器/服务器在性能上都无法与HTTP.SYS比肩。

一、HTTP.SYS简介
二、MessagePump & UseHttpSys
三、HttpSysOptions.

一、HTTP.SYS简介

HTTP.SYS本质上就是一个HTTP/HTTPS监听器,它是Windows网络子系统的一部分,是一个在内核模式下运行的网络驱动。HTTP.SYS对应的驱动文件为“%WinDir\System32\drivers\http.sys”,不要小看这个只有1M多的文件,Windows系统针对HTTP的监听、接收、转发和响应大都依赖它。如图1所示,HTTP.SYS建立在Windows网络子系统针对TCPIP协议栈的驱动(TCPIP.SYS)之上,并为用户态运行的IIS提供基础的HTTP通信服务。前面我们使用的HttpListener也建立在HTTP.SYS上面。

ASP.NET Core高性能服务器HTTP.SYS
图1 HTTP.SYS

由于HTTP.SYS是在操作系统内核态运行,所以它提供的性能优势是其他在用户态运行的同类产品无法比拟的。由于它自身提供响应缓存,所以在缓存命中的情况下根本不需要与用户态进程进行交互。它还提供了请求队列(Request Queue),如果请求的目标进程(比如IIS的工作进程)处于活动状态,它可以直接将请求分它给它,否则请求会暂存于队列中等待目标进程来提取,这样的工作模式既减少了内核态与用户态之间的上下文切换,也确保请求不会丢失。HTTP.SYS还提供连接管理,流量限制,诊断日志等功能,并提供针对Kerberos的Windows认证。

由于HTTP.SYS是一个底层共享的网络驱动,它有效地解决了端口共享的问题。用户态进程会使用地址前缀(含端口号)“接入”HTTP.SYS,后者利用提供的地址前缀来转发请求,多个用户态进程只要保证提供的地址前缀不同就可以了,所以它们可以使用相同的端口号。端口共享使每个用户进程都可以使用标准的80/443端口。

二、MessagePump & UseHttpSys

基于HTTP.SYS的服务器体现为如下这个MessagePump类型,它内部使用一个HttpSysListener对象采用注册的监听地址接入HTTP.SYS。MessagePump提供针对HTTP 1.X、HTTP 2以及HTTPS的支持。对于Windows Server 2022和Windows 11,还支持HTTP 3。IWebHostBuilder接口如下这两个UseHttpSys扩展方法用来完成针对MessagePump的注册。  

internal class MessagePump : IServer, IDisposable
{
     internal HttpSysListener Listener { get; }
     public IFeatureCollection Features { get; }
     public MessagePump(
        IOptions<HttpSysOptions> options, 
        ILoggerFactory loggerFactory,
        IAuthenticationSchemeProvider authentication);
     public Task StartAsync<TContext>(
        IHttpApplication<TContext> application,
        CancellationToken cancellationToken);
     public Task StopAsync(CancellationToken cancellationToken);
     public void Dispose();
}

public static class WebHostBuilderHttpSysExtensions
{
    [SupportedOSPlatform("windows")]
    public static IWebHostBuilder UseHttpSys(
        this IWebHostBuilder hostBuilder);
    [SupportedOSPlatform("windows")]
    public static IWebHostBuilder UseHttpSys(
        this IWebHostBuilder hostBuilder,
        Action<HttpSysOptions> options);
}

如下所示的是在Minimal API下调用UseHttpSys注册MessagePump 服务器的例子。

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello World");
app.Run();

三、HttpSysOptions

在调用UseHttpSys扩展方法注册基于HTTP.SYS的MessagePump服务器的时候,我们可以利用提供的Action<HttpSysOptions>委托对相关的配置选项进行设置。HttpSysOptions的UrlPrefixes属性返回注册的监听地址前缀,但是最终是否这种直接注册到服务器上的监听器地址,取决于IServerAddressesFeature特性的PreferHostingUrls属性,这一点与KestrelServer是一致的。  

public class HttpSysOptions
{
    public UrlPrefixCollection UrlPrefixes { get; }
    public RequestQueueMode RequestQueueMode { get; set; }
    public string? RequestQueueName { get; set; }
    public long RequestQueueLimit { get; set; }
    public AuthenticationManager Authentication { get; }
    public ClientCertificateMethod ClientCertificateMethod { get; set; }
    public long? MaxConnections { get; set; }
    public long? MaxRequestBodySize { get; set; }
    public int MaxAccepts { get; set; }
    public Http503VerbosityLevel Http503Verbosity { get; set; }
    public TimeoutManager Timeouts { get; }
    public bool AllowSynchronousIO { get; set; }
    public bool EnableResponseCaching { get; set; }
    public bool ThrowWriteExceptions { get; set; }
    public bool UnsafePreferInlineScheduling { get; set; }
    public bool UseLatin1RequestHeaders { get; set; }
}

HTTP.SYS利用请求队列来存放待处理的请求,我们可以利用RequestQueueMode属性决定创建一个新的队列或者使用现有的队列。该属性类型为如下这个RequestQueueMode枚举,枚举项Create表示创建新的队列,Attach表示使用现有的以RequestQueueName属性命名的对象,如果该队列不存在会抛出异常。CreateOrAttach提供了一个折中方案,如果指定名称的队列不存在就创建一个以此命名的新队列。该属性的默认值为Create,RequestQueueName属性默认值为Null(代表匿名队列),RequestQueueLimit属性表示队列的容量,默认值为1000。HttpSysOptions承载的很多配置选项只会应用到新创建的请求队列上。  

public enum RequestQueueMode
{
    Create,
    Attach,
    CreateOrAttach
}

HttpSysOptions的Authentication属性返回一个AuthenticationManager对象,我们利用它完成针对认证的设置。我们可以利用Schemes属性设置认证方案,该属性默认为None。如果不允许匿名访问,可以将AllowAnonymous属性设为False。如果将AutomaticAuthentication属性返回True(默认值),认证用户将自动赋值给HttpContext上下文的User属性。AuthenticationDisplayName属性用来为认证方案提供一个显示名称。

public sealed class AuthenticationManager
{
    public AuthenticationSchemes Schemes { get; set; }
    public bool AllowAnonymous { get; set; }
    public bool AutomaticAuthentication { get; set; }
    public string? AuthenticationDisplayName { get; set; }
}

[Flags]
public enum AuthenticationSchemes
{
    None = 0x0,
    Digest = 0x1,
    Negotiate = 0x2,
    Ntlm = 0x4,
    Basic = 0x8,
    Anonymous = 0x8000,
    IntegratedWindowsAuthentication = 0x6
}

HTTPS站点可以要求提供证书来对其实施认证,HttpSysOptions的ClientCertificateMethod属性用于设置请求客户端证书的方式,该属性返回如下这个ClientCertificateMethod枚举。在.NET 5之前,客户端证书采用Renegotation的方式来提取的,Renegotiation是在已经建立的SSL/TLS连接上再次发起的一轮“协商握手”,这种方式对应AllowRenegotation枚举项。由于可能带来一些性能和死锁的问题,这种方式在.NET 5之后已经默认禁止了,目前默认的方式是创建SSL/TLS连接的初始阶段就提取该证书,这种方式对应AllowRenegotation枚举项,这也是ClientCertificateMethod属性的默认值。  

public enum ClientCertificateMethod
{
    NoCertificate,
    AllowCertificate,
    AllowRenegotation
}

HttpSysOptions的MaxConnections和MaxRequestBodySize属性分别表示最大连接数和请求主体内容的最大字节数,如果它们被设置为Null,意味着忽略对应的限制。这两个属性的默认值分别Null和30,000,000。MaxAccepts属性表示接受的最大并发请求,默认值为当前处理器数量的5倍。如果并发请求数量超过限流设置,后续请求会拒绝处理,此时服务器会直接回复一个状态码为503的响应,与此同时还会根据Http503Verbosity属性设置的等级作相应的处理。如果该属性值为Basic(默认值),当前TCP连接会重置,Full和Limitmed选项会影响响应的状态描述,前者返回详细的Reason Phrase,后者采用标准的“Service Unavailable”。  

public enum Http503VerbosityLevel
{
    Basic,
    Limited,
    Full
}

HttpSysOptions的Timeouts属性返回如下这个TimeoutManager对象,我们利用它完成各种超时设置,包括请求主体内容抵达时间(EntityBody)、读取请求主体内容时间(DrainEntityBody),请求在队列中存放的时间(RequestQueue)、连接闲置时间(IdleConnection)和解析请求报头时间(HeaderWait),这些超时时间默认都是两分钟。MinSendBytesPerSecond属性表示响应数据的最小发送率,默认为每秒150字节。  

public sealed class TimeoutManager
{
    public TimeSpan EntityBody { get; set; }
    public TimeSpan DrainEntityBody { get; set; }
    public TimeSpan RequestQueue { get; set; }
    public TimeSpan IdleConnection { get; set; }
    public TimeSpan HeaderWait { get; set; }
    public long MinSendBytesPerSecond { get; set; }
}

HttpSysOptions还定义了其他一系列属性。AllowSynchronousIO属性(默认为False)表示是否运行以同步IO的方式完成针对请求和响应主体内容的读写。EnableResponseCaching属性(默认为True)表示允许响应缓存。ThrowWriteExceptions属性(默认为False)表示因断开连接导致写入响应主体内容失败是否需要抛出异常。如果将UnsafePreferInlineScheduling(默认为False)设置为True,意味着会直接在读取请求的IO线程中执行后续的应用代码,否则我们编写的应用代码会分发到线程池中进行处理。这样可以通过避免线程切换减少单个请求的处理耗时,但是会对整体的吞吐量带来负面影响。UseLatin1RequestHeaders属性(默认为False)表示是否采用Latin1字符集(ISO-8859-1)对请求报头进行编码。