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