Redis 本番障害から学んだコードレビューの勘所

Redis不適切利用による問題は本番運用が始まってから顕在化することが多く、時限爆弾みたいな存在です。事前に防ぐにはコードレビュー段階で叩くしかありません。

Redisはスクリプト言語と相性が良く、適切に利用するとRDBと比較し驚くほど高速なプログラムを組むことができます。昨年尊敬する先輩にコードレビューで斧100本くらい(レビューコメント)投げられて血まみれになりつつ学んだことを、まとめて書いてます。概要は『消えても良いデータならRedis』

# Redisのメモリが溢れたら...
（この話は事実ではなくファンタジーです。）
深夜電話で叩き起こされました。どうやらアクセス障害みたいです。
何人かで実機確認したら、まったくゲームが遊べない。データ不整合怖いのでメンテIN。

ほどなくしてRedisが溢れメモリ不足で新規書き込みが出来なくなっていると判明。サーバのメモリ容量は64GByteでこれ以上スケールアップできなかったのでRedisのメモリ容量削減に着手しました。

![スクリーンショット 2015-10-26 10.44.48.png](https://qiita-image-store.s3.amazonaws.com/0/65312/191d3301-666c-0007-53d6-f5f7e7182b06.png)

## 巨大なRedisDBは上手くプロファイルできない
ローカルだとrdmみたいなツールでツリー構造にし、どこにキーが多いか可視化できますがメモリ容量が64GbくらいのRedisサーバになってくるとツリー構造で俯瞰することできず原因となるKeyがわかりませんでした。結果コード側から原因究明に着手することになります。


## Redisの垂直分割は時間掛かりすぎて絶望する
なんとかプロファイルし原因が判りました。いつの間にか外は明るくなっています。どうやらRedisをストレージとして利用されていたみたいで、ユーザの大切なデータが保管されているため消すことは出来ませんでした。仕方なくもう一台のRedisサーバを用意して、半数のキーをコピーすることにしました。コピーバッチを書いて実行したところ2時間たっても終わりません。新しいRedisサーバにアクセスしKEY数を数えたところ5%しかコピーが完了しておらず、完了まであと38時間掛かることが判明しました。本番環境が2日止まることは容認できないと、さらに並列でデータコピーするバッチを......

# Redisサーバのよくある落ち方
Redisサーバが落ちるか、APPサーバが落ちるかは状況によります。
1. KEYSコマンドやZRANGEコマンドで数万件以上にアクセスしてI/O待ちで落ちる
2. データ設計をミスってメモリ不足で書き込めなくなって落ちる


# Redisサーバで障害発生時の基本対応
まずはメモリ不足かI/O待ちかを切り分ける。
1. 実行に時間がかかるZRANGEやkeys * コマンドを見直す 
2. 不要なKeyを削除する。
3. サーバ追加して垂直分割する。
4. データ設計を変更し、データ保管場所をRedisからRDBに変更

# コードレビューでの観点
以上のようにRedis不適切利用による問題は本番運用が始まってから顕在化することが多く、時限爆弾みたいな存在です。事前に防ぐにはコードレビュー段階で叩くしかありません。RedisサーバのI/Oが劣化しないこと、メモリ使用量が爆発しないこと、データ整合性を保てることの3つの観点でコードレビューを行います。

■ 1. Redisを使っていたら警戒する
ユーザ数が多いサービスだと1つのデータ設計ミスで、本番RedisサーバかAPPサーバが停止する可能性があります。Redisを利用している段階で最悪の事態を想像し、コードレビューでの警戒レベルを引き上げます。

■ 2. キャッシュではなくストレージとして利用していたら指摘する
大切なデータはRDBに記録して、キャッシュのみRedisに保存しましょう。基準は『消えても良いデータならRedis』です。そもそもRedisに大切なデータを記録していると、DBのトランザクションロールバック時にRedis側データをロールバックできずデータ不整合が発生します。

■ 3. Expireは設定されているか
Redisではkey毎にExpire(TTL)を設定できます。キャッシュとして利用していれば最長一ヶ月くらいのExpireで問題ないはずです。一ヶ月以上のExpireを設定する必要がある場合は、ほぼ間違いなくRedisをストレージとして利用している証拠です。RDBを利用するよう設計を変更しましょう。

■ 4. ZRANGEBYSCOREコマンドを使っていないこと
RedisのSortedSetはリアルタイムランキングといった更新され続けるリストの開発にとっても便利です。RDBでは重くて実現できない機能が実現できます。SortedSetからスコア範囲を指定して取得するコマンドがZRANGEBYSCOREです。思いもよらないデータの偏りがあるとき、1クエリーで数万件取得してしまい応答時間が1秒を超え大幅劣化するためI/O待ちでAPPサーバかRedisが死亡します。場合にもよりますが基本使ってはいけない。スコアではなくランクで範囲指定出来るZREVRANGEコマンドで取得すべきです。
<img width="940" alt="スクリーンショット 2015-10-27 15.39.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/65312/c0e6cb7f-8b71-d056-ebe3-1102a3d05914.png">


■ 5. keys * コマンドを使っていないこと
keysコマンドはワイルドカードでマッチしたKEYを返却するコマンドです。こちらも同様にデータの偏りがあると、I/O待ちでサーバが死にます。keysコマンドで線形探索するくらいならRDBに入れてIndex張ってWHERE句発行しましょう。

■ 6. どうしてもRedisをストレージとして利用しなければいけない場合もある
何事にも例外があります。例えばRedisのSortedSetのように、Redis特有の機能を利用しないと実現できない機能は少ないですが存在します。そんな時は次の観点でレビューします。

■ 6-1. ロールバックによるデータ不整合に備えた設計になっているか
DBトランザクション中に問題が発生し、ロールバックするときRedisはロールバックされません。ロールバックされRDBとRedis間でデータ不整合が発生したときに、正しいデータに切り戻される設計になっているかの観点で確認しましょう。RDBとRedisに重複してデータを持ち、ViewからはRedisにアクセスして高速化の恩恵に与り、データ更新時にRDBの値を正としてRedisデータを上書いていけば解消します。

■ 6-2. Redisのデータが消えたとき復旧できるか(必須ではない)
Redisが飛んだりデータ不整合が発生したとき復旧できるよう、事前に全復旧コマンドを用意しましょう。RDBに保存さえ出来ていれば、障害起きてからコード書いても問題ないので必須の対応ではありません。


# Redisのプロファイルはbigkeysコマンドが便利
bigkeysコマンドはサンプル調査なので、巨大DBでも現実的な時間で調査が終わります。1100万KEYのRedisに対して1分くらいです。

複数人PJの本番環境で打つと見たことあるけど消していいかわからない古いKeyが多数見つかり、消していいのか途方にくれることが多いですが改善のヒントを得ることもまれにあります。インターネットに情報が少ないため[redis-cli.c](http://download.redis.io/redis-stable/src/redis-cli.c)のbigkeysの実装を直接読んだほうが間違いなく早いです。


```shell-session:個人PJの1100万KeyあるDBをプロファイル
>> redis-cli -n 11 --bigkeys

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:CAMERA:563749' with 51 items
[00.00%] Biggest hash   found so far 'CF:GOODS:TAG:208737' with 1 fields
[00.00%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:TAG7:554718' with 56 items
[00.00%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:CAMERA:62917' with 65 items
[00.01%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:TAG10:798984' with 66 items
[00.01%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:DVD:585528' with 73 items
[00.06%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:CLOTHES:739957' with 74 items
[00.44%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:DVD:241918' with 83 items
[06.03%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:CLOTHES:452765' with 87 items
[09.01%] Sampled 1000000 keys so far
[18.02%] Sampled 2000000 keys so far
[27.03%] Sampled 3000000 keys so far
[36.03%] Sampled 4000000 keys so far
[45.04%] Sampled 5000000 keys so far
[54.05%] Sampled 6000000 keys so far
[63.06%] Sampled 7000000 keys so far
[69.93%] Biggest list   found so far 'CF:INDEX:GOODS-HIS:COMPUTER:328426' with 89 items
[72.07%] Sampled 8000000 keys so far
[81.08%] Sampled 9000000 keys so far
[90.09%] Sampled 10000000 keys so far
[99.10%] Sampled 11000000 keys so far

-------- summary -------

Sampled 11100436 keys in the keyspace!
Total key length in bytes is 588720855 (avg len 53.04)

Biggest   list found 'CF:INDEX:GOODS-HIS:COMPUTER:328426' has 89 items
Biggest   hash found 'CF:GOODS:TAG:208737' has 1 fields

0 strings with 0 bytes (00.00% of keys, avg size 0.00)
10100437 lists with 100997852 items (90.99% of keys, avg size 10.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
999999 hashs with 999999 fields (09.01% of keys, avg size 1.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
```



# 参考
[redis-cli.c](http://download.redis.io/redis-stable/src/redis-cli.c)