工廠參觀記:.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 號 ,ofollow,noindex" target="_blank">https://github.com/aspnet/HttpClientFactory
參觀 HttpClientFactory 之前先更多瞭解一下 HttpClient 的 Dispose 問題。
HttpClient 被 Dispose 時產生 TIME_WAIT 狀態的 tcp 連線的本質是在 HttpClient 被 Dispose 時,它所依賴的 HttpMessageHandler 也被 Dispose 了,管理 tcp 連線的正是 HttpMessageHandler ,.Net.Http/src/System/Net/Http/HttpMessageHandler.cs" rel="nofollow,noindex" target="_blank">HttpMessageHandler 是抽象類,落實到實際應用場景通常是SocketsHttpHandler/SocketsHttpHandler.cs" rel="nofollow,noindex" target="_blank">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 臭名昭著的問題。