Nginx:頁面輸出緩存(頁面靜態化)的設計方案?

            頁面輸出緩存(頁面靜態化)的設計                隨着網站規模的變大,訪問量提升,網站服務器越來越不堪重負,瀏覽者也會對頁面打開的速度怨聲載道。

這時候最簡單的解決方案就是增加緩存。

網站服務器的緩存有很多中,可以放在數據庫和Web應用程序之間,也可以放在Web應用程序和Web服務器之間,還可以放在Web服務器和用戶瀏覽器之間,甚至可以直接放在瀏覽器端。

其中最簡單,需要配置最少的莫過於數據庫和Web應用程序之間了,並且見效也最快,因爲對於現代計算機系統來說IO是最大的瓶頸。常見的方法就是使用MemCache或者K-V型NoSQL數據庫做緩存。

這麼安逸了一段時間之後,網站的響應速度還是會降下來,如果你不想增加服務器的話,這時需要做的就是頁面輸出緩存了。

因爲數據庫和Web應用程序端的緩存只是將數據庫中的鍵和值緩存下來,訪問量大的話有一個命中率的問題,並且一個頁面往往包含有大量的需要從數據庫查詢的值,就算數據庫緩存全部命中,這其中也需要一個查詢的過程,即時它很快。還有一個問題,就是對於Web應用程序來說,從獲取數據到輸出頁面,中間還需要一些邏輯性運算和模板的渲染過程,這個過程也會消耗一段時間。

如果將一個頁面響應的結果,也就是HTML頁面,整體緩存下來,Web服務器直接輸出緩存結果,這樣的速度基本上相當於直接輸出靜態頁面,對於服務器端的負載將會大大減輕。
對於頁面輸出緩存,或者有人稱之爲頁面靜態化,主要有以下幾個問題:


頁面個性化部分的處理。現在絕大部分網站都有登錄給功能,對於同一個頁面來說登錄前後、不同用戶登錄後都會有不同的顯示,比如說未登錄時顯示“登錄或註冊”,登錄之後顯示“歡迎用戶XXX”,這樣就會造成網站大部分頁面都不能直接緩存。

其實這個問題是在頁面個性化部分與非個性化部分合成的時間問題上。傳統網站這個個性化部分也非個性化部分是直接放在Web應用程序上的,這樣就導致不同用戶輸出頁面的不同,緩存不能進行。如果把這個過程放在頁面緩存程序之後,使緩存的結果相同就沒有問題了。

常見的方式就是使用SSI、ESI和CSI。

SSI(Server Side Include)這個現在大部分的Web服務器都支持,可以在頁面中使用<!--#include file="/user_info.shtml"-->來替代原來個性化的部分,然後在Web服務器上就會去尋找這個.shtml文件,將它與頁面合成。不過這有一個問題,ssi只能將靜態文件與頁面合成,這樣對於做頁面輸出緩存意義並不大。

ESI(Edge Side Include)正是爲了應付這種場合而出現的,它可以使用類似<esi:include src="http://example.com/1.html" alt="http://bak.example.com/2.html" οnerrοr="continue"/>的XML語法,在頁面合成過程中調用一個動態頁面,將動態頁面的內容與現有網頁合成。這樣一來就可以將某個用戶的個性化信息與緩存的頁面文件合成了。ESI最大的問題就是現有Web服務器對此支持度不高。Nginx需要將第三方模塊編譯進來來支持ESI,代理緩存服務器中Varnish原生支持ESI。

CSI(Client Side Include)或者叫做Browser Side Include,就是在用戶瀏覽器端將個性化信息與非個性化信息合成。說白了就是用ajax異步獲取個性化信息,或者直接使用iframe顯示個性化信息。實際上這個合成的過程越靠近用戶瀏覽器端越好,這樣就可以一直從Web服務器到代理服務器的緩存到CDN都可以用到這些完全相同的頁面做緩存。但是使用ajax等方式有一些問題,就是對於不支持或者不開啓的JavaScript的瀏覽器來說就不能正常訪問頁面了,現在很多網站都要考慮到手機客戶端,其中不支持JavaScript的佔大多數。並且大量使用ajax的話對用戶瀏覽器來說也是一種負擔,並且會造成頁面加載很“卡頓”的感覺。

具體的做法還是依賴實際環境來決定。


剩下需要解決的方式是如何來緩存頁面。

使用代理緩存服務器是一個比較不錯的主意,比如Squid還有比較新的Varnish,它們都可以架設在Web服務器前端作爲代理緩存服務器,將頁面緩存下來。當頁面需要更新的時候可以給它發送一個Purge請求加具體頁面URL,就可以使代理緩存服務器去訪問Web服務器,重新生成一個新的頁面,然後將現有的頁面緩存失效。這個過程中當新的用戶訪問請求達到時,代理緩存服務器還是使用之前的緩存,新的緩存頁面生成結束後才使過期的頁面失效。這樣就不用擔心更新緩存過程中所有用戶請求都去直接訪問Web服務器造成大量的壓力了。這其中有一個問題,就是觸發這個更新動作的用戶,比如說他發佈了一條回覆造成頁面更新,如果頁面更新請求與頁面緩存更新兩個動作是異步的話,假如他的網速足夠快就會看到自己剛纔發佈的帖子沒有顯示出來,造成重複發帖等動作。這個問題接下來再來想辦法處理。

如果不想另外搞一個代理服務器的話,Nginx的fastcgi cache是一個好東西,可以直接將fastcgi的頁面緩存下來。配合一個第三方的模塊cache purge,可以通過訪問/purge/url的方式來時緩存過期。另外Nginx+proxy cache+cache purge也可以達到上面Squid和Varnish的作用。

剩下還有一種方式,使用Nginx的rewrite,將緩存保存爲一個靜態文件,先檢查靜態頁面是否存在,存在的話rewrite到此,不存在則訪問Web應用程序。這樣還需要另外配合一個獨立的進程,來生成緩存頁面。當Web應用程序觸發更新時給此進程發送一個信號,緩存進程收到此信號後去Web服務器端或者Web應用程序端請求頁面(爲了做到鬆耦合和以後的服務器分離最好直接從Web服務器端),將請求生成的頁面文件直接覆蓋遠緩存的靜態文件,這樣就可以更新緩存,並且在緩存更新過程中訪問者依然可以使用之前的緩存文件。

還有一個問題,當很多用戶在同一個時間點內同時觸發一個頁面的更新動作,這樣就會導致此頁面頻繁更新,並且這些更新是不必要的,這就是所謂的驚羣問題。
防止此事件發生,一個解決方案是使用隊列。

一般頁面發生內容變化是在提交POST請求之後,在此時給緩存更新系統發送一條消息,記錄發生更新動作的類型、ID(比如說帖子更新,更新的ID)。緩存更新系統在接受到此消息時將消息進行預處理,根據配置文件計算出影響更新的頁面(此帖子的頁面,帖子列表的頁面,等等)。

將此頁面的URL插入進隊列,更新程序在隊列另一端取URL,每取一次URL就過濾一遍整個隊列,將相同的URL隊列消息刪除出隊列。這樣就可以保證更新依次頁面,就可以相應所有更新事件。取出後依據使用的緩存系統發送相應的Purge請求或者自主生成新的緩存頁面覆蓋老的野蠻來更新緩存。

這種去重複的隊列實現可以用現有的模塊,比如Python的一個Queue模塊,繼承此模塊中的類並重寫幾個方法即可實現這種去重的隊列。還可以使用現有的隊列系統,甚至直接使用數據庫來實現隊列。使用數據庫有一個好處,就是當整個系統Down掉的時候,系統重啓後隊列中的消息還存在,可以繼續更新這些頁面的緩存。如果直接使用內存中的隊列的話系統Down掉重啓就會導致一些未處理的隊列消息丟失,導致這些頁面的緩存都沒有更新。這種情況出現的機率還有出現後對系統的影響需要根據實際情況斟酌,論壇系統如果發帖不是十分頻繁,偶爾某個帖子或者回覆沒有出現並不是非常嚴重的問題。

一個好的系統應該是針對發生故障的情況而設計的。假如緩存系統出現問題,導致沒有向緩存服務器發送更新請求,這時所有更新動作都不會得到相應,導致用戶重複提交數據,可能會發生一些問題。這時如果能停掉緩存服務器,讓用戶直接訪問Web應用程序或許是一個好主意(或者如果沒有緩存的話服務器能被直接大量直接訪問幹掉),如果去做是一個問題。

Some Tips:
上面說的如果異步處理更新動作與緩存更新的話,可能會使更新觸發者看不到自己更新的數據。這時可以使用一些小花招,比如現在大部分的論壇系統在發帖後跳轉到一個“3秒後跳轉到帖子頁面”,就可以給緩存系統提供充分的更新緩存時間,對於用戶體驗來說也不會大打折扣。

有些頁面不能使用緩存,比如說帖子列表頁面。因爲帖子列表頁面是會隨着最後回覆時間來動態更新排列順序的,一個新帖或者回復可能會導致所有帖子列表分頁內容發生變化,而帖子列表分頁可能有幾千甚至幾萬頁,全部都進行更新的話顯然是不可能也沒有必要的。因此這些頁面不適用頁面輸出緩存,但我們可以只緩存帖子列表的第一頁或者前幾頁,因爲絕大部分的訪問請求都是來自與這幾個頁面,每次更新動作都觸發這幾個頁面進行緩存更新也是沒有問題的。

假如網站每日發新帖的數量非常高,導致帖子列表更新十分頻繁,這時可以使用另外一種更新觸發機制,就是按時間來觸發更新。這樣定時更新的頻率比之前按動作進行更新會少一些,並且也不會過分加重隊列的負擔。

網站首頁的信息可能會比較複雜,觸發其更新的來源可能會很多,更新一次的成本也較高,而首頁對於事實性的要求也沒有那麼高(比如說在某個板塊發佈新貼,這時在首頁的最新帖子列表中沒有看到此貼,對於絕大部分用戶來說並不是什麼問題),因此也試用於按時間進行更新的機制,並且這個更新時間的間隔可以適當的拉長一些。

類似的還有帖子回覆頁,此頁面也是一個列表排序頁面,但是是順序時間排列的(帖子列表爲倒序時間排列的),因此也十分適用與使用緩存。如果網站當初設計的功能爲顯示所有樓層,刪除某一樓層的話此位置消失,這樣的話如果刪除一個樓層將會導致此頁面和接下來所有頁面的內容發生變化。因此在網站設計時最好設計成這樣:刪除一條回覆,此回覆還在,只不過內容改爲了“評論已刪除”,這樣就不會觸發所有後來頁面進行緩存更新了。




CSI應該比較有價值,是網頁靜態化比較可行的,再加上可以跟CDN結合起來

如果要做整頁靜態化的話可以試一試這個軟件http://www.isapicache.com/,好處是不需要改寫原來程序代碼,可以自動生成靜態頁面,缺點就是需要在服務器上設置

這裏需要的是一個有一定適用性,並且可以隨着用戶提交請求來更新緩存頁面的系統,不是簡單的把所有頁面生成靜態頁面


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