HHR’s diary

日記は無理でも週記…いや、月記でがんばろう。

Redisのreplication構成+自動failover(sentinel)をVagrantで構築してnode.js(express + ioredis)から利用する

はじめに

Redisのcluster構成に関する記述はありません。

Redisの構成に関しては公式ドキュメント(英語)にあるreplicationsentinelの記述の通りです。
目新しいことは無いと思います。

node.jsのRedisクライアントにはioredisを利用します。node_redisに関する記述はありません。

expressのsession-storeにRedisを利用する前提で記述してあります。
なぜCookieだけでないのか、なぜsession-storeにRDBなどの他のミドルウェアを利用しないのか、のような話題の記述はありません。

取り扱う内容

  • Vagrant
    • 下記構成のRedisサーバを構築
  • Redis
    • replication
      • master-slave構成のこと
    • 自動failover
      • masterのdown検知時に自動で(人の手を介さずに)slaveがmasterに昇格すること
      • masterの監視はredis-sentinelを使用
  • expressからの利用
    • 構築したRedisをexpressのsession-storeとして利用

ローカルの仮想マシンでreplicationと自動failoverをしてもあまり意味がないので、可能ならばAWS複数リージョンを使うなど、IaaSを活用したかったのですが、お金がかかるので諦めました。

以下は全てmacで実行していますが、Vagrantに依存するものがほとんどのため、Vagrantが動作する環境ならばどこでも大丈夫だと思います。

Redisの準備

f:id:HHR:20160116165435p:plain:right

Vagrantを使用してRedisサーバを3台、作成します。
1台のサーバあたりメモリを500MBちょっとつかうのでメモリをとっても圧迫します…。使用しているmacのメモリの8GBでは複数仮想マシンを動作させるには辛い様子で、途中、動作が重たくなったのでchromeを閉じました。
あと、電池がよく減ります。

構成

1台はmasterのRedis、他2台はslaveのRedisを配置します。
それぞれのサーバにはRedisのプロセスとは別にsentinelのプロセスを起動します。

                  +---------------+
                  | 192.168.50.10 |
                  |---------------|
                  | master        |
                  | sentinel-1    |
                  +---------------+
                          |
+---------------+         |         +---------------+
| 192.168.50.11 |---------+---------| 192.168.50.12 |
|---------------|                   |---------------|
| slave-1       |                   | slave-2       |
| sentinel-2    |                   | sentinel-3    |
+---------------+                   +---------------+

Vagrantのインストール

brew install Caskroom/cask/vagrant

f:id:HHR:20160116165328p:plain:w270,right

Vagrantはとっても便利です。よく利用します。

今回はubuntu公式のBoxを使用しました。
余談ですが、仮想マシンに特化したメジャーなBoxは無いのでしょうか…。Boxのダウンロードも前述したメモリ使用量も電池の減りも、とにかく重いです…

それとも自分が情弱なだけですでに存在するのでしょうか。

Vagrantfile

仮想マシンの構成を定義するVagrantfileを用意します。

ココにRedisをインストールするVagrantfileとプロビジョニングのスクリプトがあったので利用させてもらいました。

ただし、これは1台の仮想マシン用のコードであるため、前述の構成に対応する変更を加えます。

Rubyは詳しくないのですが、endの数がすごいことなっています。JavaScriptのコールバックヘルみたいです。

プロビジョニングのスクリプトにも変更を加えます。
1台はmaster、他2台はslaveにする処理とsentinelを起動する処理です。
本番運用ではmasterとslaveで設定ファイルを分け、sentinelは自動起動スクリプトを登録したりするべきかと思いますが手を抜きます。

sentinelの設定ファイルを追加します。
デーモン化とPIDファイル置き場所の指定、初期時点でのmasterの場所とquorumを設定します。
quorumについては割愛します。

まとめたコードはGithubに置いてあります。

起動

vagrant up

初回はとっても長いです。 osのboxイメージ取得から処理されるので強いネットワーク回線必須です。
自分は最初、テザリング環境でトライしてむりぽを悟り、ご自宅でリトライをいたしました。わかっていましたけどね…

構成の確認

ホストマシン(ここではmac)にRedisのクライアントをインストールしてRedisの状態を確認します。
クライアントをインストールと言いつつ、サーバもインストールされてしまいますが無視します。

brew install redis
replication
$ redis-cli -h 192.168.50.10 info replication
# Replication
role:master                       <- ココ
connected_slaves:2
slave0:ip=192.168.50.11,port=6379,state=online,offset=245191,lag=0
slave1:ip=192.168.50.12,port=6379,state=online,offset=245191,lag=0
master_repl_offset:245191
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:245190

$ redis-cli -h 192.168.50.11 info replication
# Replication
role:slave                        <- ココ
master_host:192.168.50.10
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:247884
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

$ redis-cli -h 192.168.50.12 info replication
# Replication
role:slave                        <- ココ
master_host:192.168.50.10
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:248744
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

少し使った後の実行結果なのでまっさらではないですが、良い感じです。

sentinel
$ redis-cli -h 192.168.50.10 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=192.168.50.10:6379,slaves=3,sentinels=3

$ redis-cli -h 192.168.50.11 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=192.168.50.10:6379,slaves=3,sentinels=3

$ redis-cli -h 192.168.50.12 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=192.168.50.10:6379,slaves=3,sentinels=3

大丈夫そうです。

動作確認

実際にreplicationと自動failoverの動作確認をします。

replication

masterに値をセットしてslaveで取得できるかを確認します。

$ redis-cli -h 192.168.50.10 set hoge 'fuga'
OK

$ redis-cli -h 192.168.50.10 get hoge
"fuga"

$ redis-cli -h 192.168.50.11 get hoge
"fuga"

$ redis-cli -h 192.168.50.12 get hoge
"fuga"

良いですが、どうしてもローカル環境なので当たり前感が…。
AWS複数のリージョンにサーバを設置するなど、遠隔地環境でタイムラグ等の実測値がどうなるかは気になります。

自動failoverの確認

masterを擬似的に落とします。

$ redis-cli -h 192.168.50.10 DEBUG sleep 30
OK

roleがどうなったかを確認。

$ redis-cli -h 192.168.50.10 info replication
# Replication
role:slave                        <- slaveに変わった
master_host:192.168.50.11
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:374134
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:66095
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:66094

$ redis-cli -h 192.168.50.11 info replication
# Replication
role:master                       <- masterに変わった
connected_slaves:2
slave0:ip=192.168.50.10,port=6379,state=online,offset=376249,lag=0
slave1:ip=192.168.50.12,port=6379,state=online,offset=376263,lag=0
master_repl_offset:376263
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:341012
repl_backlog_histlen:35252

$ redis-cli -h 192.168.50.12 info replication
# Replication
role:slave                        <- そのまま
master_host:192.168.50.11
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:383073
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

グッドです。
sentinelも確認します。

$ redis-cli -h 192.168.50.10 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=192.168.50.11:6379,slaves=3,sentinels=3

$ redis-cli -h 192.168.50.11 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=192.168.50.11:6379,slaves=3,sentinels=3

$ redis-cli -h 192.168.50.12 -p 26379 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=192.168.50.11:6379,slaves=3,sentinels=3

がはは、グッドだー

因みにですが、slaveになった192.168.50.10には書き込めなくなります。

$ redis-cli -h 192.168.50.10 set hoge huga
(error) READONLY You can't write against a read only slave.

つまり、クライアントはRedisサーバがfailoverで切り替わった際に向き先を変更しなければ書き込みができなくなってしまいます。
せっかくRedisサーバが自動failoverしてもクライアントが対応しないと意味無いです。
クライアントがnode.jsの場合どうするかを後述します。

再度、192.168.50.10をmasterにします。

$ redis-cli -h 192.168.50.11 DEBUG sleep 30
OK

expressからの利用

f:id:HHR:20160116233047p:plain:right

自分のmacのスペック的にこれ以上は仮想マシンを追加したくなかったので、ホストマシンでnode.jsのアプリケーションを起動してそこから仮想マシン上のRedisをコールすることにします。
node.jsもプラットフォームの対応が素晴らしいのでmacに限らず大体どこでも大丈夫だと思います。

brew install node.js
npm install express-generator -g
express redis-failover-hello-world
cd redis-failover-hello-world && npm install

前述のとおり、ioredisを使用します。

npm install --save ioredis

セッションの保存先をRedisにするnpmパッケージをインストール。

npm install --save connect-redis express-session 

app.jsを書き換えて、セッションの保存先をVagrant上に構築したRedisに指定します。

起動。

$ DEBUG=ioredis:* npm start

> redis-failover-hello-world@0.0.0 start /Users/hhr/git/redis-failover-hello-world
> node ./bin/www

  ioredis:redis status[localhost:6379]: [empty] -> connecting +0ms
  ioredis:redis status[192.168.50.10:26379]: [empty] -> connecting +7ms
  ioredis:redis queue command[0] -> sentinel(get-master-addr-by-name,mymaster) +4ms
  ioredis:redis status[192.168.50.10:26379]: connecting -> connect +76ms
  ioredis:redis status[192.168.50.10:26379]: connect -> ready +1ms
  ioredis:connection send 1 commands in offline queue +0ms
  ioredis:redis write command[0] -> sentinel(get-master-addr-by-name,mymaster) +1ms
  ioredis:redis status[192.168.50.10:6379]: connecting -> connect +11ms
  ioredis:redis write command[0] -> info() +1ms
  ioredis:redis status[192.168.50.10:6379]: connect -> ready +3ms
  ioredis:redis status[192.168.50.10:26379]: ready -> close +1ms
  ioredis:connection skip reconnecting since the connection is manually closed. +0ms
  ioredis:redis status[192.168.50.10:26379]: close -> end +0ms

ふむふむ、

  1. 192.168.50.10:26379に接続(sentinelに接続)
  2. redis-cli -h 192.168.50.10 -p 26379 sentinel get-master-addr-by-name mymaster的なコマンド実行(sentinelにmasterの場所を問い合わせ)
  3. 192.168.50.10が返ってくる)
  4. 192.168.50.10:6379に接続(masterに接続)
  5. redis-cli -h 192.168.50.10 -p 6379 info的なコマンド実行(masterであることを確認している?)
  6. 192.168.50.10:26379を切断

のようです。

HTTPサーバが起動したので、ブラウザからアクセスして挙動を確認します。

http://localhost:3000

  ioredis:redis write command[0] -> set(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs,{"cookie":{"originalMaxAge":null,"expires":null,"httpOnly":true,"path":"/"}},EX,86400) +2m
GET / 200 43.978 ms - 170
  ioredis:redis write command[0] -> get(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs) +47ms
  ioredis:redis write command[0] -> expire(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs,86400) +3ms
GET /stylesheets/style.css 200 1.763 ms - 111
  ioredis:redis write command[0] -> get(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs) +162ms
  ioredis:redis write command[0] -> expire(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs,86400) +41ms
GET /favicon.ico 404 40.017 ms - 1919
  ioredis:redis write command[0] -> get(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs) +27ms
  ioredis:redis write command[0] -> expire(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs,86400) +28ms
GET /favicon.ico 404 27.736 ms - 1919

favicon.icoが無い、は無視します。
セッション情報をRedisに書き込んでそれを取得している様子が確認できます。

ブラウザの開発ツール等でHTTPヘッダやクッキーを見るとs:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs...があり、これがセッションIDであることがわかります。

ブラウザリロード。

  ioredis:redis write command[0] -> get(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs) +4m
  ioredis:redis write command[0] -> expire(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs,86400) +14ms
GET / 304 13.421 ms - -
  ioredis:redis write command[0] -> get(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs) +40ms
  ioredis:redis write command[0] -> expire(sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs,86400) +4ms
GET /stylesheets/style.css 304 2.832 ms - -

2回目以降はセッションIDの有効期限内であれば書き込みはされないようです。
有効期限切れのパターンは割愛します。

ターミナルからも見てみます。

$ redis-cli -h 192.168.50.10 keys '*'
1) "hoge"
2) "sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs"

$ redis-cli -h 192.168.50.10 get 'sess:vp5z1WR_7aK8eHjzJkbv5xXLRZEKPahs'
"{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"}}"

セッションの保存、良い感じです。
slaveに保存されているかの確認は割愛します。

failoverさせます。

redis-cli -h 192.168.50.10 DEBUG sleep 30
  ioredis:redis status[192.168.50.10:6379]: ready -> close +7m
  ioredis:connection reconnect in 2ms +0ms
  ioredis:redis status[192.168.50.10:6379]: close -> reconnecting +1ms
  ioredis:redis status[192.168.50.10:6379]: reconnecting -> connecting +2ms
  ioredis:redis status[192.168.50.11:26379]: [empty] -> connecting +1ms
  ioredis:redis queue command[0] -> sentinel(get-master-addr-by-name,mymaster) +1ms
  ioredis:redis status[192.168.50.11:26379]: connecting -> connect +1ms
  ioredis:redis status[192.168.50.11:26379]: connect -> ready +0ms
  ioredis:connection send 1 commands in offline queue +1ms
  ioredis:redis write command[0] -> sentinel(get-master-addr-by-name,mymaster) +0ms
  ioredis:redis status[192.168.50.11:6379]: connecting -> connect +2ms
  ioredis:redis write command[0] -> info() +0ms
  ioredis:redis status[192.168.50.11:26379]: ready -> close +1ms
  ioredis:connection skip reconnecting since the connection is manually closed. +1ms
  ioredis:redis status[192.168.50.11:26379]: close -> end +0ms
  ioredis:redis status[192.168.50.11:6379]: connect -> ready +1ms

おぉ、切り替わりました。 注目すべきは、HTTPアクセスが無い状態で勝手に接続先が切り替わったことです。大変よい感じです。うふふ。

さいごに

以上、ざっとですが自動failoverするRedisサーバをexpressで使用することができました。
masterがdownして、その後に復帰したらどうなるかとか無いので本当にざっとです…

個人で自動failoverする構成の需要が高くないためか、割りと調べることに苦労しました…

まぁ、AWSやherokuとかではたいてい機能が用意されているので地味なネタなのかもです。

Redis入門 インメモリKVSによる高速データ管理

Redis入門 インメモリKVSによる高速データ管理