IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Ruby China 里面我是如何设计缓存的

    Jason Lee发表于 2014-05-21 11:11:00
    love 0

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

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

    最近 24h 的平均响应时间

    流量高的那些页面 (Action)

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

    TopicsController#show

    UsersController#show (比较惨,主要是 GitHub API 请求拖慢)

    PS: 在发布这篇文章之前我有稍加修改了一下,GitHub 请求放到后台队列处理,新的结果是这样:

    TopicsController#index

    HomeController#index

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

    我们是如何做到的?

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

    Markdown 缓存

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

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

    1. 为了持久化,避免 Memcached 停掉的时候,大量丢失;
    2. 避免过多占用缓存内存;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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

    1
    2
    3
    4
    5
    6
    7
    8
    
    <% cache([topic, suggest]) do %>
    
    <%= topic.id %>">
    <%= link_to(topic.replies_count,"#{topic_path(topic)}#reply#{topic.replies_count}", :class => "count state_false") %> ... 省略内容部分
    <% end %>
    1. 用 topic 的 cache_key 作为缓存 cache views/topics/{编号}-#{更新时间}/{suggest 参数}/{文件内容 MD5} -> views/topics/19105-20140508153844/false/bc178d556ecaee49971b0e80b3566f12
    2. 某些涉及到根据用户帐号,有不同状态显示的地方,直接把完整 HTML 准备好,通过 JS 控制状态,比如目前的“喜欢“功能。
    1
    2
    3
    4
    5
    6
    7
    
    
    

    再比如 app/views/topics/_reply.html.erb

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    <% cache([reply,"raw:#{@show_raw}"]) do %>
    
    <%= user_avatar_tag(reply.user, :normal) %>
    <%= user_name_tag(reply.user) %> <%= 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" ) %>
    <%= sanitize_reply reply.body_html %> <% end %>

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    
    

    数据缓存

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

    我们会做一些比价负责的数据查询缓存,比如:GitHub Repos 获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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 可以带上的一个参数,用于检测内容是否有更新过,以减少网络开销。

    过程大概是这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    第一次请求
    
          [浏览器]                   浏览器收到,并记录到本地 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 信息

    1
    2
    3
    4
    5
    
    def show
      @topic = Topic.find(params[:id])
    
      fresh_when(etag: [@topic])
    end
    

    静态资源缓存

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

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

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    
    
      ...
      只有两个
       href="http://l.ruby-china.org/assets/front-1a909fc4f255c12c1b613b3fe373e527.css" rel="stylesheet" />
      
      ...
    
    

    一些 Tips

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


沪ICP备19023445号-2号
友情链接