1 HTTP連接管理概述
最近重讀了《HTTP權(quán)威指南》部分章節(jié),結(jié)合apache來對部分內(nèi)容進行印證并記錄下來。HTTP連接管理我們大體會談到如下內(nèi)容:串行連接,并行連接,持久連接以及管道化連接?,F(xiàn)在流行的瀏覽器如chrome,firefox都采用了并行的持久連接來提升性能,減少加載延時。本文只針對HTTP/1.0和HTTP/1.1,HTTP/2不在討論范圍。
HTTP/1.1允許在持久連接上使用管道,但是在瀏覽器上卻幾乎不會開啟管道功能,chrome之前的老版本還可以在 chrome://flags里面來選擇開啟或關(guān)閉管道功能,最新的版本已經(jīng)移除了管道相關(guān)選項。為什么不加入管道化連接提升性能呢,主要有幾個原因:其一是管道化持久連接實現(xiàn)復(fù)雜,容易出bug,且不易調(diào)試。其二是一些不標準的代理導(dǎo)致管道化容易出現(xiàn)很多難以預(yù)料的問題,導(dǎo)致開發(fā)人員調(diào)試十分復(fù)雜。更詳細的一些分析參見stackoverflow: why-is-pipelining-disabled-in-modern-browsers。
1.1 串行連接
串行連接性能最差,因為每次http事務(wù)(http事務(wù)由一次完整的http請求加上http響應(yīng)構(gòu)成)都要建立一個新的TCP連接,而且每個新的http事務(wù)都要串行執(zhí)行,比如一個頁面嵌入了3個圖片,則瀏覽器需要串行地發(fā)起4個HTTP事務(wù)來顯示頁面。除了串行加載引入的延遲外,加載一副圖片時,頁面其他地方都沒有動靜也會讓人心理上覺得速度慢。

1.2 并行連接
正是由于串行連接的問題才出現(xiàn)了并行連接。HTTP允許客戶端(如瀏覽器)打開多條連接,并行執(zhí)行多個HTTP事務(wù),加快加載速度。當然這里有一點要說明,并行連接并不是一定更快,如果并行的進程數(shù)太多會消耗很多內(nèi)存資源,此外,如果每個用戶的客戶端打開100個連接,則100個用戶同時訪問時會有10000個連接需要服務(wù)器處理,加重服務(wù)器的負擔。

因此,現(xiàn)代瀏覽器雖然用了并行連接,但是一般會將同一個域名的請求并行連接數(shù)限制到一個較小的值,此外瀏覽器的總的連接數(shù)也會有最大限制。具體數(shù)目如下表所示,[數(shù)據(jù)來源]http://www.browserscope.org/?category=network&v=top):

1.3 持久連接
雖然并行連接可以提升加載速度,但是每個HTTP事務(wù)都要新建一個連接有點浪費了。
一個WEB站點通常會打開到同一個站點的連接,比如一個WEB頁面的大部分內(nèi)嵌資源(如圖片,js文件,樣式文件)都是來自同一個WEB站點的,因此,初始化對某服務(wù)器的HTTP請求的應(yīng)用程序很可能會在不久后向那臺服務(wù)器發(fā)起更多的請求,于是,HTTP/1.1允許HTTP設(shè)備在事務(wù)處理結(jié)束后將TCP連接保持在打開狀態(tài),以便未來重用。在事務(wù)處理完成之后仍然保持打開狀態(tài)的TCP連接稱之為持久連接,持久連接會在不同事務(wù)之間保持打開狀態(tài)直到客戶端或服務(wù)器決定將其關(guān)閉。

持久連接和并行連接共用可能是目前最高效的方式,現(xiàn)代瀏覽器都是打開少量的并行連接,然后每個連接都是持久連接的方式。持久連接包括老的HTTP/1.0+的“Keep-Alive”連接以及HTTP/1.1的“persistent”連接。注意一下持久連接和并行連接不要搞混了,持久連接指的是在同一個連接不關(guān)閉情況下處理多個HTTP事務(wù),而并行連接是指多個連接并行的執(zhí)行(當然實際情況是各個連接執(zhí)行有點延遲,但是在執(zhí)行時間上是重疊的)。
管道化連接示意圖如下,可以在第一個事務(wù)的請求還沒有返回的時候發(fā)送后續(xù)請求,性能會有所提升,但是由于實現(xiàn)麻煩和容易出BUG,現(xiàn)代瀏覽器默認都會關(guān)閉管道化連接,這里就不再過多討論。

老的Keep-Alive方式使用非常廣泛,雖然在HTTP/1.1中已經(jīng)不再使用。使用Keep-Alive有一些需要注意的地方:
-
在HTTP/1.0中,
Keep-Alive默認并不會啟用??蛻舳吮仨毎l(fā)送一個Connection:Keep-Alive請求頭部來激活Keep-Alive連接。注意,客戶端的Keep-Alive請求頭部只是請求將連接保持在活躍狀態(tài),服務(wù)端不一定答應(yīng)啟用Keep-Alive會話。HTTP/1.1中默認就是持久連接,除非在請求頭顯示的加了Connection:close來關(guān)閉持久連接。服務(wù)端如果啟用Keep-Alive連接,一般會在響應(yīng)頭部中帶上如下內(nèi)容:Connection:Keep-Alive Keep-Alive:timeout=1800, max=5
表示服務(wù)器最多還能為5個HTTP事務(wù)保持持久連接,Keep-Alive的超時時間為1800秒。在Apache中對應(yīng)配置如下:
```
# KeepAlive: Whether or not to allow persistent connections (more than
KeepAlive On
# MaxKeepAliveRequests: The maximum number of requests to allow
MaxKeepAliveRequests 5
# KeepAliveTimeout: Number of seconds to wait for the next request from the
KeepAliveTimeout 1800
```
Connection:Keep-Alive必須隨每個希望保持持久連接的請求的頭部發(fā)送,如果某個請求沒有帶Keep-Alive頭部,則服務(wù)器會在這個請求后關(guān)閉該連接。此外,如果客戶端想關(guān)閉持久連接了,在請求頭部帶上一個Connection:close即可。Keep-Alive和啞代理之間有一些問題要注意下。一些代理無法理解Connection頭部,應(yīng)該在轉(zhuǎn)發(fā)請求前將Connection頭部去掉(其他需要去掉的頭部還有Prxoy-Authenticate,Proxy-Connection,Transfer-Encoding等),不然容易造成瀏覽器掛起。因為這種啞代理什么都不做,直接轉(zhuǎn)發(fā)所有請求內(nèi)容,會讓服務(wù)器誤以為自己跟客戶端建立了Keep-Alive連接,而啞代理對此一無所知,發(fā)送數(shù)據(jù)后等待服務(wù)器關(guān)閉該連接,啞代理認為這個連接上不會再有請求,客戶端新的請求都會被忽略。網(wǎng)景公司提出的方法是增加一個非標準的Proxy-Connection頭部來可以解決單個啞代理的問題,雖然Proxy-Connection沒有納入到標準中,但是現(xiàn)代瀏覽器大都在使用,比如我在Chrome上安裝了
穿越(大贊這個插件,翻墻速度杠杠的)這個插件后可以翻墻,也就是加了一層代理,這時候我訪問其他站點就會帶上Proxy-Connection頭部而不是Connection頭部。雖然如此,Proxy-Connection對于聰明代理(可以理解Connection的代理稱之為聰明代理)和啞代理共存的情況仍然有問題。
2 apache實例分析
在ubuntu14.04中安裝apache2.4,為了測試方便,以prefork模式運行,設(shè)置參數(shù)如下:
#apache2.conf
KeepAliveTimeout 1800 #一般設(shè)置5秒以內(nèi)即可,以減少內(nèi)存浪費。
KeepAlive On
MaxKeepAliveRequests 2 #設(shè)置
#mpm_prefork.conf
# prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of requests a server process serves
<IfModule mpm_prefork_module>
StartServers 1
MinSpareServers 1
MaxSpareServers 1
MaxRequestWorkers 1
MaxConnectionsPerChild 2
</IfModule>
這里我們開啟KeepAlive,然后設(shè)置最多保持的請求數(shù)為2個(加上初始的請求,一個連接可以最多發(fā)送3個請求),KeepAlive超時為1800秒。注意下prefork的配置,為了方便測試,我們設(shè)置apache子進程數(shù)為1,注意這里的MaxConnectionPerChild,設(shè)置的是2,這是什么意思呢,apache的prefork模式下,可以指定一個子進程最多處理的請求數(shù),到了數(shù)目則殺掉這個子進程,重新創(chuàng)建一個子進程,以防止內(nèi)存泄露。
開啟Keep-Alive的優(yōu)點是可以加快網(wǎng)頁加載速度,減少頻繁建立連接來降低CPU的使用率。缺點是會多占用內(nèi)存,所以如果開啟KeepAlive,那么KeepAliveTimeout不能設(shè)置太大,以免持久連接太多耗光服務(wù)器資源。至于線上環(huán)境是否開啟,視情況而定,如果服務(wù)器內(nèi)存很大配置很高,開不開啟沒多少影響,如果內(nèi)存較小,則建議關(guān)閉節(jié)省內(nèi)存。
要注意的地方來了,開啟KeepAlive后,MaxConnectionPerChild的這里的請求數(shù)怎么算呢?一個持久連接的兩次請求在apache這里算一個請求還是兩個呢?答案是1個,也就是一個持久連接的多次請求在apache的子進程這里只是同1個連接,也就是說,在我們這樣的設(shè)置下,只要間隔沒有超過1800秒發(fā)起請求,前3個請求會共用一個持久連接。
使用chrome(版本55.0.2883.95)發(fā)送一個請求,我們可以看到請求頭和響應(yīng)頭如下(不同瀏覽器頭部可能有所不同,請自行查證):

請求的頭部會帶上一個 Connection:keep-alive,響應(yīng)頭部會帶上 Connection:Keep-Alive; Keep-Alive:timeout=1800, max=2,表示服務(wù)器支持Keep-Alive,且持久連接最大請求數(shù)為2,超時時間為1800秒。接下來繼續(xù)發(fā)送2個請求,可以看到響應(yīng)頭分別帶有Connection:Keep-Alive; Keep-Alive:timeout=1800, max=1和Connection:close,也就是第3次請求之后,這個持久連接關(guān)閉,接下來瀏覽器需要另外跟apache建立起一個新的連接。
注意,由于一個持久連接的3次請求是同一個連接,我們在prefork中設(shè)置的apache一個子進程最多可以處理2個請求,那么apache什么時候才會殺死這個子進程并創(chuàng)建一個新的子進程呢?答案是在瀏覽器在1800秒內(nèi)發(fā)起6次請求后,因為瀏覽器的6次請求是在2個持久連接里面發(fā)送的,在apache這里計數(shù)只是2次連接。
如果我們把KeepAliveTimeout改成3秒,發(fā)送一次請求后,間隔超過3秒才發(fā)下一次請求,則由于超時,此時第二個請求就是一個新的連接了,由前面的實驗可以知道,apache那邊還是同一個子進程來處理這個新的連接。再過3秒后發(fā)送一個請求,這個時候apache就會殺死之前的那個子進程并創(chuàng)建一個新的子進程來建立連接處理請求了。
如果關(guān)閉KeepAlive,則每次請求都是一個新的連接,那么apache子進程建立的連接數(shù)就和你發(fā)送請求數(shù)是一樣的了。
PS: 可以通過watch命令來定時查看apache的進程變化 watch -n 1 "ps aux|grep apache|grep -v grep"。