分析Cache 在 Ruby China 里面的應(yīng)用情況
首先給大家看一下 NewRelic 的報(bào)表
最近 24h 的平均響應(yīng)時(shí)間

流量高的那些頁(yè)面 (Action)

訪問(wèn)量搞的幾個(gè) Action 的情況:
TopicsController#show

UsersController#show (比較慘,主要是 GitHub API 請(qǐng)求拖慢)

PS: 在發(fā)布這篇文章之前我有稍加修改了一下,GitHub 請(qǐng)求放到后臺(tái)隊(duì)列處理,新的結(jié)果是這樣:

TopicsController#index

HomeController#index

從上面的報(bào)表來(lái)看,目前 Ruby China 后端的請(qǐng)求,排除用戶主頁(yè)之外,響應(yīng)時(shí)間都在 100ms 以內(nèi),甚至更低。
我們是如何做到的?
Markdown 緩存
Fragment Cache
數(shù)據(jù)緩存
ETag
靜態(tài)資源緩存 (JS,CSS,圖片)
Markdown 緩存
在內(nèi)容修改的時(shí)候就算好 Markdown 的結(jié)果,存到數(shù)據(jù)庫(kù),避免瀏覽的時(shí)候反復(fù)計(jì)算。
此外這個(gè)東西也特意不放到 Cache,而是放到數(shù)據(jù)庫(kù)里面:
為了持久化,避免 Memcached 停掉的時(shí)候,大量丟失;
避免過(guò)多占用緩存內(nèi)存;
class Topic field :body # 存放原始內(nèi)容,用于修改 field :body_html # 存放計(jì)算好的結(jié)果,用于顯示 before_save :markdown_body def markdown_body self.body_html = MarkdownTopicConverter.format(self.body) if self.body_changed? end end Fragment Cache
這個(gè)是 Ruby China 里面用得最多的緩存方案,也是速度提升的原因所在。
app/views/topics/_topic.html.erb
<% cache([topic, suggest]) do %>
<div class="topic topic_line topic_<%= topic.id %>">
<%= link_to(topic.replies_count,"#{topic_path(topic)}#reply#{topic.replies_count}",
:class => "count state_false") %>
... 省略內(nèi)容部分
</div>
<% end %>
用 topic 的 cache_key 作為緩存 cache views/topics/{編號(hào)}-#{更新時(shí)間}/{suggest 參數(shù)}/{文件內(nèi)容 MD5} -> views/topics/19105-20140508153844/false/bc178d556ecaee49971b0e80b3566f12
某些涉及到根據(jù)用戶帳號(hào),有不同狀態(tài)顯示的地方,直接把完整 HTML 準(zhǔn)備好,通過(guò) JS 控制狀態(tài),比如目前的“喜歡“功能。
<script type="text/javascript">
var readed_topic_ids = <%= current_user.filter_readed_topics(@topics) %>;
for (var i = 0; i < readed_topic_ids.length; i++) {
topic_id = readed_topic_ids[i];
$(".topic_"+ topic_id + " .right_info .count").addClass("state_true");
}
</script>
再比如
app/views/topics/_reply.html.erb
<% cache([reply,"raw:#{@show_raw}"]) do %>
<div class="reply">
<div class="pull-left face"><%= user_avatar_tag(reply.user, :normal) %></div>
<div class="infos">
<div class="info">
<span class="name">
<%= user_name_tag(reply.user) %>
</span>
<span class="opts">
<%= likeable_tag(reply, :cache => true) %>
<%= link_to("", edit_topic_reply_path(@topic,reply), :class => "edit icon small_edit", 'data-uid' => reply.user_id, :title => "修改回帖")%>
<%= link_to("", "#", 'data-floor' => floor, 'data-login' => reply.user_login,
:title => t("topics.reply_this_floor"), :class => "icon small_reply" )
%>
</span>
</div>
<div class="body">
<%= sanitize_reply reply.body_html %>
</div>
</div>
</div>
<% end %>
同樣也是通過(guò) reply 的 cache_key 來(lái)緩存 views/replies/202695-20140508081517/raw:false/d91dddbcb269f3e0172bf5d0d27e9088
同時(shí)這里還有復(fù)雜的用戶權(quán)限控制,用 JS 實(shí)現(xiàn);
<script type="text/javascript">
$(document).ready(function(){
<% if admin? %>
$("#replies .reply a.edit").css('display','inline-block');
<% elsif current_user %>
$("#replies .reply a.edit[data-uid='<%= current_user.id %>']").css('display','inline-block');
<% end %>
<% if current_user && !@user_liked_reply_ids.blank? %>
Topics.checkRepliesLikeStatus([<%= @user_liked_reply_ids.join(",") %>]);
<% end %>
})
</script>
數(shù)據(jù)緩存
其實(shí) Ruby China 的大多數(shù) Model 查詢都沒有上 Cache 的,因?yàn)閾?jù)實(shí)際狀況來(lái)看, MongoDB 的查詢響應(yīng)時(shí)間都是很快的,大部分場(chǎng)景都是在 5ms 以內(nèi),甚至更低。
我們會(huì)做一些比價(jià)負(fù)責(zé)的數(shù)據(jù)查詢緩存,比如:GitHub Repos 獲取
def github_repos(user_id)
cache_key = "user:#{user_id}:github_repos"
items = Rails.cache.read(cache_key)
if items.blank?
items = real_fetch_from_github()
Rails.cache.write(cache_key, items, expires_in: 15.days)
end
return items
end
ETag
ETag 是在 HTTP Request, Response 可以帶上的一個(gè)參數(shù),用于檢測(cè)內(nèi)容是否有更新過(guò),以減少網(wǎng)絡(luò)開銷。
過(guò)程大概是這樣

Rails 的 fresh_when 方法可以幫助將你的查詢內(nèi)容生成 ETag 信息
def show @topic = Topic.find(params[:id]) fresh_when(etag: [@topic]) end
靜態(tài)資源緩存
請(qǐng)不要小看這個(gè)東西,后端寫得再快,也有可能被這些拖慢(瀏覽器上面的表現(xiàn))!
1、合理利用 Rails Assets Pipeline,一定要開啟!
# config/environments/production.rb config.assets.digest = true
2、在 Nginx 里面將 CSS, JS, Image 的緩存有效期設(shè)成 max;
location ~ (/assets|/favicon.ico|/*.txt) {
access_log off;
expires max;
gzip_static on;
}
3、盡可能的減少一個(gè)頁(yè)面 JS, CSS, Image 的數(shù)量,簡(jiǎn)單的方法是合并它們,減少 HTTP 請(qǐng)求開銷;
<head> ... 只有兩個(gè) <link rel="stylesheet" /> <script src="http://ruby-china-files.b0.upaiyun.com/assets/app-24d4280cc6fda926e73419c126c71206.js"></script> ... </head>
一些 Tips
看統(tǒng)計(jì)日志,優(yōu)先處理流量高的頁(yè)面;
updated_at 是一個(gè)非常有利于幫助你清理緩存的東西,善用它!修改數(shù)據(jù)的時(shí)候別忽略它!
多關(guān)注你的 Rails Log 里面的查詢時(shí)間,100ms 一下的頁(yè)面響應(yīng)時(shí)間是一個(gè)比較好的狀態(tài),超過(guò) 200ms 用戶就會(huì)感覺到遲鈍了。
相關(guān)文章
使用Ruby on Rails快速開發(fā)web應(yīng)用的教程實(shí)例
這篇文章主要介紹了使用Ruby on Rails快速開發(fā)web應(yīng)用的教程實(shí)例,本文來(lái)自于IBM官方技術(shù)文檔,需要的朋友可以參考下2015-04-04
Ruby的字符串與數(shù)組求最大值的相關(guān)問(wèn)題討論
這篇文章主要介紹了Ruby中的字符串與數(shù)組求最大值的相關(guān)問(wèn)題,文中還提到了sort排序方法的相關(guān)用法,需要的朋友可以參考下2016-03-03
Ruby簡(jiǎn)明教程之循環(huán)語(yǔ)句介紹
這篇文章主要介紹了Ruby簡(jiǎn)明教程之循環(huán)語(yǔ)句介紹,非常簡(jiǎn)潔的講解,可以作為語(yǔ)法備忘,需要的朋友可以參考下2014-06-06

