CLOVER🍀

That was when it all began.

Ubuntu LinuxでRedis ClusterのDockerイメージを作って遊ぶ

Redis Clusterを使ってみたいのと、合わせてそのDockerイメージを作ろうかなと思ってちょっと遊んでみました。

Redis 3から、クラスタが組めるようになったそうですね。これを試してみたいと。

参考)
オフィシャルサイトチュートリアル
Redis cluster tutorial

公式のDockerコンテナでRedis Clusterを構築する - Maverick Techlab
Redis cluster - おさかな日誌
http://tech.albert2005.co.jp/blog/2015/04/28/redis-cluster/
Redis Cluster のリシャーディングとorphaned masterの話 - CyberAgent エンジニア Advent Calendar 2014 2日目 · GitHub

これらを参考に、Redis Cluster環境を構築するためのDockerイメージを作っていってみます。

Redis Clusterって?

そもそも、Redis Clusterについてですが、ざっくり言うと

  • Redisを複数ノードで構成し、マルチマスタを組める
  • データはノード間でシャーディング
  • スレーブを作って耐障害性の向上が可能

といった代物らしいです。

Dockerイメージの作成

それでは、Dockerイメージを自分で作ってみます。

まず、オフィシャルのチュートリアルを読むと

Redis Cluster and Docker

Currently Redis Cluster does not support NATted environments and in general environments where IP addresses or TCP ports are remapped.

Docker uses a technique called port mapping: programs running inside Docker containers may be exposed with a different port compared to the one the program believes to be using. This is useful in order to run multiple containers using the same ports, at the same time, in the same server.

In order to make Docker compatible with Redis Cluster you need to use the host networking mode of Docker. Please check the --net=host option in the Docker documentation for more information.

http://redis.io/topics/cluster-tutorial

という感じでNATはサポートしてないよ、--net=hostで使おうねと書いてあるので、その前提で作ります。

必要なのは、

といったところ。

Redisのインストール先を/opt/redisとして、作成したのがこちら。
Dockerfile

FROM ubuntu:latest

ENV REDIS_VERSION 3.0.6
ENV REDIS_HOME /opt/redis

WORKDIR ${REDIS_HOME}

RUN mkdir -p ${REDIS_HOME}

RUN apt-get update

RUN apt-get install -y apt-file && \
    apt-file update && \
    apt-file search add-apt-repository && \
    apt-get install -y software-properties-common

RUN apt-add-repository ppa:brightbox/ruby-ng && \
    apt-get update && \                      
    apt-get install -y ruby2.2

RUN gem install redis

RUN cd /opt && \
    apt-get install -y \
     wget \
     gcc \
     make \
     && \
    wget -q http://download.redis.io/releases/redis-${REDIS_VERSION}.tar.gz && \
    tar -zxvf redis-${REDIS_VERSION}.tar.gz && \
    mv redis-${REDIS_VERSION} redis-src && \
    cd redis-src && \
    make && \
    make PREFIX=${REDIS_HOME} install

EXPOSE 6379

RUN mkdir conf

ADD redis.conf.template conf/redis.conf.template
ADD start-redis.sh start-redis.sh

RUN chmod a+x start-redis.sh

ENTRYPOINT ["./start-redis.sh"]

ムダにRuby 2.2を使うように設定。

RUN apt-get install -y apt-file && \
    apt-file update && \
    apt-file search add-apt-repository && \
    apt-get install -y software-properties-common

RUN apt-add-repository ppa:brightbox/ruby-ng && \
    apt-get update && \                      
    apt-get install -y ruby2.2

gemでredisも必要みたいです。

RUN gem install redis

また、「--net=host」を使うのが前提なので、ポートをDocker起動時に引数で受けとるような構成にしました。

というわけで、設定ファイルのテンプレートを用意。
redis.conf.template

port %PORT%
cluster-enabled yes
cluster-config-file conf/nodes.conf
cluster-node-timeout 5000
appendonly yes

内容は、チュートリアルにある最小構成の設定です。

起動スクリプト。こちらをENTRYPOINTに設定。
start-redis.sh

#!/bin/bash

if [ "$1" != "" ]; then
    PORT=$1
else
    PORT=7000
fi

perl -wp -e "s!%PORT%!${PORT}!" conf/redis.conf.template > conf/redis.conf

bin/redis-server conf/redis.conf

引数にポートを指定すると、その値で設定ファイルを作って起動します、と。

で、ビルド。

$ docker build -t kazuhira/redis-cluster:3.0.6 .

これで、準備完了。

Redisの各ノードの起動

それでは、Redisを起動します。まずは3ノード。

## Node1
$ docker run -it --rm --net=host --name redis-server1 kazuhira/redis-cluster:3.0.6 7000

## Node2
$ docker run -it --rm --net=host --name redis-server2 kazuhira/redis-cluster:3.0.6 7001

## Node3
$ docker run -it --rm --net=host --name redis-server3 kazuhira/redis-cluster:3.0.6 7002

これだけだと、単に3つノードがあるだけです。

クラスタを構成する

では、この3ノードをクラスタとして構成します。

ひとつ、Dockerコンテナに入って

$ docker exec -it redis-server1 bash

Redisのソースコードの中にある、「redis-trib.rb」というスクリプトを使用します。
※「/opt/redis-src」ディレクトリは、このエントリでのRedisソースコードの展開先です

# /opt/redis-src/src/redis-trib.rb

直接実行すると、ヘルプが出てきます。

# /opt/redis-src/src/redis-trib.rb
/opt/redis-src/src/redis-trib.rb:1573: warning: duplicated key at line 1573 ignored: "threshold"
Usage: redis-trib <command> <options> <arguments ...>

  create          host1:port1 ... hostN:portN
                  --replicas <arg>
  check           host:port
  info            host:port
  fix             host:port
                  --timeout <arg>
  reshard         host:port
                  --from <arg>
                  --to <arg>
                  --slots <arg>
                  --yes
                  --timeout <arg>
                  --pipeline <arg>
  rebalance       host:port
                  --weight <arg>
                  --auto-weights
                  --threshold <arg>
                  --use-empty-masters
                  --timeout <arg>
                  --simulate
                  --pipeline <arg>
  add-node        new_host:new_port existing_host:existing_port
                  --slave
                  --master-id <arg>
  del-node        host:port node_id
  set-timeout     host:port milliseconds
  call            host:port command arg arg .. arg
  import          host:port
                  --from <arg>
                  --copy
                  --replace
  help            (show this help)

For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

このスクリプトを使用して、3つのノードをクラスタに参加させます。
※今回は、全部マスターノードとして作成します

# /opt/redis-src/src/redis-trib.rb create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002
/opt/redis-src/src/redis-trib.rb:1573: warning: duplicated key at line 1573 ignored: "threshold"
>>> Creating cluster
>>> Performing hash slots allocation on 3 nodes...
Using 3 masters:
127.0.0.1:7000
127.0.0.1:7001
127.0.0.1:7002
M: 73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000
   slots:0-5460 (5461 slots) master
M: 3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001
   slots:5461-10922 (5462 slots) master
M: 4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002
   slots:10923-16383 (5461 slots) master
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join.
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000
   slots:0-5460 (5461 slots) master
M: 3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001
   slots:5461-10922 (5462 slots) master
M: 4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002
   slots:10923-16383 (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

クラスタの状態を確認してみます。こちらは、redis-cliで可能です。

# bin/redis-cli -p 7000 cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:3
cluster_current_epoch:3
cluster_my_epoch:1
cluster_stats_messages_sent:167
cluster_stats_messages_received:167

クラスタに参加しているノード。

# bin/redis-cli -p 7000 cluster nodes
3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001 master - 0 1452952573411 2 connected 5461-10922
4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002 master - 0 1452952572405 3 connected 10923-16383
73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000 myself,master - 0 0 1 connected 0-5460

で、ノードに接続してsetしてみると

# bin/redis-cli -p 7000
127.0.0.1:7000> set key1 value1
(error) MOVED 9189 127.0.0.1:7001

怒られます。

ここは、「-c」を付けて起動するとリダイレクトしてくれるようです。

# bin/redis-cli -p 7000 -c
127.0.0.1:7000> set key1 value1
-> Redirected to slot [9189] located at 127.0.0.1:7001
OK

もう少し登録。

127.0.0.1:7001> set key2 value2
-> Redirected to slot [4998] located at 127.0.0.1:7000
OK
127.0.0.1:7000> set key3 value3
OK

key3は、自分に割り当てられているようです。

ちなみに、最初にちょっとハマったのが、ノードを書く時に

127.0.0.1:7000

みたいにIPアドレスではなく、

localhost:7000

みたいにホスト名で書くと、クラスタの構成に失敗します。

こんなエラーを受け取って。

call': ERR Invalid node address specified: localhost:7000 (Redis::CommandError)

どうやら、ホスト名は使用不可で、IPアドレスで書く必要があるらしいです。

Redis-trib by using hostnames fails to join cluster · Issue #2071 · antirez/redis · GitHub

ノードを追加する

続いて、ノードを追加してみます。新しくRedisを起動。

$ docker run -it --rm --net=host --name redis-server4 kazuhira/redis-cluster:3.0.6 7003

当然、まだクラスタには参加していません。

クラスタに追加するには、「redis-trib.rb add-node」を使用します。

# /opt/redis-src/src/redis-trib.rb add-node 127.0.0.1:7003 127.0.0.1:7000

「127.0.0.1:7003」は新しいノード、「127.0.0.1:7000」は既存のノードです。

実行。

# /opt/redis-src/src/redis-trib.rb add-node 127.0.0.1:7003 127.0.0.1:7000
/opt/redis-src/src/redis-trib.rb:1573: warning: duplicated key at line 1573 ignored: "threshold"
>>> Adding node 127.0.0.1:7003 to cluster 127.0.0.1:7000
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000
   slots:0-5460 (5461 slots) master
   0 additional replica(s)
M: 3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001
   slots:5461-10922 (5462 slots) master
   0 additional replica(s)
M: 4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002
   slots:10923-16383 (5461 slots) master
   0 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:7003 to make it join the cluster.
[OK] New node added correctly.

クラスタに参加できたようです。

でも、この状態ではスロットと呼ばれるものが割り当てられていません。クライアントからのリクエストには応じることができるみたいですが…。

この状態から進めるために、reshardします。

# /opt/redis-src/src/redis-trib.rb reshard 127.0.0.1:7003
/opt/redis-src/src/redis-trib.rb:1573: warning: duplicated key at line 1573 ignored: "threshold"
>>> Performing Cluster Check (using node 127.0.0.1:7003)
M: 93af9513be1feb3f1088b003737c1f7ab80a8681 127.0.0.1:7003
   slots: (0 slots) master
   0 additional replica(s)
M: 3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001
   slots:5461-10922 (5462 slots) master
   0 additional replica(s)
M: 4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002
   slots:10923-16383 (5461 slots) master
   0 additional replica(s)
M: 73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000
   slots:0-5460 (5461 slots) master
   0 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)?

スロット(1〜16384)の範囲をどう割り振るか聞かれるので、今回は 16384 / 4で4096でいきます。

How many slots do you want to move (from 1 to 16384)? 4096

次に、データを受け取るノード(今回追加したノード)を指定します。

What is the receiving node ID? 93af9513be1feb3f1088b003737c1f7ab80a8681

そしてどのノードからスロットを動かすか聞かれるのですが、今回は全体から動かすことにして「all」と答えます。

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:all

Ready to move 4096 slots.
  Source nodes:
    M: 3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001
   slots:5461-10922 (5462 slots) master
   0 additional replica(s)
    M: 4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002
   slots:10923-16383 (5461 slots) master
   0 additional replica(s)
    M: 73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000
   slots:0-5460 (5461 slots) master
   0 additional replica(s)
  Destination node:
    M: 93af9513be1feb3f1088b003737c1f7ab80a8681 127.0.0.1:7003
   slots: (0 slots) master
   0 additional replica(s)

すると、リシャードのプランが表示されるので

  Resharding plan:
    Moving slot 5461 from 3c95936f29dd29eb32f34a8b1bfacd5e477e3360
    Moving slot 5462 from 3c95936f29dd29eb32f34a8b1bfacd5e477e3360
    Moving slot 5463 from 3c95936f29dd29eb32f34a8b1bfacd5e477e3360
    Moving slot 5464 from 3c95936f29dd29eb32f34a8b1bfacd5e477e3360
    Moving slot 5465 from 3c95936f29dd29eb32f34a8b1bfacd5e477e3360

  〜省略〜

「yes」と答えます。

Do you want to proceed with the proposed reshard plan (yes/no)? yes

あとは、リシャードされる様子が続きます…。

終了すると、クラスタに参加して、かつスロットも割り当てられたことが確認できます。

# bin/redis-cli -p 7000 cluster nodes
3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001 master - 0 1452953663376 2 connected 6827-10922
93af9513be1feb3f1088b003737c1f7ab80a8681 127.0.0.1:7003 master - 0 1452953661865 4 connected 0-1364 5461-6826 10923-12287
4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002 master - 0 1452953662369 3 connected 12288-16383
73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000 myself,master - 0 0 1 connected 1365-5460

クラスタの情報。

# bin/redis-cli -p 7000 cluster info 
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:4
cluster_size:4
cluster_current_epoch:4
cluster_my_epoch:1
cluster_stats_messages_sent:2932
cluster_stats_messages_received:2934

確認

redis-cliで接続して、データ登録してみます。

# bin/redis-cli -p 7000 -c
127.0.0.1:7000> set key4 value4
-> Redirected to slot [13120] located at 127.0.0.1:7002
OK
127.0.0.1:7002> set key5 value5
-> Redirected to slot [9057] located at 127.0.0.1:7001
OK
127.0.0.1:7001> set key6 value6
-> Redirected to slot [4866] located at 127.0.0.1:7000
OK
127.0.0.1:7000> set key7 value7
-> Redirected to slot [803] located at 127.0.0.1:7003
OK

追加したノードが、key7で現れました。

ノードを削除する

ノードを削除するには、「redis-trib.rb del-node」を使用します。

# /opt/redis-src/src/redis-trib.rb del-node 127.0.0.1:7003 93af9513be1feb3f1088b003737c1f7ab80a8681

削除には、IPアドレス、ポートとノードのIDが必要です。

ところが、これを実行すると「ノードが空じゃないよ!」と怒られます。

# /opt/redis-src/src/redis-trib.rb del-node 127.0.0.1:7003 93af9513be1feb3f1088b003737c1f7ab80a8681
/opt/redis-src/src/redis-trib.rb:1573: warning: duplicated key at line 1573 ignored: "threshold"
>>> Removing node 93af9513be1feb3f1088b003737c1f7ab80a8681 from cluster 127.0.0.1:7003
[ERR] Node 127.0.0.1:7003 is not empty! Reshard data away and try again.

ノードを削除するには、ノードが空でなくてはならないようです。

そこで、reshardしてポート7003のノードからデータを追い出してみます。
※FLUSHALLでもいいとか書いてあるエントリは見たのですが…試していません

# /opt/redis-src/src/redis-trib.rb reshard 127.0.0.1:7003
/opt/redis-src/src/redis-trib.rb:1573: warning: duplicated key at line 1573 ignored: "threshold"
>>> Performing Cluster Check (using node 127.0.0.1:7003)
M: 93af9513be1feb3f1088b003737c1f7ab80a8681 127.0.0.1:7003
   slots:0-1364,5461-6826,10923-12287 (4096 slots) master
   0 additional replica(s)
M: 3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001
   slots:6827-10922 (4096 slots) master
   0 additional replica(s)
M: 4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002
   slots:12288-16383 (4096 slots) master
   0 additional replica(s)
M: 73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000
   slots:1365-5460 (4096 slots) master
   0 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

ここで、スロットの範囲は16384を3で割った5462(切り上げ)にします。

How many slots do you want to move (from 1 to 16384)? 5462

移動先のノードは、クラスタに残るノードにして

What is the receiving node ID? 73878b5b59102996acf8c1acaba831a022cdcdcc

移動元は、削除対象のノードにします。

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:93af9513be1feb3f1088b003737c1f7ab80a8681

2つ目はないので、「done」。

Source node #2:done

すると、今度はノードを削除することができるようになります。

# /opt/redis-src/src/redis-trib.rb del-node 127.0.0.1:7003 93af9513be1feb3f1088b003737c1f7ab80a8681
/opt/redis-src/src/redis-trib.rb:1573: warning: duplicated key at line 1573 ignored: "threshold"
>>> Removing node 93af9513be1feb3f1088b003737c1f7ab80a8681 from cluster 127.0.0.1:7003
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.

削除する前に、「cluster nodes」で見るとスロットが外れています。
※結果を取り直したのでNode IDが変わってますが、お気になさらず…

# bin/redis-cli -p 7000 cluster nodes
8766435737ca2b3b64b536fc8c5b27ba771f622b 127.0.0.1:7003 master - 0 1452954698203 6 connected
3c95936f29dd29eb32f34a8b1bfacd5e477e3360 127.0.0.1:7001 master - 0 1452954697701 2 connected 7851-10922
4ca10031c591cee679e1007d201f8066992af608 127.0.0.1:7002 master - 0 1452954697198 3 connected 13312-16383
73878b5b59102996acf8c1acaba831a022cdcdcc 127.0.0.1:7000 myself,master - 0 0 7 connected 0-7850 10923-13311

とりあえず、なんとか通せた感じです。