1. 程式人生 > >HttpClient參觀記:.net core 2.2 對HttpClient到底做了什麼?

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

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

之前就一直苦於 HttpClient 的糟糕特性,大家耳熟能詳的 You are using HttpClient wrong
因為 HttpClient 實現了 IDisposable 如果用完就釋放,Tcp 連線也會被斷開,並且一個HttpClient 通常會建立很多個 Tcp 連線 。 Tcp 連線斷開的過程是有一個 Time_Wait 狀態的,因為要保證 Tcp 連線能夠斷開,以及防止斷開過程中還有資料包在傳送。這本身沒有毛病,但是如果你在使用 HttpClient 後就將其登出,並且同時處於高併發的情況下,那麼你的 Time_Wait 狀態的 Tcp 連線就會爆炸的增長,
他們佔用埠和資源而且還遲遲不消失,就像是在 嘲諷 你。所以臨時解決方式是使用靜態的 HttpClient 物件,No Dispose No Time_Wait

後來在 .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 的通道的末尾了。這是 摸出來的 鏈條 :

SocketsHttpHandle ----> HttpConnectionHandler/HttpAuthenticatedConnectionHandler ----> HttpConnectionPoolManager ----> HttpConnectionPoolManager

---> 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();
            }

可以看出,它把加鎖執行的內容減少了,將檢查車子的工作放到鎖外,沒想到這樣一個操作,在Linux中提升了60% 的效能。減少了小夥伴之間的等待時間。

那麼 靜態的HttpClient 和 HttpClientFactory 的二者使用,哪個效能更好呢? 我認為是前者,在高併發的實驗過程中也確實如此。因為 靜態HttpClient 只有一個訊息通道,從頭用到尾,這樣無疑是最高效的。而HttpClientFactory 需要銷燬 HttpMessageHandle 銷燬 HttpMessageHanlde 的過程是鏈條中的節點一個一個被摧毀的過程,直到最後的Tcp 連線池也被銷燬。在使用Service.AddHttpClient 時需要設定生存週期,這就是HttpMessageHandle 的生存時長,我認為應該將其設定的長一些,這樣HttpMessageHandle 或者叫做訊息通道 就可以多多的被重複利用。

當然我遇到的問題 是否真的是因為 HttpClient 效能的提升而解決,現在也不能確定。還需要進一步檢測驗證。