redisをsentinelとAliasIPを利用して冗長化 | サイバーエージェント 公式エンジニアブログ
どうも初めまして2012年度入社の社内ニート予備軍editnukiです。
普段は引きこもって WebSocketで監視もリアルタイムに を書いた社内ニートさんの下でコミュニティサービスのインフラをやっております。
運用面以外ではrpmパッケージ作ったりしています。

さて、本題ですがコミュニティサービスでもredisを利用したいという声が最近多くなりいくつかのサービスではredisを導入しているのですがマスターとなるredisが死ぬと更新系が一切できなくなるため、マスターが死んだ時はアプリの向き先をスレーブに変更しなければなりません。
今までのredisの構成としては下図の様な構成でした。


redis構成図1

図1.今までの構成

redisの2.6系がリリースされた時に「sentinel」というフェイルオーバーの機能が追加されました。 詳細は公式ドキュメントをご参照ください。
フェイルオーバーしたとしてもアプリ側にマスターが切り替わったことを通知することはできないため、sentinelを導入しても同様の手運用の部分は変わりませんでした。
先日GMO MEDIAの宇津井さんがRedis Sentinelを運用してみたお話というブログを書かれていて、宇津井さんはiptablesを利用した手法を利用されています。
宇津井さんも書かれていますが、この時はフェイルバック機能を持っていなかったのです。先日RELEASE NOTESを読んでいて、ver2.6.13からフェイルバックできるようになりました。
[NEW] Sentinel: turn old master into a slave when it comes back.
というわけで、私が検証した方法の設定例を上げていきますです。
図1の構成にもう1台追加します。


redis構成図2

図2. sentinel用サーバ追加

redisサーバにsentinelを導入しても良いのですが、私は監視とサービスを分けたいのであえて別サーバにsentinelを切り分けてます。
この様に1台追加するだけでフェイルオーバーはできるのですが、このままではweb-serverからはマスターを直に見ていてフェイルオーバーした後もマスターに対して更新処理を行おうとします。
なので私はマスターに対してAliasIPを付与し、web-serverからはそのAliasIPへ更新処理を行うようにしました。 今回はこのAliasIPがキモになります。


redis構成図3

図3. マスターへAliasIP追加

この様にすればAliasIPの付け替えをするだけで更新処理の向き先を変更が可能になります。
そして、次は「じゃAliasIPの付け替えはどうやってやるの?」ということになると思うのですが、sentinelにはフェイルオーバー時にclient-reconfig-scriptを使って指定したスクリプトを実行させることができます。
私のsentinel.confの設定例になります。
PATHがおかしいのは私が自分でビルドしたrpmパッケージなので気にしないでください。
この辺は適当に合わせてもらえればいいと思います。

port 26379
daemonize yes


loglevel notice
logfile /usr/local/redis/logs/sentinel.log
pidfile /usr/local/redis/logs/sentinel.pid


sentinel monitor ca-redis-sentinel redis-sentinel01 6379 1
sentinel down-after-milliseconds ca-redis-sentinel 10000
sentinel failover-timeout ca-redis-sentinel 900000
sentinel can-failover ca-redis-sentinel yes
sentinel parallel-syncs ca-redis-sentinel 2
sentinel client-reconfig-script ca-redis-sentinel /usr/local/redis/bin/alias_ip_change.sh


ポイントだけ説明しておくと
sentinel monitor ca-redis-sentinel redis-sentinel01 6379 1

「ca-redis-sentinel 」というのはただの監視グループの名前です。「redis-sentinel01」のところは実IPでも可能です、私は/etc/hostsに全部指定しているのでホスト名で書いてしまってます。
最後の1というのは複数のsentinelサーバがいる時に何台以上のsentinelがマスターダウンを検知したらフェイルオーバーをするものかです。
今回はsentinelは1台なのでここは必ず1になります。2にするとフェイルオーバーしないのでご注意を。

sentinel parallel-syncs ca-redis-sentinel 2

スレーブを追加する時はこの2という数字をスレーブの台数の数字に変更してsentinelの再起動をしてください。

sentinel client-reconfig-script ca-redis-sentinel /usr/local/redis/bin/alias_ip_change.sh

フェイルオーバー時のスクリプトですが、やっつけ&力技で書いてしまったので汚いのですが下記の通りとなっています。 AliasIPなどは各環境に合わせて記述してくださいませ。

#!/bin/bash


LOG="/usr/local/redis/logs/sentinel.log"
IP="/sbin/ip"
ETH_DEVICE="eth0"
ALIAS_DEVICE="eth0:0"
ALIAS_IP="エイリアスIP"
PREFIX="プレフィックス"
NETMASK="ネットマスク"
BROADCAST="ブロードキャスト"
REDIS_CLI="/usr/local/redis/bin/redis-cli"
SENTINEL_PORT="26379"
REDIS_COMMAND="info"
GREP="/bin/grep"
CUT="/bin/cut"
SSH_USER="sshできる適当なユーザ名"


# 旧マスターからAliasIPを取り除くssh $SSH_USER@$ALIAS_IP -o "StrictHostKeyChecking no" "sudo $IP addr delete $ALIAS_IP/$PREFIX broadcast $BROADCAST dev $ETH_DEVICE label $ALIAS_DEVICE"
echo "DELETE $ALIAS_IP from Master server" >> $LOG


# 新マスターにAliasIPを付与する
MASTER_IP_CHECK=`$REDIS_CLI -p $SENTINEL_PORT $REDIS_COMMAND | $GREP master0 | $CUT -d '=' -f 4 | $CUT -d ':' -f 1`
ssh $SSH_USER@$MASTER_IP_CHECK "sudo $IP addr add $ALIAS_IP/$PREFIX broadcast $BROADCAST dev $ETH_DEVICE label $ALIAS_DEVICE"
echo "ADD $ALIAS_IP to new Master servre" >> $LOG

exit 0;

(※我ながらひどいスクリプトだと思うので、いつか綺麗にしておきます・・・)

AliasIPを取り除く時にサーバのIPではなくAliasIPへsshをしており、AliasIPの付いてるサーバが変わるとknown_hostsで怒られてスクリプトがうまく動かなくなるので、「-o "StrictHostKeyChecking no"」オプションを利用しています。そうするとサーバが変わっていてもknown_hostsを勝手に書き換えてくれます。 このスクリプトはredisサーバのIPを記述していないのでサーバが増えても変更する必要がないです。
あとは、このスクリプトに実行権限は付けておきましょう。
 # chmod a+x /usr/local/redis/bin/alias_ip_change.sh

検証方法
フェイルオーバーの検証としてサーバごとshutdownしてみる、redisのプロセスを起動スクリプトで落とすの2パターンをやってみました。 更新処理はsentinel用のサーバからAliasIPにsetを大量に発行するものをbashで書いて実行しました。

結果
マスター昇格するスレーブサーバでの処理
$ /usr/local/redis/bin/redis-cli monitor
~一部抜粋~
1373885267.764209 [0 sentinelサーバIP:50811] "SLAVEOF" "NO" "ONE"
1373885268.267852 [0 sentinelサーバIP:50811] "INFO"
1373885268.468494 [0 sentinelサーバIP:62899] "SUBSCRIBE" "__sentinel__:hello"
1373885268.468538 [0 sentinelサーバIP:62898] "INFO"
マスター昇格するredisではSLAVEOF NO ONEを実行してマスターに昇格させていることがわかります 更新処理ですが、既存のコネクションは旧マスターとのコネクションなのでタイムアウトします。 新規でコネクションを張った場合は20秒程度で更新処理を再開することができました。 アプリ側でredisへのタイムアウトを20~30秒にしておけばよいと思います。 タイムアウトしたら再度setする処理とか入れておくと更によいかと。

注意点
この手法を利用する時の注意点をいくつかあげておきます。

1. client-reconfig-scriptの設定をするsentinelは1つに
 sentinelプロセスは1つでなくても良いが複数プロセス(サーバ)で起動した時にclient-reconfig-scriptを記述すると全部のsentinelでスクリプトを実行してしまいます(私が検証したら全部のsentinelで実行しようとしてました)

2. スレーブ用のredisは自動起動offにしておきましょう。
 スレーブの参照用VIPを立てた場合、redisが落ちたりサーバの再起動が起きるとマスターとレプリを張らずに独立したredisとして立ち上がるのでデータの不整合が起きている。  VIPを立てた場合はLBから切り離して手動で起動・slaveofを実行する必要がある

3. スレーブの台数を増やしすぎない
 redisは0からデータを取りに行くのでフェイルオーバーが起きると新マスターから全台データを取得しに行くので大量のトラフィックが発生する。

4. redis.confにレプリの設定を記述しない
 slaveofコマンドは初期構築時に手動で実行し、confには記述しないでください。フェイルオーバーが起きてredisが再起動した時にconfに記述されてるサーバへslaveofコマンドを実行して構成が謎になります。レプリを張るのは初期構築時に全て手動で実行してください。

これで私は夜中にredisが
_人人 人人_ 
> 突然の死 <
   ̄Y^Y^Y^Y ̄
になっても対応しなくて済むようになると思いますワーイヽ(゚∀゚)メ(゚∀゚)メ(゚∀゚)ノワーイ
翌日対応で十分( ー`дー´)キリッ
maxmemoryの設定(実メモリの半分)忘れんなよ ウワァァァァァァヽ(`Д´)ノァァァァァァン!
全部のキーにexpireかけない運用とかやめてね(´;ω;`)ブワッ
    ,-∧,,∧-- 、 
   / (-ω-` ) / 
   r-くっ⌒cソ、 /   ん、障害? 大丈夫起きてるよ。 
  ノ '、 , 、 _, ' / /    ちょっと横になるだけ。 
.(_,.       ././   ちょっと長い瞬きするだけ…
,(.,_ `'ー-、_,,..ノ/ 
  ~`''ー--‐' 
ばいちっ。