HttpClient,該 using 還是 static?
14 | 28,298 |
很早之前看過一篇關於 HttpClient 生命週期的討論,當時沒什麼感覺,這陣子研究 NSwag,注意到 NSwag 產生的客戶端程式庫,HttpClient 預設被當作建構式參數,由外界傳入生命週期自理。 但一方面它又提供參數允許採用每次呼叫自動建立的可拋做法,並可選擇是否使用完畢要不要 Dispose(),這個設計引發我的好奇。在 NSwag Github 找到解釋:
由於 HttpClient 的最佳使用方式,江湖上尚無共識,一派主張因每個 HttpClient 都會建立新連線,為免耗盡 Socket 資源應儘量共用。另一派則認為長期使用現在連線,會遇到 DNS 異動將無法更新。 故設計人員將 HttpClient 何時建立及消滅設成選項,魚或熊掌讓開發者自己選。
這個發現喚起我的回憶,未來用 HttpClient 的機會愈來愈多,趁此釐清觀念也好,參考文章做了功課筆記備忘:
- You're using HttpClient wrong and it is destablizing your software by Simmon Timms
- Singleton HttpClient? Beware of this serious behaviour and how to fix it by Ali Kheyrollahi
- .NET HttpClient 的缺陷和文档错误让开发人员倍感沮丧 原文:Jonathan Allen 翻譯:谢丽
- 探討 HttpClient 可能的問題、HttpClient 無法反應 DNS 異動的解決方式 by Yowko's Notes (含實驗與效能評測)
問題怎麼發生的?
HttpClient 推出於 .NET 4.5,我是幾年前升級 .NET Core 才注意到它 HttpClient 頗有取代 WebClient 之勢( .NET Core 早期版本只有 HttpClient 沒有 WebClient),與 WebClient 相比具有支援 DNS 解析快取、Cookie / 身分驗證設定,可同時發送多個 Request、基於 HttpWebRequest/HttpWebResponse(易於測試)、 IO 相關方法均採非同步等優點,缺點是不支援 FTP。
由於 HttpClient 有實作 IDisposable 介面,加上它涉及網路連線等 Unmanaged 資源,依據 .NET 開發人員多年所受訓練的反射動作:遇到 IDiposable 就該用 using 包覆使用範圍,用畢要儘早釋放。
這樣的概念用在 SqlConnection/OracleConnection 等 IDbConnection 物件完全沒問題,原因是資料庫連線在底層會實作 Connection Pool,當 using 結束 IDbConnection 被 Dispose(),並不真的切斷資料庫連線,而是放進 Pool 裡待用,再遇到 new IDbConnetion() 再不另建新連線,而是從 Pool 取出資料庫連線重複使用。 因此,以下寫法沒啥問題:(排除可合併為單一查詢的可能)
for (var i = 0; i < 1000; i++)
{
using (var cn = new SqlConnection(cnStr))
{
//資料庫作業
}
}
既然 HttpClient 也是 IDisposable,背後有使用網路連線,所以 .NET 開發者依直覺寫成這樣也是合情合理滴:
for (var i = 0; i < 1000; i++)
{
using (var c = new HttpClient())
{
//使用 HttpClient 連上同一個網站作業
}
}
問題來了,由於 HttpClient 底層沒有實作 Connetion Pool 無法重複使用連線,以上程式將對同一個台網站開啟 1000 條 Socket 連線,而這 1000 條連線在 HttpClient Dispose() 後會以 TIME_WAIT 狀態繼續存活 240 秒才被真的釋放。當殘留連線數量龐大時,除了消耗記憶體資源,甚至可能用光可用 Socket Port 號碼,導致無法再建立新連線。TIME_WAIT 是 TCP/IP 協定的一部分,不該為此調整,重複使用連線才是治本之道。延伸閱讀:HttpClient 殘留連線與耗盡 Socket Port 實測
官方建議
好,HttpClient 不該 using,那該怎麼做?MSDN 文件有一段說明:
HttpClient 應該只建立一份並重複利用。為每次請求建立新的 HttpClient,重度使用下可能用光 Socket Port 導致 SocketException。以下是正確的寫法:
public class GoodController : ApiController
{
// OK
private static readonly HttpClient HttpClient;
static GoodController()
{
HttpClient = new HttpClient();
}
}
而 HttpClient 的以下方法是 Thread-Safe 的,在多執行緒下同時呼叫也不會打架,可安心服用:
- CancelPendingRequests
- DeleteAsync
- GetAsync
- GetByteArrayAsync
- GetStreamAsync
- GetStringAsync
- PostAsync
- PutAsync
- SendAsync
共用 HttpClient 副作用
共用靜態 HttpClient 可共用連線避免 TIME_WAIT 連線殘留,但這也衍生新問題 - 當 HttpClient 使用 xxx.yyy.zzz DNS 名稱連上網站,它會記憶 DNS 解析結果,但因缺乏失效機制快取將永久有效,若 DNS 記錄修改,必須重新啟動程序才會重新解析 DNS 取得新 IP。 在一些實務情境,程式可沒法說重啟就重啟,針對此有個簡單解法是對特定網站指定 ConnectionLeaseTimeout,強迫 .NET 在一段時間後關閉連線,下次重建連線將可重新解析 DNS。
var sp = ServicePointManager.FindServicePoint(new Uri("http://xxx.yyy.zzz"));
sp.ConnectionLeaseTimeout = 600*1000; // 10分鐘
實務應用上,若 HttpClient 用於 Web API 客戶端,Web API URL 是固定的,可在 HttpClient 建構時一併指定 ConnectionLeaseTimeout;若為動態傳入 URL 參數,則需每次存取網站前針對該 URL 設定。除此之外,還有 Dispose HttpClient、指定 HttpClient.DefaultRequestHeaders.ConnectionClose = true 等做法,也可克服 HttpClient 不反映 DNS 異動問題。延伸閱讀:HttpClient 無法反應 DNS 異動的解決方式
那 WebClient 呢?
首先,WebClient 已是昨日黃花,WebClient 官方文件已明白揭示:
We don't recommend that you use the WebClient class for new development. Instead, use the System.Net.Http.HttpClient class. 白話:寫新程式就別再 WebClient 惹 ,用 HttpClient 吧! (除非是 .NET 3.5/4.0)
WebClient 沒被設計成可共用,由 Headers、QueryString、Credentials 這些與特定連線相關的屬性可證明,而 Download*/Upload* 方法均非 Thread-Safe (多緒執行時會丟 NotSupportedException),使用時採隨用隨建,用完即拋策略就對了,每次建立 WebClient 都會開新連線,但,就這樣吧。
HttpClient 的進化
ASP.NET Core 2.1 / .NET 4.6 起推出 HttpClientFactory,藉由 Connection Pool 機制一舉改善前述問題。詳情可參考 Yowko 的文章:在 .NET Core 與 .NET Framework 上使用 HttpClientFactory
特此感謝 LienFa Huang 、fredli 分享。
結論
總結心得如下:
- WebClient 會被 HttpClient 取代,已不建議再用
- HttpClient 不適合採取隨用隨建用完即拋策略,可能殘留大量 TIME_WAIT 連線甚至導致 Socket Port 耗盡出錯
- HttpClient 正確使用方法為只建立一份,以 static 方式共用
- HttpClient 支援 DNS 解析快取,但永久有效無法反映 DNS 異動,此點可透過指定 ConnectionLeaseTimeout 或強制關閉連線克服
- .NET Framework 4.6+ / .NET Core 2.1+ 起新增 HttpClientFactory,透過 ConnectionPool 概念一舉解決連線共用及 DNS 異動更新問題
Study of HttpClient singleton issue.
Comments
# by fredli
用 HttpClientFactory,Pool與DNS問題都解決了 https://docs.microsoft.com/zh-tw/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
# by Jeffrey
to fredli,已補充於本文,感謝分享。
# by 胖賊
黑暗大大, 是不是筆誤啊? [HttpClient 的進化] ASP.NET 2.1 / .NET 4.6 應該是 ASP Core 2.1 / .NET 4.6 ??
# by Jeffrey
to 胖賊,身為以錯字見長的部落客,不經意間露個兩手也是很合情合理滴... Orz 感謝指正。
# by Victor
vy Yowko's Notes 應該是 by Yowko's Notes
# by Jeffrey
to Victor, 感謝指正,已修改。
# by xiang
黑大 請教一下 當GoodController被大量連線呼叫時, 此時HttpClient還是會不斷建立,這樣是否還是有問題? 還是說 HttpClient 直接用singleton方式注入 比較好?
# by Jeffrey
to xiang, 建立 HttpClient 的程式寫在靜態建構式 static GoodController(),從頭到尾只會建立一次。
# by JACKY
請問一下 static HttpClient 在asp.net 的action 中被使用 這樣URI 或 POST DATA 會被突然改掉嗎 例如有兩個request進來 一個要POST A網站 一個要POST B網站 目前我的CODE還是每一個都NEW client = new HttpClient (handler); response = client.PostAsJsonAsync (new Uri (wr.EXCUTE_URL), jsonobj).ConfigureAwait (false).GetAwaiter ( ).GetResult ( );
# by Jeffrey
to JACKY,HttpClient 的 Get**、Post** 方法基本上都是 Thread-Safe 的,同時叫用不會互相干擾,而依據官方建議,應避免每次建立新的以免拖累效能: HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. 參考:https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?redirectedfrom=MSDN&view=net-5.0#remarks
# by JACKY
TO Jeffrey 我的程式 其實是一個轉介 所以每次 HttpClient 所帶的HEADER與憑證都會不同 會不同根據前端的需求而改變 如果使用static的情況下 如果在還沒發動前不知到是否會有被置換的風險 有拜讀您的文章我知道在POST** GET** 都是 Thread-Safe
# by Jeffrey
to JACKY, 若 Header 與憑證的種類是有限的,例如有五種,我會先建好五個 HttpClient,依需求選用其中一種,類似 HttpFactory Named Client 的概念。( https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0#named-clients ) 若情境是每次的 Header 都不一樣,可以建立 HttpRequestMessage 每次客製: var request = new HttpRequestMessage(); request.Headers.Add("HeaderName", headerValue); var response = await client.SendAsync(request);
# by Ho.Chun
請問黑大,使用 HttpClient 時,有遇到過這個訊息嗎 ? 🤔 "System.IO.IOException: Unable to read data from the transport connection: 因為執行緒結束或應用程式要求,所以已中止 I/O 操作。" 我使用 static readonly HttpClient Client = new HttpClient() 然後並沒有使用任何 using() 和 .Dispose()
# by Jeffrey
to Ho.Chun, 由訊息判斷像是網路連線被外力切斷,直覺是偏網路面的問題,建議改用 PowerShell Invoke-WebRequest/curl、.NET WebClient 測試同一 URL 對照看看。