akishin999の日記

調べた事などを書いて行きます。

Redis でダウンタイム無しの再起動

Apache でいうところの「graceful restart」的な機能が Redis には無いのかを調べてみたのですが、どうやらそういった機能そのものは無いようでした。
では Redis の場合はサービスを無停止でバージョンアップなどを行い、プロセスを再起動するにはどうしたらいいのでしょうか?

ダウンタイム無しでの再起動については、公式の「Redis Administration」内の「Upgrading or restarting a Redis instance without downtime」という部分で触れられています。

Upgrading or restarting a Redis instance without downtime
http://redis.io/topics/admin

どうやら「ダウンタイム無しでプログラムの更新を行いたい場合にはレプリケーションを使用する」ということのようです。

流れとしては

  • 稼働中の Redis のスレーブサーバとして新しい Redis のインスタンスを用意する
  • もしサーバが一台しかない場合は、同一サーバ上にマスタとは別のポートでスレーブを立てる
  • スレーブを起てたらログを見て最初の同期が完了するのを待つ
  • 同期が完了したら INFO コマンドで実際にキーの数が一致してるかを確認
  • クライアントの設定をスレーブサーバを参照するように変更する
  • マスタ側でモニタコマンドを実行し、リクエストが来ていない事を確認する
  • スレーブ側で SLAVEOF NO ONE を実行してマスタに昇格してから、旧マスタを停止する

といった感じ。

参照中心だったり、書き込みがバッチや管理画面などからが中心で運営側でコントロール可能な場合などには比較的容易に再起動に入れそうです。
試したわけではありませんが、書き込みリクエストが多い場合、マスタを切り替える部分のタイミングが結構難しいんじゃないかなー、なんて気はしますが・・・。
実際どうなんでしょうね?

Sharding している場合の懸念点

で、ライブラリの実装方式にもよりますが、この方法だと Sharding をしている場合に「切り替え後のサーバと切り替え前のサーバとを同一 Shard として認識してくれない」といった問題が発生する可能性がありそうです。
使用しているライブラリが Hash の生成元として「ホスト名」や 「IP アドレス」「ポート番号」の組合せを使用しているような場合、マスタとスレーブとで異なる Hash が生成されてしまう為、それぞれ別の Shard として認識されてしまいます。

当たり前といえば当たり前の挙動なんですが、サーバの Hash の元となる文字列を指定できないライブラリなどでは対応が若干面倒かも知れません。
そういった場合には、ライブラリに手を入れるより、ロードバランサなどを間に入れて、アプリケーションからは「同一 Shard は同一 IP アドレス + 同一ポートでアクセスできるようにする」といった方法のが楽かもですね。

ちなみに、Ruby の redis-rb もデフォルトでは「redis://#{location}/#{db}」という形式の文字列から作成した Hash が Shard 追加時の Hash として使用されるようですが、ちゃんと明示的に指定することもできるようになっています。
さすが良く出来てますね。

というわけで簡単ではありますが検証してみました。

Sharding 環境の用意

まずは Sharding 環境を用意します。

以下のようにするとお手軽に複数台のインスタンスを起動できます。

# redis-server --port 6379 >> redis1.log &
# redis-server --port 6380 >> redis2.log &
# redis-server --port 6381 >> redis3.log &

ちゃんとプロセスが起動して LISTEN している事を確認します。

# ps -ef | grep [r]edis
root      8714  2484  0 21:18 pts/0    00:00:00 redis-server --port 6379
root      8717  2484  0 21:18 pts/0    00:00:00 redis-server --port 6380
root      8720  2484  0 21:18 pts/0    00:00:00 redis-server --port 6381
# netstat -tln | grep 63*
tcp        0      0 0.0.0.0:6379                0.0.0.0:*                   LISTEN
tcp        0      0 0.0.0.0:6380                0.0.0.0:*                   LISTEN
tcp        0      0 0.0.0.0:6381                0.0.0.0:*                   LISTEN

テストデータの投入

準備ができたら以下のような Ruby コードを書きます。

# -*- coding: utf-8 -*-

require 'redis'
require 'redis/distributed'

REDIS_HOSTS = [
  {url: "redis://192.0.2.1:6379/0", id: "redis-node1"},
  {url: "redis://192.0.2.1:6380/0", id: "redis-node2"},
  {url: "redis://192.0.2.1:6381/0", id: "redis-node3"},
]

redis = Redis::Distributed.new(REDIS_HOSTS)

KEYS = ('a'..'z').map {|c| c * 3}

# ランダムな値を Redis に設定する
KEYS.each { |key|
  redis.set(key, "key:#{key}")
}

ポイントは接続 URL だけでなく、Shard 毎に id 文字列を指定している部分。
実行すると先ほど起動した Redis に Sharding されてデータが格納されます。

確認してみると以下のようにキーが分散されていました。

# redis-cli -p 6379 KEYS '*'
1) "www"
2) "ccc"
3) "vvv"
4) "fff"
5) "mmm"
6) "yyy"
# redis-cli -p 6380 KEYS '*'
 1) "aaa"
 2) "ggg"
 3) "ooo"
 4) "zzz"
 5) "nnn"
 6) "kkk"
 7) "ttt"
 8) "xxx"
 9) "jjj"
10) "iii"
11) "rrr"
12) "bbb"
13) "uuu"
# redis-cli -p 6381 KEYS '*'
1) "ddd"
2) "lll"
3) "sss"
4) "qqq"
5) "eee"
6) "hhh"
7) "ppp"

スレーブサーバの準備

データの投入が完了したので、切り替え手順をイメージして新たにスレーブを用意します。

# redis-server --port 6400 >> redis-slave.log &

プロセスが増えている事を確認します。

# ps -ef | grep [r]edis
root      8714  2484  0 21:18 pts/0    00:00:01 redis-server --port 6379
root      8717  2484  0 21:18 pts/0    00:00:00 redis-server --port 6380
root      8720  2484  0 21:18 pts/0    00:00:00 redis-server --port 6381
root      8889  2484  0 21:31 pts/0    00:00:00 redis-server --port 6400

スレーブ用のプロセスが起動した事を確認できたら、redis-cli を使って早速スレーブとして動作させます。
Redis をスレーブとして動作させるには以下を実行するだけです。
ここでは一つ目のプロセス(ポート 6379)のスレーブとしました。

# redis-cli -p 6400 SLAVEOF 127.0.0.1 6379
OK

レプリケーション接続の状態は以下のコマンドで確認できます。

# redis-cli -p 6400 INFO Replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_priority:100
slave_read_only:1
connected_slaves:0

「master_link_status」が「up」となっていればマスタとの接続は確立できています。

ここで手順ではログで同期状況を確認、とありましたが、データ量が少ないためすぐに同期は完了しているはずです。
以下のようにしてキーをマスタと比較して確認してみます。

# redis-cli -p 6379 KEYS '*'
1) "www"
2) "ccc"
3) "vvv"
4) "fff"
5) "mmm"
6) "yyy"
# redis-cli -p 6400 KEYS '*'
1) "ccc"
2) "fff"
3) "www"
4) "vvv"
5) "mmm"
6) "yyy"

順序は一致しないようですが、同じキーがちゃんと同期されている事が分かります。

マスタの切り替え

問題なさそうなので、マスタを停止します。

# ps -ef | grep [6]379
root      8714  2484  0 21:18 pts/0    00:00:01 redis-server --port 6379
# kill -TERM 8714

マスタが停止したら、スレーブをマスタに昇格します。
スレーブをマスタに昇格させるには、「SLAVEOF NO ONE」を実行します。

# redis-cli -p 6400 SLAVEOF NO ONE
OK

これでスレーブはマスタになりました。

切り替え後の Sharding

ここまでで無停止更新は出来ました。
後は入れ替えた後のサーバに対して Sharding が問題なく動作するかです。
以下のように今度はキーを取得するようにプログラムを修正して、スレーブからマスタと同じ値が取れるかを確認します。

# -*- coding: utf-8 -*-

require 'redis'
require 'redis/distributed'

REDIS_HOSTS = [
  {url: "redis://192.0.2.1:6400/0", id: "redis-node1"},  # 停止したマスタを除去しスレーブを指定
  {url: "redis://192.0.2.1:6380/0", id: "redis-node2"},
  {url: "redis://192.0.2.1:6381/0", id: "redis-node3"},
]

redis = Redis::Distributed.new(REDIS_HOSTS)

KEYS = ('a'..'z').map {|c| c * 3}

# Redis から値を取得する
KEYS.each { |key|
  p redis.get(key)
}

Shard の配列から先ほど停止した旧マスタを除外し、マスタに昇格させたスレーブ(新マスタ)を追加します。
新マスタを追加する際の id には、必ず旧マスタと同一のものを指定します。

実行結果は以下のようになりました。

"key:aaa"
"key:bbb"
"key:ccc"
"key:ddd"
"key:eee"
"key:fff"
"key:ggg"
"key:hhh"
"key:iii"
"key:jjj"
"key:kkk"
"key:lll"
"key:mmm"
"key:nnn"
"key:ooo"
"key:ppp"
"key:qqq"
"key:rrr"
"key:sss"
"key:ttt"
"key:uuu"
"key:vvv"
"key:www"
"key:xxx"
"key:yyy"
"key:zzz"

問題なく全てのキーが取得できています。

というわけで、redis-rb を使用して Sharding を行う場合には基本的に id は指定しておいた方が良さそうです。
ミドルウェア側にサービスに影響なく再起動できる方法があると、運用するのが少しは気楽になりますね。



Redis Cookbook
Redis Cookbook
posted with amazlet at 13.06.05
Tiago Macedo Fred Oliveria
Oreilly & Associates Inc
売り上げランキング: 27,988

Redis in Action
Redis in Action
posted with amazlet at 13.06.05
Josiah L. Carlson
Manning Pubns Co
売り上げランキング: 43,263