工厂参观记:.NET Core 中 HttpClientFactory 如何解决 HttpClient 臭名昭著的问题
在 .NET Framework 与 .NET Core 中 HttpClient 有个臭名昭著的问题,HttpClient 实现了 IDispose 接口,但当你 Dispose 它时,它不会立即关闭所使用的 tcp 连接,而是将 tcp 连接置为 TIME_WAIT 状态,240秒(4分钟)后才真正关闭连接。对于高并发的场景,比如每秒 1000 个请求,每个请求都用到 HttpClient ,4分钟内会堆积24万个 tcp 连接,这样的连接爆棚会拖垮服务器。为了避开这个坑,通常采用的变通方法是使用静态的 HttpClient ,但会带来另外一个臭名还没昭著的问题,当 HttpClient 请求的主机名对应的 IP 地址变更时,HttpClient 会蒙在鼓里毫不知情,除非重启应用程序。
为了彻底解决这两个问题,解救广大 .NET 开发人员,HttpClientFactory 在 .NET Core 2.1 中闪亮登场。那 HttpClientFactory 是如何解决问题的呢?让我们一起来参观一下这个有点特别的工厂。
工厂地址在微软市 github 区 aspnet 街 105584022 号 ,https://github.com/aspnet/HttpClientFactory
参观 HttpClientFactory 之前先更多了解一下 HttpClient 的 Dispose 问题。
HttpClient 被 Dispose 时产生 TIME_WAIT 状态的 tcp 连接的本质是在 HttpClient 被 Dispose 时,它所依赖的 HttpMessageHandler 也被 Dispose 了,管理 tcp 连接的正是 HttpMessageHandler ,HttpMessageHandler 是抽象类,落实到实际应用场景通常是 SocketsHttpHandler ,SocketsHttpHandler 通过 HttpConnectionPoolManager 管理着 HttpConnectionPool ,池中养着一堆 HttpConnection 对应的 tcp 连接,Dispose SocketsHttpHandler 影响的通常不是一个 tcp 连接,而是一池 tcp 连接,也就是会将整个池中的所有 tcp 连接都置于 TIME_WAIT 状态,并发量越大,池中的连接越多,Dispose 的杀伤力越大,大到可以会引发 socket exhaustion 。所以,要想解决这个问题就要减少 Dispose 操作,最极端的情况就是使用静态的 HttpClient ,永不 Dispose ,但如前所述这样做的副作用很大。
既要 Dispose HttpClient,又要控制好火候,这是解决这个棘手问题的关键,而 HttpClientFactory 也正是从这个角度出发打造出了一个可定时 Dispose 的工厂。
HttpClientFactory 创建 HttpClient 实例的主要代码如下:
public HttpClient CreateClient(string name)
{
//...
var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
var client = new HttpClient(entry.Handler, disposeHandler: false);
StartHandlerEntryTimer(entry);
//..
return client;
}
为了解决 HttpMessageHandler 的 Dispose 问题,HttpClientFactory 工厂设计制造出了一款新型 HttpMessageHandler —— LifetimeTrackingHttpMessageHandler ,一个有保质期的 HttpMessageHandler (默认是 2 分钟),新生产的 LifetimeTrackingHttpMessageHandler (之后简称 handler)会被放入 _activeHandlers ,过了保质期的 handler 会被放入 _expiredHandlers (有个 Timer 专门在 ExpiryTimer_Tick 回调方法中负责检查保质期), 而在 _expiredHandlers 中的 handler 们会被进一步检查,有个 CleanupTimer 专门在 CleanupTimer_Tick 回调方法中每隔10秒负责检查,进一步检查什么呢?检查这些过期产品(handler)是否可以作废(Dispose),怎么检查的?通过 WeakReference ,代码如下:
internal class ExpiredHandlerTrackingEntry
{
private readonly WeakReference _livenessTracker;
public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
{
Name = other.Name;
_livenessTracker = new WeakReference(other.Handler);
InnerHandler = other.Handler.InnerHandler;
}
public bool CanDispose => !_livenessTracker.IsAlive;
public HttpMessageHandler InnerHandler { get; }
public string Name { get; }
}
如果 _expiredHandlers 中的 handler 已经被 GC 回收(同时也说明对应的 HttpClient 也被 GC 回收),那就 Dispose 掉它。
HttpClientFactory 就是这样通过 2 个定时器有条不紊地控制着 Dispose HttpMessageHandler 释放 TCP 连接的火候,避免在同一时间 Dispose 太多 HttpMessageHandler 引发的 socket exhaustion 解决了 HttpClient 臭名昭著的问题。