基於資源的HTTP Cache的實現介紹

robbin發表於2009-09-05
我們都知道瀏覽器會快取訪問過網站的網頁,瀏覽器通過URL地址訪問一個網頁,顯示網頁內容的同時會在電腦上面快取網頁內容。如果網頁沒有更新的話,瀏覽器再次訪問這個URL地址的時候,就不會再次下載網頁,而是直接使用本地快取的網頁。只有當網站明確標識資源已經更新,瀏覽器才會再次下載網頁。

[size=medium]一、什麼是HTTP Cache[/size]

對於瀏覽器的這種網頁快取機制大家已經耳熟能詳了,舉個例子來說,JavaEye的新聞訂閱地址:http://www.iteye.com/rss/news , 當瀏覽器或者訂閱程式訪問這個URL地址的時候,JavaEye的伺服器在response的header裡面會傳送給瀏覽器如下狀態標識:

Etag	"427fe7b6442f2096dff4f92339305444"
Last-Modified Fri, 04 Sep 2009 05:55:43 GMT


這就是告訴瀏覽器,新聞訂閱這個網路資源的最後修改時間和Etag。於是瀏覽器把這兩個狀態資訊連同網頁內容在本地進行快取,當瀏覽器再次訪問JavaEye新聞訂閱地址的時候,瀏覽器會傳送如下兩個狀態標識給JavaEye伺服器:

If-None-Match	"427fe7b6442f2096dff4f92339305444"
If-Modified-Since Fri, 04 Sep 2009 05:55:43 GMT


就是告訴伺服器,我本地快取的網頁最後修改時間和Etag是什麼,請問你伺服器的資源有沒有在我上次訪問之後有更新啊?於是JavaEye伺服器會核對一下,如果該使用者上次訪問之後沒有更新過新聞,那麼根本就不必生成這個RSS了,直接告訴瀏覽器:“沒什麼新東西,你還是看自己快取的網頁吧”,於是伺服器就傳送一個304 Not Modified的訊息,其他什麼都不用幹了。

這就是HTTP層的Cache,使用這種基於資源的快取機制,不但大大節省伺服器程式資源,而且還減少了網頁下載次數,節約了很多網路頻寬。

[size=medium]二、HTTP Cache究竟有什麼作用?[/size]

我們通常的動態網站程式設計,伺服器端程式根本就不去處理瀏覽器傳送過來的If-None-Match和If-Modified-Since狀態標識,只要有請求就生成網頁傳送給瀏覽器。對於一般情況來說,使用者不會總是沒完沒了重新整理一個頁面,所以大家並不認為這種基於資源的快取有什麼太大的作用,但實際情況並非如此:

[size=medium]1、像Google這種比較智慧的網路爬蟲可以有效識別資源的狀態資訊,如果使用這種快取機制,可以大大減少爬蟲的爬取次數。[/size]

比方說Google每天爬JavaEye網站大概15萬次左右,但實際上JavaEye每天有更新的內容不會超過1萬個網頁。因為很多內容更新比較快,因此Google就會反覆不停的爬取,這樣本身就造成了很多資源的浪費。如果我們使用HTTP Cache,那麼只有當網頁內容發生改變的時候,才會真正進行爬取,其他時候我們直接告訴Google的爬蟲304 Not Modified就可以了。這樣不但降低了伺服器本身的負載和爬蟲造成的網路頻寬消耗,實際上也大大提高了Google爬蟲的工作效率,豈不是皆大歡喜?

[size=medium]2、很多內容更新不頻繁的網頁,儘管使用者不會頻繁的重新整理,但是從一個比較長的時間段來看使用HTTP Cache,仍然可以起到很大的快取作用。[/size]

比方說一些歷史討論帖子,已經過去了幾個月了,這些帖子內容很少更新。使用者可能通過搜尋,收藏連結,文章關聯等方式時不時訪問到這個頁面。那麼只要使用者訪問過一次以後,後續所有訪問伺服器直接傳送304 Not Modified就可以了,不用真正生成頁面。

[size=medium]3、對於歷史帖子使用HTTP Cache可以避免爬蟲反覆的爬取。[/size]

比方說JavaEye的論壇帖子列表頁面,分頁到20頁後面的帖子已經很少有人直接訪問了,但是從伺服器日誌去看,每天仍然有大量爬蟲反覆爬取這些分頁到很後面的頁面。這些頁面由於使用者很少去點選,所以基本上沒有被應用程式的memcached快取住,每次訪問都會造成很高的資源消耗,爬蟲隔一段時間就爬一次,對伺服器是很大的負擔。如果使用了HTTP Cache,那麼只要爬蟲爬過一次以後,以後無論爬蟲爬多少次,都可以直接返回304 Not Modified了,極大的節省了伺服器的負載。

[size=medium]三、如何在應用程式裡面使用HTTP Cache[/size]

如果我們要在自己的程式裡面實現HTTP Cache,是件非常簡單的事情,特別是對Rails來說只需要新增一點點程式碼,以上面的JavaEye新聞訂閱來說,只要新增一行程式碼:

def news
fresh_when(:last_modified => News.last.created_at, :etag => News.last)
end


用最新新聞文章作為Etag,該文章最後修改時間作為資源的最後修改時間,這樣就OK了。如果瀏覽器傳送過來的標識和伺服器標識一致,說明內容沒有更新,直接傳送304 Not Modified;如果不一致,說明內容更新,瀏覽器本地的快取太古老了,那麼就需要伺服器真正生成頁面了。

以上只是一個最簡單的例子,如果我們需要根據狀態做一些更多的工作也是很容易的。比方說JavaEye部落格的RSS訂閱地址: http://robbin.iteye.com/rss

@blogs = @blog_owner.last_blogs
@hash = @blogs.collect{|b| {b.id => b.post.modified_at.to_i + b.posts_count}}.hash
if stale?(:last_modified => (@blog_owner.last_blog.post.modified_at || @blog_owner.last_blog.post.created_at), :etag => @hash)
render :template => "rss/blog"
end


這個實現稍微複雜一些。我們需要判斷部落格訂閱所有的輸出文章是否有更新,所以我們用部落格文章內容最後修改時間和部落格的評論數量做一個hash,然後用這個hash值作為資源的Etag,那麼只要這些部落格文章當中任何文章內容被修改,或者有新評論,都會改變Etag值,從而通知瀏覽器內容有更新了。

除了RSS訂閱之外,JavaEye網站還有很多地方適合使用HTTP Cache,比方說JavaEye論壇的版面列表頁面,一些經常喜歡泡論壇的使用者,可能時不時會上來重新整理一下版面, 看看有沒有新的帖子,那麼我們就不必每次使用者請求的時候都去執行程式,生成頁面給他。我們判斷一下如果沒有新帖子的話,直接告訴他304 Not Modified就可以了,在沒有使用HTTP Cache之前的版面Action程式碼:

def board
@topics = @forum.topics.paginate...
@announcements = (params[:page] || 1).to_i == 1 ? Topic.find :all, :conditions => ...
render :action => 'show'
end


新增HTTP Cache以後,程式碼如下:

def board
@topics = @forum.topics.paginate...
if logged_in? || stale?(:last_modified => @topics[0].last_post.created_at, :etag => @topics.collect{|t| {t.id => t.posts_count}}.hash)
@announcements = (params[:page] || 1).to_i == 1 ? Topic.find :all, :conditions...
render :action => 'show'
end
end


對於登入使用者,不使用HTTP Cache,這是因為登入使用者需要實時接收站內簡訊通知和訂閱通知,因此我們只能對匿名使用者使用HTTP Cache,然後我們使用當前所有帖子id和回帖數構造hash作Etag,這樣只要當前分頁列表頁面有任何帖子發生改變或者有了新回帖,就更新頁面,否則就不必重新生成頁面。

論壇帖子頁面實際上也可以使用HTTP Cache,只不過Etag的hash演算法稍微複雜一些,需要保證帖子的任何改動都要引起hash值的改變,示例程式碼如下:


def show
@topic = Topic.find params[:id]
user_session.update_....... if logged_in?
Topic.increment_counter(...) if ......
@posts = @topic.post_by_page params[:page]
posts_hash = @posts.collect{|p| {p.id => p.modified_at}}.hash
topic_hash = @topic.forum_id + @topic.sys_tag_id.to_i + @topic.title.hash + @topic.status_flag.hash
ad_hash = ... (廣告的hash演算法,略)
if logged_in? || stale?(:etag => [posts_hash, topic_hash, ad_hash])
render
end
end


要分別根據主題貼,該分頁的所有回帖和帖子頁面的廣告內容進行hash,計算出來一個唯一的Etag值,保證任何改動都會生成新的Etag,這樣就搞定了,是不是很簡單!這種帖子的快取非常有效,可以避免Rails去render頁面和下載頁面,極大的減輕了伺服器負載和頻寬。

再舉一個需求比較特殊的例子:對於知識庫搜尋相關文章的推薦頁面,比方說:http://www.iteye.com/wiki/topic/462476,也就是本文的相關文章推薦內容,我們並不希望使用者和爬蟲每次訪問這個頁面都實際執行一遍全文檢索,然後構造頁面內容,在一個相對不長的時間範圍內,這篇文章的相關推薦文章改變的概率不大,因此我們希望比方說5天之內,使用者重複訪問該頁面,就直接返回304 Not Modified,那麼Rails沒有直接的設施給我們使用,需要我們稍微瞭解一些Rails的機制,自己編寫,程式碼示例如下:

def topic
@topic = Topic.find(params[:id])
unless logged_in?
if request.not_modified?(5.days.ago)
head :not_modified
else
response.last_modified = Time.now
end
end
end


每次使用者請求,我們判斷使用者是否5天之內訪問過該頁面,如果訪問過,直接返回304 Not Modified,如果沒有訪問過,或者上次訪問已經超過了5天,那麼設定最近修改時間為當前時間,然後生成頁面給使用者。是不是很簡單?

在給JavaEye網站所有的RSS訂閱輸出新增了HTTP Cache以後,通過一天的觀察發現,超過一半的RSS訂閱請求已經被快取了,直接返回304 Not Modified,所以效果非常明顯,由於JavaEye網站每天RSS訂閱的動態請求就超過了10萬次,因此新增HTTP Cache可以減輕不少伺服器的負擔和頻寬消耗。除此之外,新聞文章頁面,整個論壇頻道,知識庫相關推薦文章頁面都可以新增HTTP Cache,粗粗計算下來,JavaEye這些頁面統統使用HTTP Cache以後,網站整體效能至少可以提高10%。

相關文章