1. 程式人生 > >工廠參觀記:.NET Core 中 HttpClientFactory 如何解決 HttpClient 臭名昭著的問題

工廠參觀記:.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 臭名昭著的問題。