分享 总结 Web 应用中常用的各种 Cache

quakewang · 2014年05月19日 · 最后由 ceclinux-github 回复于 2018年02月28日 · 32194 次阅读
本帖已被管理员设置为精华贴

总结 web 应用中常用的各种 cache

cache 是提高应用性能重要的一个环节,写篇文章总结一下用过的各种对于动态内容的 cache。 文章以 Nginx,Rails,Mysql,Redis 作为例子,换成其他 web 服务器,语言,数据库,缓存服务都是类似的。 以下是 3 层的示意图,方便后续引用:


                          +-------+
1                         | Nginx |
                          +-+-+-+-+
                            | | |
            +---------------+ | +---------------+
            |                 |                 |
        +---+---+         +---+---+         +---+---+
2       |Unicorn|         |Unicorn|         |Unicorn|
        +---+---+         +---+---+         +---+---+
            |                 |                 |
            |                 |                 |
            |             +---+---+             |
3           +-------------+  D B  +-------------+
                          +-------+

1. 客户端缓存

一个客户端经常会访问同一个资源,比如用浏览器访问网站首页或查看同一篇文章,或用 app 访问同一个 api,如果该资源和他之前访问过的没有任何改变,就可以利用 http 规范中的 304 Not Modified 响应头 ( http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 ),直接用客户端的缓存,而无需在服务器端再生成一次内容。 在 Rails 里面内置了 fresh_when 这个方法,一行代码就可以完成:

class ArticlesController
  def show
    @article = Article.find(params[:id])
    fresh_when :last_modified => @article.updated_at.utc, :etag => @article
  end
end

下次用户再访问的时候,会对比 request header 里面的 If-Modified-Since 和 If-None-Match,如果相符合,就直接返回 304,而不再生成 response body。

但是这样会遇到一个问题,假设我们的网站导航有用户信息,一个用户在未登陆专题访问了一下,然后登陆以后再访问,会发现页面上显示的还是未登陆状态。或者在 app 访问一篇文章,做了一下收藏,下次再进入这篇文章,还是显示未收藏状态。解决这个问题的方法很简单,将用户相关的变量也加入到 etag 的计算里面:

fresh_when :etag => [@article.cache_key, current_user.id]
fresh_when :etag => [@article.cache_key, current_user_favorited]

另外提一个坑,如果 nginx 开启了 gzip,对 rails 执行的结果进行压缩,会将 rails 输出的 etag header 干掉,nginx 的开发人员说根据 rfc 规范,对 proxy_pass 方式处理必须这样(因为内容改变了),但是我个人认为没这个必要,于是用了粗暴的方法,直接将 src/http/modules/ngx_http_gzip_filter_module.c 这个文件里面的这行代码注释掉,然后重新编译 nginx:

//ngx_http_clear_etag(r);

或者你可以选择不改变 nginx 源代码,将 gzip off 掉,将压缩用 Rack 中间件来处理:

config.middleware.use Rack::Deflater

除了在 controller 里面指定 fresh_when 以外,rails 框架默认使用 Rack::ETag middleware,它会自动给无 etag 的 response 加上 etag,但是和 fresh_when 相比,自动 etag 能够节省的只是客户端时间,服务器端还是一样会执行所有的代码,用 curl 来对比一下。 Rack::ETag 自动加入 etag:

curl -v http://localhost:3000/articles/1
< Etag: "bf328447bcb2b8706193a50962035619"
< X-Runtime: 0.286958
curl -v http://localhost:3000/articles/1 --header 'If-None-Match: "bf328447bcb2b8706193a50962035619"'
< X-Runtime: 0.293798

用 fresh_when:

curl -v http://localhost:3000/articles/1 --header 'If-None-Match: "bf328447bcb2b8706193a50962035619"'
< X-Runtime: 0.033884

2. Nginx 缓存

有一些资源可能会被调用很多,又无关用户状态,并且很少改变,比如新闻 app 上的列表 api,购物网站上 ajax 请求分类菜单,可以考虑用 Nginx 来做缓存。 主要有 2 种实现方法: A. 动态请求静态文件化 在 rails 请求完成以后,将结果保存成静态文件,后续请求就会直接由 nginx 提供静态文件内容,用 after_filter 来实现一下:

class CategoriesController < ActionController::Base
  after_filter :generate_static_file, :only => [:index]

  def index
    @categories = Category.all
  end

  def generate_static_file
    File.open(Rails.root.join('public', 'categories'), 'w') do |f|
      f.write response.body
    end
  end
end

另外我们需要在任何分类更新的时候,删除掉这个文件,避免缓存不刷新的问题:

class Category < ActiveRecord::Base
  after_save :delete_static_file
  after_destroy :delete_static_file

  def delete_static_file
    File.delete Rails.root.join('public', 'categories')
  end
end

Rails 4 之前,处理这种生成静态文件缓存可以用内置的 caches_page,rails 4 之后变成了一个独立 gem actionpack-page_caching,和手工代码对比一下,

class CategoriesController < ActionController::Base
  caches_page :index

  def update
    #...
    expire_page action: 'index'
  end
end

如果只有一台服务器,这个方法简单又实用,但是如果有多台服务器,就会出现更新分类只能刷新自己本身这台服务器缓存的问题,可以用 nfs 来共享静态资源目录解决,或者用第 2 种:

B. 静态化到集中缓存服务 首先我们得让 Nginx 有直接访问缓存的能力:

upstream redis {
  server redis_server_ip:6379;
}

upstream ruby_backend {
  server unicorn_server_ip1 fail_timeout=0;
  server unicorn_server_ip2 fail_timeout=0;
}

location /categories {
  set $redis_key $uri;
  default_type   text/html;
  redis_pass redis;
  error_page 404 = @httpapp;
}

location @httpapp {
  proxy_pass http://ruby_backend;
}

Nginx 首先会用请求的 uri 作为 key 去 redis 里面获取,如果获取不到(404)就转发给 unicorn 进行处理,然后改写 generate_static_file 和 delete_static_file 方法:

redis_cache.set('categories', response.body)
redis_cache.del('categories')

这样除了集中管理以外,还能够设置缓存的失效时间,对于一些更新无时效性要求的数据,就可以不用处理刷新机制,简单地固定时间刷新一次:

redis_cache.setex('categories', 3.hours.to_i, response.body)

3. 整页缓存

Nginx 缓存在处理带参数资源或者有用户状态的请求时候,就非常难以处理,这个时候可以用到整页缓存。 比如说分页请求列表,我们可以将 page 参数加入到 cache_path:

class CategoriesController
  caches_action :index, :expires_in => 1.day, :cache_path => proc {"categories/index/#{params[:page].to_i}"}
end

比如说我们只需要针对 rss 输出进行缓存 8 小时:

class ArticlesController
  caches_action :index, :expires_in => 8.hours, :if => proc {request.format.rss?}
end

再比如说对于非登陆用户,我们可以缓存首页:

class HomeController
  caches_action :index, :expires_in => 3.hours, :if => proc {!user_signed_in?}
end

4. 片段缓存

如果说前面 2 种缓存能够用到的场景有限,那么片段缓存是适用性最广的。

场景 1:我们需要在每个页面一段广告代码,用来显示不同广告,如果没有使用片段缓存,那么每个页面都会要去查询广告的代码,并且花费一定时间去生成 html 代码:

- if advert = Advert.where(:name => request.controller_name + request.action_name, :enable => true).first
  div.ad
    = advert.content

加了片段缓存以后,就可以少去这个查询:

- cache "adverts/#{request.controller_name}/#{request.action_name}", :expires_in => 1.day do
  - if advert = Advert.where(:name => request.controller_name + request.action_name, :enable => true).first
    div.ad
      = advert.content

场景 2:阅读文章,文章的内容可能比较长时间都不会改变,经常变化可能是文章评论,就可以对文章主体部分加上片段缓存:

- cache "articles/#{@article.id}/#{@article.updated_at.to_i}" do
  div.article
    = @article.content.markdown2html

节约了生成 markdown 语法转换到 html 时间,这里用文章最后更新时间作为 cache key 的一部分,文章内容如果有改变,缓存自动失效,默认 activerecord 的 cache_key 方法也是用 updated_at,你也可以加入更多的参数,比如 article 上有评论数的 counter cache,更新评论数的时候不会更新文章时间,可以将这个 counter 也加入到 key 的一部分

场景 3:复杂页面结构的生成 数据结构比较复杂的页面,在生成的时候避免不了大量的查询和 html 渲染,用片段缓存,可以将这部分时间大大地节约,以我们网站游记页面 http://chanyouji.com/trips/109123 (请允许小小地打个广告,带点流量)来说: 需要获取天气数据,照片数据,文本数据等,同时还要生成 meta,keyword 等 seo 数据,而这些内容又是和其他动态内容交叉,片段缓存就可以分开多个:

- cache "trips/show/seo/#{@trip.fragment_cache_key}", :expires_in => 1.day do
  title #{trip_name @trip}
  meta name="description" content="..."
  meta name="keywords" content="..."

body
  div
    ...
- cache "trips/show/viewer/#{@trip.fragment_cache_key}", :expires_in => 1.day do
  - @trip.eager_load_all

小贴士,我在 trip 对象里面加了一个 eager_load_all 方法,缓存没有命中的时候,查询的时候避免出现 n+1 问题:

def eager_load_all
  ActiveRecord::Associations::Preloader.new([self], {:trip_days => [:weather_station_data, :nodes => [:entry, :notes => [:photo, :video, :audio]]]}).run
end

小技巧 1:带条件的片段缓存 和 caches_action 不同,rails 自带的片段缓存是不支持条件的,比如说我们想未登陆用户给他用片段缓存,而登陆用户不使用,写起来就很麻烦,我们可以改写一下 helper 就可以了:

  def cache_if (condition, name = {}, cache_options = {}, &block)
    if condition
      cache(name, cache_options, &block)
    else
      yield
    end
  end

- cache_if !user_signed_in?, "xxx", :expires_in => 1.day do

小技巧 2:关联对象的自动更新 常使用对象 update_at 时间戳来作为 cache key,可以在关联对象上加上 touch 选项,自动更新关联对象时间戳,比如我们可以在更新或者删除文章评论的时候,自动个更新:

class Article
  has_many :comments
end

class Comment
  belongs_to :article, :touch => true
end

5. 数据查询缓存

通常来说 web 应用性能瓶颈都出现在 DB IO 上,做好数据查询缓存,减少数据库的查询次数,可以极大提高整体响应时间。 数据查询缓存分 2 种: A. 同一个请求周期内的缓存 举一个显示文章列表的例子,输出文章标题和文章类别,对应代码如下

# controller
  def index
    @articles = Article.first(10)
  end

# view
- @articles.each do |article|
  h1 = article.name
  span = article.category.name

会发生 10 条类似的 sql 查询:

SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = ?

rails 内置了 query cache( https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb ),在同一个请求周期内,如果没有 update/delete/insert 的操作,会对相同的 sql 查询进行缓存,如果文章类别都是相同的话,真正去查询数据库只会有 1 次。

如果文章类别都不一样,就会出现 N+1 查询问题(常见的性能瓶颈),rails 推荐的解决方法是用 Eager Loading Associations ( http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations )

def index
  @articles = Article.includes(:category).first(10)
end

查询语句会变成

SELECT `categories`.* FROM `categories` WHERE `categories`.`id` in (?,?,?...)

B. 跨请求周期的缓存 同请求周期缓存所带来性能优化是很有限的,很多时候我们需要用跨请求周期的缓存,将一些常用的数据(比如 User model)缓存,对于 active record 来说,利用统一的查询接口来 fetch cache,利用 callback 来 expire cache,就很容易实现,而且有一些现成的 gem 可以来用。

比如说 identity_cache ( https://github.com/Shopify/identity_cache )

class User < ActiveRecord::Base
  include IdentityCache
end

class Article < ActiveRecord::Base
  include IdentityCache
  cached_belongs_to :user
end

# 都会命中缓存
User.fetch(1)
Article.find(2).user

这个 gem 的优点是代码实现简单,cache 设置灵活,也方便扩展,缺点是需要用不同的查询方法名(fetch),以及额外的关系定义。

如果想在无数据缓存的应用无缝加入缓存功能,推荐@hooopo 做的 second_level_cache ( https://github.com/hooopo/second_level_cache ) 。

class User < ActiveRecord::Base
  acts_as_cached(:version => 1, :expires_in => 1.week)
end

#还是使用find方法,就会命中缓存
User.find(1)
#无需额外用不一样的belongs_to定义
Article.find(2).user

实现原理是扩展了 active record 底层 arel sql ast 处理( https://github.com/hooopo/second_level_cache/blob/master/lib/second_level_cache/arel/wheres.rb ) 它的优点是无缝接入,缺点是扩展比较困难,对于只获取少量字段的查询无法缓存。

6. 数据库缓存

编辑中

这 6 种缓存,分布在客户端到服务器端不同的位置,所能够节约的时间也正好从多到少依次排列。

:thumbsup: 4,5,6 期待中

期待中!楼主又要出精品文章了!

@quakewang 突如其来的大招啊!

rails4 好像砍了几个?

  • 客户端缓存,在这里指的应该就是 ETag 了吧,现在已经做为 middleware,默认对静态和动态内容做缓存。只要有一个改动,都会刷新。fresh_when 设置不好,缓存失效反而容易出错。
  • Page Caching 和 Action Caching 都已经废弃。
  • 建议加上"对象缓存",和数据查询有点类似。

坐等楼主更新!

好赞!谢谢楼主!RubyChina 多些这种帖就好了…

今天流量爆发刚搞完...早看到白天就不用花时间研究了 - -

好贴!!!

先点收藏,然后 点喜欢, 然后来评论, 然后慢慢品味。 话说 很喜欢 这种接地气的 分享。

真是好帖子全部是作者实战经验的总结,可以帮助到不少人。深深佩服 70 后技术人(认识几个)做研究的态度,反观自己太浮躁及大部分 80 后都很浮躁都想渴望快速成功,欲速则不达哎。。。,楼主畅游记的代码开源吗?(或者涉及到这种技术干货部分代码可以开源吗呵呵?)

好帖必頂 👍

why not use Varnish?某些场景

图用什么画的啊?

很棒……

实战经验,赞!

丫的太棒了......涨姿势...

受益匪浅

楼主干货颇多,受益良多

都是干货,学习了

非常棒,学习了。

非常精彩,补充一下,还有 ORM 对象缓存:http://robbinfan.com/blog/38/orm-cache-sumup 和缓存服务器缓存(比如 Varnish http://www.360doc.com/content/10/1026/11/737570_64093653.shtml)。

不过后者是位于 Nginx 上层,并不在 LZ 的三个层次里面。

刚才试了一下 Rack::Deflater

siege -t30s -c 10 'http://127.0.0.1'​

                                     使用后              不是使用
Transactions:                 187 hits            242 hits
Availability:                   100.00 %           100.00 %
Elapsed time:                29.03 secs         29.18 secs
Data transferred:           0.39 MB            1.17 MB
Response time:             1.01 secs          0.69 secs
Transaction rate:           6.44 trans/sec   8.29 trans/sec
Throughput:                   0.01 MB/sec      0.04 MB/sec
Concurrency:                    6.51                 5.69
Successful transactions:     187              242
Failed transactions:            0                    0
Longest transaction:          9.16              4.67
Shortest transaction:         0.09              0.09

发现压缩比例 在 77% 左右。

对于 2.Nginx 缓存的 'A. 动态请求静态文件化' 其实可以直接用 Rails 自带的 caches_page(Rails4 已将抽成 gem 了)。效果是一样。@quakewang 你觉得呢?

建议:对于 3 整页缓存 标题 应该改成 action 缓存。因为整页缓存 是 caches_page 的说法

31 楼 已删除

@lgn21st 题外话,对评论 按钮 的频率是否 应该做个限制。 比如 我有时候 手快了 多点了一下。就多发了一条评论。

#9 楼 @leekelby Rack::ETag 和 fresh_when 的区别我已经补充到原文。 Page Caching 和 Action Caching 在 rails 4 里面是独立了一个 gem,并不是废弃。

#30 楼 @meeasyhappy 是一样的,我已经补充到原文。 关于 3 的标题,action cache 其实也是缓存整个请求,和 2 差异在于:检查命中缓存的操作一个在 nginx,一个在 ruby,我再想想是否有更好的标题名。

#34 楼,只是一个 建议而已,看你。

涨姿势了

我们在用 cloudflare 可以帮助管理 2,3 的缓存不用自己弄;不知道国内有类似的服务没有

#37 楼 @knwang 安全宝、加速乐之类的能实现这些功能,但是设计的出发点不只是为这种用途,估计用起来还是会有点别扭。

#37 楼 @knwang 另外不管是 cloudflare、安全宝、加速乐之类的,所有请求都要从 cdn 过一遍,这个是否会影响网站性能呢?

超出我的知识范围了 ...

赞一个!

非常好的文章啊!

。。。牛逼,我发现我不会 rails 了

#40 楼 @xmonkeycn 其他的不清楚,cloudflare 是把 DNS 都包过来,所以对缓存页面的请求是完全不经过你的服务器的。

补充几个对时间和当前用户缓存的策略:需要显示当前时间或者 n 个小时前 这种推到客户端用 js 实现 如果页面差异对不同的用户很小可以考虑 1)服务器端贪婪缓存,客户端用 JS 异步实现用户差异化 2)做 action / 页面 / 片段缓存,用 after_action 针对用户差异化 - 这个我没有自己试过,如果有做过的可以谈谈效果怎样

好帖,顶一个

关于数据查询缓存,可以推荐一下 flyerhzm 的 simple_cacheable,在某些需要缓存某个复杂计算结果的场景挺方便的,http://rails-bestpractices.com/blog/posts/24-simple_cacheable

马克看看。

讚呀,清晰多了

另外這個廣告打得好 😄

#52 楼 @hepochen 随便问问,你自己先试过没有……

#52 楼 @hepochen ETag 在 Rails Controller 里面处理合理的话,是可以节省 Views, JSON 部分的执行时间的

#54 楼 @huacnlee 是的,ETag 在 request 过来的时候就进行校验,这样基本上执行的时间就接近于 0 了;但rails框架默认使用Rack::ETag middleware,它会自动给无etag的response加上etag这样的 MiddleWare,很令人费解,如果是后置计算,response 都已经得到了(完整得运行了一次),hash 虽然效率很高,但这个逻辑就很鸡肋。

在这方面来说,ETag 和 last_modified 扮演的角色是一样的。

@hepochen 有价值的,节省浏览器下载的时间

上图的首页 ETag 是 Rails 自动产生的,/topics/:id 页面是手工调用 fresh_when 方法的

#53 楼 @aptx4869 我们不是“试”,而是实践了很久,复杂程度远超过本文的。

但你这个问法很奇怪,知识本身没有那么强烈的感情色彩,多数时候,对的就是对的,错的就是错的。如果我说错了,请告诉我;如果自己没有能力分辨什么是对什么是错,可能最好的办法,就不要用这样的态度去『随便问问』(希望没有冒犯到你)。

我可能表达不够清楚,ETag 本身的作用是节省带宽(就是下载时间),但是校验 ETag 如果是这种后置的算法,那这个就很难算上优化的手段。 但是很多人会有这样的误解(304意味着)直接用客户端的缓存,而无需在服务器端再生成一次内容,程序没有特殊处理,不是这样的。

@quakewang 所以我才说在知道可以用last_modified的前提下,实在没有必要去修改Nginx源码;再则,Nginx处理Gzip的性能高很多,不是么?

in 查询一般都是主键查询,或者是有 index,不会有全表查询问题.

这个你是对的,不过建议还是说明下,因为 ORM 的调用还是很容易进入子查询的逻辑,这个时候,对于那些还没有完全理解索引概念的朋友们,优化优化,结果越来越糟。

@huacnlee @quakewang 提个建议,应该能看出我并不是一名新手或者不知所以的人,所以,一些基本的概念,不用说明的。

#59 楼 @hepochen 对于动态请求来说,etag 和 last modified 都是需要后置校验,没办法提前到 nginx 这层面校验.我不选择用 last modified 是因为 etag 更加灵活,可以用更新时间+其他参数,比如我文章中提到过的当前用户 id,评论数等来做.

#55 楼 @hepochen 明白了,你原文都没看懂,就更别谈去试了……不过 LZ 语言表达是有点不太清楚,不熟悉然后还匆匆随便看看的人可能会被误导 原文提到的refresh_whenRack::ETag完全是两个东西…… 实际上你就算在 application.rb 里一开始就

config.middleware.delete 'Rack::ETag'

refresh_when还是一样能正常工作,就如字面意识上一样,如果 cache key 命中就跳过 html/json 渲染,直接返回 304 (实际上这 middleware 应该推荐删掉……确实没啥用……早就该废弃了) 实际测试下来差距还是挺明显的 (测试均已删除Rake::ETag 中间件):

使用 fresh_when

HTTP statuses returned
304 ┃ 67 hits ┃ 97.1% ┃ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
200 ┃  2 hits ┃  2.9% ┃ ░░

Request duration - by sum ┃ Hits ┃    Sum ┃   Mean ┃ StdDev ┃    Min ┃    Max ┃    95 %tile
#index.JSON               ┃   69 ┃  1.05s ┃   15ms ┃    3ms ┃   11ms ┃   28ms ┃   10ms-26ms

不使用 fresh_when:

HTTP statuses returned
200 ┃ 106 hits ┃ 100.0% ┃ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

Request duration - by sum ┃ Hits ┃    Sum ┃   Mean ┃ StdDev ┃    Min ┃    Max ┃    95 %tile
#index.JSON               ┃  106 ┃  2.08s ┃   19ms ┃    5ms ┃   15ms ┃   52ms ┃   14ms-39ms

#62 楼 @aptx4869 你要把我搞疯掉了。

你这个不用做测试就知道什么结果了呀,我没有说refresh_when的问题呀,我是说Rack::ETag这种 ETag 的 MiddleWare 的逻辑就是累赘。

是不是我的表达能力太弱了?还是已经冒犯到了你?

#62 楼 @aptx4869 #63 楼 @hepochen Rack::ETag 不是累赘,对于非性能瓶颈的请求,没必要手工加 fresh_when/stale 等判断,就算多生成一次 response body,浪费一些服务器资源也无所谓,客户端还是能够享受到 304 带来的好处.

#61 楼 @quakewang

不仅仅是 markdown 转 html,就是单纯的 rails helper 生成 html,或者复杂页面 view template 渲染,也需要花费 10ms 级别的时间(没办法,ruby 不够快),对于访问量大的页面,片段缓存能够节约掉这部分时间也是相当可观的

评论可以修改的,所以刚才没有看到这段。其它方式生成 HTML,这个怎么做缓存都不过分,脚本语言,没有办法的;我特指 Markdown 转 HTML,这个真的没有必要。

对于动态请求来说,etag 和 last modified 都是需要后置校验,没办法提前到 nginx 这层面校验.我不选择用 last modified 是因为 etag 更加灵活,可以用更新时间+其他参数,比如我文章中提到过的当前用户 id,评论数等来做.

前置校验,不是指放在 Nginx 中处理,而是指 request 过来的时候,先校验头部信息,如果 match cache 的,直接 status=304,并且返回空内容;而不需要再继续运算完整的 response

我总感觉有些奇怪的,是动态请求来说这样的前提把自己画地为牢了?

我不选择用 last modified 是因为 etag 更加灵活

另外,需要请教下的:实际上 last_modified 就是字符串,并非一定要时间戳或者其它什么格式,因为 HTTP 处理的是超文本;真有限制,也是各种 WebFrame 内建的限制。我这样的理解是不是错误的?

我始终觉得,因为 ETag 跑去改 Nginx 的源码,太草率了。当然,实际自己遇到这样的问题,可能也会类似的做法,dirty but quick. 但当做知识进行分享,感觉不是很好。

Rack::ETag 不是累赘,对于非性能瓶颈的请求,没必要手工加 fresh_when/stale 等判断,就算多生成一次 response body,浪费一些服务器资源也无所谓,客户端还是能够享受到 304 带来的好处.

是如此,我当然知道……

相信看到这里的朋友对 304 有了更好的理解。

如果真要做优化,这种通用的 MiddleWare,确实非常鸡肋。我保留这个看法,因为这是正确的。

但多数的产品用通用的就可以了,因为性能问题不会如此突出;如果要针对自己产品做类似Rack::ETag的处理,就需要确定自己的规则,当然,代码并不会增加太多,估计就 10 行内的事情。

好了,我该说的都说完了。希望对诸位有所帮助。

#66 楼 @hepochen 嘛你是说得不太清楚,和 LZ 原文一样主题不明确,其实只需要指出开启 nginx 自带 etag 支持时,应该将Rack::ETag删掉就完事了……其实Rack::ETag好像是在 nginx 原生支持 etag 之前写出来的,干的事情和 nginx 现在的 etag 模块基本没啥两样,也就性能会差点

#67 楼 @aptx4869 nginx 自带 etag 只支持静态资源

#68 楼 @quakewang 额……和 nginx 某第三方模块记混了,不是原生的……大概原生不支持动态内容 etag 也确实因为这样实现太土鳖了点……

#59 楼 @hepochen 再则,Nginx处理Gzip的性能高很多,不是么? 额,不测不知道,实际测试在我这里结果是这样的

在 nginx 里 gzip off 在 rails 用 Rack::Deflater压缩:

Concurrency Level:      2
Time taken for tests:   5.475 seconds
Complete requests:      300
Failed requests:        0
Total transferred:      6706366 bytes
HTML transferred:       6423600 bytes
Requests per second:    54.79 [#/sec] (mean)
Time per request:       36.503 [ms] (mean)
Time per request:       18.251 [ms] (mean, across all concurrent requests)
Transfer rate:          1196.11 [Kbytes/sec] received

在 nginx 里 gzip on, rails 不使用压缩:

Concurrency Level:      2
Time taken for tests:   5.616 seconds
Complete requests:      300
Failed requests:        0
Total transferred:      6699512 bytes
HTML transferred:       6423600 bytes
Requests per second:    53.42 [#/sec] (mean)
Time per request:       37.438 [ms] (mean)
Time per request:       18.719 [ms] (mean, across all concurrent requests)
Transfer rate:          1165.03 [Kbytes/sec] received

出乎预料 nginx 压缩反而变慢了,这说不通啊……

王叔 V5

nginx 只用 gzip_static on 发送静态资源就够啦,绝大部分动态内容都是不压缩的比较快

gzip on 的话也可以配置哪些内容 gzip 哪些不 gzip

像压缩过的 js, png 图片,gzip 对瘦身效果很不明显,但是耗费很多服务器和客户端的 CPU, 还是不 gzip 的好

gzip_static 还有个好处:在编译 asset 时就比较压缩效果,压缩比少于一定量就不 gzip

#59 楼 @hepochen 你把自己的位子放的太高啦,把一些基础的知识点写出来能够对新手有帮助就行了,你也别觉得每个人都是来教育你的,你反应有些偏激了。

如果 etag 是这样被中间件处理的,实是累赘至极,它或许为某些特殊场景设计的吧,如果通用场景这样使用,实际上必然带来性能的下降。

我觉得这个应该就是为了节省网络带宽传输把?这点性能下降和网络传输性能相比应该还是后者更重要吧。

@ruohanc 我非常想帮助别人,但你这样的说法真让人寒心。

如果你觉得我的位置太高,会不会因为你的位置太低?如果你觉得每个人都在教育我,会不会因为你太敏感了,为不存在的事情替我着急?如果你觉得我的反应有些偏激,会不会因为你承受别人指出(别人的)错误的压力阀太低了?

ETag 应该前置校验,这样除了减少网络传输外,(基本)不会产生性能下降。另外,你不要偷换概念,这不是“这点”,不进行前置校验,绝不会是“这点”。

当然,如果我设计一个 WebFrame,应该也会引入类似Rack::ETag的中间件,作为通用组件,它的局限性是可以被容忍的。

对于动态请求来说,etag 和 last modified 都是需要后置校验

@ruohanc 我非常希望你也能明白,@quakewang 类似的说法是错误的,这并不是基础知识,而是错误的知识。

@quakewang 进行分享,这是好的事情,分享的意义,并不单纯帮助了别人,也给了别人帮助自己的机会。

另外,需要请教下的:实际上 last_modified 就是字符串,并非一定要时间戳或者其它什么格式,因为 HTTP 处理的是超文本;真有限制,也是各种 WebFrame 内建的限制。我这样的理解是不是错误的?

貌似没有人能帮助我,为了回复 @ruohanc 你的这个帖子,我仔细翻阅了http://www.ietf.org/rfc/rfc2616.txt的协议,这个论述,我估计会在某些浏览器中不支持。好吧,也算你帮助了我。

我已经到了俯首而为孺子牛的年纪,所以,并不太容易有类似生气的过激反应,倒是高中时,同学跑过来问问题,然后辅导之,然后她开心地说自己就应该不耻下问,这不是耍流氓么……

我在文前的反问,并非 troll 行为,只是当年自己跟下属说类似的话时,那时没有人告诉我,这样是错的。


@aptx4869 测试的前提比结果更重要些,比如确保 status 回来的都是 200,确保 GZip 压缩的 level 是一致的,比如 text 类型的 js/css/html 几个分开比对一下;另外,结果还是感觉不科学,再看看 Rails 对 Gzip 的实现是纯 Ruby 还是用了 C。如果,结果还是不科学(这就真的太不科学了),哈哈,欢迎来笑话我。


一说真话,就遭人嫌,继续感受下吧。

@luikore

nginx 只用 gzip_static on 发送静态资源就够啦,绝大部分动态内容都是不压缩的比较快

gzip_static 是预压缩,就是 Nginx 会检测.gz 的对应已经压缩好的文件是否存在……

gzip on 的话也可以配置哪些内容 gzip 哪些不 gzip

这个是必须要配置的。

像压缩过的 js, png 图片,gzip 对瘦身效果很不明显,但是耗费很多服务器和客户端的 CPU, 还是不 gzip 的好

压缩过的 js如果是指自己下面这句话的,呃,有效果才奇怪…… png 图片怎么敢用 GZip?不,不,没有特殊情况下,text 类的都应该开启 GZip,它的效率还是很高的。

用 gzip_static 还有个好处:在编译 asset 时就比较压缩效果,压缩比少于一定量就不 gzip

唉,Nginx 有个gzip_min_length参数可以设置……


还是那句话,希望对大家有所帮助。 但坦率地说,有点后悔参合进来。

#72 楼 @luikore 嗯,图片 gzip 还可能增大:

Don't use gzip for image or other binary files.

Image file formats supported by the web, as well as videos, PDFs and other binary formats, are already compressed; using gzip on them won't provide any additional benefit, and can actually make them larger

https://developers.google.com/speed/docs/best-practices/payload#GzipCompression

#74 楼 @hepochen 您说的知识点大多是对的,我说这话两个出发点

  1. 楼主总结了自己的经验,拿出来分享,虽然可能有点问题,但是没看到什么根本性的错误 (即使是你指出的那点,也只是没有精确描述到极致,所以不能算是错). 所以这是一个好帖
  2. 您反驳的意见都很有道理,而且大多也有出处的引用,很让人信服。只是言语间透露着的"优越感", 并不是很匹配您"俯首而为孺子牛"的定位

为了避免再次回帖与您争辩,我引用一下让我觉得不是很舒服的部分,有这样的开端,后面同学对您的不礼貌也就是情理之中了。

楼主是在传播错误的知识,为什么没有人指出呢?

提个建议,应该能看出我并不是一名新手或者不知所以的人,所以,一些基本的概念,不用说明的。

如果自己没有能力分辨什么是对什么是错,可能最好的办法,就不要用这样的态度去『随便问问』(希望没有冒犯到你)

后续我不再回复这个歪楼的话题,还是回归主题吧。

#72 楼 @luikore 不对吧,动态内容也是压缩了比较快啊,算上网络传输时间,还是会将 CPU 时间省回来的,比方说我这即使是 100M 的的宽度,但通常只有网速测试、p2p 下载大文件什么的才会满速到 10m 左右的速度,正常上网下载网页也就 300k 的速度,即使是比较小的内容,比方说只压缩节省 1k 多的内容,网络传输也能省个 3ms,而开启压缩花不了那么多时间,何况现在随便个网页都有 100k 左右,gzip 节省时间是很明显的 参见这篇文章: GZIP encoding = happier users?

#74 楼 @hepochen gzip 这种标准库肯定都是用 C 实现的啦,不过这也不足以解释这种差异啊,我本来预期它们性能应该是一样的……翻了下文档,Ruby 这边 gzip level 默认是 6, nginx 那默认是 1……这更奇怪了……测得同一个 API,看 log 都是 status 200

#77 楼 @aptx4869 那要压缩比高的才行... png 自带和 gzip 差不多的 DEFLATE 再压缩也很难变小的。jpeg, uglify 过的 js, 几百字节的小动态响应都是信息熵密度爆棚,gzip 也很难瘦身了

很精彩 . 对缓存有了新的认识 . 感谢 @hepochen @quakewang 两位的分享 .

#7 楼 @Kabie rails4 去掉了 page caching and action caching.

很精彩 受教了

apache http server 也有这个问题,现在还是一个 bug https://issues.apache.org/bugzilla/show_bug.cgi?id=39727 https://issues.apache.org/bugzilla/show_bug.cgi?id=45023

另外,etag 另外的几个很好的用途似乎没提 https://gist.github.com/6a68/4971859#etags-have-other-really-cool-applications

原文对 rails view cache (😱) 的概括让我更觉得,Single Page App 才是今后大方向,老实说大家有没有同感。

很赞的一篇文章

也很感谢 hepochen quakewang 两者的分享 😄

感谢分享,不过 cache_if 和 cache_unless 已经支持了。http://guides.rubyonrails.org/caching_with_rails.html

@quakewang 请问蝉游记的 mobile app api 是基于 ruby 的解决方案么?

#88 楼 @quakewang 谢谢,那我就有信心了。

Tengine(nginx 淘宝版本) http://tengine.taobao.org/changelog.html 支持 gzip_clear_etag 声明,可以通过 gzip_clear_etag off; 设置不去除 etag,避免 hack nginx 源码,:)

91 楼 已删除

不错,不错,值得学习参考

93 楼 已删除
94 楼 已删除
95 楼 已删除
96 楼 已删除
97 楼 已删除
98 楼 已删除
99 楼 已删除
huacnlee Cache 在 Ruby China 里面的应用 提及了此话题。 06月07日 03:14
ylt Rails / Nginx 与 Weak Etag 提及了此话题。 06月21日 14:11
huopo125 Rails 3 和 Rails 4 中 ETags 工作原理 提及了此话题。 07月19日 11:20

感谢 @hepochen 的精彩分析。也感谢楼主分享。

有个问题 关于动态数据静态文件化的

class CategoriesController < ActionController::Base
  after_filter :generate_static_file, :only => [:index]

  def index
    @categories = Category.all
  end

  def generate_static_file
    File.open(Rails.root.join('public', 'categories'), 'w') do |f|
      f.write response.body
    end
  end
end

这样写难道不会每一次查询都重新写一次文件吗

lyb124553153 回复

Nginx 那边当发现你访问的时候会直接去指定目录的文件里面找,相当于该接口在你第一次访问完之后就不会再进到 Rails 项目里面来,统统都是 Nginx 把请求返回了

好帖,发现自己需要看下 Nginx 了😃

goodboyRyan rails 直到网站挂掉才知道缓存多重要 提及了此话题。 04月03日 10:57
需要 登录 后方可回复, 如果你还没有账号请 注册新账号