MySQLのMemcached Pluginを使ってみた

この記事は [Akatsuki Advent Calendar 2016](http://qiita.com/advent-calendar/2016/aktsk) の3日目です。

mysqlのmemcached pluginを使ってみたので書いてみます。


# 前提
Rails/MySQL/MemcachedをAWSで運用しています。


# 経緯
- ElastiCacheを使用する上でmemcachedの冗長化方法について悩んでいました。
- 調べる最中にmysqlのmemcached pluginを使ってみたので書いてみます。

## 冗長化に求めていた要件
- RedisをElastiCacheで運用しているとMulti-AZによる自動復旧がありますが、自動復旧の仕組みがmemcachedにも欲しいという要望がありました。
- 落ちたときのキャッシュデータ消失は許容可能です。


# 調べてみた

ElastiCacheでmemcachedを運用している上で、どのような冗長化方法があるのでしょうか。

## AWSのクラスタ機能
- AWSには[ElastiCacheクラスタ](https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/UserGuide/Clusters.html)という機能があり、コンシステントハッシュ方式で水平分割します。
- 1台が死ぬと、そのノードに書き込んだデータは損失しますが、生き残ったノードの中で再度ノードを選択するので、キャッシュは消えますが、システムは継続可能です。
- デメリットとして、一時的に接続不能な状態からノードが復旧した時に、復旧したノードに残っている古いデータにアクセスしてしまいます。

一見要件を満たしているように見えますが、使っているシステムの都合上、スケールアウトした複数サーバから、共通のキャッシュデータを読み込むことも多かったため、Aのサーバからはデータが読み込めるが、Bのサーバからデータが読み込めない（キーが保存されていない別ノードにアクセスしてしまう）ということも起きうり、今回の要件としては使いづらかったです。

## Rubyのよさげなgem

- 複数のmemcachedノードに、同じデータを書き込んでくれる仕組みがあれば、上記の問題が解決できそうです。
- [double_write_cache_stores](https://github.com/hirocaster/double_write_cache_stores)というgemがあり、2台のmemcachedを指定すれば、activeとstandbyの両方のノードに書き込んでくれます。

使ってみたところ、activeノードが落ちても、standbyノードのデータが読み込み可能になるわけではなく(gemの中身も見ました)、データの冗長化は保証されますが、自動フェイルオーバーがされないので、こちらも今回の要件としては使いづらかったです。

## プロキシを立てる

- いろいろありますが、例えば[mcrouter](https://github.com/facebook/mcrouter)というfacebookが使っているツールがあります。
- 自動フェイルオーバーや、パラメータ調整もいろいろできそうで、かなり高機能です。

wktkな気分でいましたが、用意されているツールがUbuntu前提だったりして、Amazon Linuxでは茨の道でした。とは言え、他にも[twemproxy](https://github.com/twitter/twemproxy)などもあり、プロキシツールを検討するのはありだと思います。


## MySQLのmemcached plugin

- 調べているうちに、MySQLのmemcached pluginに出会いました。
- MySQLのinnoDBをmemcachedとして使用できます。

これだと、Multi-AZの自動復旧の恩恵を受けられるため、信頼性高まるかも？と期待したので、触ってみました。


### 設定方法

- 非常にシンプルで、RDSのオプショングループでmemcached_pluginを指定して起動するだけです。
- m1シリーズ以上じゃないと使えないらしいのがタマにキズです。

![スクリーンショット 2016-12-03 0.20.54.png](https://qiita-image-store.s3.amazonaws.com/0/95146/948a72a2-048f-4665-22ab-c6d9dde3fd82.png "スクリーンショット 2016-12-03 0.20.54.png")


# 測ってみた

- 気になるのは性能です。
- RDSはm1シリーズ以上じゃないとダメなので、m1.smallを使います。
- 対するElastiCacheはt2.smallを使いました。 (~~目の前にt2.smallがあったのでクラスを合わせずこれでいいやと怠慢を発揮したなんて口が裂けても言えない。~~)


### 測定方法

- 前提として、memcachedはRailsのcache storeに使用しています。

```ruby
config.cache_store = :dalli_store,
```

- 10000回のread/writeをそれぞれ3回ずつ実行し、平均を取得します。

```ruby
Benchmark.realtime do
  0.upto(10000) { |n| Rails.cache.write(n,n) }
end

Benchmark.realtime do
  0.upto(10000) { |n| Rails.cache.read(n) }
end
```

### 結果

| サービス | read | write |
|:-----|:-----:|:-----:|
| memcached (ElastiCache) | 2.322223666 | 2.495266701 |
| memcached plugin (RDS)  | 3.113506614 | **118.4347181** |

遅せぇぇ...
デフォルトではmemcached pluginのwrite速度が非常に遅く、話になりまてんでした...


## チューニングしてみる
- [DAEMON_MEMCACHED_R_BATCH_SIZE](https://dev.mysql.com/doc/refman/5.6/ja/innodb-parameters.html#sysvar_daemon_memcached_r_batch_size)
- [DAEMON_MEMCACHED_W_BATCH_SIZE](https://dev.mysql.com/doc/refman/5.6/ja/innodb-parameters.html#sysvar_daemon_memcached_w_batch_size) 

というパラメータがあります。
commit単位のパラメータで、（え、memcachedやのにcommit？）デフォルトが1だったので、10000に変更してみました。


### チューニング後の結果

| サービス | read | write |
|:-----|:-----:|:-----:|
| memcached (ElastiCache) | 2.322223666 | 2.495266701 |
| memcached plugin (RDS)  | 2.984877883 | **3.211960071** |

- memcached pluginのwriteの速度が大幅に改善しました。DAEMON_MEMCACHED_W_BATCH_SIZEが特に有効だということが分かります。
- それでもまだ若干ElastiCacheより遅いです。


# 所感
- チューニング次第ではMySQLのmemcached pluginは使用する選択肢になり得るかなという所感です。
- 大量アクセスによるネットワークのオーバヘッドもかなりあると思うので、あくまで参考値ということで。

# その他
- MAX_SIMULTANEOUS_CONNECTIONSという接続数パラメータが、デフォルト値1024だったので使用に当たっては変更必須かもです。
- ~~てかRedis使えやとかいうマサカリはやめましょう。~~