Rails Cache 在 Ruby China 里面的应用

huacnlee for Ruby China · 发布于 2014年5月21日 · 最后由 rina 回复于 2016年12月22日 · 10673 次阅读
2
本帖已被设为精华帖!

看最近 @quakewang 分享的 《总结 web 应用中常用的各种 cache》,我也搭车分享一下在 Ruby China 里面,我们是如何做 Cache 的。

首先给大家看一下 NewRelic 的报表

最近 24h 的平均响应时间

流量高的那些页面 (Action)

访问量搞的几个 Action 的情况:

  1. TopicsController#show
  2. UsersController#show (比较惨,主要是 GitHub API 请求拖慢) PS: 在发布这篇文章之前我有稍加修改了一下,GitHub 请求放到后台队列处理,新的结果是这样:
  3. TopicsController#index
  4. HomeController#index

从上面的报表来看,目前 Ruby China 后端的请求,排除用户主页之外,响应时间都在 100ms 以内,甚至更低。

我们是如何做到的?

  • Markdown 缓存
  • Fragment Cache
  • 数据缓存
  • ETag
  • 静态资源缓存 (JS,CSS,图片)

Markdown 缓存

在内容修改的时候就算好 Markdown 的结果,存到数据库,避免浏览的时候反复计算。

此外这个东西也特意不放到 Cache,而是放到数据库里面:

  1. 为了持久化,避免 Memcached 停掉的时候,大量丢失;
  2. 避免过多占用缓存内存;
class Topic
  field :body # 存放原始内容,用于修改
  field :body_html # 存放计算好的结果,用于显示

  before_save :markdown_body
  def markdown_body
    self.body_html = MarkdownTopicConverter.format(self.body) if self.body_changed?
  end
end

Fragment Cache

这个是 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") %>
  ... 省略内容部分

</div>
<% end %>
  1. 用 topic 的 cache_key 作为缓存 cache views/topics/{编号}-#{更新时间}/{suggest 参数}/{文件内容 MD5} -> views/topics/19105-20140508153844/false/bc178d556ecaee49971b0e80b3566f12
  2. 某些涉及到根据用户帐号,有不同状态显示的地方,直接把完整 HTML 准备好,通过 JS 控制状态,比如目前的“喜欢“功能。 html <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 %>

同样也是通过 reply 的 cache_key 来缓存 views/replies/202695-20140508081517/raw:false/d91dddbcb269f3e0172bf5d0d27e9088 同时这里还有复杂的用户权限控制,用 JS 实现;

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

数据缓存

其实 Ruby China 的大多数 Model 查询都没有上 Cache 的,因为据实际状况来看,MongoDB 的查询响应时间都是很快的,大部分场景都是在 5ms 以内,甚至更低。

我们会做一些比价负责的数据查询缓存,比如: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 可以带上的一个参数,用于检测内容是否有更新过,以减少网络开销。

过程大概是这样

第一次请求

      [浏览器]                   浏览器收到,并记录到本地 Cache
         |                         |
         |  [GET /index.html]      | [HTTP status 200]
         |                         | [ETag: abc]
         |                         |
  [Rails Controller]               |
         |                         |
      [Views]                      |
         |-------------------------|-

第二次请求 /index.html

      [浏览器]                   浏览器收到,并记录到本地 Cache
         |                         |                          |
         |  [GET /index.html]      | [HTTP status 304]        | [HTTP Status 200]
         |  [ETag: abc]            | [ETag: abc]              | [ETag: efg]
         |                         |                          |
  [Rails Controller] --------------|                          |
         |                      ETag 相同                      |
         |                                                    |
      [Views] ------------------------------------------------|-
                                ETag 不同

Rails 的 fresh_when 方法可以帮助将你的查询内容生成 ETag 信息

def show
  @topic = Topic.find(params[:id])

  fresh_when(etag: [@topic])
end

静态资源缓存

请不要小看这个东西,后端写得再快,也有可能被这些拖慢(浏览器上面的表现)!

1、合理利用 Rails Assets Pipeline,一定要开启!

# config/environments/production.rb
config.assets.digest = true

2、在 Nginx 里面将 CSS, JS, Image 的缓存有效期设成 max;

location ~ (/assets|/favicon.ico|/*.txt) {
  access_log        off;
  expires           max;
  gzip_static on;
}

3、尽可能的减少一个页面 JS, CSS, Image 的数量,简单的方法是合并它们,减少 HTTP 请求开销;

<head>
  ... 
  只有两个
  <link href="//ruby-china-files.b0.upaiyun.com/assets/front-1a909fc4f255c12c1b613b3fe373e527.css" rel="stylesheet" />
  <script src="//ruby-china-files.b0.upaiyun.com/assets/app-24d4280cc6fda926e73419c126c71206.js"></script>
  ...
</head>

一些 Tips

  1. 看统计日志,优先处理流量高的页面;
  2. updated_at 是一个非常有利于帮助你清理缓存的东西,善用它!修改数据的时候别忽略它!
  3. 多关注你的 Rails Log 里面的查询时间,100ms 一下的页面响应时间是一个比较好的状态,超过 200ms 用户就会感觉到迟钝了。
共收到 40 条回复
59
rockliu · #1 · 2014年5月21日 2 个赞
1551
LinuxGit · #2 · 2014年5月21日

Good job.Thanks.

2685
joseen · #3 · 2014年5月21日

:thumbsup:

7733
yukihiro_matz · #4 · 2014年5月21日

👏

11666
xieyunzi · #5 · 2014年5月21日

感谢分享经验

96
alsotang · #6 · 2014年5月21日

搭车放个 cnodejs.org 的 newrelic

973
debugger · #7 · 2014年5月21日

mark, thanks.

5259
lina · #8 · 2014年5月21日

果断收藏。

162
quakewang · #9 · 2014年5月21日

我那篇还没写完,也准备要加"给最需要的部分做缓存"这一节,先搭车放个我们网站New Relic的图:

2
huacnlee · #10 · 2014年5月21日

#9楼 @quakewang 为何你的有 GC Execution 这项?

162
quakewang · #11 · 2014年5月21日

#10楼 @huacnlee 你用的newrelic gem没升级吧,某个版本以后有的.

我们现在用的gctools/oobgc 基本上请求中GC的情况很少很少了.

396
linjunpop · #12 · 2014年5月21日 1 个赞

#10楼 @huacnlee 加个 initializer 写 GC::Profiler.enable

https://docs.newrelic.com/docs/ruby/garbage-collection

9800
pynix · #13 · 2014年5月21日

UsersController#show 是显示用户github repo吗?

这个可以放到前端来吧?

2
huacnlee · #14 · 2014年5月21日

#13楼 @pynix 里面有一个功能是获取 GitHub 数据的,已经改成队列处理了

9800
pynix · #15 · 2014年5月21日

#14楼 @huacnlee js api搞不定?

2
huacnlee · #16 · 2014年5月21日

#15楼 @pynix 那个效果没那么好

9800
pynix · #17 · 2014年5月21日

#16楼 @huacnlee 整在前端来为服务器减压啊。。。

1411
zeeler · #18 · 2014年5月21日

这个很有用;另外,有没有想过尝试换种架构,API + webapp方式,用rails或sinatra实现API,所有前端用webapp方式来做。。。

96
fatman13cc · #19 · 2014年5月21日

马克看看。

4472
assyer · #20 · 2014年5月21日

想请教一下,markdown为何不放到纯前端渲染计算?

2
huacnlee · #21 · 2014年5月21日

#20楼 @assyer 浏览器计算也要时间的啊,况且还每次刷新都计算,我们目前这种方案只有在修改的时候才会计算 Markdown

96
jayliud · #22 · 2014年5月21日

#18楼 @zeeler 确定可以由js搞定全部复杂的view逻辑? 还是说部分以API+webapp,这样意义不大架构也复杂吧

11314
Zoker · #23 · 2014年5月22日

多谢分享!

96
fatman13cc · #24 · 2014年5月26日

RoR官方文档# RFC says only cache for 1 yearexpires 1y;

6878
zqalyc · #25 · 2014年5月26日

谢谢分享,正好用到

96
cloudaice · #26 · 2014年5月28日

@huacnlee ,何愁ruby-china不是中国最好的社区。

96
playsoso · #27 · 2014年5月29日

找了好久,终于找到了收藏

96
playsoso · #28 · 2014年5月29日

http://robbinfan.com/blog/38/orm-cache-sumup 里面有 传统关系型数据库 优化指南

8658
so_zengtao · #29 · 2014年5月29日

其实google analytics不错的 我一直用的GA

3226
xieyu33333 · #30 · 2014年5月29日

#18楼 @zeeler 发现社区沉淀的内容还是有赖于搜索引擎,目前至少百度是做不到抓取JS生成的页面内容的。

1411
zeeler · #31 · 2014年5月29日

#30楼 @xieyu33333 没错,这个很重要

32楼 已删除
33楼 已删除
96
zdsunshine0640 · #34 · 2014年6月22日

学习了

291
frankel · #35 · 2014年6月26日

没在ruby-china.org的源代码里面见到引用new relic的js,这是怎么做到的呢

8972
ane · #36 · 2014年7月04日

仔细阅读了一下,发现和rails guide上的很一致。

15996
zinwa_lin · #37 · 2014年12月30日

marked.

96
feng88724 · #38 · 2015年6月09日

Fragment Cache,不太理解

4938
dy1901 · #39 · 2015年9月08日

#30楼 @xieyu33333 正常用户访问用前端渲染,搜索引擎可以根据user agent用v8做server端渲染,直接输出html给搜索引擎。

20103
helloyokoy · #40 · 2015年11月03日

点赞~

96
tdeng · #41 · 2016年6月13日

值得学习。感谢分享!

17863
rina · #42 · 2016年12月22日

提个bug放这里。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册