FackBook的BigPiple

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 後,瀏覽器解析和執行他們

瀏覽器先發送請求,然後服務器進行查找數據,生成頁面,返回html 代碼,最後瀏覽器進行渲染頁面。這種模式有非常明顯的缺陷:流程中的操作有着嚴格的順序,如果前面的一個操作沒有執行結束,後面的操作就不能執行,即操作之間是不能重疊。這樣就造成性能的瓶頸:服務器生成一個頁面的內容時,瀏覽器是空閒的,顯示空白內容;而當瀏覽器加載渲染頁面內容時,服務器又是空閒的,時間與性能的浪費由此產生。

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 的不同操作階段就可以像流水線一樣進行執行了。

BigPipe 打破了原有的順序執行,將頁面分成不同的pagelet ,如此一來,所有的pagelet 的執行時間累加起來還是原有的時間。但是, 通過疊加不同pagelet 的不同階段的執行時間,使總的運行時間大大減少,這就是Bigpipe減少頁面加載時間的祕密

BigPipe實現原理

瞭解了BigPipe 的核心思想後,我們討論它的實現原理。當瀏覽器訪問服務器時,服務器接受請求並對其進行檢查。如果請求有效,服務器端不做任何的查詢,而是立刻返回一個http request 給瀏覽器,內容是一段html 代碼,包括html<head> 標籤和<body> 標籤的一部分。<head>標籤包括BigPipe 的js文件和css文件,這個js 文件用來解析後面接收的http response,因爲後面傳輸的內容都爲js腳本。未封閉的<body>標籤中,是顯示頁面的邏輯結構和pagelet 的佔位符的模板,例如:

<body>

<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>

上述模板使用css-div 描述了頁面的結構,不同的div 標籤對應不同的pagelet,id 對應了pagelet 的名稱。將這個response 返回給瀏覽器後,服務器開始對每個pagelet 的內容進行查詢,加載,生成。當一個pagelet的內容生成好,立刻調用flush()函數,將其返回給客戶端,傳輸的數據是以json 格式的,包括這個pagelet 需要的CSS 和JS,以及html 內容和一些元數據。例如:

<script type=”text/javascript”>

big_pipe.onPageletArrive(

{id:”pagelet_composer”,

content:”<HTML>”,

css:”[..]“,

js:”[..]“,

…}

);

</script>

其中”content”表示這個pagelet 的內容,是html 源碼,特殊字符如“”/需要進行轉義;”id”表示content要顯示的位置,即爲對應的pagelet 的id標籤;”css”表示需要下載的CSS 資源的路徑;”js”表示需要下載的JS 腳本的路徑。爲了避免文件路徑過長,所以在前面需要對css 和js 文件的路徑進行轉換,轉換後爲5 位字符串:不同的pagelet 可能會加載同一個css 或js 文件,所以要避免重複下載。

雖然每個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源碼。從用戶的角度看來,頁面時逐步呈現的。初始的頁面顯示的更快,可以有效減短用戶感覺到的延遲。

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()函數進行批處理請求,把本來應該串行的請求訪問併發的執行。可以這樣寫:

do {
 
$mrc = curl_multi_exec($mh, $active);
 
}
 
while($mrc==CURLM_CALL_MULTI_PERFORM);
 
while ($active &amp;&amp; $mrc == CURLM_OK){
 
if (curl_multi_select($mh) != -1){
 
do {
 
$mrc = curl_multi_exec($mh,$active);
 
}
 
while($mrc==CURLM_CALL_MULTI_PERFORM);
 
}
 
}

但是會碰到一個問題,多個請求是同時返回結果的。當所有的pagelet 的頁面請求所消耗的時間差不多時,可以達到很好的性能,但是當有的消耗時間很長(執行一條複雜的查詢)的情況下,批處理就會阻塞在那裏,等每個請求都返回結果了才結束。而在這段時間致服務器會阻塞在那裏不返回任何內容,而瀏覽器更是沒有響應,這樣就違背了BigPipe 的原理。另外一種實現方法是使用stream_select函數。跟上一種方法類似,不過可以使用PHP5中新增的stream_socket_client()函數鏈接而不是之前PHP函數中的fsocketopen()函數。

while (count($sockets)) {
 
$read = $write = $sockets;
 
$n = stream_select($read,$write, $e, $timeout);
 
if ($n &gt; 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;
 
}
 
}

這樣實現也可以做到服務器的併發訪問,但是會碰到和上一種方法同樣的問題:服務器的阻塞問題。所以,可以採用另一種方法,用多進程模擬多線程。使用PHP 的擴展模塊pctnl模塊中的pcntl_fork()函數來生成子進程, 用不同的子進程去處理不同的pagelet 的頁面內容。如果子進程返回內容,則返回給瀏覽器。或者,修改curl模塊。使其可以支持回調函數,當併發請求中一個請求完成時,立刻調用回調函數。這兩種方法目前還在探索中。

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文件的路徑保存下來,在判斷所有的內容加載完成後統一向服務器發送請求。

BigPipe具體實現細節

如上文討論的那樣,具體實現如下:當用戶訪問該頁面時,在第一個flush 的Response 內容中,返回大部分的HTML 代碼,包括完整的<heaad>標籤,和一個未封閉的<body>,其中<head>標籤中有需要導入的文件的路徑,如一些公共的css 文件和BigPipe.js 文件,<body>標籤有頁面的主要佈局,第二塊flush 的內容爲一段js腳本,處理BigPipe 對象的生成,以及js 和css 文件的路徑和字符串的映射

var bigPipe = new bigPipe();

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 的內容了,例如:

<script type=”text/javascript” >

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 的是

</body>

</html>

即爲最後的標籤。

結論

經過上面的討論,我們可以發現,使用BigPipe 技術優化頁面可以有四個好處:

1. 減少頁面的加載時間

2. 使頁面分步輸出,改善用戶體驗

3. 使頁面結構化,提高可讀性,更加便於維護

4. 每個pagelet 都是相互獨立的,如果有一個pagelet 的內容不能加載,並不會影響其他的pagelet 的內容顯示。

同時,BigPipe 是一項比較新的理念, 在去年六月份才由Facebook 的工程師提出,應該說有很大的發展空間。BigPipe 的原理非常簡單,並不會引入很多額外的負擔,適用範圍很廣,容易上手。幾乎所有的網頁都可以採用BigPipe 的理念去進行優化,尤其對於是有着海量數據和網頁比較大的網站,將會以低成本帶來高回報。一般來講,網站越大,腳本和樣式表越多,瀏覽器版本越舊,網絡環境越差,優化的結果越可觀。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章