原文連結:blog.zhuliang.ltd/back-end/co…
從Task起,終於在.NET CORE 2.1 等到 HttpClient 的Pool。。。
HttpClient 日常使用及坑點:
在C#中,平時我們在使用HttpClient的時候,會將HttpClient包裹在using內部進行宣告和初始化,如:
using(var httpClient = new HttpClient())
{
//other codes
}
複製程式碼
至於為什麼?無外乎是:專案程式碼中就是這樣寫的,依葫蘆畫瓢/別人就是這樣用的/在微軟官方的ASP.NET教程中也是這麼幹的。
說的技術範點:當你使用繼承了IDisposable介面的物件時,建議在using程式碼塊中宣告和初始化,當using程式碼段執行完成後,會自動釋放該物件而不需要手動進行顯示Dispose操作。
但這裡,HttpClient這個物件有點特殊,雖然繼承了IDisposable介面,但它是可以被共享的(或者說可以被複用),且執行緒安全。從專案經驗來看,倒是建議在整個應用的生命週期內,複用HttpClient例項,而不是每次RPC請求的時候就例項化一個。(之前在優化公司一個web專案的時候,也曾經因為HttpClient載過一次坑,後面我會進行簡述。)
我們先來用個簡單的例子做下測試,看為什麼不要每次RPC請求都例項化一個HttpClient:
public class Program
{
static void Main(string[] args)
{
HttpAsync();
Console.WriteLine("Hello World!");
Console.Read();
}
public static async void HttpAsync()
{
for (int i = 0; i < 10; i++)
{
using (var client = new HttpClient())
{
var result = await client.GetAsync("http://www.baidu.com");
Console.WriteLine($"{i}:{result.StatusCode}");
}
}
}
}
複製程式碼
執行專案輸出結果後,通過netstate檢視下TCP連線情況:
- 雖然專案已經執行結束,但是連線依然存在,狀態為" TIME_WAIT"(繼續等待看是否還有延遲的包會傳輸過來。)。
預設在windows下,TIME_WAIT狀態將會使系統將會保持該連線 240s。
- 這裡也就引出了我上面說的載過的一次坑:在高併發的情況下,連線來不及釋放,socket被耗盡,耗盡之後就會出現喜聞樂見的一個錯誤:
#使用jemter壓測復現錯誤資訊:
Unable to connect to the remote serverSystem.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.
複製程式碼
說白話:就是會出現“各種套接字問題”。(碼WCF的童鞋可能更加記憶尤新,問題追根溯源都是換湯不換藥。)
熊廠裡面能夠搜尋出來的解決方法,基本都是“減少超時時間”,但人為減少了超時時間會出現各種莫名其妙的錯誤。且無法避免伺服器遲早崩潰的問題。
可以通過登錄檔進行修改預設值:[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay])
那麼如何處理這個問題?答案已經在上面說了,“複用HttpClient”即可。如:
public class Program
{
private static readonly HttpClient _client = new HttpClient();
static void Main(string[] args)
{
HttpAsync();
Console.WriteLine("Hello World!");
Console.Read();
}
public static async void HttpAsync()
{
for (int i = 0; i < 10; i++)
{
var result = await _client.GetAsync("http://www.baidu.com");
Console.WriteLine($"{i}:{result.StatusCode}");
}
}
}
複製程式碼
-
可以看到,原先10個連線變成了1給連線。(請不要在意兩次示例的目標IP不同---SLB導致的,都是百度的ip)
-
另外,因為複用了HttpClient,每次RPC請求的時候,實際上還節約了建立通道的時間,在效能壓測的時候也是很明顯的提升。曾經因為這一舉動,將web專案的TPS從單臺600瞬間提升到了2000+,頁面請求時間也從1-3s減少至100-300ms,甚是讓測試組小夥伴膜拜(當然也包括了一些業務程式碼的細調。),但知道箇中緣由後,一個小改動帶來的專案效能提升。。。會讓人上癮:)
-
至於如何建立一個靜態HttpClient進行復用,大家可以按專案實際來,如直接建立一個“全域性”靜態物件,或者通過各類DI框架來建立均可。
但這麼調整HttpClient的引用後,依然存在一些問題可能會影響到你的專案(尚未影響到我:P),如:
- 因為是複用的HttpClient,那麼一些公共的設定就沒辦法靈活的調整了,如請求頭的自定義。
- 因為HttpClient請求每個url時,會快取該url對應的主機ip,從而會導致DNS更新失效(TTL失效了)
那麼有沒有辦法解決HttpClient的這些個問題?直到我遇到了 HttpClientFactory,瞬間寫程式碼幸福感倍升,也感慨新時代的童鞋們真的太幸福了,老一輩踩的坑可以“完美”規避掉了。
HttpClientFactory優勢:
HttpClientFactory 是ASP.NET CORE 2.1中新增加的功能。
- “完美”解決了我多年來遇到的這些坑,可以更加專注於業務程式碼。
- HttpClientFacotry很高效,可以最大程度上節省系統socket。(“JUST USE IT AND FXXK SHUT UP”:P)
- Factory,顧名思義HttpClientFactory就是HttpClient的工廠,內部已經幫我們處理好了對HttpClient的管理,不需要我們人工進行物件釋放,同時,支援自定義請求頭,支援DNS更新等等等。
從微軟原始碼分析,HttpClient繼承自HttpMessageInvoker,而HttpMessageInvoker實質就是HttpClientHandler。
HttpClientFactory 建立的HttpClient,也即是HttpClientHandler,只是這些個HttpClient被放到了“池子”中,工廠每次在create的時候會自動判斷是新建還是複用。(預設生命週期為2min)
還理解不了的話,可以參考Task和Thread的關係,以前碰到HttpClient這個問題的時候,就一直在想微軟什麼時候官方出一個HttpClient的Factory,雖然時隔了這麼多年直到.NET CORE 2.1才出,但也很是興奮。
HttpClientFactory使用方法:
藉助ASP.NET CORE MVC,可以很方便的進行HttpClient的使用
- 在Startup.cs中進行註冊
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//other codes
services.AddHttpClient("client_1",config=> //這裡指定的name=client_1,可以方便我們後期服用該例項
{
config.BaseAddress= new Uri("http://client_1.com");
config.DefaultRequestHeaders.Add("header_1","header_1");
});
services.AddHttpClient("client_2",config=>
{
config.BaseAddress= new Uri("http://client_2.com");
config.DefaultRequestHeaders.Add("header_2","header_2");
});
services.AddHttpClient();
//other codes
services.AddMvc().AddFluentValidation();
}
}
複製程式碼
- 使用,這裡直接以controller為例,其他地方自行DI
public class TestController : ControllerBase
{
private readonly IHttpClientFactory _httpClient;
public TestController(IHttpClientFactory httpClient)
{
_httpClient = httpClient;
}
public async Task<ActionResult> Test()
{
var client = _httpClient.CreateClient("client_1"); //複用在Startup中定義的client_1的httpclient
var result = await client.GetStringAsync("/page1.html");
var client2 = _httpClient.CreateClient(); //新建一個HttpClient
var result2 = await client.GetStringAsync("http://www.site.com/XXX.html");
return null;
}
}
複製程式碼