楊建:網站加速--Cache爲王篇

楊建:網站加速--Cache爲王篇(2008-12-08 20:14:58)
--提升性能的同時爲你節約10倍以上成本
From: http://blog.sina.com.cn/iyangjian

一,Cache, 王道也
二,Cache 基本原理介紹
三,我劃分的3個刷新級別
四,我對HTTP協議做的一點創新(?maxage=6000000)
五,Yslow優化網站性能的14條軍規點評
六,上線了 !=  Finished
七,提速度同時節約成本方法彙總
-----------------------------------------------------------------------------------------

一,Cache,王道也

我覺得系統架構不應該僅僅是搭建一個強硬的能承受巨大併發壓力的後臺,前端頁面也是需要架構的而且同等重要,不理解前臺的的後臺工程師是不合格的。中國人講究鋼柔相濟,後臺強硬只能說你內功深厚,前端用的巧,那叫四兩撥千斤。

一般後臺工程師很少關心前端如何使用自己的資源,而前端工程師,不知道自己的一個簡單的用法會對後端造成多大影響。我會給出一些數據,來震撼下你的眼球。

二,Cache 基本原理介紹 (參考Caching Tutorial)

爲什麼使用Cache?
1,減少延遲,讓你的網站更快,提高用戶體驗。
2,避免網絡擁塞,減少請求量,減少輸出帶寬。
補充一個cache的原則:不更新的資源就不應該讓它再次產生HTTP請求,如果強制產生了請求,那麼就看看能否返回304。

Cache的種類?
瀏覽器Cache,代理Cache,網關Cache。
後端還有 disk cache ,server cache,php cache,不過不屬於我們今天討論範圍。

Cache如何工作的?
1,如果響應頭告訴cache別緩存它,cache不對它做緩存;
2,如果請求需要驗證的或者是需要安全性的,它將不被緩存;
3,如果響應頭裏沒有ETag或Last-Modifed header這類元素,而且也沒有任何顯式的信息告訴如何對數據保鮮,則它被認爲不可緩存。
4,在下面情況下,一個緩存項被認爲是新鮮的(即,不需到原server上檢查就可直接發送給client):
    它設置了一個過期時間或age-controlling響應頭,而且現在仍未過期。
    如果瀏覽器cache裏有某個數據項,並且被被設置爲每個會話(session)過程中只檢查一次;
    如果一個代理cache裏能找個某個數據項,並且它是在相對較長時間之前更新過的。
    以上情況會認爲數據是新鮮的,就直接走cache,不再查詢源server。
5,如果有一項過期了,它將會讓原server去更新它,或者告訴cache這個拷貝是否還是可用的。

怎麼控制你的Cache?
Meta tags :在html頁面中指定,這個方法只被少數瀏覽器支持,Proxy一般不會讀你html的具體內容然後再做cache決策的。

Pragma: no-cache : 一般被大家誤用在http響應頭中,這不會產生任何效果。而實際它僅僅應該用在請求頭中。不過google的Server: GFE/1.3 響應中卻這樣用,難道人家也誤用了呢。

Date: 當前主機GMT時間。

Last-Modified : 文件更新GMT時間,我在響應頭中帶上這個元素的時候,通常瀏覽器在cache時間內再發請求都會稍帶上If-Modified-Since,讓我們判斷需要重新傳輸文件內容,還是僅僅返回個304告訴瀏覽器資源還沒更新,需要緩存策略的服務器肯定都得支持的。有了這個請求,head請求在基本沒太多用處了,除非在telnet上調試還能用上。

If-Modified-Since :  用在請求頭裏,見Last-Modified 。

Etag: 標識資源是否發生變化,etag的生成算法各是各樣,通常是用文件的inode+size+LastModified進行Hash後得到的,可以根據應用選擇適合自己的。Last-Modified 只能精確到秒的更新,如果一秒內做了多次更新,etag就能派上用場。貌似大家很少有這樣精確的需求,浪費了http header的字節數,建議不要使用。

Expires :  指定緩存到期GMT的絕對時間,這個是http 1.0裏就有的。這個元素有些缺點,一,服務器和瀏覽器端時間不一致時會有問題。二,一旦失效後如果忘記重新設置新的過期時間會導致cache失效。三,服務器端需要根據當前Date時間 + 應該cache的相對時間去計算這個值,需要cpu開銷。 我不推薦使用。

Cache-Control:
這個是http 1.1中爲了彌補 Expires 缺陷新加入的,現在不支持http 1.1的瀏覽器已經很少了。
max-age: 指定緩存過期的相對時間秒數,max-ag=0或者是負值,瀏覽器會在對應的緩存中把Expires設置爲1970-01-01 08:00:00 ,雖然語義不夠透明,但卻是我最推薦使用的。
s-maxage: 類似於max-age,只用在共享緩存上,比如proxy.
public: 通常情況下需要http身份驗證的情況,響應是不可cahce的,加上public可以使它被cache。
no-cache: 強制瀏覽器在使用cache拷貝之前先提交一個http請求到源服務器進行確認。這對身份驗證來說是非常有用的,能比較好的遵守 (可以結合public進行考慮)。它對維持一個資源總是最新的也很有用,與此同時還不完全喪失cache帶來的好處,因爲它在本地是有拷貝的,但是在用之前都進行了確認,這樣http請求並未減少,但可能會減少一個響應體。
no-store:  告訴瀏覽器在任何情況下都不要進行cache,不在本地保留拷貝。
must-revalidate: 強制瀏覽器嚴格遵守你設置的cache規則。
proxy-revalidate: 強制proxy嚴格遵守你設置的cache規則。
用法舉例:  Cache-Control: max-age=3600, must-revalidate

其他一些使用cache需要注意的東西,不要使用post,不要使用ssl,因爲他們不可被cache,另外保持url一致。只在必要的地方,通常是動態頁面使用cookie,因爲coolie很難cache。至於apache如何支持cache和php怎麼用header函數設置cache,暫不做介紹,網上資料比較多。

如何設置合理的cache時間 ?
http://image2.sinajs.cn/newchart/min/n/sz000609.gif?1230015976759
拿我分時圖舉例,我們需要的更新頻率是1分鐘。但爲了每次都拿到最新的資源,我們在後面加了個隨機數,這個數在同一秒內的多次刷新都會變化。我們的js雖然能夠很好的控制,一分鐘只請求一次,但是如果用戶點了刷新按紐呢?這樣的調用是完全cache無關的,連返回304的機會都沒有。

試想,如果很多人通過同一個代理出去的,那麼所有的請求都會穿透代理,弄不好被網管封掉了。如果我們做只做一秒的cache,對直接訪問源服務器的用戶沒太多影響,但對於代理服務器來說,他的請求可能會從10000 req/min 減少爲 60 req/min ,這是160倍。

對於我們行情圖片這樣的情況,刷新頻率爲1分鐘,比較好的做法是把後面的隨機數(num)修改爲 num=t-t%60 其中t是當前時間戳,這樣你一分鐘內刷這個url是不變的,下一分鐘會增加1,會再次產生一個新請求。而我的max-age設置爲默認59秒,即使設置120秒其實也沒什麼影響。可能你會說萬一趕上臨界點可能拿不到最新的數據,其實對用戶來說,用那個多變的隨即數和我這個分鐘級的隨即數,看到的效果是相同的下面我給你分析一下:如果用戶打開了我們的分時間頁面,當前隨即數對他來說是新的,所以他會拿到一個當前最新的圖片,然後他點了刷新按紐,用戶會產生http請求,即使url 沒變,服務器有最新圖片也一定會返回,否則返回304,一分鐘後js刷新圖片,分鐘數加了1,會得到全新資源。這和那個隨時變化的隨即數效果有區別嗎?都拿到了最新的數據,但是卻另外收益了cache帶來的好處,對後端減少很多壓力。

三,我劃分的3個刷新級別

名詞解釋 全新請求: url產生了變化,瀏覽器會把他當一個新的資源(發起新的請求中不帶If-Modified-Since)。

1,在地址欄中輸入http://sports.sinajs.cn/today.js?maxage=11地址按回車。重複n次,直到cache時間11秒過去後,才發起請求,這個請求是全新的,不帶If-Modified-Since。

2,按F5刷新.  發起一個全新的請求,然後按F5會產生一個帶If-Modified-Since的請求,如果返回304,將不再發起新的請求,直到第一次請求設置的cache過期,然後發起一個全新的請求。

3, ctrl+F5 ,總會發起一個全新的請求。

下面是按F5刷新的例子演示: http://sports.sinajs.cn/today.js?maxage=11
( 如果這個值大於瀏覽器最大cache時間maxage,將以瀏覽器最大cache爲準)

----------------------------------------------------------發起一個全新請求
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive

HTTP/1.x 200 OK
Server: Cloudia
Last-Modified: Mon, 24 Nov 2008 11:03:02 GMT
Cache-Control: max-age=11    (瀏覽器會cache這個頁面內容,然後將cache過期時間設置爲當前時間+11秒)
Content-Length: 312
Connection: Keep-Alive
---------------------------------------------------------- 按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
If-Modified-Since: Mon, 24 Nov 2008 11:03:02 GMT   (按F5刷新,If-Modified-Since將上次服務器傳過來的Last-Modified時間帶過來)
Cache-Control: max-age=0

HTTP/1.x 304 Not Modified   
Server: Cloudia
Connection: Keep-Alive
Cache-Control: max-age=11   (這個max-age有些多餘,瀏覽器發現Not Modified,將使用本地cache數據,但不會重新設置本地過期時間)
----------------------------------------------------------
繼續按F5刷新n次.......

這11秒內未產生http請求.直到11秒過去了...............
----------------------------------------------------------按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive  (cache過期後,發起的是一個全新的請求,未帶)

HTTP/1.x 200 OK
Server: Cloudia
Last-Modified: Mon, 24 Nov 2008 11:03:02 GMT
Cache-Control: max-age=11
Content-Encoding: deflate
Content-Length: 312
Connection: Keep-Alive
Content-Type: application/x-javascript
----------------------------------------------------------按F5刷新
GET /today.js?maxage=11 HTTP/1.1
Host: sports.sinajs.cn
Connection: keep-alive
If-Modified-Since: Mon, 24 Nov 2008 11:03:02 GMT
Cache-Control: max-age=0

HTTP/1.x 304 Not Modified
Server: Cloudia
Connection: Keep-Alive
Cache-Control: max-age=11
----------------------------------------------------------


四,我對HTTP協議做的一點創新(?maxage=6000000)

上面看到了url後面有  ?maxage=xx  這樣的用法,這不是一個普通的參數,作用也不僅僅是看起來那麼簡單。他至少有以下幾個好處:

1,可以控制HTTP header的的 max-age 值。
2, 讓用戶爲每個資源靈活定製精確的cache時間長度。
3, 可以代表資源版本號。

首先談論對後端的影響:
服務器實現那塊,不用再load類似mod_expires,mod_headers 這樣額外的module,也不用去加載那些規則去比較,它屬於什麼目錄,或者什麼文件類型,應該cache多少時間,這樣的操作是需要開銷的。

再說說對前端的影響:
比如同一個分時行情圖片,我們的分時頁中需要1分鐘更新,而某些首頁中3分鐘更新好。不用js控制的話,那我cache應該設置多少呢?   有了maxage就能滿足這種個性化定製需求。

另一種情況是,我們爲了cache,把某個圖片設置了一個永久cache,但是由於需求,我必須更新這個圖片,那怎麼讓用戶訪問到這個更新了的圖片呢?從yahoo的資料和目前所有能找到的資料中都描述了同一種方法,更改文件名字,然後引用新的資源。 我覺得這方法太土, 改名後,老的還不能刪除,可能還有地方在用,同一資源可能要存兩份,再修改,又得改個名,存3份,不要不把inode當資源。我就不那樣做,只需要把maxage=6000000 修改成 maxage=6000001 ,問題就解決了。

maxage=6000000 所產生的威力 (內存塊消耗減少了250倍  ,請求數減少了37倍) :
體育那邊要上一個新功能,一開始動態獲取那些數據,我覺得那樣太浪費動態池資源,就讓他們把xml文件到轉移到我的js池上來,爲了方便,他們把那個84k的flash文件也放在了一起,而且是每個用戶必須訪問的。說實在的,我不歡迎這種大塊頭,因爲它不可壓縮,按正常來說,它應該代表一個3M的文件。我的服務器只這樣設計的,如果一次發送不完的就暫存在內存裏,每個內存塊10k,如果不帶參數默認maxage=120 。 我發現,由於這個文件,10w connections的時候,我消耗了10000個內存塊。我自己寫的申請連續內存的算法也是消耗cpu地,一個84k的文件,發送一次後,剩餘的64k就應該能裝的下,於是我把最小內存塊大小改爲64K。 這樣消耗10w conn的時候消耗1500個左右內存快,雖然內存消耗總量沒怎麼變小,但是它能更快的拿到64K的連續內存資源,cpu也節約下來了。接下來我讓meijun把所應用的flash資源後面加上maxage=6000000 (大概=79天,瀏覽器端最長cache能達到着個就不錯了), 10w connections的時候,只消耗了不到40個內存塊,也就是說內存塊消耗減少了250倍  ,請求數減少了37倍。  35w+ connections, 5.67w req/s的時候也就消耗100塊左右,比線性增加要少很多。也就是這點發現讓我有了做這個技術分享的衝動,其他都是順便講講。


五,Yslow優化網站性能的14條軍規點評

其中黑色部分,跟後端是緊密相連的,在我們的內容中都已經涉及到了,而且做了更深入的討論。蘭色部分,5,6,7是相關頁面執行速度的,構建前端頁面的人應該注意的。 11屬於避免使用的方法。 紅色部分我着重說一下:

gzip 我不推薦使用,因爲有些早期IE支持的不好,它的表現爲直接用IE訪問沒問題,用js嵌進去,就不能正常解壓。這樣的用戶佔比應該在2%左右。這個問題我跟蹤了近一個月,差點放棄使用壓縮。後來發現我以前用deflate壓縮的文件卻能正常訪問。 改用deflate問題解決。apache 1.x使用mod_gzip ,到了 2.x 改用cmod_deflate,不知道是否跟這個原因有關。 另外對於小文件壓縮來說,deflate 可比 gzip 省不少字節。

減少 DNS 查詢: 這裏也是有個取捨的,一般瀏覽器最多隻爲一個域名創建兩個連接通道。 如果我一個頁面嵌了 image.xx.com 的很多圖片,你就會發現,圖片從上往下一張張顯示出來這個過程。這造成了瀏覽器端的排隊。 我們可以通過增加域名提高併發度,例如 image0.xx.com ,image1.xx.com ,image2.xx.com,image3.xx.com 這樣併發度就提上去了,但是會造成很多cache失效,那很簡單,假如我們對文件名相加,對4取mod,就能保證,某個圖片只能通過某個域名進行訪問。不過,我也很反對一頁面請求了數十個域名,很多域名下只有一到兩個資源的做法,這樣的時間開銷是不划算的。

另外,我在這裏再添一個第15條:錯開資源請求時間,避免瀏覽器端排隊。
隨着ajax的廣泛使用,動態刷新無處不在,體育直播裏有個頁面調用了我一個域名下的6個文件,3個js,3個xml。刷新頻率大致是兩個10秒的,兩個30秒的,兩個一次性載入的。觀察發現正常響應時間都在7ms,但是每過一會就會出現一次在100ms以上的,我就很奇怪,服務器負載很輕呢。meijun幫我把刷新時間錯開,11秒的,9秒的,31秒的,這樣響應在100ms以上的概率減少了好幾倍,這就是所謂的細節決定成敗吧。

1. 儘可能的減少 HTTP 的請求數     [content]
2. 使用 CDN(Content Delivery Network)     [server]
3. 添加 Expires 頭(或者 Cache-control )     [server]
4. Gzip 組件     [server]
5. 將 CSS 樣式放在頁面的上方     [css]
6. 將腳本移動到底部(包括內聯的)     [javascript]
7. 避免使用 CSS 中的 Expressions     [css]
8. 將 JavaScript 和 CSS 獨立成外部文件     [javascript] [css]
9. 減少 DNS 查詢     [content]
10. 壓縮 JavaScript 和 CSS (包括內聯的)     [javascript] [css]
11. 避免重定向     [server]
12. 移除重複的腳本     [javascript]
13. 配置實體標籤(ETags)     [css]
14. 使 AJAX 緩存    

六,上線了 !=  Finished

奧運期間我按1500w~2000w connections在線,設計了一套備用系統,現在看來,如果用戶真達到了這個數目我會很危險,因爲有部分服務器引入了32bit的centos 5未經實際線上檢驗,而我當時簡單的認爲它應該和centos 4表現出一樣的特性。所以現在未經過完全測試的lib庫和新版本,我都很謹慎的使用。沒在真實環境中檢驗過,不能輕易下結論。

很多項目組好象不停的忙,做新項目,上線後又繼續下個新項目,然後時不時的轉過頭去修理以前的bug。如果一個項目上線後,用戶量持續上升,就應該考慮優化了,一個人訪問,和100w人訪問,微小的修改對後端影響是不能比較的,不該請求的資源就讓它cache在用戶的硬盤上,用戶訪問塊了,你也省資源。上線僅僅代表可以交差了而已,對於技術人員來說持續的對一個重要項目進行跟蹤和優化是必要的。


七,提速度同時節約成本方法彙總

1,編寫節約的HTTP服務器 (高負載下速度明顯提升,節約5~10倍服務器)
對一些重要的服務器量身定做。或者選用比較高效的開源軟件進行優化。

2,不同服務混合使用  (節約1~2倍服務器)
如果我們一臺服務器只支持30w conn的話,那麼剩餘的75% cpu資源,95%的內存資源,和幾乎所有的磁盤資源都可以部署動態池系統,我覺得DB對網卡中斷的消耗還是有限的,我也不用新買網卡了。

3,對於純數據部分啓用新的域名(速度有提升,上行帶寬節約1倍以上)
比如我們另外購買了sinajs.cn 來做數據服務,以避免cookie,節約帶寬. Cookie不但會浪費服務器端處理能力,而且它要上行數據,而通常情況上行比下行慢。

4, 使用長連接 (速度明顯提升,節約帶寬2倍以上,減少網絡擁塞3~無數倍)
對於一次性請求多個資源,或在比較短的間隔內會有後續請求的應用,使用長連接能明顯提升用戶體驗,減少網絡擁塞,減少後端服務器建立新連接的開銷。

5,數據和呈現分離,靜態數據和動態數據分離 (速度明顯提升,同時節約3倍帶寬)
div+css 數據和呈現分離以後,據說文件大小能降到以前的1/3。
把頁面中引用的js文件分離出來,把動態部分和靜態部分也分離開來。

6,使用deflate壓縮算法 (速度明顯提升,節約3.33倍帶寬)
一般來說壓縮過的文件大小不到以前的30% 。
將上面分離出來的數據進行壓縮(累計節約帶寬10倍)。

7, 讓用戶儘可能多的Cache你的資源 (速度明顯提升,節約3~50倍服務器資源和帶寬資源)
將上面分離出來的css和不經常變動的js數據部分cache住合適的時間。(理想情況,累計節約帶寬30~500倍) 。

以上改進可以讓速度大幅度提升的同時,服務器資源節約 5~20 倍 ,減少網絡擁塞3~無數倍, 上行帶寬節約1倍以上,下行帶寬節約30~500倍,甚至更多。
發佈了183 篇原創文章 · 獲贊 4 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章