Webアプリケーションでよく使われる様々なcacheをまとめます

24955 ワード

add by zhj:まだ見ていないので、暇があればもっと詳しく見てください.
原文:https://ruby-china.org/topics/19389
Cacheは応用性能を高める重要な一環であり,使用した動的コンテンツに対する様々なcacheをまとめた文章を書く.記事では,Nginx,Rails,Mysql,Redisを例に,他のウェブサーバ,言語,データベース,キャッシュサービスに置き換えると類似している.以下は3階層の概略図で、後続の参照を容易にします.
                          +-------+

1                         | Nginx |

                          +-+-+-+-+

                            | | |

            +---------------+ | +---------------+

            |                 |                 |

        +---+---+         +---+---+         +---+---+

2       |Unicorn|         |Unicorn|         |Unicorn|

        +---+---+         +---+---+         +---+---+

            |                 |                 |

            |                 |                 |

            |             +---+---+             |

3           +-------------+  D B  +-------------+

                          +-------+




1.クライアントキャッシュ
ブラウザでWebサイトのトップページにアクセスしたり、同じ記事を表示したり、appで同じapiにアクセスしたりするなど、同じリソースにアクセスすることがよくあります.このリソースが以前にアクセスしたものと何の変更もなければ、http仕様の304 Not Modified応答ヘッダ(http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5)を利用して、クライアントのキャッシュを直接使用することができます.サーバ側でコンテンツを再生成する必要はありません.Railsにはfreshが内蔵されていますwhenという方法では、1行のコードで完了できます.
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-Nene-Martchを比較し、一致すればresponse bodyを生成せずに304に戻ります.
しかし、これは問題に直面します.もし私たちのウェブサイトのナビゲーションにユーザー情報があるとしたら、1人のユーザーがログインしていないテーマにアクセスし、ログインしてからアクセスすると、ページに表示されているのかログインしていない状態なのかがわかります.あるいはappで1つの文章にアクセスして、コレクションをして、今度この文章に入って、やはり未コレクションの状態を表示します.この問題を解決する方法は簡単で、ユーザー関連の変数も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


コントロールでfreshを指定する以外はその他、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_フィルタで実現します.
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


1台のサーバしかない場合、この方法は簡単で実用的ですが、複数のサーバがある場合、更新分類が自分自身のサーバキャッシュのみをリフレッシュできるという問題が発生します.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


同様のsqlクエリが10件発生します.
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種類のキャッシュは,クライアントからサーバ側まで異なる位置に分布し,節約できる時間もちょうど多くから少ない順に並べられている.