BigPipe學習研究
1. 技術背景—FaceBook頁面加載技術
試想這樣一個場景,一個經常訪問的網站,每次打開它的頁面都要要花費6秒;同時另外一個網站提供了相似的服務,但響應時間只需3 秒,那么你會如何選擇呢?數據表明,如果用戶打開一個網站,等待3~4 秒還沒有任何反應,他們會變得急躁,焦慮,抱怨,甚至關閉網頁并且不再訪問,這是非常糟糕的情況。所以,網頁加載的速度十分重要,尤其對于擁有遍布全球的5億用戶的Facebook(全球最大的社交服務網站)這樣的大型網站,有著大量并發請求、海量數據等客觀情況,速度就成了必須攻克的難題之一。
2010年初的時候,Facebook 的前端性能研究小組開始了他們的優化項目,經過了六個月的努力,成功的將個人空間主頁面加載耗時由原來的5 秒減少為現在的2.5 秒。這是一個非常了不起的成就,也給用戶來帶來了很好的體驗。在優化項目中,工程師提出了一種新的頁面加載技術,稱之為Bigpipe。目前淘寶和Facebook面臨的問題非常相似:海量數據和頁面過大,如果可以在詳情頁、列表頁中使用bigpipe,或者在webx中集成bigpipe,將會帶來明顯的頁面加載速度提升。
2. 相關介紹
2.1 網站前端優化的重要性
《高性能網站建設指南》一書中指出,只有10%~20%的最終用戶響應時間是花費在從Web服務器獲取HTML文檔并傳送到瀏覽器中的。如果希望能夠有效地減少頁面的響應時間,就必須關注剩余的80%~90%的最終用戶體驗。做個比較,如果對后臺業務邏輯進行優化,效率提高了50%,但最終的頁面響應時間只減少了5%~10%,因為它所占的比重較少。如果對前端進行性能優化,效率提升50%,則會使最終頁面響應時間減少40%~45%。這是多么可觀的數字!另外,前端的性能優化一般比業務邏輯的優化更加容易。所以,前端優化投入小,見效快,性價比極高,需要投入更多的關注。
2.2 BigPipe與AJAX
Web2.0的重要特征是網頁顯示大量動態內容,即web2.0注重網頁與用戶的交互。其核心技術是AJAX,如今所有主流網站都或多或少使用AJAX。與AJAX類似,BigPipe實現了分塊兒的概念,使頁面能夠分步輸出,即每次輸出一部分網頁內容。接下來討論BigPipe與AJAX的區別。
簡單的說,BigPipe比AJAX有三個好處:
1. AJAX 的核心是XMLHttpRequest,客戶端需要異步的向服務器端發送請求,然后將傳送過來的內容動態添加到網頁上。如此實現存在一些缺陷,即發送往返請求需要耗費時間,而BigPipe技術使瀏覽器并不需要發送XMLHttpRequest請求,這樣就節省時間損耗。
2. 使用AJAX時,瀏覽器和服務器的工作順序執行。服務器必須等待瀏覽器的請求,這樣就會造成服務器的空閑。瀏覽器工作時,服務器在等待,而服務器工作時,瀏覽器在等待,這也是一種性能的浪費。使用BigPipe,瀏覽器和服務器可以并行同時工作,服務器不需要等待瀏覽器的請求,而是一直處于加載頁面內容的工作階段,這就會使效率得到更大的提高。
3. 減少瀏覽器發送到請求。對一個5億用戶的網站來說,減少了使用AJAX額外帶來的請求,會減少服務器的負載,同樣會帶來很大的性能提升。
基于以上三點,Facebook 在進行頁面優化時采用了BigPipe技術。目前淘寶主搜索結果頁中,需要加載類目,相關搜索,寶貝列表,廣告等內容,前端這里使用php的curl的批處理來并發的訪問引擎獲取相應的數據,并進行分步輸出。這種模式還是與bigpipe有些不同,這點后面會講到。一般來講,在頁面比較大,而且比較復雜,樣式表和腳本比較多的情況下,使用BigPipe來優化輸出頁面是比較合適的。另外非常重要的一點,BigPipe并不改變瀏覽器的結構與網絡協議,僅使用JS就可以實現,用戶不需要做任何的設置,就會看到明顯的訪問時間縮短。
3. 目前的問題
接下來討論現有的瓶頸。面對網頁越來越大的情況,尤其是大量的css文件和js文件需要加載,傳統的頁面加載模型很難滿足這樣的需求,直接結果就是頁面加載速度變慢,這絕不是我們希望看到的。目前的技術實現中,用戶提出頁面訪問請求后,頁面的完整加載流程如下:
1. 用戶訪問網頁,瀏覽器發送一個HTTP請求到網絡服務器。
2. 服務器解析這個請求,然后從存儲層取數據,接著生成一個html文件內容,并在一個HTTP Response中把它傳送給客戶端。
3. HTTP response在網絡中傳輸。
4. 瀏覽器解析這個Response ,創建一個DOM樹,然后下載所需的CSS和JS文件。
5. 下載完CSS 文件后,瀏覽器解析他們并且應用在相應的內容上。
6. 下載完JS 后,瀏覽器解析和執行他們
圖1
完整流程見圖1。圖中左側表示服務器,右側表示瀏覽器。瀏覽器先發送請求,然后服務器進行查找數據,生成頁面,返回html代碼,最后瀏覽器進行渲染頁面。這種模式有非常明顯的缺陷:流程中的操作有著嚴格的順序,如果前面的一個操作沒有執行結束,后面的操作就不能執行,即操作之間是不能重疊。這樣就造成性能的瓶頸:服務器生成一個頁面的內容時,瀏覽器是空閑的,顯示空白內容;而當瀏覽器加載渲染頁面內容時,服務器又是空閑的,時間與性能的浪費由此產生。
圖2
考慮圖2中現有的服務模型,橫軸表示花費的時間。黃色表示在服務器的生成頁面內容的時間,白色表示網絡傳輸時間,藍色表示在瀏覽器渲染頁面的時間。可以看出,現有的模式造成很大的時間浪費。考慮圖3中的情況,圖中綠色表示服務器從春儲層取查數據花費的時間,在海量數據下,當執行一條很費時的查詢語句時(如下圖右側),服務器就就阻塞在那里沒有其他操作,而瀏覽器更是得不到任何反饋。這會造成非常不友好的用戶體驗,用戶不知道什么原因使他們等待很長時間。
圖3
4. BigPipe思想與原理
面對上述問題,我們看下BigPipe的解決辦法。BigPipe提出分塊的概念,即根據頁面內容位置的不同,將整個頁面分成不同的塊兒—稱為pagelet。該技術的設計者Changhao Jiang是研究電子電路的博士,可能從微機上得到了啟發,將眾多pagelet加載的不同階段像流水線一樣在瀏覽器和服務器上執行,這樣就做到了瀏覽器和服務器的并行化,從而達到重疊服務器端運行時間和瀏覽器端運行時間的目的。使用BigPipe不僅可以節省時間,使加載的時間縮短,而且可以同過pagelet的分步輸出,使一部分的頁面內容更快的輸出,從而獲得更好的用戶體驗。BigPipe中,用戶提出頁面訪問請求后,頁面的完整加載流程如下:
1. Request parsing:服務器解析和檢查http request
2. Datafetching:服務器從存儲層獲取數據
3. Markup generation:服務器生成html 標記
4. Network transport : 網絡傳輸response
5. CSS downloading:瀏覽器下載CSS
6. DOM tree construction and CSS styling:瀏覽器生成DOM 樹,并且使用CSS
7. JavaScript downloading: 瀏覽器下載頁面引用的JS文件
8. JavaScript execution: 瀏覽器執行頁面JS代碼
這個8 個流程幾乎與上文中提到現有的模式沒有區別,但這整個流程只是一個pagelet 的完整流程,而多個pagelet 的不同操作階段就可以像流水線一樣進行執行了。
圖4
圖4 中,可以看出BigPipe對原有的模式進行的改進。瀏覽器發送訪問請求,然后瀏覽器分步返回不同的pagelet的內容,具體實現將在后面介紹。考慮圖5中的改進,BigPipe 打破了原有的順序執行,將頁面分成不同的pagelet ,如此一來,所有的pagelet 的執行時間累加起來還是原有的時間。但是, 通過疊加不同pagelet 的不同階段的執行時間,使總的運行時間大大減少,這就是Bigpipe減少頁面加載時間的秘密。
FaceBook的頁面被分成了很多不同的pagelets,如圖:
圖5
5. BigPipe實現原理
了解了BigPipe 的核心思想后,我們討論它的實現原理。當瀏覽器訪問服務器時,服務器接受請求并對其進行檢查。如果請求有效,服務器端不做任何的查詢,而是立刻返回一個http response給瀏覽器,內容是一段html代碼,包括html<head> 標簽和<body> 標簽的一部分。<head>標簽包括BigPipe的js文件和css文件,這個js文件用來解析后面接收的http response,因為后面傳輸的內容都為js腳本。未封閉的<body>標簽中,是顯示頁面的邏輯結構和pagelet 的占位符的模板,例如:
<div></div>
<div></div>
<div></div>
<div>
<div>
<div id=”hotnews”></div>
<div id=”societynews”></div>
<div id=”financialnews”></div>
<div id=”ITnews”></div>
<div id=”sportsnews”></div>
</div>
<div></div>
</div>
<div></div>
big_pipe.onPageletArrive(
{id:”pagelet_composer”,
content:”<HTML>”,
css:”[..]“,
js:”[..]“,
…}
);
</script>
雖然每個pagelet都有要加載的js 文件,但是所有的js文件都是在最后加載,這樣有利于加快頁面加載速度。客戶端,當通過調用“onPageletArrive(json)”函數,第一次影響傳輸的JS腳本中的函數解析了傳入的json數據,接著下載需要的CSS,然后把html內容顯示到響應的div標簽位置上。多個pagelets 的CSS文件可以同時下載,CSS 下載完成的pagelet先顯示。
在BigPipe中,js被給予了比CSS和content更低的優先級。這樣, 只有當所有的pagelets都顯示了,BigPipe才開始去下載JS文件。所有的JS 文件都下載完成后,Pagelets的JS初始化代碼開始執行,按照下載完成時間的先后順序。在這個高度并行的系統中,幾個的pagelet 所要執行的不同的階段可以同時執行。例如,瀏覽器可以給兩個pagelets下載CSS 資源,同時瀏覽器可以渲染另外一個pagelet 的內容,同時服務器仍然在為另一個pagelet生成html源碼。從用戶的角度看來,頁面是逐步呈現的。初始的頁面顯示的更快,可以有效減短用戶感覺到的延遲。
6. BigPipe實現問題討論
6.1 服務器端的并行化
理想情況下,服務器端的實現是并行處理不同的pagelet 的內容,這樣可以提升性能。服務器并發處理多個pagelet的內容時,一個pagelet內容生成好了,立刻將其flush給瀏覽器。但是PHP是不支持線程,所以服務器無法利用多線程的概念去并發的加載多個pagelet 的內容。對于小型網站來說,使用串行的加載pagelet的內容就已經可以達到優化的要求了。對于大型網站,為了達到更快的速度,服務器端可以選擇并發的獨立不同的pagelet 的內容,具體實現有以下幾種方式:
1. java 多線程。后臺邏輯使用java,可以使用java的多線程機制去同時加載不同的pagelet 的內容,加載完成后加頁面內容返回給瀏覽器。在最后的引用部分可以看到網上用java多線程實現的例子。
2. 使用PHP實現。PHP不支持線程,無法像java使用多線程的機制來并發處理不同pagelet的內容。但是,Facebook 和淘寶主搜索的業務邏輯是用PHP實現的,所以我們必須考慮如何在PHP下完成并發處理。PHP 擴展中有curl 模塊,可以在該模塊中curl_multi_fetch()函數進行批處理請求,把本來應該串行的請求訪問并發的執行。可以這樣寫:
$mrc = curl_multi_exec($mh, $active);
}
while($mrc==CURLM_CALL_MULTI_PERFORM);
while ($active && $mrc == CURLM_OK){
if (curl_multi_select($mh) != -1){
do {
$mrc = curl_multi_exec($mh,$active);
}
while($mrc==CURLM_CALL_MULTI_PERFORM);
}
}
$read = $write = $sockets;
$n = stream_select($read,$write, $e, $timeout);
if ($n > 0) {
foreach ($read as $r) {
$id = array_search($r, $sockets);
$data = fread($r, 8192);
if (strlen($data) == 0) {
fclose($r);
unset ($sockets[$id]);
}else {
$retdata[$id] .= $data;
}
}
$retdata[$id] = preg_replace('/^HTTP(.*?)\r\n\r\n/is',<em>, $retdata[$id]);</em>
foreach ($write as $w) {
if (!is_resource($w))continue;
$id = array_search($w, $sockets);
fwrite($w, "GET /" . $url[$id] . "HTTP/1.0\r\nHost: " . $hosts[$id] ."\r\n\r\n");
$status[$id] = 1;
}
}else {
break;
}
}
6.2 直接調用flush函數輸出
到這里,可能會有這樣的疑問,為什服務器不直接把生成好的HTML內容分部flush()返回給客戶端,而是使用json 格式傳遞,然后用js解析呢?這不是多此一舉么?實際上,這也是目前主搜索前端使用的方法。我們看看使用BigPipe方式的兩大好處:
(1) 如果直接調用flush()函數輸出html 源碼,當模塊較多的情況,模塊間必須按順序加載,在html 前面的模塊必須先加載完,后面的才能加載,這樣也就沒辦法每個模塊同時顯示一些內容。例如下面的html:
上面3 個div分別代表3個模塊,如果直接分部輸出html ,服務器端必須先加載完畢div1模塊中的內容并flush 出去后,才能繼續加載div2的內容,如果flush 順序不一樣,輸出的html 結構肯定就會出問題,這樣就導致前臺頁面沒辦法同時顯示3 個loading。因為這樣flush必須要有先后順序。而如果采用JS的話,可以前臺顯示3 個loading,而且不需要關心到底哪個模塊先加載完,這樣還能發揮后臺多線程處理數據的優勢。
(2) 使用JS這種方式可以是頁面結構更加清晰,管理更加方便。同時做到了頁面邏輯結構和數據解耦,首先返回的是頁面的結構,接著不斷地返回js腳本,然后動態添加頁面內容,而不是所有完整的html源碼一起輸出,增加了可維護性。
6.3 訪問者是爬蟲或者訪問者瀏覽器禁止使用JS的情況
我們知道BigPipe使用js 腳本加載頁面,那么當用戶在瀏覽器里設置禁止使用js腳本(雖然人數很少),就會造成加載頁面失敗,這同樣是非常不好的用戶體驗。對搜索引擎的爬蟲來講,同樣會遇到類似的問題。解決辦法是當用戶發送訪問請求時,服務器端檢測user-agent和客戶端是否支持js腳本。如果user-agent顯示是一個搜索引擎爬蟲或者客戶端不支持js,就不使用BigPipe ,而用原有的模式,從而解決問題。
6.4 對SEO的影響
這是一個必須考慮的問題,如今是搜索引擎的時代,如果網頁對搜索引擎不友好,或者使搜索引擎很難識別內容,那么會降低網頁在搜索引擎中的排名,直接減少網站的訪問次數。在BigPipe中,頁面的內容都是動態添加的,所以可能會使搜索引擎無法識別。但是正如前面所說,在服務器端首先要根據user-agent判斷客戶端是否是搜索引擎的爬蟲,如果是的話,則轉化為原有的模式,而不是動態添加。這樣就解決了對搜索引擎的不友好。
6.5 融合其他技術
除了使用BigPipe,Facebook的頁面加載技術還融合了其他的頁面優化技術,具體如下:
6.5.1 資源文件的G-zip壓縮
這是非常重要的技術,使用G-zip對css和js文件壓縮可以使大小減少70%,這是多么誘人的數字!在網絡傳輸的文件中,主要就是樣式表和腳本文件。如此可以大大減小傳輸的內容,使頁面加載速度變得更快。具體實現可以借助服務器來進行,例如Apache,使用mod_deflate模塊來完成具體配置為: AddOutputFilterByType DEFLATE text/html text/css application/xjavascript
6.5.2 將js文件進行了精簡
對js文件進行精簡,可以從代碼中移除不必要的字符,注釋以及空行以減小js文件的大小,從而改善加載的頁面的時間。精簡js 腳本的工具可以使用JSMin,使用精簡后的腳本的大小會減少20%左右。這也是一個很大的提升。
6.5.3 將css和js文件進行合并
這是前端優化的一項原則,將多個樣式表和js文件進行合并,這樣的話,將會減少http的請求個數。對于上億用戶的網站來說,這也會帶來性能的提升,大約會減少5%左右的時間損耗。
6.5.4 使用外部JS和CSS
同樣是前端優化的一項原則。純粹就速度來言,使用內聯的js和css速度要更快,因為減少了http請求。但是,使用外部的文件更有利于文件的復用,這與面向對象編程的概念很像。更為重要的是,雖然在第一次的加載速度慢一點,但css文件和js腳本是可以被瀏覽器緩存。即之后用戶的多次訪問中,使用外部的js和css將會將會更好的提升速度。
6.5.5 將樣式表放在頂部
和上面內容相似,這也是一種規范,將html 內容所需的css 文件放在首部加載是非常重要的。如果放在頁面尾部,雖然會使頁面內容更快的加載(因為將加載css文件的時間放在最后,從而使頁面內容先顯示出來),但是這樣的內容是沒有使用樣式表的,在css 文件加載進來后,瀏覽器會對其使用樣式表,即再次改變頁面的內容和樣式,稱之為“無樣式內容的閃爍”,這對于用戶來說當然是不友好的。實現的時候將css文件放在<head>標簽中即可。
6.5.6 將腳本放在底部實現“barrier”
支持頁面動態內容的Js腳本對于頁面的加載并沒有什么作用,把它放在頂部加載只會使頁面更慢的加載,這點和前面的提到的css文件剛好相反,所以可以將它放在頁尾加載。是用戶能看到的頁面內容先加載,js文件最后加載,這樣會使用戶覺得頁面速度更快。Bigpipe實現一個“barrier”的概念,即當所有的pagelet的內容全部加載好了之后,瀏覽器再向服務器發送js的http請求。可以在BigPipe.js 中將所有的pagelet 所需的js文件的路徑保存下來,在判斷所有的內容加載完成后統一向服務器發送請求。
7. BigPipe具體實現細節
如上文討論的那樣,具體實現如下:當用戶訪問該頁面時,在第一個flush的Response內容中,返回大部分的HTML代碼,包括完整的<heaad>標簽,和一個未封閉的<body>,其中<head>標簽中有需要導入的文件的路徑,如一些公共的css文件和BigPipe.js文件,<body>標簽有頁面的主要布局,第二塊flush的內容為一段js腳本,處理BigPipe對象的生成,以及js和css文件的路徑和字符串的映射。
bigPipe.setResourceMap({
aaaaa:{
“name”: “js/list1.js”,
“type”: “js”,
“src”: “js/list1.js”
}
);
setResourceMap(json)為BigPipe 中的函數,功能是設置文件的映射。”aaaaa”應該是在服務器隨即生成的五位字符串,name表示文件名稱,type為文件的類型,可以是”js”或”css”,”src”為文件的路徑。在下面的頁面中,就可以使用”aaaaa”來替代”js/list1.js”了,減少了復雜性。接下來flush 的是每一個pagelet 的內容了,例如:
bigPipe.onPageletArrive({
id:”list1″,
content:”this is list 1 <\/br><img src =\”img13.jpg\” \/>”,
css:["eeeee"],
js:["aaaaa"],
“resource_map”:{
aaaaa:{
“name”: “js/list1.js”,
“type”: “js”,
“src”: “js/list1.js”
} ,
“eeeee”: {
“name”: “css/list1.css”,
“type”: “css”
“src”: “css/list1.css”
}
}
});
</script>
onPageletArrive(json_arrive)也是BigPipe 的函數,功能是動態添加頁面的內容和加載pagelet 所需的文件,函數的參數為json格式的數據。其參數含義是:“id”用來尋找pagelet 標簽;“content”是html 頁面內容,在找到對應的pagelet的標簽之后,將content內動態添加到html頁面中;“css”為該Pagelet 所需的css文件,這里的css文件可能在之前導入過了;“js”為該pagelet所需的js文件,同樣,有可能在之前的pagelet已經導入過了。在函數實現過程中,因為js文件是最后加載的,可以把這些js的路徑存入到一個數組當中(去掉重復的),在最后一起加載。resource_map”為該pagelet 所單獨需要加載的js和css文件,同樣也是json格式的,結構與前面的setResource()中的參數一樣。最后flush的是:
</html>
即為最后的標簽。
8. 結論
經過上面的討論,我們可以發現,使用BigPipe技術優化頁面可以有四個好處:
1.減少頁面的加載時間。
2.使頁面分步輸出,改善用戶體驗。
3.使頁面結構化,提高可讀性,更加便于維護。
4.每個pagelet都是相互獨立的,如果有一個pagelet的內容不能加載,并不會影響其他的pagelet的內容顯示。
同時,BigPipe是一項比較新的理念, 在去年六月份才由Facebook的工程師提出,應該說有很大的發展空間。BigPipe 的原理非常簡單,并不會引入很多額外的負擔,適用范圍很廣,容易上手。幾乎所有的網頁都可以采用BigPipe的理念去進行優化,尤其對于是有著海量數據和網頁比較大的網站,將會以低成本帶來高回報。一般來講,網站越大,腳本和樣式表越多,瀏覽器版本越舊,網絡環境越差,優化的結果越可觀。
9. 引用與參考資料
1. 作者的博客:http://www.facebook.com/note.php?note_id=389414033919
2. bigpipe技術的ppt:http://twork.taobao.net/books/237
3. bigpipe的java實現:http://codemonkeyism.com/facebook-bigpipe-java/
4. 一篇介紹bigpipe的文章:http://www.54chen.com/architecture/rose-pipe-http-54chen.html
5. 另一篇挺有用的文章:http://www.cnblogs.com/BearsTaR/archive/2010/06/18/facebook_html_chunk.html
6. 人人網類似bigpipe的技術–rosepipe:http://www.54chen.com/architecture/rose-open-source-portal-framework.html
7.《高性能網站建設指南》by Steve Souder, Copyright 2007 Steve Sounder, 978-0-596- 52930-7。