四個全新維度,優(yōu)化你的HTTP性能到極致
無論你在做前端、后端還是運維,HTTP都是不得不打交道的網(wǎng)絡(luò)協(xié)議。它是最常用的應(yīng)用層協(xié)議,對它的優(yōu)化,既能通過降低時延帶來更好的體驗性,也能通過降低資源消耗帶來更高的并發(fā)性。
可是,學(xué)習(xí)HTTP不久的同學(xué),很難全面說出HTTP的所有優(yōu)化點。這既有可能是你沒好好準備過大廠的面試,也有可能你沒有加入一個快速發(fā)展的項目,當產(chǎn)品的用戶量不斷翻番時,需求會倒逼著你優(yōu)化HTTP協(xié)議。
這篇文章是根據(jù)我在2019年GOPS全球運維大會上海站的演講PPT,重新提煉文字后的總結(jié)。我希望能從四個全新的維度,帶你覆蓋絕大部分的HTTP優(yōu)化技巧。這樣,即使還不需要極致方法去解決當前的性能瓶頸,也會知道優(yōu)化方向在哪,當需求來臨時,能夠到Google上定向查閱資料。
第一個維度,是從編碼效率上,更快速地把消息轉(zhuǎn)換成更短的字符流。這是最直接的性能優(yōu)化點。
一、編碼效率優(yōu)化
如果你對HTTP/1.1協(xié)議做過抓包分析,就會發(fā)現(xiàn)它是用“whitespace-delimited”方式編碼的。用空格、回車來編碼,是因為HTTP在誕生之初追求可讀性,這樣更有利于它的推廣。
然而在當下,這種低效的編碼方式已經(jīng)嚴重影響性能了,所以2009年Google推出了基于二進制的SPDY協(xié)議,大幅提升了編碼效率。2015年,稍做改進后它被確定為HTTP/2協(xié)議,現(xiàn)在50%以上的站點都在使用它。
這是編碼優(yōu)化的大方向,包括即將推出的HTTP/3。
然而這些新技術(shù)到底是怎樣提升性能的呢?那還得拆開了看,先從數(shù)據(jù)的壓縮談起。你抓包看到的是數(shù)據(jù),它并不等于信息。數(shù)據(jù)其實是信息和冗余數(shù)據(jù)之和,而壓縮技術(shù),就是盡量地去除冗余數(shù)據(jù)。
壓縮分為無損壓縮和有損壓縮。針對圖片、音視頻,我們每天都在與有損壓縮打交道。比如,當瀏覽器只需要縮略圖時,就沒有必要浪費帶寬傳輸高清圖片。而高清視頻做過有損壓縮后,在肉眼無法分清時,已經(jīng)被壓縮了上千倍。
這是因為,聲音、視頻都可以做增量壓縮。還記得曾經(jīng)的VCD嗎?當光盤有劃痕時,整張盤都無法播放,就是因為那時的視頻做了增量壓縮,而且關(guān)鍵幀太少,導(dǎo)致關(guān)鍵幀損壞時,后面的增量幀全部無法播放了。
再來看無損壓縮,你肯定用過gzip,它讓http body實現(xiàn)了無損壓縮。肉眼閱讀壓縮后的報文全是亂碼,但接收端解壓后,可以看到發(fā)送端的原文。然而,gzip的效率其實并不高,以Google推出的brotli做對比,你就知道它的缺陷了:
評價壓縮算法時,我們重點看兩個指標:壓縮率和壓縮速度。上圖中可以看到,無論用gzip 9個壓縮級別中的哪一個,它的壓縮率都低于brotli(相比gzip,壓縮級別它還可以配置為10),壓縮速度也更慢。所以,如果可以,應(yīng)該盡快更新你的gzip壓縮算法了。
說完對body的壓縮,再來看HTTP header的壓縮。對于HTTP/1.x來說,header就是性能殺手。特別是當下cookie泛濫的時代,每次請求都要攜帶幾個KB的頭部,很浪費帶寬、CPU、內(nèi)存!HTTP2通過 HPACK 技術(shù)大幅度降低了header編碼后的體積,這也是HTTP3的演進方向。HPACK 到底是怎樣實現(xiàn) header 壓縮的呢?
HPACK通過Huffman算法、靜態(tài)表、動態(tài)表對三種header都做了壓縮。比如上圖中,method GET存在于靜態(tài)表,用1個字節(jié)表示的整數(shù)2表達即可;user-agent Mozilla這行頭部非常長,當它第2次出現(xiàn)時,用2個字節(jié)的整數(shù)62表示即可;即使它第1次出現(xiàn)時,也可以用Huffman算法壓縮Mozilla這段很長的瀏覽器標識符,可以獲得最多5/8的壓縮率。
靜態(tài)表中只存放最常見的header,有的只有name,有的同時包括name和value。靜態(tài)表的大小很有限,目前只有61個元素。
動態(tài)表應(yīng)用了增量編碼的思想,即,第1次出現(xiàn)時加入動態(tài)表,第2次出現(xiàn)的時候,傳輸它在動態(tài)表中的序號即可。
Huffman編碼在winrar等壓縮軟件中廣為使用,但HPACK中的Huffman有所不同,它使用的是靜態(tài)huffman編碼。即,它統(tǒng)計了互聯(lián)網(wǎng)上幾年內(nèi)的HTTP頭部,按照每個字符出現(xiàn)的概率,重建huffman樹,這樣,根據(jù)規(guī)則,出現(xiàn)次數(shù)最多的a、c、e或者1、2、3這些字符就只用5個bit位表示,而很少出現(xiàn)的字符則用幾十個bit位表示。
說完header,再來看http body的編碼。這里只舉3個例子:1、只有幾十字節(jié)的小圖標,沒有必要用獨立的HTTP請求傳輸,根據(jù)RFC2397的規(guī)則,可以把它直接嵌入到HTML或者CSS文件中,而瀏覽器在解析時會識別出它們,就像下圖中的頭像:
2、JS源碼文件中,可能有許多小文件,這些文件中也有許多空行、注釋,通過WebPack工具,先在服務(wù)器端打包為一個文件,并去除冗余的字符,編碼效果也很好。
3、在表單中,可以一次傳輸多個元素,比如既有復(fù)選框,也可以有文件。這就減少了HTTP請求的個數(shù)。
可見,http協(xié)議從header到 body,都有許多編碼手段,可以讓傳輸?shù)膱笪母绦。裙?jié)省了帶寬,也降低了時延。
編碼效率優(yōu)化完后,再來看“信道”,這雖然是通訊領(lǐng)域的詞匯,但用來概括HTTP的優(yōu)化點非常合適,這里就借用下了。
二、信道利用率優(yōu)化
信道利用率包括3個優(yōu)化點,第一個優(yōu)化點是多路復(fù)用!高速的低層信道上,可以跑許多低速的高層信道。比如,主機上只有一塊網(wǎng)卡,卻能同時讓瀏覽器、微信、釘釘收發(fā)消息;一個進程可以同時服務(wù)幾萬個TCP連接;一個TCP連接上可以同時傳遞多個HTTP2 STREAM消息。
其次,為了讓信道有更高的利用率,還得及時恢復(fù)錯誤。所以,TCP工作的很大一部分,都是在及時的發(fā)現(xiàn)丟包、亂序報文,并快速的處理它們。
最后,就像經(jīng)濟學(xué)里說的,資源總是稀缺的。有限的帶寬下,如何公平的對待不同的連接、用戶和對象呢?比如下載頁面時,如果把CSS和圖片以同等優(yōu)先級下載就有問題,圖片晚點顯示沒關(guān)系,但CSS沒獲取到頁面就無法顯示。另外,傳輸消息時,報文頭報并不承載目標信息,但它又是必不可少的,如何降低這些控制信息的占比呢?
我們先從多路復(fù)用談起。廣義上來說,多線程、協(xié)程都屬于多路復(fù)用,但這里我主要指http2的stream。因為http協(xié)議被設(shè)計為client先發(fā)request,server才能回復(fù)response,這樣收發(fā)消息,是沒辦法跑滿帶寬的。最有效率的方式是,發(fā)送端源源不斷地發(fā)請求、接收端源源不斷地發(fā)響應(yīng),這對于長肥網(wǎng)絡(luò)尤為有效:
HTTP2的stream就是這樣復(fù)用連接的。我們知道,chrome對一個站點最多同時建立6個連接,而有了HTTP2后,只需要一個連接就能高效的傳輸頁面上的數(shù)百個對象。我特意讓我的個人站點www.taohui.pub同時支持HTTP1和HTTP2,下圖是連接視角上HTTP2和HTTP1的區(qū)別。
熟悉chrome Network網(wǎng)絡(luò)面板的同學(xué),肯定很熟悉waterfall,它可以幫助你分析HTTP請求到底慢在哪里,是請求發(fā)出的慢,還是響應(yīng)接收的慢,又或者是解析得太慢了。下圖還是我的站點在waterfall視角下的對比。
從這兩張圖可以看出,HTTP2全面優(yōu)于HTTP1。
再來看網(wǎng)絡(luò)錯誤的恢復(fù)。在應(yīng)用層,lingering_time通過延遲關(guān)閉連接來避免瀏覽器因RST錯誤收不到http response,而timeout則是用定時器及時發(fā)現(xiàn)錯誤并釋放資源。
在傳輸層,通過timestamp=1可以讓TCP更精準的測量出定時器的超時時間RTO。當然,timestamp還有一個用途,就是防止長肥網(wǎng)絡(luò)中的序列號回繞。
什么是序列號回繞呢?我們知道,TCP每個報文都有序列號,它不是指報文的次序,而是已經(jīng)發(fā)送的字節(jié)數(shù)。由于它是32位整數(shù),所以最多可以處理232也就是4.2GB的飛行中報文。像上圖中,當1G-2G這些報文在網(wǎng)絡(luò)中飛行時間過長時,就會與5G-6G報文重疊,引發(fā)錯誤。
網(wǎng)絡(luò)錯誤還有很多種,比如報文的次序也是無法保證的。打開tcp_sack可以減少亂序時的重發(fā)報文量,降低帶寬消耗。
用Chrome瀏覽器直接下載大文件時,網(wǎng)絡(luò)不好時,一出錯就得全部重傳,體驗很差。改用迅雷下載就快了很多。這是因為迅雷把大文件拆成很多小塊,可以多線程下載,而且每個小塊出錯后,重新下載這一個塊即可,效率很高。這個斷點續(xù)傳、多線程下載技術(shù),就是HTTP的Range協(xié)議。如果你的服務(wù)是緩存,也可以使用Range協(xié)議,比如Nginx的Slice模塊就做了這件事。
實際上對于網(wǎng)絡(luò)錯誤恢復(fù),最精妙的算法是擁塞控制,它可以全面提升網(wǎng)絡(luò)性能。有同學(xué)會問,TCP不是有流量控制,為什么還會發(fā)生網(wǎng)絡(luò)擁塞呢?這是因為,TCP鏈路中的各個路由器,處理能力并不互相匹配。
就像上圖,R1的峰值網(wǎng)絡(luò)是700M/s,R2的峰值網(wǎng)絡(luò)是600M/s,它們都需要通過R3才能到達R4。然而,R3的最大帶寬只有1000M/s!當R1、R2中的TCP全速使用各自帶寬時,就會引發(fā)R3丟包。擁塞控制就是解決丟包問題的。
自1982年TCP誕生起,就在使用傳統(tǒng)的擁塞控制算法,它是發(fā)現(xiàn)丟包后再剎車減速,效果很不好。為什么呢?你可以觀察下圖,路由器中會有緩沖隊列,當隊列為空時,ping的時延最短;當隊列將滿時,ping的時延很大,但還未發(fā)生丟包;當隊列已滿時,丟包才會發(fā)生。
所以,當隊列出現(xiàn)積壓時,丟包沒有發(fā)生。雖然此時峰值帶寬不會減少,但網(wǎng)絡(luò)時延變大了,這是要避免的。而測量驅(qū)動的擁塞控制算法,就在隊列剛出現(xiàn)積壓這個點上開始剎車減速。在當今內(nèi)存越來越便宜,隊列越來越大的年代,新算法尤為有效。
當Linux內(nèi)核更新到4.9版本時,原先的CUBIC擁塞控制算法就被替換為Google的BBR算法了。從下圖中可以看到,當丟包率達到0.01%時,CUBIC就沒法用了,而BBR并沒有問題,直到丟包率達到5%時BBR的帶寬才劇烈下降。
再來看資源的平衡分配。為了公平的對待連接、用戶,服務(wù)器會做限速。比如下圖中的Leacky Bucket算法,它能夠平滑突增的流量,更公平的分配帶寬。
再比如HTTP2中的優(yōu)先級功能。一個頁面上有幾百個對象,這些對象的重要性不同,有些之間還互相依賴。比如,有些JS文件會包含jQuery.js,如果同等對待的話,即使先下載完前者,也無法使用。
HTTP2允許瀏覽器下載對象時,根據(jù)解析規(guī)則,在stream中設(shè)置每一個對象的weight優(yōu)先級(255最大,0最?。6鞔?、資源服務(wù)器都會根據(jù)優(yōu)先級,分配內(nèi)存和帶寬,提升網(wǎng)絡(luò)效率。
最后看下TCP的報文效率,它也會影響之上的HTTP性能。比如開啟Nagle算法后,網(wǎng)絡(luò)中的小報文數(shù)量大幅減少,考慮到40字節(jié)的報文頭部,信息占比更高。
Cork算法與Nagle算法相似,但會更激進的控制小報文。Cork與Nagle是從發(fā)送端控制小報文,quickack則從接收端控制純ack小報文的數(shù)量,提高信息占比。
說完相對微觀一些的信道,我們再來從宏觀上看第三個優(yōu)化點:傳輸路徑的優(yōu)化。
三、傳輸路徑優(yōu)化
傳輸路徑的第一個優(yōu)化點是緩存,瀏覽器、CDN、負載均衡等組件中,緩存無處不在。
緩存的基本用法你大概很熟悉了,這里我只講過期緩存的用法。把過期緩存直接丟掉是很浪費的,因為“過期”是客戶端的定時器決定的,并不代表資源真正失效。所以,可以把它的標識符帶給源服務(wù)器,服務(wù)器會判斷緩存是否仍然有效,如果有效,直接返回304和空body就可以了,非常節(jié)省帶寬。
對于負載均衡而言,過期緩存還能夠保護源服務(wù)器,限制回源請求。當源服務(wù)器掛掉后,還能以過期緩存給用戶帶來降級后的服務(wù)體驗,這比返回503要好得多。
傳輸路徑的第二個優(yōu)化點是慢啟動。系統(tǒng)自帶的TCP協(xié)議棧,為了避免瓶頸路由器丟包,會緩緩加大傳輸速度。它的起始速度就叫做初始擁塞窗口。
早期的初始擁塞窗口是1個MSS(通常是576字節(jié)),后來改到3個MSS(Linux 2.5.32),在Google的建議下又改到10個MSS(Linux 3.0)。之所以要不斷提升起始窗口,是因為隨著互聯(lián)網(wǎng)的發(fā)展,網(wǎng)頁越來越豐富,體積也越來越大。起始窗口太小,就需要更長的時間下載第一個網(wǎng)頁,體驗很差。
當然,修改起始窗口很簡單,下圖中是Linux下調(diào)整窗口的方法。
修改起始窗口是常見的性能優(yōu)化手段,比如CDN廠商都改過起始窗口,下圖是主流CDN廠商2014和2017年的起始窗口大小。
可見,有些窗口14年調(diào)得太大了,17年又縮回去了。所以,起始窗口并不是越大越好,它會增加瓶頸路由器的壓力。
再來看傳輸路徑上,如何從拉模式升級到推模式。比如index.html文件中包含,在HTTP/1中,必須先下載完index.html,才能去下載some.css,這是兩個RTT的時間。但在HTTP/2中,服務(wù)器可以通過2個stream,同時并行傳送index.html和some.css,節(jié)約了一半的時間。
其實當出現(xiàn)丟包時,HTTP2的stream并行發(fā)送會嚴重退化,因為TCP的隊頭阻塞問題沒有解決。
上圖中的SPDY與HTTP2是等價的。在紅綠色這3個stream并發(fā)傳輸時,TCP層仍然會串行化,假設(shè)紅色的stream在最先發(fā)送的,如果紅色報文丟失,那么即使接收端已經(jīng)收到了完整的藍、綠stream,TCP也不會把它交給HTTP2,因為TCP自身必須保證報文有序。這樣并發(fā)就沒有保證了,這就是隊頭阻塞問題。
解決隊頭阻塞的辦法就是繞開TCP,使用UDP協(xié)議實現(xiàn)HTTP,比如Google的GQUIC協(xié)議就是這么做的,B站在幾年前就使用它提供服務(wù)了。
UDP協(xié)議自身是不能保證可靠傳輸?shù)?,所以GQUIC需要重新在UDP之上實現(xiàn)TCP曾經(jīng)做過的事。這是HTTP的發(fā)展方向,所以目前HTTP3就基于GQUIC在制定標準。
最后,再從網(wǎng)絡(luò)信息安全的角度,談?wù)勅绾巫鰞?yōu)化。它實際上與編碼、信道、傳輸路徑都有關(guān)聯(lián),但其實又是獨立的環(huán)節(jié),所以放在最后討論。
四、信息安全優(yōu)化
互聯(lián)網(wǎng)世界的信息安全,始于1995年的SSL3.0。到現(xiàn)在,許多大型網(wǎng)站都更新到2018年推出的TLS1.3了。
TLS1.2有什么問題呢?最大問題就是,它支持古老的密鑰協(xié)商協(xié)議,這些協(xié)議現(xiàn)在已經(jīng)不安全了。比如2015年出現(xiàn)的FREAK中間人攻擊,就可以用Amazon上的虛擬機,分分鐘攻陷支持老算法的服務(wù)器。
TLS1.3針對這一情況,取消了在當前的計算力下,數(shù)學(xué)上已經(jīng)不再安全的非對稱密鑰協(xié)商算法。在Openssl的最新實現(xiàn)中,僅支持5種安全套件:
TLS1.3的另一個優(yōu)勢是握手速度。在TLS1.2中,由于需要2個RTT才能協(xié)商完密鑰,才誕生了session cache和session ticket這兩個工具,它們都把協(xié)商密鑰的握手降低為1個RTT。但是,這兩種方式都無法應(yīng)對重放攻擊。
而TLS1.2中的安全套件協(xié)商、ECDHE公鑰交換這兩步,在TLS1.3中被合并成一步,這大大提升了握手速度。
如果你還在使用TLS1.2,盡快升級到1.3吧,除了安全性,還有性能上的收益。
小結(jié)
HTTP的性能優(yōu)化手段眾多,從這四個維度出發(fā),可以建立起樹狀的知識體系,囊括絕大部分的HTTP優(yōu)化點。
編碼效率優(yōu)化包括http header和body ,它可以使傳輸?shù)臄?shù)據(jù)更短小緊湊,從而獲得更低的時延和更高的并發(fā)。同時,好的編碼算法也可以減少編解碼時的CPU消耗。
信道利用率的優(yōu)化,可以從多路復(fù)用、錯誤發(fā)現(xiàn)及恢復(fù)、資源分配這3個角度出發(fā),讓快速的底層信道,有效的承載慢速的應(yīng)用層信道。
傳輸路徑的優(yōu)化,包括各級緩存、慢啟動、消息傳送模式等,它能夠讓消息更及時的發(fā)給瀏覽器,提升用戶體驗。
當下互聯(lián)網(wǎng)中的信息安全,主要還是建立在TLS協(xié)議之上的。TLS1.3從安全性、性能上都有很大的提升,我們應(yīng)當及時的升級。
希望這些知識能夠幫助你全面、高效地優(yōu)化HTTP協(xié)議!