HttpClient參觀記:.net core 2.2 對HttpClient到底作了什麼?

.net core 於 10月17日發佈了 ASP.NET Core 2.2.0 -preview3,在這個版本中,我看到了一個很讓我驚喜的新特性:HTTP Client Performance Improvements ,並且在Linux上性能提高了60% !html

以前就一直苦於 HttpClient 的糟糕特性,你們耳熟能詳的 You are using HttpClient wrong
由於 HttpClient 實現了 IDisposable 若是用完就釋放,Tcp 鏈接也會被斷開,而且一個HttpClient 一般會創建不少個 Tcp 鏈接 。 Tcp 鏈接斷開的過程是有一個 Time_Wait 狀態的,由於要保證 Tcp 鏈接可以斷開,以及防止斷開過程當中還有數據包在傳送。這自己沒有毛病,可是若是你在使用 HttpClient 後就將其註銷,而且同時處於高併發的狀況下,那麼你的 Time_Wait 狀態的 Tcp 鏈接就會爆炸的增加,
他們佔用端口和資源並且還遲遲不消失,就像是在 嘲諷 你。因此臨時解決方式是使用靜態的 HttpClient 對象,No Dispose No Time_Waitweb

後來在 .net core2.1 中,引入了 HttpClientFactory 來解決這一問題。 HttpClientFactory 直接負責給 HttpClient 輸入 全新的 HttpMessageHandle 對象,而且管理 HttpMessageHandle 的生殺大權,這樣斷開 Tcp 鏈接的操做都由 HttpClientFactory 來用一種良好的機制去解決。安全

上面說了一堆,其實和主題關係不大。 由於我在實際生產環境中,不管使用靜態的 HttpClient 仍是使用 HttpClientFactory ,在高併發下的狀況下 Tcp 鏈接都陡然上升。直到我將 .net core 2.1 升級到 .net core 2.2 preview 問題彷佛奇蹟般的解決了。在介紹 .net core 2.2 如何提高 HttpClient 性能的時候,須要先簡單介紹下 HttpClient :併發

上面說到了 HttpMessageHandle ( 顧名思義:Http消息處理器 ) 它是一個抽象類,用來幹嗎的呢? 處理請求,又是顧名思義。 HttpClient 的發送請求函數 :SendAsync()函數

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption,
            CancellationToken cancellationToken)
        {
                  ....
        }

最後調用的就是 HttpMessageHandle 的 SendAsync 抽象函數。高併發

事實上經過閱讀源碼發現,幾乎全部繼承 HttpMessageHandle 的子類都有一個 HttpMessageHandle 類型的屬性 : _handle,而每一個子類的 SendAsync 函數都調用 _handle 的 SendAsync()。咱們知道在初始化一個 HttpClient 的時候或者使用 HttpClientFactory 建立一個HttpClient 的時候都須要新建 或者傳入一個 HttpMessageHandle 我把它叫作起始消息處理器。 很容易想像,HttpClient 的 SendAsync 函數是 一個 HttpMessageHandle 調用 下一個 HttpMessageHanlde 的SendAsync,而下一個 HttpMessageHandle 的SendAsync 是調用下下一個HttpMessageHandle 的 SendAsync 函數。每個HttpMessageHandle 都有其本身的職責。
層層嵌套,環環相扣,循環往復,生生不息,額不對,這樣下去會死循環。 直到它到達終點,也就是Tcp 鏈接創建,拋棄回收,發送請求的地方。 因此 HttpClient 的核心 就是由這些 HttpMessageHandle 扣起來,打形成一個 消息通道。 每一個請求都無一例外的 經過這個通道,找到它們的最終歸宿。性能

這其中的順序究竟是啥,我並不關心,我只關心其中一個 環:SocketsHttpHandle 由於.net core 2.2 就是從這個環開始動了手術刀,怎麼動的,按照上面的說法,咱們從 SocketHttpHandle 開始順藤摸瓜。其實顧名思義 SocketsHttpHandle 已經很接近 HttpClient 的通道的末尾了。這是 摸出來的 鏈條 :this

SocketsHttpHandle ----> HttpConnectionHandler/HttpAuthenticatedConnectionHandler ----> HttpConnectionPoolManager.net

---> HttpConnectionPool線程

最後一個加粗是有緣由的,由於咱們摸到尾巴了,HttpConnectionPool( 顧名思義 Http 鏈接 池) 已經不繼承 HttpMessageHandle 了 ,它就是咱們要找的終極,也是請求最終獲取鏈接的地方,也是.net core 2.2 在這條鏈中的 操刀的地方。

接下來就要隆重介紹 手術過程。手術的位置在哪裏? 就是獲取 Tcp 鏈接的函數。咱們看手術前的樣子,也就是System.Net.Http 4.3.3 版本的樣子。

List<CachedConnection> list = _idleConnections;
 lock (SyncObj)
            {
       
                while (list.Count > 0)
                {
                    CachedConnection cachedConnection = list[list.Count - 1];
                    HttpConnection conn = cachedConnection._connection;

                    list.RemoveAt(list.Count - 1);
                    if (cachedConnection.IsUsable(now, pooledConnectionLifetime, pooledConnectionIdleTimeout) &&
                        !conn.EnsureReadAheadAndPollRead())
                    {
    
                        if (NetEventSource.IsEnabled) conn.Trace("Found usable connection in pool.");
                        return new ValueTask<(HttpConnection, HttpResponseMessage)>((conn, null));
                    }

                    
                    if (NetEventSource.IsEnabled) conn.Trace("Found invalid connection in pool.");
                    conn.Dispose();
                }
                if (_associatedConnectionCount < _maxConnections)
                {
                    if (NetEventSource.IsEnabled) Trace("Creating new connection for pool.");
                    IncrementConnectionCountNoLock();
                    return WaitForCreatedConnectionAsync(CreateConnectionAsync(request, cancellationToken));
                }
                else
                {
                  
                    if (NetEventSource.IsEnabled) Trace("Limit reached.  Waiting to create new connection.");
                    var waiter = new ConnectionWaiter(this, request, cancellationToken);
                    EnqueueWaiter(waiter);
                    if (cancellationToken.CanBeCanceled)
                    {
                        
                        waiter._cancellationTokenRegistration = cancellationToken.Register(s =>
                        {
                            var innerWaiter = (ConnectionWaiter)s;
                            lock (innerWaiter._pool.SyncObj)
                            {
                                if (innerWaiter._pool.RemoveWaiterForCancellation(innerWaiter))
                                {
                                    bool canceled = innerWaiter.TrySetCanceled(innerWaiter._cancellationToken);
                                    Debug.Assert(canceled);
                                }
                            }
                        }, waiter);
                    }
                    return new ValueTask<(HttpConnection, HttpResponseMessage)>(waiter.Task);
                }

整個過程一目瞭然,list 是存放 閒置的Tcp鏈接 的鏈表,當一個 請求 千辛萬苦到了這裏,它要開始在鏈表的末尾開始 查找有沒有能夠用的 小跑車(Tcp鏈接),先把從小跑車 從 車庫(list)裏搬出來,而後檢查下動力系統,輪子啥的,若是發現壞了( 當前鏈接不可用 ,已經被服務端關閉的,或者有異常數據的 等等 ), 你須要用把這個壞的車給砸了( 銷燬Tcp鏈接 ),再去搬下一個小跑車。

若是能夠用,那麼很幸運,這個請求能夠馬上開着小跑車去飆車(發送數據)。若是這個車庫的車全是壞的或者一個車都沒有,那麼這個請求就要本身造一個小跑車 ( 創建新的TCP 鏈接 )。 這裏還有一個點,小跑車數量是有限制的。假如輪到你了,你發現車庫裏沒有車,你要造新車,可是系統顯示車子數量已經達到最大限制了,因此你就要等 小夥伴 ( 別的請求 ) 把 小跑車用完後開回來,或者等車庫裏的壞車 被別的小夥伴砸了。

整個過程看起來好像也挺高效的,可是請注意 lock (SyncObj) 上述全部操做的都被上鎖了,這些操做同時只能有一個小夥伴操做,這樣作的緣由固然是爲了安全,防止兩個請求同時用了同一個Tcp鏈接,這樣的話車子會被擠壞掉的。 因而小夥伴們都一個一個的排着隊。 試想,當咱們的請求不少不少的時候,隊伍很長很長,那每一個請求執行的時間久會變長。

那有沒有什麼方法能夠加快速度呢? 實際上是有的,事實上危險的操做 只是從 list 中去取車,和造新車。防止搶車和兩個小夥伴造了同一個車。因而手術後的樣子是這樣的:

while (true)
            {
                CachedConnection cachedConnection;
                lock (SyncObj)
                {
                    if (list.Count > 0)
                    {
                        cachedConnection = list[list.Count - 1];
                        list.RemoveAt(list.Count - 1);
                    }
                    else
                    {
      
                        if (_associatedConnectionCount < _maxConnections)
                        {
                    .
                            IncrementConnectionCountNoLock();
                            return new ValueTask<HttpConnection>((HttpConnection)null);
                        }
                        else
                        {
               
                            waiter = EnqueueWaiter();
                            break;
                        }
                 
                    }
                }

                HttpConnection conn = cachedConnection._connection;
                if (cachedConnection.IsUsable(now, pooledConnectionLifetime, pooledConnectionIdleTimeout) &&
                    !conn.EnsureReadAheadAndPollRead())
                {
                    if (NetEventSource.IsEnabled) conn.Trace("Found usable connection in pool.");
                    return new ValueTask<HttpConnection>(conn);
                }

                if (NetEventSource.IsEnabled) conn.Trace("Found invalid connection in pool.");
                conn.Dispose();
            }

能夠看出,它把加鎖執行的內容減小了,將檢查車子的工做放到鎖外。此外 將 lock...while 變成了while...lock 這樣有什麼影響呢:能夠減小線程之間的競爭,如評論所說,lock...while 是霸道的,一線程阻塞,萬線程等待競爭,而 while...lock 全部線程展開公平的競爭,你們持有鎖幾乎是相同的概率。

沒想到這樣一個操做,在Linux中提高了60% 的性能。減小了小夥伴之間的等待時間。

那麼 靜態的HttpClient 和 HttpClientFactory 的兩者使用,哪一個性能更好呢? 我認爲是前者,在高併發的實驗過程當中也確實如此。由於 靜態HttpClient 只有一個消息通道,從頭用到尾,這樣無疑是最高效的。而HttpClientFactory 須要銷燬 HttpMessageHandle 銷燬 HttpMessageHanlde 的過程是鏈條中的節點一個一個被摧毀的過程,直到最後的Tcp 鏈接池也被銷燬。可是 靜態HttpClient 有個DNS 解析沒法更新的硬傷,因此仍是應該 使用HttpClientFactory 。 在使用Service.AddHttpClient 時須要設置生存週期,這就是HttpMessageHandle 的生存時長,我認爲應該將其設置的長一些,這樣HttpMessageHandle 或者叫作消息通道 就能夠多多的被重複利用,由於HttpClientFactory 能夠給不一樣HttpClient實例注入相同的HttpMessageHandle

看完這篇文章 還能夠看下這篇文章的姊妹篇:工廠參觀記:.NET Core 中 HttpClientFactory 如何解決 HttpClient 臭名昭著的問題

固然我遇到的問題 是否真的是由於 HttpClient 性能的提高而解決,如今也不能肯定。還須要進一步檢測驗證。

相關文章
相關標籤/搜索