ElastiCacheのRedisのSETが詰まった時おれたちは

# あらすじ
AWS上にWEBサービスを構築しておりました所、徐々に全体的なレスポンスが悪化していっておりました。よくあることです。計測してみた所、サマリ結果をキャッシュしていたElastiCacheのRedisがネックになっていた事が発覚しました。
更に詳細に計測してみると、どうやらRedisに対するSETになんか8秒ほどかかっている模様、おっと、何かがおかしい。

![キャプチャ2.PNG](https://qiita-image-store.s3.amazonaws.com/0/126264/d56e0442-c63f-ab24-c866-05af4fc7629b.png)

現場は騒然とし始めます。一刻も早くこのボトルネックを潰さないと大変なことになる、こうして我々の残業はスタートを切りました。
こうした試行錯誤の顛末が、誰かの役に立てばと思ってこの記事を残します。

# 結論先取
- ElastiCacheの弱いインスタンスはネットワーク帯域が狭い
    - ElastiCacheのデフォルトのインスタンスが `r3.large` なのには意味がある
- キャッシュをしたけりゃ現金を払え

# GETしか絞れないSLOWLOG、そして雰囲気キャッシュ
AWSコンソール上でメトリクスを確認してみると、CPU使用率はスパイクして5%程度、メモリ使用量にも十分な余裕がありました(詳細な数値は失念しましたが)。我々は首をひねります。
この時点で、Redisのシングルスレッドという性質が原因では無いことは明らかです。

こういう時は、いつだって初歩の初歩から始めるのがベストです。Redisに `SLOWLOG` という便利そうなコマンドが存在することを確認した私は、スローログを片っ端から洗い出して原因を追求しようとしました。
コマンドは `$ redis-cli -h <endpoint> slowlog get` です。 `$ redis-cli -h <endpoint>` を実行してから `slowlog get` するのも一つの手段です。便利なサブコマンド補完が我々をサポートしてくれるでしょう！
しかしSLOWLOGのサブコマンドはGETかLENしか指定出来ません、私が求めているのはSETのスローログなのに！
これには大いに悩まされました。何故なら、我々の環境下では一番遅いGETでも0.08秒程度しか掛かっていなかったからです！
この数字がキャッシュとして十分な速度なのかはともかく、目下のボトルネックの原因とは到底思えません。

次に、続々とクエリを投げ込まれるRedisの処理状態を把握したいと思い、MONITORしました。
コマンドは `$ redis-cli -h <endpoint> monitor` です。
ログに吐き出すのも有効でしょう。
ともかく、Redisの負荷を計測結果ではなく目視で確認したかったのです。

するとどうでしょう、時折、異様に巨大なvalueがSETされているのが確認できました。
およそ130万文字のJSON文字列がStringとしてSETされていたのです。おっと、これは明らかに嫌な臭いがします。
JSONをRedisに保存するのなら、巨大なJSON文字列よりもHashで保存したほうがどう考えても適切な予感がします。これだ！　我々は歓喜の声を挙げました。終電が無くなる前には帰れるぞ！　……果たして本当にそうでしょうか？
RedisのドキュメントのString型の項目には次の通りに書かれています。

```
A String value can be at max 512 Megabytes in length.
(文字列は最大512MBまで可能です)
```

おっと。我々がネックだと思いこんでいた文字列は130万文字前後、約1.3MBです。この程度のデータ量で、ネックになることなどありえるのでしょうか？
ターミナルから直接1.3MB分の文字列をSETしようとするとターミナル自体はフリーズしますが、内部的に処理しきれないとは到底思えません。これは検証が必要でしょう。

# 検証
私は手元の端末にRedisをインストールし、最も手に馴染むRubyで検証を開始しました。
手順は簡単です、 `$ gem install redis` でRedis用のGemをインストールし、極めて簡潔なコードを書くだけです。
試しに150万文字で検証してみましょう。

```rb
require 'redis'
require 'benchmark'
redis = Redis.new
Benchmark.bm do |r|
  r.report 'redis_test' do
    redis.set('test', "A" * 1500000)
  end
end

puts "result string length: #{redis.get('test').length}"
```

シンプルですね。

```
       user     system      total        real
redis_test  0.010000   0.000000   0.010000 (  0.002447)
result string length: 1500000
```

結果も全然余裕みたいです。この時点でSETしてるvalueが原因ではないことがわかってきます。
もっと限界を目指しましょう。1億文字で検証してみます。

```
       user     system      total        real
redis_test  0.070000   0.040000   0.110000 (  0.138954)
result string length: 100000000
```

ちょっと遅くなりましたが、まあ全然許容範囲でしょう。1億文字(約100MB)でこの速度なら、130万文字のSETが大きなボトルネックになるとは到底思えません。

5億文字いきましょう。

```
       user     system      total        real
redis_test  0.290000   0.230000   0.520000 (  9.759761)
result string length: 500000000
```

かなり致命的な遅さになってきました。5億文字(約500MB)をSETし始めると明らかにネックになりそうです。気をつけましょう（マシンの状態を考慮して複数回実行する必要性は本件には無いので、省略致します）。

ちなみに6億文字だと上限を超過するのでエラーを起こします。当然ですね。

# たった一つのやり取りに導かれ
130万文字のSETが原因でないとしたら、一体何がこのボトルネックを引き起こしているのでしょうか？
片っ端からドキュメントやユースケースを調査していた所、とあるGoogleグループのやり取りを発見しました( https://groups.google.com/forum/#!searchin/redis-db/size/redis-db/n7aa2A4DZDs/3OeEPHSQBAAJ )。

```
Stefano Fratini 

Memcache recommends values no bigger than 1mb
(Memcacheは1MB以上のvalueを推奨してないんだ)
I would suggest the same for Redis. Even more so not going over the 100kb is most likely a good idea
(Redisにも同じことが言えると思う。100KBを超えるのは良くないよ)
Especially if you are hosting on AWS, having bigger objects lead to networking bottlenecks (the amazon guys are very stingy with bandwidth - and smarty so)
(特に、AWSを使ってるんだったら、デカいオブジェクトはネットワークのボトルネックに繋がるからさ(Amazonの帯域は小さいからね！))
```
**"特に、AWSを使ってるんだったら、デカいオブジェクトはネットワークのボトルネックに繋がるからさ"**

ネットワーク！！！
考えても見ませんでした、ElastiCacheのインスタンスのネットワークがネックになるなど！
我々がElastiCacheで使用していたRedisインスタンスは `t2.small` でした。デフォルトインスタンスの費用があまりに高価だったため、省コストの為にインスタンスタイプをケチっていたのです。
t2.smallのネットワークパフォーマンスは次の通りです。
`低から中`
どっちだよ！！
ともかく、 `t2.small` のネットワークパフォーマンスは劣悪な予感がします。
よくよく考えると、ElastiCacheのデフォルトインスタンスは `r3.large` です。AWS側が金儲けの為に強めのインスタンスをデフォルトに設定しているとは考えづらいです。
`r3.large` のネットワークパフォーマンスは `中` ……中か……一体何を以て中としているのか、何が低なのかわからん……だが `t2.small` よりは遥かにまともそうだ……そこで、ネットワークパフォーマンスが中の内もっとも安価な `m3.medium` に変更してみます。
するとどうでしょう、平均8秒掛かっていたRedisのSETが2秒へと縮まりました！
ああ、やはり強めのインスタンスをデフォルトにしているのには意味があったのです！

# 結末
ひとまずは `m3.medium` で2秒掛かってもサービス上は問題ないと判断されました。本当でしょうか？
ネットワークパフォーマンスが `高` かつCPUやメモリがそこそこでお安いインスタンスが存在すれば我々の抱えている問題は十分に解決できたのではないでしょうか？
もし、このWEBサービスが十分に認知され、データ量が増えてきた時、またElastiCacheのインスタンスタイプの見直しが発生すると思われます。その時はきっと、CPUやメモリのリソース込みで `r3.2xlarge` あたりのメモリ最適化インスタンスかつネットワークパフォーマンス `高` のインスタンスを選択出来ると信じています。
結局の所、ネットワークパフォーマンスを現金で買うしか我々に解決策は存在しなかったという事です。
Redisは悪くない。頼れるやつだ。
我々はこうして本案件をクローズしたのでした。

まあ、インスタンス強くしただけなんですけどね。

いい感じにキャッシュをやるならいい感じに現金を払う、これに尽きます。