LoginSignup
1
0

More than 5 years have passed since last update.

RailsアプリケーションにおけるMemcachedへのModelオブジェクトキャッシュ

Last updated at Posted at 2018-12-05

この記事はAkatsuki Advent Calendar 2018 - Adventarの5日目の記事です。
4日目はWataru SanukiさんのiOSで利用する画像圧縮フォーマットASTCのススメ - スマゲでした。

はじめに

MemcachedへModelオブジェクトをキャッシュさせる自作Gemを紹介し、それを通してどれぐらい性能改善が期待できるか確認していきたいと思います。

Memcachedの利点

MemcachedにModelオブジェクトをキャッシュする利点は以下の通りだと想定しています。
- DBからデーターを読み込む回数を軽減させ、DBに対する集中負荷を減らす。
- オブジェクトのロードを高速化する。

上記の想定を元に、Modelに関連された(Associted)オブジェクト達をキャッシュにいれてそこから取り出すようにすれば、
DBへの負荷を減らし、Railsアプリケーションをもっと高速化できるのではないかと思い、一つのGemを作って検証してみました。

自作Gem

リポジトリ

https://github.com/begaborn/simple_cache

概要

SimpleCacheはあるオブジェクトとそれに関連された(Association)オブジェクトをキャッシュするライブラリです。

例えば、以下のような1対多関係のModelがあるとします。

User.rb
class User < ApplicationRecord
  has_many :user_characters, cache: true 
end
UserCharacter.rb
class UserCharacter < ApplicationRecord
end

最初は、 以下のコードを実行すると、 user オブジェクトに関連された user_characters オブジェクト達をデーターベースから取り出します(retrieve)。同時に、 user_characters オブジェクトをMemcachedにキャッシュさせます。

そのあと、このコードが再度実行されると、このオブジェクトをDBからではなく、Memcachedから取り出します。

user_character = User.take.user_characters.first

Screen Shot 2018-11-12 at 8.01.53 PM.png

もし、データーの更新・作成・削除が発生した場合にトランザクションのコミットのタイミングでキャッシュデーターはリフレッシュされます。

Installl

さっそくRailsアプリケーションに入れてみます。

gem 'ar-simple-cache', github: 'begaborn/simple_cache'
bundle install

基本、入れるだけで何もやる必要はありません。

本当にキャッシュしているか確認

初回ロード時は、SQLを発行し、Modelオブジェクトを生成することが確認できます。

rails c
pry(main)> User.find(100000067).user_characters

D, [2018-12-02T06:38:50.156468 #19543] DEBUG -- : [Shard: slave1]  User Load (49.5ms)  SELECT  `users`.* FROM `users` WHERE (`users`.`deleted_at` IS NULL) AND `users`.`id` = 100000067 LIMIT 1
D, [2018-12-02T06:38:50.208980 #19543] DEBUG -- :   UserCharacter Load (6.2ms)  SELECT `user_characters`.* FROM `user_characters` WHERE (`user_characters`.`deleted_at` IS NULL) AND `user_characters`.`user_id` = 100000067

[#<UserCharacter:0x007fdadd7e4838
  id: 1,
...

2番目からは、SQLを発行せず、Memcachedから持ってくることが確認できます。

pry(main)> User.find(100000067).user_characters
[#<UserCharacter:0x007fdadd7e4838
  id: 1,
...

ベンチマーク

どれぐらい性能改善が期待できるか確認していきます。
測定環境は以下の通りになります。

MacBook 3.1 GHz Intel Core i7 Mem:16G
OS:MacOS
DB:MySql5.7 
MacBookローカル上にMemcachedとMySqlが動いている状態。
※MemcachedとMySQLそれぞれ立ててやりたかったんですが、今回はパスします。

オブジェクトロード時の速度測定

読み込み速度を測定するために、以下のベンチマークテストコードを実行してみました。

Benchmark.bm do |x|
  x.report do
    10000.times do 
      user = User.find(100000067)
      account = user.user_account
      characters = user.user_characters
      c = characters.first
    end
  end
end

UserCharacterのレコード件数:10件
SimpleCache未使用 - 毎回DBからデータを取り出し、Modelオブジェクトを生成

       user     system      total        real
  41.950000   1.980000  43.930000 ( 50.296216)

SimpleCache使用 - ModelオブジェクトをMemcachedにキャッシュさせ、キャッシュから読み込む。

       user     system      total        real
  21.070000   1.360000  22.430000 ( 24.641330)

キャッシュからオブジェクトを読むことで、約40%〜50%程度速度が改善されています。

オブジェクト更新とロードを繰り返した場合の速度測定

以下のテストコードを実行してみます。

Benchmark.bm do |x|
  x.report do
    10000.times do 
      # ロード
      user = User.find(100000067)
      account = user.user_account
      characters = user.user_characters
      c = characters.first

      # 更新
      c.lv = i % 10
      c.save!
    end
  end
end

SimpleCache未使用

       user     system      total        real
  66.230000   3.240000  69.470000 ( 80.891619)

SimpleCache使用

       user     system      total        real
 147.510000   8.700000 156.210000 (177.592329)

すごく遅くなったことがわかります。これは当たり前の結果ですね。
毎回更新処理が走るので、オブジェクトを生成してキャッシュして、作ったキャッシュを使用せずまたリフレッシュして。。。を繰り返すからです。

そのため、更新頻度が高いものはキャッシュをオフにした方がよいと思います。
実は、SimpleCacheはキャッシュをオフにする機能も持っています。
以下のように設定することで、 「 User に関連されている(Association) user_charactersはキャッシュしません 」 という設定が可能です。

User.rb
class User < ApplicationRecord
  has_many :user_characters, cache: false
end

重いオブジェクトロード時の速度測定

UserCharacter:2000件
SimpleCache未使用
※あまりにも時間かかるので、ループ回数を10000 → 1000回に変更しました。

       user     system      total        real
   4.240000   0.240000   4.480000 (  5.101447)

SimpleCache使用

      user     system      total        real
 130.520000   1.320000 131.840000 (132.392052)

一応Rails.cache.readで直接読むようにして、検証してみましたが、結果はほぼ変わっていませんでした。

Benchmark.bm do |x|
  x.report do
    1000.times do 
      user = User.find(100000067)
      account = user.user_account
      #characters = user.user_characters
      characters = Rails.cache.read('key_name') # 直接読み込んでみる
      c = characters.first
    end
  end
end

大量のオブジェクトをMemcachedから取り出す場合は、むしろDBから取り出して生成するよりも何倍も遅くなりました。
関連オブジェクトが大量にある場合もキャッシュをオフにした方が良いかもせれません。

注意事項

このGemを使う上で注意事項です。

  • 以下のようなActive Recordコールバックをスキップするメソッドを使って、データーを更新・削除した場合は、キャッシュがリフレッシュされません。 なんだかの理由で以下のメソッドを必ず使いたい場合は、 cache: false にすることをお勧めします。
decrement
decrement_counter
delete
delete_all
increment
increment_counter
toggle
update_column
update_columns
update_all
update_counters
  • has_many または has_one に以下のオプションを指定した場合はキャッシュしません。
:as, :through, :primary_key, :source, :source_type, :inverse_of
  • 現在バージョンでは、以下のメソッドのみキャッシュ機能をサポートしています。
find, has_many, has_one
  • まだ開発&検証の最中です!なにか問題とかあればご連絡ください。歓迎です!

まとめ

この記事でベンチマークを通して以下のことが確認できました。
- DBからデーターを読み込む回数を軽減できた。
- オブジェクトのロードが高速化できた。
- 更新頻度が高いものはキャッシュしないほうがよい。オブジェクトをキャッシュするならマスターデーターなどほぼリードオンリのものがおすすめ。
- 一つのKeyに対して重いオブジェクトをValueとしてMemcachedに入れると逆に遅くなる。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0