この記事は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
リポジトリ
概要
SimpleCacheはあるオブジェクトとそれに関連された(Association)オブジェクトをキャッシュするライブラリです。
例えば、以下のような1対多関係のModelがあるとします。
class User < ApplicationRecord
has_many :user_characters, cache: true
end
class UserCharacter < ApplicationRecord
end
最初は、 以下のコードを実行すると、 user
オブジェクトに関連された user_characters
オブジェクト達をデーターベースから取り出します(retrieve)。同時に、 user_characters
オブジェクトをMemcachedにキャッシュさせます。
そのあと、このコードが再度実行されると、このオブジェクトをDBからではなく、Memcachedから取り出します。
user_character = User.take.user_characters.first
もし、データーの更新・作成・削除が発生した場合にトランザクションのコミットのタイミングでキャッシュデーターはリフレッシュされます。
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はキャッシュしません
」 という設定が可能です。
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に入れると逆に遅くなる。