在項目中使用HttpClient多是很廣泛,尤爲在當下微服務大火形勢下,若是服務之間是http調用就少不了跟http客戶端找交道.因爲項目用戶規模不一樣以及應用場景不一樣,不少時候可能不須要特別處理也.然而在一些高併發場景下必需要作一些優化.html
項目是快遞公司的快件軌跡查詢項目,目前平均每小時調用量千萬級別.軌跡查詢以Oracle爲主要數據源,Mongodb爲備用,當Oracle不可用時,數據源切換到Mongodb.今年菜鳥團隊加入後,主要數據遷移到了阿里雲上,以Hbase爲主要存儲.其中Hbase數據查詢服務由數據解析組以Http方式提供.原有Mongodb棄用,雲上數據源變爲主數據源,Oracle做爲備用.當數據源切換之後,主要的調用方式也就變成了http方式.在第10月初第一輪雙11壓測試跑上,qps不達標.固然這個問題很好定位,由於十一假之間軌跡域組內已經進行過試跑,當時查的是oracle.十一假期回來後,只有這一處明顯的改動,很容易定位到問題出如今調用上.但具體是雲上Hbase慢,仍是網絡傳輸問題(Hbase是阿里雲上的服務,軌跡查詢項目部署在IDC機房).經過雲服務,解析組和網絡運維的配合,肯定問題出如今應用程序上.在Http服務調用處打日誌記錄,發現如下問題:web
能夠看到每隔一段時間,就會有很多請求的耗時明顯比其它的要高.編程
致使這種狀況可能多是HttpClient反覆建立銷燬形成引發來銷,首先憑經驗多是對HttpClient進行了Dispose操做(Using(HttpClient client=new HttpClient){...})json
若是你裝了一些第三方插件,當你寫的HttpClient沒有被Using包圍的時候會給出重構建議,建議加上Using或者手動dispose.然而實際中是否要dispose還要視狀況而定,對於通常項目你們的感受多是不加也沒有大問題,加了也還ok.可是實際項目中,通常不建議反覆從新建立這個對象,關於HttpClient是否須要Dispose請看這裏api
在對這個問題的答案裏,提問者指出微軟的一些示例也是沒有使用using的.下面一個比較熱的回答指出HttpClient生命週期應該和應用程序生命週期一致,只要應用程序須要Http請求,就不該用把它Dispose掉.下面的一個仍然相對比較熱的回答指出通常地,帶有Dispose方法的對象都應當被dispose掉,可是HttpClient是一個例外.數組
固然以上只是結合本身的經驗對一個你們均可能比較困惑的問題給出一個建議,實際上對於通常的項目,用仍是不用Dispose都不會形成很大問題.本文中上面提到的問題跟HttpClient也沒有關係,由於程序中使用的Http客戶端是基於HttpWebRequest
封裝的.服務器
問題排查及優化網絡
通過查詢相關資料以及同行的經驗分享(這篇文章給了很大啓發)併發
查看代碼,request.KeepAlive = true;查詢微軟相關文檔,這個屬性實際上是設置一個'Keep-alive'請求header,當時同事封裝Http客戶端的場景現場無從得知,然而對於本文中提到的場景,因爲每次請求的都是同一個接口,所以保持持續鏈接顯然可以減小反覆建立tcp鏈接的開銷.所以註釋掉這一行再發布測試,以上問題便不復出現了!oracle
固然實際中作的優化毫不僅僅是這一點,若是僅僅是這樣,一句話就可以說完了,你們都記住之後就這樣作就Ok了.實際上還參考了很多你們在實際項目中的經驗或者坑.下面把整個HttpClient代碼貼出來,下面再對關鍵部分進行說明.
public static string Request(string requestUrl, string requestData, out bool isSuccess, string contentType = "application/x-www-form-urlencoded;charset=utf8") { string apiResult = ""; isSuccess = false; if (string.IsNullOrEmpty(requestData)) { return apiResult; } HttpWebRequest request = null; HttpWebResponse response = null; try { byte[] buffer = Encoding.UTF8.GetBytes(requestData); request = WebRequest.Create($"{requestUrl}") as HttpWebRequest; request.ContentType = "application/json"; request.Method = "POST"; request.ContentLength = buffer.Length; request.Timeout =200; request.ReadWriteTimeout = Const.HttpClientReadWriteTimeout request.ServicePoint.Expect100Continue = false; request.ServicePoint.UseNagleAlgorithm = false; request.ServicePoint.ConnectionLimit = 2000 request.AllowWriteStreamBuffering = false; request.Proxy = null; using (var stream = request.GetRequestStream()) { stream.Write(buffer, 0, buffer.Length); } using (response = (HttpWebResponse)request.GetResponse()) { string encoding = response.ContentEncoding; using (var stream = response.GetResponseStream()) { if (string.IsNullOrEmpty(encoding) || encoding.Length < 1) { encoding = "UTF-8"; //默認編碼 } if (stream != null) { using (StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(encoding))) { apiResult = reader.ReadToEnd(); //byte[] bty = stream.ReadBytes(); //apiResult = Encoding.UTF8.GetString(bty); } } else { throw new Exception("響應流爲null!"); } } } isSuccess = true; } catch (Exception err) { isSuccess = false; LogUtilities.WriteException(err); } finally { response?.Close(); request?.Abort(); } return apiResult; }
首先是TimeOut
問題,不只僅是在高併發場景下,實際項目中建議不論是任何場景都要設置它的值.在HttpWebRequest對象中,它的默認值是100000
毫秒,也就是100秒.若是服務端出現問題,默認設置將會形成嚴重阻塞,對於普通項目也會嚴重影響用戶體驗.返回失敗讓用戶重試也比這樣長時間等待體驗要好.
ReadWriteTimeout不少朋友可能沒有接觸過這個屬性,尤爲是使用.net 4.5裏HttpClient對象的朋友.有過Socket編程經驗的朋友可能會知道,socket鏈接有鏈接超時時間和傳輸超時時間,這裏的ReadWriteTimeout
相似於Socket編程裏的傳輸超時時間.從字面意思上看,就是讀寫超時時間,防止數據量過大或者網絡問題致使流傳入很長時間都沒法完成.固然在通常場景下你們能夠徹底不理會它,若是因爲網絡緣由形成讀寫超時也頗有可能形成鏈接超時.這裏之因此設置這個值是因爲實際業務場景決定的.你們可能已經看到,以上代碼對於ReadWriteTimeout
的設置並不像Timeout
同樣設置爲一個固定值,而是放在了一個Const類中,它其實是讀取一個配置,根據配置動態決定值的大小.實際中的場景是這樣的,因爲壓測環境沒法徹底模擬真實的用戶訪問場景.壓測的時候都是使用單個單號進行軌跡查詢,可是實際業務中容許用戶一次請求查詢最多多達數百個單號.一個單號的軌跡記錄通常都是幾十KB大小,若是請求的單號數量過多數量量就會極大增長長,查詢時間和傳輸時間都會極大增長,爲了保證雙11期間大多數用戶能正常訪問,必要時會把這個時間設置的很小(默認是3000毫秒),讓單次查詢量大的用戶快速失敗.
以上只是一種備用方案,不得不認可,既然系統容許一次查詢多個單號,所以在用戶在沒有達到上限以前全部的請求都是合法的,也是應該予以支持的,所以以上作法實際上有損用戶體驗的,然而系統的資源是有限的,要必要的時候只能犧牲特殊用戶的利益,保證絕大多數用戶的利益.雙11已經渡過,實際中雙11也沒有改動以上配置的值,可是作爲風險防範增長動態配置是必要的.
這裏再多差一下嘴,就是關於
ContentLength
它的值是能夠不設置的,不設置時程序會自動計算,可是設置的時候必定要設置字節數組的長度,而不是字符串的長度,由於包含中文時,根據編碼規則的不一樣,中文字符可能佔用兩個字節或者更長字節長度.
關於 request.ServicePoint.Expect100Continue = false; request.ServicePoint.UseNagleAlgorithm = false;
這兩項筆者也不是特別清楚,看了相關文檔也沒有特別明白,還請了解的朋友指點,你們共同窗習進步.
request.ServicePoint.ConnectionLimit = 2000
是設置最大的鏈接數,很多朋友是把這個數值設置爲65536,實際上單臺服務器web併發鏈接遠太不到這個數值.這裏根據項目的實際狀況,設置爲2000.以防止處理能力不足時,請求隊列過長.
request.AllowWriteStreamBuffering = false;
根據[微軟文檔(https://docs.microsoft.com/zh-cn/dotnet/api/system.net.httpwebrequest.allowwritestreambuffering?redirectedfrom=MSDN&view=netframework-4.8#System_Net_HttpWebRequest_AllowWriteStreamBuffering)]這個選項設置爲true時,數據將緩衝到內存中,以便在重定向或身份驗證請求時能夠從新發送數據.
最爲重要的是,文檔中說將 AllowWriteStreamBuffering 設置爲 true 可能會在上傳大型數據集時致使性能問題,由於數據緩衝區可能會使用全部可用內存。
因爲發送的請求僅僅是單號,數據量很小,而且不多有用戶一個單號反覆查詢的需求.加上可能會有反作用.這裏設置爲false.
request.Proxy = null;
這裏是參考了一個一位網友的文章,裏面提到默認的Proxy致使超時怪異行爲.因爲解決問題是在10月份,據寫這篇文章已經有一段時間了,所以再尋找時便找不到這篇文章了.有興趣的朋友能夠本身搜索一下.不少朋友可能會關心,經過以上配置到底有沒有解決問題.實際中以上配置後已經經歷了雙11峯值qps過萬的考驗.下面給出寫本文時候請求耗時的監控
可能看了這個圖,有些朋友仍是會有疑問,經過上面日誌截圖能夠看到,除了耗時在100ms以上的請求外,普通的耗時在四五十毫秒的仍是有不少的,可是下面這個截圖裏都是在10到20區間浮動,最高的也就30ms.這實際上是因爲在壓測的過程當中,發現Hbase自己也有不穩定的因素(大部分請求響應耗時都很平穩,可是偶爾會有個別請求婁千甚至數萬毫秒(在監控圖上表現爲一個很突兀的線,通常習慣稱爲毛刺),這在高併發場景下是不能接受的,問題反饋之後阿里雲對Hbase進行了優化,優化之後耗時也有所降低.)