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

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

時長 消息 nts esp ner gis token 抽象 cli

.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 性能的提升而解決,現在也不能確定。還需要進一步檢測驗證。

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