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

quakewang · 发布于 2014年5月19日 · 最后由 h_minghe 回复于 2017年3月29日 · 21740 次阅读
162
本帖已被设为精华帖!

总结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种缓存,分布在客户端到服务器端不同的位置,所能够节约的时间也正好从多到少依次排列。

共收到 91 条回复
10485

:plus1:

59

:thumbsup: 4,5,6期待中

2019

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

3

@quakewang 突如其来的大招啊!

2474

rails4好像砍了几个?

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

坐等楼主更新!

827

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

4482

292

good job

1107

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

3469

好贴!!!

1286

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

96

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

1510

好帖必頂 👍

5173

why not use Varnish?某些场景

6224

图用什么画的啊?

341

很棒……

7294

实战经验,赞!

96

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

2419

受益匪浅

5059

楼主干货颇多,受益良多

6571

都是干货,学习了

96

非常棒,学习了。

1026

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

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

1286

刚才试了一下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%左右.

1286

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

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

31楼 已删除
1286

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

162

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

162

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

1286

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

12767

涨姿势了

3191

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

1173

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

1173

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

1031

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

赞一个!

96

非常好的文章啊!

742

楼主加油!

7614

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

3191

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

3191

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

8807

好帖,顶一个

377

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

1411

不错

96

马克看看。

96

讚呀,清晰多了

另外這個廣告打得好 😄

96

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

De6df3

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

96

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

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

De6df3

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

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

162

#52楼 @hepochen rails的fresh_when是做了特殊处理的,在判断到etag match时候,只返回304 header,而不会去执行后续代码.可以参考源码: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/conditional_get.rb#L82 我里面提到的和Rack::Cache的对比curl实验也说明了这一点 至于last_modified,我在实际应用中因为采用fresh when/stale这种方式手工处理etag,就没必要混合使用,2者用其一就可以了.除非你有last modified未触发时间失效,但是又会出现etag改变的特殊情况才需要混用.

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

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

96

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

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

96

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

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

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

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

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

162

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

96

#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

96

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

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

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

162

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

96

#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. 但当做知识进行分享,感觉不是很好。

96

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

是如此,我当然知道……

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

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

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

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

96

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

162

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

96

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

96

#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压缩反而变慢了,这说不通啊……

5489

王叔V5

2880

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

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

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

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

96

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

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

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

96

@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参数可以设置……


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

8

#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

96

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

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

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

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

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

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

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

96

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

96

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

2880

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

10594

学习下

5130

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

4594

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

5178

很精彩 受教了

7772

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才是今后大方向,老实说大家有没有同感。

10775

很赞的一篇文章

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

4933

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

1249

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

1249

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

332

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

91楼 已删除
96

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

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

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

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