HTTP性能極限優化


無論你在做前端、后端還是運維,HTTP都是不得不打交道的網絡協議。它是最常用的應用層協議,對它的優化,既能通過降低時延帶來更好的體驗性,也能通過降低資源消耗帶來更高的並發性。

可是,學習HTTP不久的同學,很難全面說出HTTP的所有優化點。這既有可能是你沒好好准備過大廠的面試😊,也有可能你沒有加入一個快速發展的項目,當產品的用戶量不斷翻番時,需求會倒逼着你優化HTTP協議。

這篇文章是根據我在2019年GOPS全球運維大會上海站的演講PPT,重新提煉文字后的總結。我希望能從四個全新的維度,帶你覆蓋絕大部分的HTTP優化技巧。這樣,即使還不需要極致方法去解決當前的性能瓶頸,也會知道優化方向在哪,當需求來臨時,能夠到Google上定向查閱資料。

 

第一個維度,是從編碼效率上,更快速地把消息轉換成更短的字符流。這是最直接的性能優化點。

一、編碼效率優化

如果你對HTTP/1.1協議做過抓包分析,就會發現它是用“whitespace-delimited”方式編碼的。用空格、回車來編碼,是因為HTTP在誕生之初追求可讀性,這樣更有利於它的推廣。

然而在當下,這種低效的編碼方式已經嚴重影響性能了,所以2009年Google推出了基於二進制的SPDY協議,大幅提升了編碼效率。2015年,稍做改進后它被確定為HTTP/2協議,現在50%以上的站點都在使用它。

這是編碼優化的大方向,包括即將推出的HTTP/3。

然而這些新技術到底是怎樣提升性能的呢?那還得拆開了看,先從數據的壓縮談起。你抓包看到的是數據,它並不等於信息。數據其實是信息和冗余數據之和,而壓縮技術,就是盡量地去除冗余數據。

壓縮分為無損壓縮和有損壓縮。針對圖片、音視頻,我們每天都在與有損壓縮打交道。比如,當瀏覽器只需要縮略圖時,就沒有必要浪費帶寬傳輸高清圖片。而高清視頻做過有損壓縮后,在肉眼無法分清時,已經被壓縮了上千倍。這是因為,聲音、視頻都可以做增量壓縮。還記得曾經的VCD嗎?當光盤有划痕時,整張盤都無法播放,就是因為那時的視頻做了增量壓縮,而且關鍵幀太少,導致關鍵幀損壞時,后面的增量幀全部無法播放了。

再來看無損壓縮,你肯定用過gzip,它讓http body實現了無損壓縮。肉眼閱讀壓縮后的報文全是亂碼,但接收端解壓后,可以看到發送端的原文。然而,gzip的效率其實並不高,以Google推出的brotli做對比,你就知道它的缺陷了:

評價壓縮算法時,我們重點看兩個指標:壓縮率和壓縮速度。上圖中可以看到,無論用gzip 9個壓縮級別中的哪一個,它的壓縮率都低於brotli(相比gzip,壓縮級別它還可以配置為10),壓縮速度也更慢。所以,如果可以,應該盡快更新你的gzip壓縮算法了。

說完對body的壓縮,再來看HTTP header的壓縮。對於HTTP/1.x來說,header就是性能殺手。特別是當下cookie泛濫的時代,每次請求都要攜帶幾個KB的頭部,很浪費帶寬、CPU、內存!HTTP2通過HPACK技術大幅度降低了header編碼后的體積,這也是HTTP3的演進方向。 HPACK到底是怎樣實現header壓縮的呢?

HPACK通過Huffman算法、靜態表、動態表對三種header都做了壓縮。比如上圖中,method GET存在於靜態表,用1個字節表示的整數2表達即可;user-agent Mozilla這行頭部非常長,當它第2次出現時,用2個字節的整數62表示即可;即使它第1次出現時,也可以用Huffman算法壓縮Mozilla這段很長的瀏覽器標識符,可以獲得最多5/8的壓縮率。

靜態表中只存放最常見的header,有的只有name,有的同時包括name和value。靜態表的大小很有限,目前只有61個元素。

動態表應用了增量編碼的思想,即,第1次出現時加入動態表,第2次出現的時候,傳輸它在動態表中的序號即可。

Huffman編碼在winrar等壓縮軟件中廣為使用,但HPACK中的Huffman有所不同,它使用的是靜態huffman編碼。即,它統計了互聯網上幾年內的HTTP頭部,按照每個字符出現的概率,重建huffman樹,這樣,根據規則,出現次數最多的a、c、e或者1、2、3這些字符就只用5個bit位表示,而很少出現的字符則用幾十個bit位表示。
說完header,再來看http body的編碼。這里只舉3個例子:1、只有幾十字節的小圖標,沒有必要用獨立的HTTP請求傳輸,根據RFC2397的規則,可以把它直接嵌入到HTML或者CSS文件中,而瀏覽器在解析時會識別出它們,就像下圖中的頭像:

2、JS源碼文件中,可能有許多小文件,這些文件中也有許多空行、注釋,通過WebPack工具,先在服務器端打包為一個文件,並去除冗余的字符,編碼效果也很好。

3、在表單中,可以一次傳輸多個元素,比如既有復選框,也可以有文件。這就減少了HTTP請求的個數。

可見,http協議從header到 body,都有許多編碼手段,可以讓傳輸的報文更短小,既節省了帶寬,也降低了時延。

 

編碼效率優化完后,再來看“信道”,這雖然是通訊領域的詞匯,但用來概括HTTP的優化點非常合適,這里就借用下了。

二、信道利用率優化

信道利用率包括3個優化點,第一個優化點是多路復用!高速的低層信道上,可以跑許多低速的高層信道。比如,主機上只有一塊網卡,卻能同時讓瀏覽器、微信、釘釘收發消息;一個進程可以同時服務幾萬個TCP連接;一個TCP連接上可以同時傳遞多個HTTP2 STREAM消息。


其次,為了讓信道有更高的利用率,還得及時恢復錯誤。所以,TCP工作的很大一部分,都是在及時的發現丟包、亂序報文,並快速的處理它們。

最后,就像經濟學里說的,資源總是稀缺的。有限的帶寬下,如何公平的對待不同的連接、用戶和對象呢?比如下載頁面時,如果把CSS和圖片以同等優先級下載就有問題,圖片晚點顯示沒關系,但CSS沒獲取到頁面就無法顯示。另外,傳輸消息時,報文頭報並不承載目標信息,但它又是必不可少的,如何降低這些控制信息的占比呢?

我們先從多路復用談起。廣義上來說,多線程、協程都屬於多路復用,但這里我主要指http2的stream。因為http協議被設計為client先發request,server才能回復response,這樣收發消息,是沒辦法跑滿帶寬的。最有效率的方式是,發送端源源不斷地發請求、接收端源源不斷地發響應,這對於長肥網絡尤為有效:

HTTP2的stream就是這樣復用連接的。我們知道,chrome對一個站點最多同時建立6個連接,而有了HTTP2后,只需要一個連接就能高效的傳輸頁面上的數百個對象。我特意讓我的個人站點www.taohui.pub同時支持HTTP1和HTTP2,下圖是連接視角上HTTP2和HTTP1的區別。

熟悉chrome Network網絡面板的同學,肯定很熟悉waterfall,它可以幫助你分析HTTP請求到底慢在哪里,是請求發出的慢,還是響應接收的慢,又或者是解析得太慢了。下圖還是我的站點在waterfall視角下的對比。

從這兩張圖可以看出,HTTP2全面優於HTTP1。

再來看網絡錯誤的恢復。在應用層,lingering_time通過延遲關閉連接來避免瀏覽器因RST錯誤收不到http response,而timeout則是用定時器及時發現錯誤並釋放資源。

在傳輸層,通過timestamp=1可以讓TCP更精准的測量出定時器的超時時間RTO。當然,timestamp還有一個用途,就是防止長肥網絡中的序列號回繞。

什么是序列號回繞呢?我們知道,TCP每個報文都有序列號,它不是指報文的次序,而是已經發送的字節數。由於它是32位整數,所以最多可以處理232也就是4.2GB的飛行中報文。像上圖中,當1G-2G這些報文在網絡中飛行時間過長時,就會與5G-6G報文重疊,引發錯誤。

網絡錯誤還有很多種,比如報文的次序也是無法保證的。打開tcp_sack可以減少亂序時的重發報文量,降低帶寬消耗。

用Chrome瀏覽器直接下載大文件時,網絡不好時,一出錯就得全部重傳,體驗很差。改用迅雷下載就快了很多。這是因為迅雷把大文件拆成很多小塊,可以多線程下載,而且每個小塊出錯后,重新下載這一個塊即可,效率很高。這個斷點續傳、多線程下載技術,就是HTTP的Range協議。如果你的服務是緩存,也可以使用Range協議,比如Nginx的Slice模塊就做了這件事。

實際上對於網絡錯誤恢復,最精妙的算法是擁塞控制,它可以全面提升網絡性能。有同學會問,TCP不是有流量控制,為什么還會發生網絡擁塞呢?這是因為,TCP鏈路中的各個路由器,處理能力並不互相匹配。

就像上圖,R1的峰值網絡是700M/s,R2的峰值網絡是600M/s,它們都需要通過R3才能到達R4。然而,R3的最大帶寬只有1000M/s!當R1、R2中的TCP全速使用各自帶寬時,就會引發R3丟包。擁塞控制就是解決丟包問題的。

自1982年TCP誕生起,就在使用傳統的擁塞控制算法,它是發現丟包后再剎車減速,效果很不好。為什么呢?你可以觀察下圖,路由器中會有緩沖隊列,當隊列為空時,ping的時延最短;當隊列將滿時,ping的時延很大,但還未發生丟包;當隊列已滿時,丟包才會發生。

所以,當隊列出現積壓時,丟包沒有發生。雖然此時峰值帶寬不會減少,但網絡時延變大了,這是要避免的。而測量驅動的擁塞控制算法,就在隊列剛出現積壓這個點上開始剎車減速。在當今內存越來越便宜,隊列越來越大的年代,新算法尤為有效。

當Linux內核更新到4.9版本時,原先的CUBIC擁塞控制算法就被替換為Google的BBR算法了。從下圖中可以看到,當丟包率達到0.01%時,CUBIC就沒法用了,而BBR並沒有問題,直到丟包率達到5%時BBR的帶寬才劇烈下降。

再來看資源的平衡分配。為了公平的對待連接、用戶,服務器會做限速。比如下圖中的Leacky Bucket算法,它能夠平滑突增的流量,更公平的分配帶寬。


再比如HTTP2中的優先級功能。 一個頁面上有幾百個對象,這些對象的重要性不同,有些之間還互相依賴。比如,有些JS文件會包含jQuery.js,如果同等對待的話,即使先下載完前者,也無法使用。

HTTP2允許瀏覽器下載對象時,根據解析規則,在stream中設置每一個對象的weight優先級(255最大,0最小)。而各代理、資源服務器都會根據優先級,分配內存和帶寬,提升網絡效率。

最后看下TCP的報文效率,它也會影響之上的HTTP性能。比如開啟Nagle算法后,網絡中的小報文數量大幅減少,考慮到40字節的報文頭部,信息占比更高。

Cork算法與Nagle算法相似,但會更激進的控制小報文。Cork與Nagle是從發送端控制小報文,quickack則從接收端控制純ack小報文的數量,提高信息占比。

 

說完相對微觀一些的信道,我們再來從宏觀上看第三個優化點:傳輸路徑的優化。

三、傳輸路徑優化

傳輸路徑的第一個優化點是緩存,瀏覽器、CDN、負載均衡等組件中,緩存無處不在。

緩存的基本用法你大概很熟悉了,這里我只講過期緩存的用法。把過期緩存直接丟掉是很浪費的😊,因為“過期”是客戶端的定時器決定的,並不代表資源真正失效。所以,可以把它的標識符帶給源服務器,服務器會判斷緩存是否仍然有效,如果有效,直接返回304和空body就可以了,非常節省帶寬。


對於負載均衡而言,過期緩存還能夠保護源服務器,限制回源請求。當源服務器掛掉后,還能以過期緩存給用戶帶來降級后的服務體驗,這比返回503要好得多。

傳輸路徑的第二個優化點是慢啟動。系統自帶的TCP協議棧,為了避免瓶頸路由器丟包,會緩緩加大傳輸速度。它的起始速度就叫做初始擁塞窗口。


早期的初始擁塞窗口是1個MSS(通常是576字節),后來改到3個MSS(Linux 2.5.32),在Google的建議下又改到10個MSS(Linux 3.0)。 之所以要不斷提升起始窗口,是因為隨着互聯網的發展,網頁越來越豐富,體積也越來越大。起始窗口太小,就需要更長的時間下載第一個網頁,體驗很差。

當然,修改起始窗口很簡單,下圖中是Linux下調整窗口的方法。


修改起始窗口是常見的性能優化手段,比如CDN廠商都改過起始窗口,下圖是主流CDN廠商2014和2017年的起始窗口大小。


可見,有些窗口14年調得太大了,17年又縮回去了。所以,起始窗口並不是越大越好,它會增加瓶頸路由器的壓力。

再來看傳輸路徑上,如何從拉模式升級到推模式。比如index.html文件中包含<LINK href=”some.css”>,在HTTP/1中,必須先下載完index.html,才能去下載some.css,這是兩個RTT的時間。但在HTTP/2中,服務器可以通過2個stream,同時並行傳送index.html和some.css,節約了一半的時間。


其實當出現丟包時,HTTP2的stream並行發送會嚴重退化,因為TCP的隊頭阻塞問題沒有解決。

上圖中的SPDY與HTTP2是等價的。在紅綠色這3個stream並發傳輸時,TCP層仍然會串行化,假設紅色的stream在最先發送的,如果紅色報文丟失,那么即使接收端已經收到了完整的藍、綠stream,TCP也不會把它交給HTTP2,因為TCP自身必須保證報文有序。這樣並發就沒有保證了,這就是隊頭阻塞問題。

解決隊頭阻塞的辦法就是繞開TCP,使用UDP協議實現HTTP,比如Google的GQUIC協議就是這么做的,B站在幾年前就使用它提供服務了。

UDP協議自身是不能保證可靠傳輸的,所以GQUIC需要重新在UDP之上實現TCP曾經做過的事。這是HTTP的發展方向,所以目前HTTP3就基於GQUIC在制定標准。

 

最后,再從網絡信息安全的角度,談談如何做優化。它實際上與編碼、信道、傳輸路徑都有關聯,但其實又是獨立的環節,所以放在最后討論。

四、信息安全優化

互聯網世界的信息安全,始於1995年的SSL3.0。到現在,許多大型網站都更新到2018年推出的TLS1.3了。


TLS1.2有什么問題呢?最大問題就是,它支持古老的密鑰協商協議,這些協議現在已經不安全了。比如2015年出現的FREAK中間人攻擊,就可以用Amazon上的虛擬機,分分鍾攻陷支持老算法的服務器。


TLS1.3針對這一情況,取消了在當前的計算力下,數學上已經不再安全的非對稱密鑰協商算法。在Openssl的最新實現中,僅支持5種安全套件:

TLS1.3的另一個優勢是握手速度。在TLS1.2中,由於需要2個RTT才能協商完密鑰,才誕生了session cache和session ticket這兩個工具,它們都把協商密鑰的握手降低為1個RTT。但是,這兩種方式都無法應對重放攻擊。

而TLS1.2中的安全套件協商、ECDHE公鑰交換這兩步,在TLS1.3中被合並成一步,這大大提升了握手速度。

如果你還在使用TLS1.2,盡快升級到1.3吧,除了安全性,還有性能上的收益。

小結

HTTP的性能優化手段眾多,從這四個維度出發,可以建立起樹狀的知識體系,囊括絕大部分的HTTP優化點。

編碼效率優化包括http header和body ,它可以使傳輸的數據更短小緊湊,從而獲得更低的時延和更高的並發。同時,好的編碼算法也可以減少編解碼時的CPU消耗。

信道利用率的優化,可以從多路復用、錯誤發現及恢復、資源分配這3個角度出發,讓快速的底層信道,有效的承載慢速的應用層信道。

傳輸路徑的優化,包括各級緩存、慢啟動、消息傳送模式等,它能夠讓消息更及時的發給瀏覽器,提升用戶體驗。

當下互聯網中的信息安全,主要還是建立在TLS協議之上的。TLS1.3從安全性、性能上都有很大的提升,我們應當及時的升級。

希望這些知識能夠幫助你全面、高效地優化HTTP協議!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM