概要
RedisClusterのチュートリアルを読みつつ疑問を解消していく。
Redis自体がシンプルな仕組みなこともあり、ドキュメントを読むだけでおよそ理解できるものと思われるので、下手な追加説明はあまりしていません。
以下、引用の箇所は筆者が和訳したもの、それ以外が筆者のコメントになります。
Redis cluster tutorial
このドキュメントはRedisClusterについて書かれています。
distributed systemのコンセプトを簡単にまとめたものになります。
どうやってクラスタを構成し、テストし、運用するかを要点を絞ってまとめたものです。詳細はspecの方をご確認ください。
しかし、可用性と一貫性についての説明も試みています。
プロダクションで使うにはspecificationも読むことをおすすめしますが、とりあえず試したい方はこちらをお読み下さい。
Redis Cluster 101
clusterは複数のノードで自動的にデータを共有します。
ある程度の可用性を担保しますが、大きな障害時にはクラスタ全体がfailします。
- 複数ノードに自動的にデータが分散します。
- ある程度のノードが死んでもクラスタは継続し続けます。
Redis Cluster TCP ports
各ノードは2つのtcp connectionを開きます。
ひとつはクライアントとの通信(デフォルトで6379)、もうひとつはノード間のp2p通信用(クライアント通信port番号+10000番で行われる。)で、情報共有に適したbinaryプロトコルでやりとりされる。
これはノードfailの検知にも使われる。
p2pの方のノードはredis クラスタ以外からは使用しないでください。
note: 各ノードが動くためには、portを2つ開けておき、クラスタ間、クライアント間で連絡取れることが必要。
→ノード間でp2pで情報をやりとりするので、Redis-sentinelのようなマスタが不要です。
Redis Cluster and Docker
redis clusterはNATの通信(ipやportがチェンジする)をサポートしてないので、dockerなどを使う場合は、ネットワークインターフェースをhostにする(docker commandで
--net=host
)を使う。
Redis Cluster data sharding
shardingは、各ノードはそれぞれhash slotを持っていて、各データに対してkeyのhashを計算して該当のhash slotに格納する。
slotは16384(2^14)個あり、これを各ノードで分担する形になる。
例えばマスタが3ノードの場合、
- Node A contains hash slots from 0 to 5500.
- Node B contains hash slots from 5501 to 11000.
- Node C contains hash slots from 11001 to 16384.
ノードの追加・削除も簡単。
Dを追加したい場合は、クラスタに混ぜた後にslotを割り当てる。
Aを削除したい場合は、B,C,Dにslotを割り当てた後にAをクラスタから除外する。
slotの移動は無停止で可能。
→ノードの追加、削除がオンラインで行える。
redisはひとつのコマンドで複数のkeyの操作をサポートしているが、クラスタの場合はそのkeyすべてが同じslotになければならない。これはhashtagsと呼ばれる。
→slotが異なるとreshardingされた場合に別ノードに散らばってしまうのと、後述するがプロキシなどの仕組みを持たないため。
同一slotに格納したい場合は、Keyに特殊な文字を用いることで調整が可能。詳細はこちら
Redis Cluster master-slave model
一定のマスタノードが死んでも可用性を維持するために、クラスタはマスタスレーブ型を採用していて、複数のレプリカを作ることができる。
例えば、ABCがあってBが死んた場合、Bの担当するhash slotは利用できなくなってしまう。しかしA1B1C1というslaveがいれば、Bが死んだ場合はB1がマスタに昇格しクラスタは動作し続ける。
もしB1も死んでしまいBのスレーブがなくなると動作は継続できなくなる。
→仕組み上、スレーブの昇格には多少のダウンタイムがある。
Redis Cluster consistency guarantees
Redisは強一貫性を保証しない。言い換えるとあるパターンではデータがロストする。
一つ目の理由は非同期レプリケーションによるもの。
- クライアントがマスタBに書き込む。
- BがクライアントにOKを返す。
- BはスレーブであるB1B2B3に非同期に書き込みを行う。
ここで、レイテンシを低くするためにBはスレーブへの書き込みが完了する前にクライアントにOKを返す。
そのためスレーブに書き込む前にBが死ぬと、最後の書き込みが届いていないスレーブがマスタに昇格し、最後の書き込みが永久に失われる。
これはどのDatabaseでも起こる問題なので、強一貫性を確保しようと思ったら、レイテンシを犠牲にしてレスポンスを返す前にデータをディスク(または他のノード)に書き込む。
性能を取るか一貫性を取るかはトレードオフを迫られる。redisも必要であればwaitコマンドを使い同期的にレプリケーションすることで一貫性を担保できる。
しかしそれでもredisは一貫性を失うケースがある。
例えばABCA1B1C1がいて、クライアントZ1がいた場合を考える。ここで分断が起きてB,Z1だけ外れた場合、Z1はBへの書き込みを続けるが、他方ではB1がマスタに昇格している。しばらくしてBが自身がはぐれたことを検知して死んだ場合、その書き込みは失われる。
この場合、少数派(今回はB)がどのくらいの時間クラスタとはぐれたら書き込みを拒否するかという「node timeout」パラメータを適切に設定する必要がある。
→ 現実にはクライアントと一部のマスタだけ分断されるケースはほとんど考えられないとは思います。
Redis Cluster configuration parameters
クラスタ構成時に使われるパラメータを紹介する。これらはredis.confに書かれている。
パラメータの意味するところはこのページを読み進めれば明らかになるだろう。
- cluster-enabled : そのサーバをクラスタのノードとしたいならyes
- cluster-config-file : Note これはユーザーが書き込むものではなくredisがクラスタ内の設定や自身の状態を適宜記録しておくもので、再起動時に使われる。
- cluster-node-timeout : そのノードが生きているかのチェック時間。これを過ぎてクラスタとの接続が確認できなければ、そのノードは死んだとみなされ、クエリは止められる。
- cluster-slave-validity-factor : 0のとき、スレーブはマスタの接続が切り離されて一定時間経つかに関係なく常にマスタに昇格しようとする。数字が正の数の場合、node timeoutと掛けあわせた数字が「maximum disconnection time」となり、マスタの接続が確認できなくてもその時間の間はスレーブはマスタに昇格しない。
この間、マスタが再度クラスタにジョインするまでは可用性が失われる状態になる。
→設定によるが、普通はこの間はクラスタへの書き込みができなくなる。
- cluster-migration-barrier : マスタが確認してるスレーブ数の最小値。
詳細は「 replica migration」を参照。
→これの数を下回った場合、そのマスタは動作を止める。
- cluster-require-full-coverage : (デフォルトである)yesの場合、一部のkey space(slot)が利用できなくなった場合に書き込みを停止する。noの場合は、処理できる書き込みのみ継続し続ける。
Creating and using a Redis Cluster
Note: 手動で構築するのは理解に役立つが、さっさとたちあげたい場合は「Creating a Redis Cluster using the create-cluster script」へどうぞ。
まずはクラスタモードの空ノードを作成する。これは、redisではクラスタ構築には、別途命令を出す必要があるため、まずはクラスタ対応のノードを立ち上げておく。
最小の設定は以下。
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
クラスタが有効になっているのが確認できるだろう。nodes.confはredis自身で起動時に作成し適宜更新されるので手出し無用。
Note: クラスタ構成は最小で3台必要だが、初めはそれにスレーブ3台を合わせて6台構成にすることをおすすめする。
そうするために、redis serverを複数立てるためこんな風にディレクトリを用意しておく。
mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005
そしてredis.confを各ディレクトリにつくる。redis-serverのbinaryを各ターミナルで以下のように起動する。
cd 7000
../redis-server ./redis.conf
各ノードにはIDがふられる。
[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1
このIDは各ノードの識別子として使われる。portやIPは変わる可能性があるため。
このIDをNode IDと呼ぶ。
Creating the cluster
これで必要なredis-serverは用意出来た。これらをクラスタにしていく。
これはredis-tribというruby scriptで行う。このスクリプトはクラスタ生成、チェックやreshardなどクラスタ関連の操作を色々行う。
まずはredis gemをインストールする。
gem install redis
クラスタの作成はシンプルに
./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
レプリカはスレーブの数を表し、ここでは全6台でスレーブが一つなので、マスタ3台、スレーブ3台構成になる。
yesを打つとクラスタ構成を始め、以下のメッセージが出れば完了だ。
[OK] All 16384 slots covered
16384slot全てが準備できた。
Creating a Redis Cluster using the create-cluster script
もっとシンプルな方法もある。
ただutils/create-cluster directory を見ればいい。
今回の構成を真似たければ以下のとおりだ。
create-cluster start
create-cluster create
停止したければ
create-cluster stop.
詳しくはreadme参照。
Playing with the cluster
まだクライアントを用意していない。(各言語のクライアントのどれかを使うか)redis-cliを使おう。
以下がサンプルの動作確認だ。
$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7000> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"
Note: もちろん、自分で構築したIP,portになっている。
redis-cliは基本的な動作をサポートしているため、redirectが走っている様子がわかる。
賢いクライアントは、無駄にredirectを発生させないようにcacheを実装していたりする。もし実態が変わったことを検知したら随時更新していく。
→クライアントによっては、どのノードがどのslotのマスタかを保持していたり、定期的にクラスタの状態を確認したりする。
Writing an example app with redis-rb-cluster
failoverやreshardingの前にクライアントの簡単な動作確認をしよう。
ここではredis-rb-clusterの基本的な使い方を2つ見せよう。
これはただ値をsetしているだけだ。
コマンドラインにはこんな感じで出力されるだろう。
SET foo0
0SET foo1
SET foo2
And so forth...
このサンプルは主にエラー時の状態を可視化するために作られていて、各クエリはrescue blockにラップされている。
7行目でRedis Clusterオブジェクトを生成していて引数でstartup nodeを選んでいる。
ここでは全部書く必要はなく、最初にクラスタへ接続した際に残りのノードも取得する。
rcが普通のredisオブジェクトとして使える。
値はredis自体に格納しているので、再起動しても途中から再開する。
エラー時には標準出力に表示される。
Note:sleepで処理速度を調節しているが、もしこれがなければ10k ops/sくらい出る
Resharding the cluster
クラスタのreshardingです。これをする間、example.rbを動かし続けててください。作業中に変化があったのが解ると思います。
あるいはsleepをコメントアウトして、書き込みの問題を起こしてもいいでしょう。
reshardingはhash slotを別のノードに移動することです。redis-tribで実行できます。
./redis-trib.rb reshard 127.0.0.1:7000
一つのノードだけ指定してもクライアントが他のノードも自動的に見つけてくれます。現状、redis-tribはadminで操作します。
M: 685f70137f76dc7870ae115ae21c6972445b6ce7 172.17.0.2:7000
slots:0-5460 (5461 slots) master
1 additional replica(s)
S: 6940d6662a6fb826d2e7fe17fe1152c95f702db8 172.17.0.6:7000
slots: (0 slots) slave
replicates d030f4f02e51b005306826236d8a814cad928a2a
M: b5a926a13e25ee4f93e2fb6062a6622be95f2e77 172.17.0.7:7000
slots:11643-16383 (4741 slots) master
0 additional replica(s)
M: d030f4f02e51b005306826236d8a814cad928a2a 172.17.0.3:7000
slots:5461-11642 (6182 slots) master
1 additional replica(s)
S: b42834c1aec8dac0167118d9323828932239ff97 172.17.0.5:7000
slots: (0 slots) slave
replicates 685f70137f76dc7870ae115ae21c6972445b6ce7
How many slots do you want to move (from 1 to 16384)? // どのくらいのslotを移動する?
What is the receiving node ID? // どこから?
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1: // どこへ?
Source node #2: // IDを選んでエンターを押していき、終わったらdone。
A more interesting example application
上記の例はあまり良くない。書き込むが、何が書き込まれているのが正しいのかチェックしていない。
fooだと必ず42slotに書き込まれるか確認する。
redis-rb-cluster repositoryには面白いものがある。consistency-test.rb
incrコマンドを送り続け、書き続けるとこうなる。
- 現在の値を書き込む
- 適宜読み込み、値があってるか確認する。
これで一貫性チェックができる。クラスタがどのくらいwrite readをロストしたのかわかる。
Adding a new node
ノードの追加は、まず空ノードを追加してデータを移行する流れになる。
マスタの追加とノードの追加の2つを試そう。
同じようにredis-serverを立ち上げよう。
そして追加するコマンドを叩く。
./redis-trib.rb add-node <new server> <cluster server>
これでクラスタにノードが加わった。
Note: これで新しいノードが(マスタとして)加わった。しかしこのノードはまだ、
- slotを保有してない(空のまま)
- なぜならまだslotを割り当てられてないから。
前に書いたのと同じようにslotをreshardingして割りあてることができる。
Adding a new node as a replica
スレーブとして立てるには2つ方法がある。
ひとつは--slaveオプションをつけること
./redis-trib.rb add-node --slave <target> <cluster>
あるいは、どのマスタのスレーブになるか指定できる。
./redis-trib.rb add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006 127.0.0.1:7000
Replicas migration
あるマスタのスレーブは、他のマスタのスレーブになることができる。
Note: マスタスレーブ移行時にはデフォルトでは、該当slotへのアクセスが落ち(5秒ほど)、その後クラスタ全体が一定時間(1秒ほど)落ちる。
データロストを許容できるなら設定で変更できると思いますが。
シャーディングは問題なく行える。
まとめ
基本的にこれまでのMaster-slave構成と同じように使えそう。(redis-sentinel使ったことない。。)
クライアントによるが、自動フェイルオーバーとCLUSTER INFOなどのおかげで、常に最適なノードへリクエストをすることができそう。
次回はJavaでのクライアントを検証する。