#どんなサイズにも対応可。Docker ComposeとRedisクラスタ4.0「port-forwarding」を使って、Redisクラスタを作成しよう!
Redis4.0の一般向け提供が先日開始されたことにより、Dockerユーザー待望のRedisクラスタ機能、「NAT/port-forwarding」のサポートが始まりました。そのおかげで、Dockerのエフェメラルポートを使ってRedisクラスタを実行させることが可能となりました。これで「host-mode networking」もしくは「静的に割り当てられたポート」を使わずに、Docker上でRedisクラスタを実行できるようになりました。
English original available here.
要約
Redis 4.0 より 「Dockerネットワークのカプセル化」のサポートが始まり、よりダイナミックなデプロイが可能となった。
$ git clone https://github.com/aprice-/redisclustercompose
$ cd redisclustercompose
$ docker-compose up -d --scale redis=9
Github
Github参照: https://github.com/aprice-/redisclustercompose
4.0では何が変わった?
設定パラメーターが新しく3つ加わりました。その結果、Redisによりクラスタの他メンバーに告知する「IPアドレス」「データポート(例:6379)」「クラスタポート(例:16379)」がコントロール可能に!最も注目すべき点は、ポート単位での設定が可能になったことです。これでポート毎に10,000番地飛ばして設定する縛りが無くなりました。
-
cluster-announce-ip
: The IP address to announce. -
cluster-announce-port
: The data port to announce. -
cluster-announce-bus-port
: The cluster bus port to announce.
どうしてそれが重要なの?
Dockerの魅力はダイナミックに、且つ摩擦なくサービスが組み立てられるその柔軟性にありますが、Redisクラスタ3.0ではそれを可能にする主要な2つの機能「NAT/port-forwarding」と「ネットワークのカプセル化」が使用不可であるため。
NATの背景としてのRedisクラスタ3.0(うまくいかないバージョン)
Redis 3.0はデフォルトで、自動認識したローカルIPアドレスと設定ポートを通知します。そのため、デフォルトのbridge
ネットワークでRedisを実行すると、「内部プライベートIPアドレス」と「Internal-Onlyポート」がピアとクライアントに通知されます。
この際、NATの背後のRedisノードがクライアントに接続不可能な「MOVED redirection」で応答してしまう可能性があります。
Redisクラスタ3.0におけるHost-mode networkingを使った回避策(なんとかうまくいくバージョン)
Docker上でRedisクラスタ3.0にできることは、「host-modeネットワーク」を使ってそれぞれのノードのポートを静的に定義することだけです。Host-modeネットワークでは、ホストのネットワーク・スタックを使い、コンテナがホストの「パブリックIPアドレス」を通知します。Dockerはポート転送を行わないため、ポート・コリジョンを管理するのはユーザー次第となります。
たとえば任意のDockerホストで n個のRedisノードを実行するとすると、それぞれのノードはポートが選択され且つredis.conf
内で定義されている必要があります。6ノード以上でのクラスタ構成が推奨されていることを考え合わせると、これではすぐに扱いが厄介になると想像されます。それと同時に、Docker SwarmとDocker設定が冗長になってしまいます。
NATの背景としてのRedis クラスタ 4.0
Redisクラスタ 4.0では新しいオプションが利用可能です。Redisが任意の「IPアドレス」「ポート」「クラスタポート」を通知できるため、NAT状態に対応した設定が可能です。もうhost-modeネットワークを使う必要はありません。ただしRedisプロセスを始める前に、「IPアドレス」「ポート」「クラスタポート」をノードの redis.conf
に設定しなければいけません。
これでMOVED redirectionはどれも正しいIPとポートを指すようになりました。静的設定でRedisクラスタを実行しているユーザーには、これで十分かと思います。しかしクラスタのスケジューラーを使うのであれば、事前にこれらの設定を行わなければならないのは障害です。残りのブログでは、動的にRedisクラスタのノードをプロビジョニングできる「ディスカバリー」のメカニズムを追求していきます。
NATのディスカバリー
クラスタの各ノードは、コンテナ毎にNAT設定が行え、且つ対応するredis.conf
値が設定できて初めて、単独の共有コンテナ・設定を有することができます。「Docker compose」「Docker swarm」および他の「クラスタリング・ソリューション」のサービス定義は、たった一行のRedisコンテナ・テンプレートに集約できます。たとえばdocker-compose --scale
のたった一行で、Redisサービスはスケール可能になったのです!
Dockerがコンテナ起動時にランダムに割り当てられたポートを制御するので、どのポートが割り当てられたかを確認するには、「Docker API」をクエリーする必要があります。いくつかのアプローチが考えられますが、ここでは公式の「Redis Docker Image」をカスタマイズし、簡単なHTTPディカバリー・エージェントに統合させたやり方を探っていきます。
1. エージェント
フル機能のDockerソケットで不必要に全Redisコンテナをさらしてしまうよりも、Docker APIをHTTPの薄いラッパーで包む方が理にかなっていると考えます。その他にも利用できるサービス・ディスカバリのオプションがたくさんあります。ここでは非常にシンプルでカスタムな例を紹介します。
エージェント・コンテナは公式な「nodejs alpine docker image」に基づいて作られた「nodejs express application」です。利用できる唯一のルートは、コンテナIDでパラメタ化されたGETのみです。 目標は、Docker APIを使ってリクエストされたコンテナIDをもとに、動的に割り当てられたポートを探すことです。
エージェントはrequestを使って、Docker Unix Socket経由でDocker APIをクエリーします。Dockerの「unix socket」はDocker volumeとして起動時にコンテナに渡されます。Docker APIからコンテナ情報を得るのは極めて簡単です:
request({
method: 'GET',
url: `http://unix:/var/run/docker.sock:/containers/${id}/json`,
headers: {host: 'localhost'},
json: true
}, (error, res, body) => {
if (error) {
callback(error, null);
return;
}
if (res.statusCode >= 200 && res.statusCode <= 299) {
callback(null, body);
} else {
callback(new Error(body.message));
}
})
Docker経由でどのコンテナの情報も入手可能になったので、あとはexpressルートを定義して、リターンされたオブジェクトからポート情報を読み出します。
app.get('/:id', (req, res) => {
dockerInspect(req.params.id, (error, container) => {
if (error) {
res.status(400);
res.send(error);
} else {
let portInfo = container.NetworkSettings.Ports["6379/tcp"];
let cportInfo = container.NetworkSettings.Ports["16379/tcp"];
if (portInfo && cportInfo) {
let port = portInfo[0].HostPort;
let cport = cportInfo[0].HostPort;
res.send(`${clusterAnnounceIp}:${port}@${cport}`);
} else {
res.send("");
}
}
});
});
Redis4.0のCLUSTER NODES
と同じフォーマットで結果が返ってきます: <IP>:<port>@<cluster-bus-port>
.
$ curl http://192.168.50.5:3000/2bddf88b0904
192.168.50.5:34176@34175
ここではRedisコンテナを調整して、この情報を用いてNAT設定を行います。
必要なら環境変数INTERFACE
を用い、告知するネットワーク・インタフェースを選んでください。
2. Redisコンテナ
公式なlibrary/redisコンテナのalpine-variantに基づき、RedisコンテナはENTRYPOINT
を上書きし、起動時にディスカバリー・サービスのクエリーを行って、NAT設定を見つけます。
ディスカバリー・エージェントはhost-mode networkingで正しい「announce IPアドレス」を取得するので、コンテナ内からDockerホストにクエリーするだけでいいのです:
gateway=$(/sbin/ip route|awk '/default/ { print $3 }') # Find the gateway IP address
network_info=$(curl -s http://${gateway}:3000/$(hostname)) # Query the discovery agent
# returns: 192.168.50.5:34176@34175
cluster_announce_ip=$(echo ${network_info} | cut -d ':' -f 1) # Cut out the different parts of <IP>:<port>@<cluster-bus-port>`
ports=$(echo ${network_info} | cut -d ':' -f 2)
cluster_announce_port=$(echo ${ports} | cut -d '@' -f 1)
cluster_announce_bus_port=$(echo ${ports} | cut -d '@' -f 2)
そして新しいcluster-announce-ip
、cluster-announce-port
そしてcluster-announce-bus-port
設定をredis-server
実行に加えてください。
set -- redis-server "$@" "--cluster-announce-port ${cluster_announce_port}" "--cluster-announce-bus-port ${cluster_announce_bus_port}" "--cluster-announce-ip ${cluster_announce_ip}"
3. クラスタ作成
発見したNAT設定により、ひとたびクラスタの全メンバーがオンラインになれば、クラスタ作成可能になります。公式なRedisクラスタのRubyアドミニストレーション・スクリプトはredis-trib.rb
で、クラスタの作成例は以下です。
ruby redis-trib.rb create --replicas 1 <ip>:<port>[]
Docker cliを併用すれば、どんなサイズのクラスタも作成できます。
docker ps -q -f label=redis | # loop over each container labeled 'redis'
{
while read x; do # inspect each container for its private IP address
private_ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $x)
cluster_hosts=\"$cluster_hosts $private_ip:6379\" # Append it to the list of cluster hosts
done
ruby redis-trib.rb create --replicas 1 $cluster_hosts # Form the cluster
}
これで各メンバーが引き合わされ、初期ハッシュ・スロット・オーナーシップの設定が行われます。これでクラスタは使用可能になりました!
Docker Composeを使って
Docker Composeを使えば、個々のサービスを設定できます。コンテナに追加インスタンスを立ち上げることで、スケールも自在です。上に記した静的設定の制限のために、Redisクラスタ3.0では、単独サービスのもとで複数ノードを実行することは不可能です。Redis4.0では、このディスカバリー方法で各コンテナが同一のテンプレートを共有することができるようになったので、可能となったのです。
redis:
image: redis:discover
build: ./redis
command: --cluster-enabled yes --bind 0.0.0.0
ports:
- 6379
- 16379
labels:
- redis
depends_on:
- discover
これでサービスにdocker-compose up --scale redis=<num>
を使って、お望みの数だけコンテナ・インスタンスを立ち上げることが可能になりました。
$ docker-compose up -d --scale redis=6
$ docker exec redisclustercompose_redis_1 exec redis-cli cluster nodes
48836a86da5d54d56393f866befde47efacc256d 192.168.50.5:34238@34237 master - 0 1504307943992 2 connected 5461-10922
53eb5604cf752e3811b5db01bddb1eb5d580d142 192.168.50.5:34244@34243 slave 48836a86da5d54d56393f866befde47efacc256d 0 1504307944996 5 connected
8b2b5e4eec8d25eab59bdedd0eb356465f3a4102 192.168.50.5:34246@34245 master - 0 1504307942000 1 connected 0-5460
dc71aa602eb6ab6f0d4c71208061b7230f53dc50 192.168.50.5:34240@34239 slave 8b2b5e4eec8d25eab59bdedd0eb356465f3a4102 0 1504307943000 4 connected
e1ab6ad9c5b806f564486b1bb8be2567244d9f5c 192.168.50.5:34242@34241 master - 0 1504307944000 3 connected 10923-16383
3b061c92f30154046c2eba2f858a81ff0c95d2c1 192.168.50.5:34236@34235 myself,slave e1ab6ad9c5b806f564486b1bb8be2567244d9f5c 0 1504307943000 6 connected
Reddieバンドル
GithubのDocker Composeコードに、Reddieはあらかじめ含まれています。また、あなたが作成する新しいクラスターに繋がるようにも設定してあります。Docker Composeがコンテナをビルドし終わったら、https://localhost
までナビゲートしてできたての作品に見惚れましょう!
まとめ
Docker上の多くのRedisクラスタ・ディプロイメントにとって、新機能の「NAT/port-forwarding」は、ただ単にhost-mode networkingからbridge
やoverlay
networkに変えられるということを意味するのでしょう。
しかし、このブログでみてきたようにSwarm・Kubernetes・Nomadなどのクラスタスケジューラーを用いてRedisクラスタのノードをスケジュールすると、Redis4.0の新機能をうまく融合させた形でRedisクラスタを扱うことが可能となります。
実行時にディスカバリー・メカニズムを使ってRedisを再設定しなければならないのは、残念なことに非常に複雑なオーバーヘッドであります。もし(例えばインバウンドのコネクション情報を使ったり、クライアントコネクションにてクラスタのバスポートを提供したりして)Redisが動的に自身を再設定できるようになれば、Redisクラスタの動的スケジューリングはもっと簡単になるのではないでしょうか。
もちろん欠けている部分もまだまだあります。永続記憶装置、新しくスケジュールされたノードをクラスターに結合させたり、ハッシュスロットをリシャーディングしたりする、などが例として挙げられます。