在項目中使用HttpClient可能是很普遍,尤其在當下微服務大火形勢下,如果服務之間是http調用就少不了跟http客戶端找交道.由於項目用戶規模不同以及應用場景不同,很多時候可能不需要特別處理也.然而在一些高並發場景下必須要做一些優化.
項目是快遞公司的快件軌跡查詢項目,目前平均每小時調用量千萬級別.軌跡查詢以Oracle為主要數據源,Mongodb為備用,當Oracle不可用時,數據源切換到Mongodb.今年菜鳥團隊加入后,主要數據遷移到了阿里雲上,以Hbase為主要存儲.其中Hbase數據查詢服務由數據解析組以Http方式提供.原有Mongodb棄用,雲上數據源變為主數據源,Oracle作為備用.當數據源切換以后,主要的調用方式也就變成了http方式.在第10月初第一輪雙11壓測試跑上,qps不達標.當然這個問題很好定位,因為十一假之間軌跡域組內已經進行過試跑,當時查的是oracle.十一假期回來后,只有這一處明顯的改動,很容易定位到問題出現在調用上.但具體是雲上Hbase慢,還是網絡傳輸問題(Hbase是阿里雲上的服務,軌跡查詢項目部署在IDC機房).通過雲服務,解析組和網絡運維的配合,確定問題出現在應用程序上.在Http服務調用處打日志記錄,發現以下問題:
可以看到每隔一段時間,就會有不少請求的耗時明顯比其它的要高.
導致這種情況可能可能是HttpClient反復創建銷毀造成引起來銷,首先憑經驗可能是對HttpClient進行了Dispose操作(Using(HttpClient client=new HttpClient){...})
如果你裝了一些第三方插件,當你寫的HttpClient沒有被Using包圍的時候會給出重構建議,建議加上Using或者手動dispose.然而實際中是否要dispose還要視情況而定,對於一般項目大家的感覺可能是不加也沒有大問題,加了也還ok.但是實際項目中,一般不建議反復重新創建這個對象,關於HttpClient是否需要Dispose請看這里
在對這個問題的答案里,提問者指出微軟的一些示例也是沒有使用using的.下面一個比較熱的回答指出HttpClient生命周期應該和應用程序生命周期一致,只要應用程序需要Http請求,就不應用把它Dispose掉.下面的一個仍然相對比較熱的回答指出一般地,帶有Dispose方法的對象都應當被dispose掉,但是HttpClient是一個例外.
當然以上只是結合自己的經驗對一個大家都可能比較困惑的問題給出一個建議,實際上對於一般的項目,用還是不用Dispose都不會造成很大問題.本文中上面提到的問題跟HttpClient也沒有關系,因為程序中使用的Http客戶端是基於HttpWebRequest
封裝的.
問題排查及優化
經過查詢相關資料以及同行的經驗分享(這篇文章給了很大啟發)
查看代碼,request.KeepAlive = true;查詢微軟相關文檔,這個屬性其實是設置一個'Keep-alive'請求header,當時同事封裝Http客戶端的場景現場無從得知,然而對於本文中提到的場景,由於每次請求的都是同一個接口,因此保持持續連接顯然能夠減少反復創建tcp連接的開銷.因此注釋掉這一行再發布測試,以上問題便不復出現了!
當然實際中做的優化絕不僅僅是這一點,如果僅僅是這樣,一句話就能夠說完了,大家都記住以后就這樣做就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進行了優化,優化以后耗時也有所下降.)