天天看點

超高性能管線式HTTP請求(實踐·原理·實作)

超高性能管線式HTTP請求(實踐·原理·實作)

這裡的高性能指的就是網卡有多快請求發送就能有多快,基本上一般的伺服器在一台用戶端的壓力下就會出現明顯延時。

該篇實際是介紹pipe管線的原理,下面主要通過其高性能的測試實踐,解析背後資料流量及原理。最後附帶一個簡單的實作

pipe之是以能比正常請求方式性能高出這麼多,主要有以下幾點

1:管線式發送,每條request不要等response回複即可直接發送下一個(重點不在于使用的是同一條線路,而且不約等待回複)

2:多條請求打包發送,在網絡條件合适的情況下一個包可以包含多條request

3:隻要伺服器允許隻需要建立極少tcp連結 (因為非區域網路的TCP線路一般都遵循慢啟動,網絡正常情況下需要一定時間後效率才能達到最高)

先直接看對比測試方法

測試内容單一客戶的使用盡可能快的方式向伺服器發送一定量(10000條)請求,并接收傳回資料

對于單一用戶端對伺服器進行http請求,一般我們的方式

1:單程序或線程輪詢請求(這個效能自然很低,原因會講到,也不用測試)

2:多條線程提前準備資料等待信号(對用戶端性能要求較高)

3:提前準備一組線程同時輪詢操作

4:使用系統/平台自帶異步發送機制(實際就是平台線程池的方式,發送與接收使用從線程池中的不同線程)

對于測試方案1,及方案2測試中性能較低沒有可比性,後面測試不會展示其結果

以下展示後面2種測試方法及目前要說的管線式的方式

先講管線式(pipe)測試方案(原理在後面會講到),測試中使用100條管線(管道),實際上更少甚至一條管線也是能達到近似的性能,不過多數伺服器nginx限制一條管可以持續發送request的數量(大部分是100也有部分會是200或是更高),每條管線發送100個請求。

然後是線程組的方式準備100條線程(100條線程并不是很多不會對系統本身有明顯影響),每條線程輪詢發送100個request。

異步方式的方式,10000全部送出發送線程,由線程池控制接收。

測試環境:普通家用PC,i5 4核,12G ,100Mb電信帶寬

測試資料:

GET http://www.baidu.com HTTP/1.1

Content-Type: application/x-www-form-urlencoded

Host: www.baidu.com

Connection: Keep-Alive

這裡就是測試最常用的baidu,如果測試接口性能不佳,大部分請求會在應用伺服器排隊,難以直覺提現pipe的優勢(其實就是還沒有用到pipe的能力,伺服器就先阻塞了) 

下文中所有關于pipe的測試都是使用PipeHttpRuner  (http://www.cnblogs.com/lulianqi/p/8167843.html 為該測試工具的下載下傳位址,使用方法及介紹)

先直接看管道式的表現:(截圖全部為windows自帶任務管理器及資料總管)

超高性能管線式HTTP請求(實踐·原理·實作)
超高性能管線式HTTP請求(實踐·原理·實作)

先解釋下截圖含義,後面的截圖也都是同樣的含義

第一副為任務管理器的截圖實線為接收資料,虛線為發送資料,取樣0.5s,每一個正方形的刻度為1.5s(因為任務管理器繪圖政策速率上升太快過高的沒有辦法顯示,不過還是可以看到時間線)

第二副為資料總管,添加了3個采樣器,紅色為CPU占用率,藍色為網絡接收速率,綠色為網絡發送速率。

測試中 一次原始請求大概130位元組,加上tcp,ip標頭,10000條大概也隻有1.5Mb(標頭不會太多因為管道式請求裡會有多個請求放到一個包裡的情況,不過大部分伺服器無法有這麼快的響應速度會有大量重傳的情況,實際上傳流量可能遠大于理論值)

一次的回包大概在60Mb左右(因為會有部分連接配接中途中斷是以不一定每次測試都會有10000個完整回複)

可以看到使用pipe形式性能表現非常突出,總體完成測試僅僅使用了5s左右

發送本身壓力比較小,可以看到0.5秒即到達峰值,其實這個時候基本10000條request已經發送出去了,後面的流量主要來自于伺服器端緩存等待(TCP window Full)來不及處理而照成是重傳,後面會講到。

再來看看response的接收,基本上也僅僅使用了0.5s即達到了接收峰值,使用大概5s 即完成了全部接收,因為測試中cpu占用上升并不明顯,而對于response的接收基本上是從tcp緩存區讀出後直接就存在了内容裡,也沒有涉及磁盤操作(是以基本上可以說對于pipe這個測試并沒有發揮出其全部性能,瓶頸主要在網絡帶寬上)。

再來看下線程組的方式(100條線程每條100次)

超高性能管線式HTTP請求(實踐·原理·實作)
超高性能管線式HTTP請求(實踐·原理·實作)

下面是異步接收的方式

超高性能管線式HTTP請求(實踐·原理·實作)
超高性能管線式HTTP請求(實踐·原理·實作)

很明顯的差距,對于線程組的形式大概使用了25秒,而異步接收使用了超過1分鐘的時間(異步接收的模式是平台推薦的發送模式,正常應用情況下性能是十分優越的,而對于過高的壓力不如自定義的線程組,主要還是因為其使用了預設的線程池,而預設線程池不可能在短時間開100條線程出來用來接收資料,是以大量的回複對線程池裡的線程就會有大量的切換,通過設定預設線程池數量可以提高測試中的性能)。更為重要的是這2者中的無論哪一種方式在測試中,cpu的占用都幾乎是滿的(即是說為了完成測試計算機已經滿負荷工作了,很難再有提高)

後面其實還針對jd,toabao,youku,包括公司自己的伺服器進行過測試,測試結果都是類似的,隻要伺服器不出問題基本上都有超過10倍的差距(如果用戶端帶寬足夠這個差距會更大)。

下面我們再對接口形式的HTTP進行簡單一次測試

這裡選用網易電商的接口(電商的接口一般可承受的壓力比較大,這裡前面已經确認測試不會對其正常使用造成實質的影響)

http://you.163.com/xhr/globalinfo/queryTop.json?__timestamp=1514784144074 (這裡是一個擷取商品清單的接口)

測試資料設定如下

超高性能管線式HTTP請求(實踐·原理·實作)
超高性能管線式HTTP請求(實踐·原理·實作)
超高性能管線式HTTP請求(實踐·原理·實作)
超高性能管線式HTTP請求(實踐·原理·實作)

請求量還是10000條接收的response資料大概有326Mb 30s之内完成。基本上是網絡的極限,此時cpu也基本無然後壓力(100條管線,每條100個請求)

這裡其實請求是帶時間戳的,因為測試時使用的是同一個時間戳,是以實際對應用伺服器的影響不大,真實測試時可以為每條請求設定不同時間戳(這裡是因為要示範使用了線上公開服務,測試時請使用測試服務)

注意,這裡的測試如果選擇了性能較低的測試對象,大部分流量會在伺服器端排隊等候,導緻吞吐量不大,這實際是伺服器端處理過慢,與用戶端關系不大。

一般情況下一台普通的pc在使用pipe進行測試時就可以讓伺服器出現明顯延時

正常的http一般實作都是連接配接完成後(tcp握手)發生request流向伺服器,然後及進入等待,收到response後才算結束(如下圖)

超高性能管線式HTTP請求(實踐·原理·實作)

當然http1.1 即支援keep alive,完成一次收發後完全可以不關閉連接配接使用同一個連結發生下一個請求(如下圖)

超高性能管線式HTTP請求(實踐·原理·實作)

這種方式對性能的提升還是比較明顯的,特别早些年伺服器性能有限,網絡資源匮乏,RTT大(網絡時延大)。不過對如今的情況,其實這些都已經不是最主要的問題了

可以明顯看到上面的模式,是一定要等到response到達後,用戶端才能發起下一個request的,如果應用伺服器需要時間處理,所有後面的請求都需要等待,即使不需要任何處理直接回複給用戶端,請求,回複在網絡上的時間也是必須完整的等下去,而且由于tcp傳輸本身的特性,速率是逐漸上升的,這樣斷斷續續的發送接收十分影響tcp迅速達到線路性能最大值。

pipe (管線式)正是回避了上面的問題,他不需要等回複達到即可直接發送(事實上http1.1協定也從來沒有講過必須要等response到達後用戶端才能發送下一個請求,隻是為了友善應用層業務實作,一般的http庫都是這樣實作的,而現在看到的絕大多少http伺服器都是預設支援pipe的),這樣發送與接收即可以分離開來(如下圖)

超高性能管線式HTTP請求(實踐·原理·實作)

在事實情況下,發生可能會比這個圖表現的更快,請求1,2,3,4很可能被放到一個tcp包裡被一次性全部發出去(這種模式也給部分應用帶來了麻煩,後面會講到)

超高性能管線式HTTP請求(實踐·原理·實作)

對于pipe相對真實的情況如上圖,多個請求會被打包在一起被發送,甚至有時是所有request發送完成後,伺服器才開始回複第一個response。

超高性能管線式HTTP請求(實踐·原理·實作)

而普通的keepalive的模式如上圖,一條線代表一個請求,不僅一次隻能發送一個,而且必須等待回複後才能發下一個。

下面看下實際測試中pipe的模式具體是什麼模樣的

超高性能管線式HTTP請求(實踐·原理·實作)

可以看到握手完成後(實際上握手時間也不長隻用了4ms),随後即直接開始了request的發送,可以看到後面的一個tcp包裡直接包含了完整的12個請求。在沒有收到任何一個回複的情況下,就可以把所有要發送的請求提前全部發出(伺服器已經關閉了Nagle算法)。

超高性能管線式HTTP請求(實踐·原理·實作)

由于發送速度過快直到發出一大半近70個request的時候第一個tcp确認包序号為353的包(隻是确認包不是response)才發出(327的ack),而且伺服器很快就發現下一個包出問題了并引發了TCP DUP ACK (https://ask.wireshark.org/questions/29216/why-are-duplicate-tcp-acks-being-seen-in-wireshark-capture 産生原因可以參考這裡)

【TCP DUP ACK 出現在接收方發現資料包缺口時(資料包失序),這種情況就會發送重複的ACK,這不僅用于快重傳,會觸發比快重傳更快的恢複機制(Fast Retransmission)如果發現重複的ACK,但是封包中未發現缺口,這表示你捕獲的是資料來源(而不是接收方),這是十分正常的如果資料在發往接收方的時候發生了丢失。你應該會看到一個重傳包】

其實就是說伺服器沒有發現下一個包後面又發了3次(一共4次·)TCP DUP ACK 都是針對353的,是以後面用戶端很快就重傳了TCP DUP ACK 所指定的丢失的包(即下面看到的362)

後面還可以看到由于過快的速度,還造成了部分的失序列(out of order)。不過需要說明的是,這些錯誤在tcp的傳輸中是很常見的,tcp有自己的一套高效的機制對這些錯誤進行恢複,即便有這些錯誤的存在也不會對pipe的實際性能造成影響。

如果伺服器異常誤包不能馬上被恢複可能會造成指數退避的情況如下圖

超高性能管線式HTTP請求(實踐·原理·實作)

高速收發帶來的問題,不僅有丢包,失序,重傳,無論是用戶端還是伺服器都會有接收視窗耗盡的情況,如果接收端視窗耗盡會出現TCP ZeroWIndow / Window full。 是以無論是用戶端還是伺服器都需要快速讀取tcp緩沖區資料

超高性能管線式HTTP請求(實踐·原理·實作)

通過對TCP流的檢查可以确定在本次測試中的部分管道的100條request是全部發出後,response才逐漸被伺服器發出

現在看一下response的回複情況

超高性能管線式HTTP請求(實踐·原理·實作)

因為response本身很大,而用戶端的MSS隻有1460 (上面看到的1506不是超過了MSS的意思,實際該資料包隻有1424,加上48個位元組的TCP標頭,20位元組的ip標頭,14位元組的以太網標頭一共是1506,正常tcp標頭為20位元組因為這個tcp包被拆包了,是以標頭裡多了28個位元組的options)是以一個response被拆成了多個包。

通過封包不難看出這個response在網絡中傳輸大概花了1ms不到的時間(大概730微秒),因為看到是過濾掉過端口(指定管道)的流量,實際上在這不到1ms的時間裡另外的管道也是可能同時在接收資料的。

現在我們可以來說下pipe弊端

實際pipe早就被http1.1所支援,并且大部分nginx伺服器也支援并開啟了這一功能。

相比普通的http keepalive傳輸 pipe http 解決了HOL blocking (Head-of-Line Blocking),而正是不再遵循一發一收的模式,使得應用層不能直接将每個請求與回複一一對應起來,對部分需要送出并區分傳回結果的POST一類的請求,這種方式顯的不是很友好。

解決方法其實也很簡單,在應用服務上為request于response加上唯一标簽即可以區分,或者直接使用HTTP2.0(https://tools.ietf.org/pdf/rfc7540.pdf)(這也是2.0的一個重要改進,http2.0也是通過類似的方式為其每個幀添加辨別目前stream的id來實作區分的)

 下面是pipe與正常http的簡單對比

<col>

pipe 管線式HTTP

普通HTTP 1.1

使用同一條tcp線路

使用不同連結(支援keepalive 可以保持連結)

不用等待回複即可以直接發送下一個請求

同一個連結必須收到回複後才能發起下一個請求

一次/一包可以同時發送多個請求

一次隻能發送一個請求

如下為pipe的.NET簡單實作類庫,及應用該類庫的deom 測試工具

超高性能管線式HTTP請求(實踐·原理·實作)

實作過程還是比較簡單的可直接參看GitHub工程,MyPipeHttpHelper為實作pipe的工具類(代碼中有較詳細的注釋),PipeHttpRuner為使用該工具類編寫的測試工具

https://github.com/lulianqi/PipeHttp/ (工程位址)

https://github.com/lulianqi/PipeHttp/tree/master/MyPipeHttpHelper (類庫位址)

https://github.com/lulianqi/PipeHttp/tree/master/PipeHttpRuner (測試deom位址)

繼續閱讀