この記事はPORT Advent Calendar7日目の穴埋めです。
Ruby on Railsの高速化戦略として最もポピュラーなのは、Fragment CacheやローレベルCacheの活用だと思います。私の所属しているチームが運用しているキャリアパーク!でも、多くのページでこれらのキャッシュが使用されています。
キャッシュストアへのアクセス遅延
Railsでキャッシュを活用する場合、多くはバックエンドのデータストアとしてRedisもしくはMemcacheが用いられます。
これらのデータストアはAmazon ElasticCacheなどのクラウドサービスを用いてネットワーク上に設置するのが一般的です。ローカル環境で開発しているとついつい忘れがちなのですが、ネットワーク上に存在するキャッシュストアにアクセスする際には、ネットワークを介することによる遅延が発生してしまいます(多くの場合において無視できるレベルですが・・・)。
ですが、例えば一つのwebページを表示する際にFragment Cacheを5箇所に分けて使用しているなど、リモートのキャッシュストアに複数回のアクセスを行っていたりすると、この遅延も無視できないものになってきます。
LruReduxで超高速なMemoryCacheを
上記のようなネットワーク遅延が気になる場合、MemoryStoreをキャッシュストアとして利用すれば解決できるかもしれません。Rails標準のActiveSupport::Cache
でもMemoryStoreはサポートされていますが、今回はより高速なLruReduxを紹介します。
LruReduxて何?
自分もあまり詳しくはありませんが、Lru(Least Recently Used)というキャッシュアルゴリズムをRubyで使用するためのライブラリだと認識しています。
ベンチマーク
試しに、ActiveSupport::Cache::MemoryStore
とLruRedux
のパフォーマンスを比較するためにベンチマークを撮ってみました。
このページのbody要素をそれぞれのキャッシュストアに1000回別のkeyで保存し、取り出すという操作を行ってみました。
require 'net/http'
uri = 'http://api.rubyonrails.org/classes/ActiveSupport/Cache/MemoryStore.html'
body = Net::HTTP.get_response(URI.parse(uri)).body
memory_cache = ActiveSupport::Cache::MemoryStore.new
lru_redux = LruRedux::TTL::ThreadSafeCache.new(1000, 24.hours)
Benchmark.bm 12 do |r|
r.report 'memory' do
1000.times do |n|
memory_cache.write("key-#{n}", body)
end
1000.times do |n|
tmp = memory_cache.fetch("key-#{n}")
end
end
r.report 'lru_redux' do
1000.times do |n|
lru_redux["key-#{n}"] = body
end
1000.times do |n|
tmp = lru_redux["key-#{n}"]
end
end
end
結果は以下のようになりました。ついでにRedisの結果も含めてあります。
total | real | |
---|---|---|
MemoryStore | 0.430 | 0.438 |
LruRedux | 0.020 | 0.020 |
Redis | 0.620 | 0.632 |
通常のMemoryStoreと比較して、LruReduxだと約20倍以上のパフォーマンスが出ていそうです。
Railsでの具体的な実装
色々と方法が考えられると思いますが、自分はconfig/initializers
にグローバルなインスタンスとして定義しました。
MemoryStore = LruRedux::TTL::ThreadSafeCache.new(100, 24.hours)
説明するまでもないですがLruRedux::TTL::ThreadSafeCache
は、時間指定ありでなおかつスレッドセーフという意味です。LruRedux
にも様々なクラスが存在しますが、基本はこれを使えば間違いないと思われます。第一引数の100
は最大保持数(keyの)で、24.hour
はデフォルトの保持時間です。
さらに、通常のFragmentCacheと同様の感覚で使えるように、ビューヘルパーを定義しました。
def memory_cache(key, &block)
MemoryStore[key] ||= capture(&block)
end
# ---- 使用例 ----
# = memory_cache 'key' do
# = render 'partical_name'
まとめ
ここまでで説明したようにLruReduxは非常に高速ですが、通常ここまでのパフォーマンスは必要とされません。また、ホストマシンのメモリを消費するので、あまり多用するわけにはいきませんし、複数台で稼働している場合は扱いが難しくなりがです。本番で使う場合は注意が必要です。
最後になりますが、PORT株式会社では自社サービスを支えてくれる優秀なRubyエンジニアを募集しています(Rubyエンジニア以外も)。
もくもく会も行なっていますので、ぜひ一緒にもくもくしましょう!
PORTもくもく会