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

この記事は[Akatsuki Advent Calendar 2018 - Adventar](https://adventar.org/calendars/2990)の5日目の記事です。
4日目はWataru Sanukiさんの[iOSで利用する画像圧縮フォーマットASTCのススメ - スマゲ](http://smartgames.hatenablog.com/entry/2018/12/04/001708)でした。
# はじめに 
MemcachedへModelオブジェクトをキャッシュさせる自作Gemを紹介し、それを通してどれぐらい性能改善が期待できるか確認していきたいと思います。

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

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

# 自作Gem 
## リポジトリ 
https://github.com/begaborn/simple_cache 

## 概要 
[SimpleCache](https://github.com/begaborn/simple_cache)はあるオブジェクトとそれに関連された(Association)オブジェクトをキャッシュするライブラリです。

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

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

```ruby:UserCharacter.rb
class UserCharacter < ApplicationRecord
end
```

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

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

```
user_character = User.take.user_characters.first
```

<img width="643" alt="Screen Shot 2018-11-12 at 8.01.53 PM.png" src="https://qiita-image-store.s3.amazonaws.com/0/81150/c378131d-91d2-9647-5a2a-95bc4d87403d.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,
...
```

２番目からは、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それぞれ立ててやりたかったんですが、今回はパスします。
```

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

```ruby
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)
```
キャッシュからオブジェクトを読むことで、約４０％〜５０％程度速度が改善されています。

### オブジェクト更新とロードを繰り返した場合の速度測定
以下のテストコードを実行してみます。

```ruby
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](https://github.com/begaborn/simple_cache)はキャッシュをオフにする機能も持っています。
以下のように設定することで、 「 `User` に関連されている(Association)  `user_charactersはキャッシュしません` 」　という設定が可能です。

```ruby: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で直接読むようにして、検証してみましたが、結果はほぼ変わっていませんでした。

```ruby
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に入れると逆に遅くなる。