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

quakewang · May 19, 2014 · Last by ceclinux-github replied at February 28, 2018 · 32224 hits
Topic has been selected as the excellent topic by the admin.

总结 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 Floor has deleted

@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 Floor has deleted

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

93 Floor has deleted
94 Floor has deleted
95 Floor has deleted
96 Floor has deleted
97 Floor has deleted
98 Floor has deleted
99 Floor has deleted
huacnlee in Cache 在 Ruby China 里面的应用 mention this topic. 07 Jun 03:14
ylt in Rails / Nginx 与 Weak Etag mention this topic. 21 Jun 14:11
huopo125 in Rails 3 和 Rails 4 中 ETags 工作原理 mention this topic. 19 Jul 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

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

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

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

You need to Sign in before reply, if you don't have an account, please Sign up first.