2016年5月12日木曜日

RabbitMQ の 冗長化を考える

はじめに

RabbitMQ の冗長化の機能として
Clustering(クラスタリング)と QueueMirroring(Queueミラーリング)が提供されているようです。
今回は、これらを使って冗長化を考えてみたいと思います。
Cluster の Node では、Queue、Exchange、Binding、User などが共有できるようです。
しかし Queue だけは、他と扱いが異なるということです。
Queue は、Cluster 内のどこか1つの Node が HomeNode となり保持される仕組みになっているようです。
HomeNode 以外の Node は、Queue を参照する形になるようです。
そのため HomeNode が落ちてしまうと他の Node は、Queue の参照できるけれども
Queue にあるメッセージが参照できなくなるという事態に陥ってしまうようです。
さらにメッセージが Persistence でなければメッセージ自体もなくなってしまう。
これをカバーするものが QueueMirroring となるようです。
HomeNode のメッセージを他の Node でも共有しようという仕組みのようです。
始める前に...
RabbitMQ がインストール済であること
RabbitMQ サーバーにログイン可能でリソースにアクセスできるユーザが作成済であること
(ユーザとパスワードを "user01" と "passwd" としています。
ManagementPlugin が有効になっていて CommandLineTool がインストールていること
以上が前提になります。
このあたりはわたしも『yum の Local Repository に RabbitMQ をインストール』にまとめていますので
よろしければ、こちらも合わせて、ご参考に。
ではでは!Getting Started!


環境

今回は、以下の2台のサーバー(vm-1st と vm-2nd)で冗長化構成を考えたいと思います。
RabbitMQ Server (vm-1st)

ホスト名 vm-1st
IP 192.168.10.110
ミドルウェア RabbitMQ server 3.6.1
  ユーザ  user01   パスワード  passwd
OS CentOS-7-x86_64-Minimal-1511.iso
VirtualBox 5.0.16


RabbitMQ Server (vm-2nd)

ホスト名 vm-2nd
IP 192.168.10.120
ミドルウェア RabbitMQ server 3.6.1
  ユーザ  user01   パスワード  passwd
OS CentOS-7-x86_64-Minimal-1511.iso
VirtualBox 5.0.16



準備

Clustering と QueueMirroring の設定をするにあたりホスト名とその名前解決が必要になるようです。
( ! ) 注意 RabbitMQ のリソース
ホスト名を変更すると User や Queue などが消えてしまう?
Queue、Exchange、Binding、User などは、ホスト名に関連付けされて
/var/lib/rabbitmq/mnesia ディレクトリ以下に保存されるようです。
(Mnesia という分散データベースが使われているようです。
実際、消えてはないのですが、変更前のホスト名で保存されているので
アクセスできなくなってしまうようです。
それぞれ以下のように設定します。
ホスト名を設定した後は、一度、再起動しておきます。
vm-1st

/etc/hostname
vm-1st
/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.10.110 vm-1st
192.168.10.120 vm-2nd
reboot


vm-2nd

/etc/hostname
vm-2nd
/etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.10.110 vm-1st
192.168.10.120 vm-2nd
reboot

ホスト名を変更した場合
guest 以外のユーザが存在するか確認します。
rabbitmqadmin list users name tags

+--------+---------------+
|  name  |     tags      |
+--------+---------------+
| guest  | administrator |
+--------+---------------+
guest しか存在しない場合、恐らく User が消えてしまっていると思います。
ユーザとパスワードを "user01" と "passwd" にして再作成します。
リソースへのアクセス権限と Web の管理コンソールを使用するための Role も設定します。
rabbitmqctl add_user user01 passwd
rabbitmqctl set_permissions -p / user01 ".*" ".*" ".*"
rabbitmqctl set_user_tags user01 administrator
もう一度ユーザを確認します。
rabbitmqadmin list users name tags

+--------+---------------+
|  name  |     tags      |
+--------+---------------+
| guest  | administrator |
| user01 | administrator |
+--------+---------------+
user01 が追加されていれば OK です。
ユーザ作成は、Main の ClusterNode だけで OK です。
Sub の ClusterNode は、Main の ClusterNode の情報がレプリケートされます。

Clustering の設定方法には、いくつかあるのですが、
今回は、ManagementPlugin の CommandLineTool を使おうと思います。
そのためには、Cluster に参加する Node は、同じ ErlangCookie を持っていないといけないようです。
(コマンド実行時の認証に必要なようです。
ErlangCookie に特別な制限は、なくアルファベットの文字列であればなんでもよいようです。
とりあえず、vm-1st と vm-2nd に適当な同じ ErlangCookie 文字列を設定します。
手順としては、

  1. RabbitMQ サーバーを停止してから ErlangCookie を変更する。
  2. ErlangCookie を変更した後、RabbitMQ サーバーを再起動する。

とします。
vm-1st と vm-2nd のターミナルからそれぞれ以下のコマンドを実行します。
vm-1st

systemctl stop rabbitmq-server.service
echo MYERLANGCOOKIESTR > /var/lib/rabbitmq/.erlang.cookie
systemctl start rabbitmq-server.service

vm-2nd

systemctl stop rabbitmq-server.service
echo MYERLANGCOOKIESTR > /var/lib/rabbitmq/.erlang.cookie
systemctl start rabbitmq-server.service

ErlangCookie
vm-1st の ErlangCookie を vm-2nd にコピーするのもよいと思います。
vm-2nd のターミナルから以下のコマンドを実行します。
vm-2nd

mv /var/lib/rabbitmq/.erlang.cookie /var/lib/rabbitmq/.erlang.cookie_bk
scp root@vm-1st:/var/lib/rabbitmq/.erlang.cookie /var/lib/rabbitmq/.erlang.cookie
chown rabbitmq /var/lib/rabbitmq/.erlang.cookie
準備は、これで終わりです。


Clustering の設定

今、vm-1st と vm-2nd の Clustering がどうなっているか確認してみましょう。
まず、vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqctl cluster_status

Cluster status of node 'rabbit@vm-1st' ...
[{nodes,[{disc,['rabbit@vm-1st']}]},
 {running_nodes,['rabbit@vm-1st']},
 {cluster_name,<<"rabbit@vm-1st">>},
...
見てみると "rabbit@vm-1st" という Cluster があり
このサーバー(vm-1st) だけが参加していて起動している。
こんな感じでしょうか?

次に、vm-2nd のターミナルから以下のコマンドを実行します。
vm-2nd

rabbitmqctl cluster_status

Cluster status of node 'rabbit@vm-2nd' ...
[{nodes,[{disc,['rabbit@vm-2nd']}]},
 {running_nodes,['rabbit@vm-2nd']},
 {cluster_name,<<"rabbit@vm-2nd">>},
...
こちらも "rabbit@vm-2nd" という Cluster があり
このサーバー(vm-2nd) だけが参加していて起動している。

整理してみると
vm-1st と vm-2nd のどちらも
それぞれが Cluster を持っていて自サーバーのみ参加していて起動している。
Disk Node と RAM Node
Node の保存先に Disk と RAM があるようだ。
保存先に RAM を選ぶとパフォーマンスを稼げるということらしいが
一般的には、Disk を選択するのが無難なようだ。

ではでは、vm-2nd を vm-1st の Cluster に参加させてみましょう。
手順としては、

  1. vm-2nd を offline にする。
  2. vm-2nd を vm-1st の Cluster に参加させる。
  3. vm-2nd を online にする。

のようになります。
vm-2nd のターミナルから以下のコマンドを実行します。
vm-2nd

rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@vm-1st
rabbitmqctl start_app

ちゃんと Cluster に参加できたか確認します。
vm-1st と vm-2nd の両方のターミナルで以下のコマンドを実行します。
vm-1st

rabbitmqctl cluster_status

Cluster status of node 'rabbit@vm-1st' ...
[{nodes,[{disc,['rabbit@vm-1st','rabbit@vm-2nd']}]},
 {running_nodes,['rabbit@vm-2nd','rabbit@vm-1st']},
 {cluster_name,<<"rabbit@vm-1st">>},
...

vm-2nd

rabbitmqctl cluster_status

[{nodes,[{disc,['rabbit@vm-1st','rabbit@vm-2nd']}]},
 {running_nodes,['rabbit@vm-1st','rabbit@vm-2nd']},
 {cluster_name,<<"rabbit@vm-1st">>},
...
どうなったでしょう?
ちょっと見てみると
vm-1st と vm-2nd のどちらにも
"rabbit@vm-1st" という Cluster があり
そこにどちらも参加していてどちらも起動している。
いけてそうな感じですね!
Clustering の解除
vm-2nd を vm-1st の Cluster から解除する手順としては、

  1. vm-2nd を offline にする。
  2. vm-2nd を vm-1st の Cluster から解除する。
  3. vm-2nd を online にする。

のようになります。
vm-2nd のターミナルから以下のコマンドを実行します。
vm-2nd

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
RabbitMQ と Erlang の version
RabbitMQ の version が違う Node は、Cluster に参加できないようです。
同様に Erlang の version が違う Node も Cluster に参加できないようです。
Clustering された RabbitMQ の version up は、なかなか大変?!


Queue を宣言する

とりあえず、Queue がないと始まらないので宣言をします。
Queue名を "myQueue" とします。
vm-1st と vm-2nd のどちらでも構わないのですが
ここでは、vm-2nd を HomeNode にして Queue を宣言したいと思います。
vm-2nd のターミナルから以下のコマンドを実行します。
vm-2nd

rabbitmqadmin declare queue name=myQueue

宣言できたか確認します。
デフォルト(無名)の Exchange に Bind されているかも確認します。
vm-2nd

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 0        |
+---------+---------+---------------+----------+
Queue名が "myQueue"、durable が "True"、HomeNode が "vm-2nd" で宣言されているようです。
rabbitmqadmin list bindings

+------------+-------------+-------------+
|   source   | destination | routing_key |
+------------+-------------+-------------+
|            | myQueue     | myQueue     |
+------------+-------------+-------------+
Queue名を RoutingKey にしてデフォルトの Exchange に Bind されているようです。
Queue の HomeNode
デフォルトでは、Queue を宣言した Node が HomeNode になるようです。
以下で HomeNode の決定条件を変更できるようです。
  Queue 作成時の x-queue-master-locator 引数
  queue-master-locator policy
  configuration ファイル
HomeNode の決定条件として以下の値が設定可能なようです。
  min-masters
  client-local (デフォルト)
  random
Queue の削除
Queue を指定して以下のコマンドを実行します。
rabbitmqadmin delete queue name=myQueue

HomeNode でない vm-1st からはどう見えているか確認したいと思います。
vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 0        |
+---------+---------+---------------+----------+
vm-2nd と同じように見えているのが分かると思います。

HomeNode でない vm-1st からメッセージを送信するとどうなるか試してみたいと思います。
vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqadmin publish routing_key=myQueue payload="Hello?"
送信されたメッセージが Queue に届いたか確認します。
vm-1st と vm-2nd の両方のターミナルで以下のコマンドを実行します。
vm-1st

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 1        |
+---------+---------+---------------+----------+

vm-2nd

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 1        |
+---------+---------+---------------+----------+
ちゃんと届いているようです。
Queue には、HomeNode があり Master / Slave のような構成になっているが
どこの Node からメッセージを送信しても Cluster 内のすべての Queue にレプリケートされるようです。
ある意味、MultiMaster的な動きになっていると思います。
メッセージの取り出しと削除
メッセージを取り出す場合は、Queue を指定して以下のコマンドを実行します。
rabbitmqadmin get queue=myQueue requeue=false
requeue を省略、または "true" にすると取り出したメッセージがもう一度、Queue に戻されます。

メッセージを削除する場合は、Queue を指定して以下のコマンドを実行します。
rabbitmqadmin purge queue name=myQueue

QueueMirroring していない状態で HomeNode になっている vm-2nd が落ちるとどうなるか試してみます。
vm-2nd の RabbitMQ サーバーを停止させた後、vm-1st から Queue がどう見えているか確認します。
vm-1st と vm-2nd のターミナルでそれぞれ以下のコマンドを実行します。
vm-2nd

systemctl stop rabbitmq-server.service

vm-1st

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd |          |
+---------+---------+---------------+----------+
Queue は見えているもののメッセージが見えなくなりました。

vm-2nd が復帰した時、メッセージがどうなっているか確認したいと思います。
vm-2nd の RabbitMQ サーバーを起動させた後、vm-1st と vm-2nd から Queue が
どう見えているか確認します。
vm-1st と vm-2nd のターミナルでそれぞれ以下のコマンドを実行します。
vm-2nd

systemctl start rabbitmq-server.service

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 0        |
+---------+---------+---------------+----------+

vm-1st

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 0        |
+---------+---------+---------------+----------+
メッセージが消えてしまっているのが分かると思います。
HomeNode が落ちてもメッセージが消えないように QueueMirroring の設定をすることにします。
DeliveryMode
メッセージ送信時に DeliveryMode を persistent にすると
送信したメッセージがディスク領域に保存されるようになるため
HomeNode が落ちてもメッセージが消えてしまうことはないようです。
ただし HomeNode が落ちている間、メッセージが見えなくなるので取り出しはできないようです。

DeliveryMode を persistent にしてメッセージを送信する。
rabbitmqadmin publish routing_key=myQueue payload="Hello?" properties='{"delivery_mode":2}'
HomeNode を停止する。
systemctl stop rabbitmq-server.service
メッセージを取り出してみる。
rabbitmqadmin get queue=myQueue requeue=false

*** Not found: /api/queues/%2F/myQueue/get
Not found になりメッセージを取り出せない。
HomeNode を起動させて Queue を確認する。
systemctl start rabbitmq-server.service

rabbitmqadmin list queues name durable node messages

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 1        |
+---------+---------+---------------+----------+
メッセージは、残っている。
メッセージを取り出してみる。
rabbitmqadmin get queue=myQueue requeue=false

+-------------+----------+---------------+---------+---------------+------------------+-------------+
| routing_key | exchange | message_count | payload | payload_bytes | payload_encoding | redelivered |
+-------------+----------+---------------+---------+---------------+------------------+-------------+
| myQueue     |          | 0             | Hello?  | 6             | string           | False       |
+-------------+----------+---------------+---------+---------------+------------------+-------------+

rabbitmqadmin list queues name durable node messages

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-2nd | 0        |
+---------+---------+---------------+----------+
メッセージが取り出せた。


QueueMirroring の設定

Clustering の設定と Queue の宣言に続いて QueueMirroring の設定に入りたいと思います。
policy を使って設定することになるようです。
設定する policy は、ha-mode とその値の ha-params になるようです。
先ほど宣言した Queue を Cluster 内のすべての Node で Mirroring するようにします。
policy名を "myPolicy" とします。
vm-1st と vm-2nd のどちらでも構わないのですが
とりあえず、vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqctl set_policy myPolicy "^myQueue$" '{"ha-mode":"all"}'
rabbitmqctl set_policy コマンドの書式

rabbitmqctl set_policy    policy名    パターン    policy設定
  • policy名

    policy名を指定する。

    パターン

    policy を適用する Queue を正規表現を使って指定する。

    policy設定

    設定する policy をJSON 形式で指定する。

ha-mode と ha-params
ha-mode は、3つあり ha-params と合わせて以下のように設定できるようです。
  • all

    すべての Node で mirroring する。(ha-params はない)

    例)  '{"ha-mode":"all"}'

    exactly

    指定した数の Node で mirroring する。

    ha-params には、Node数を指定する。

    例)  '{"ha-mode":"exactly","ha-params":2}'

    nodes

    指定した Node で mirroring する。

    ha-params には、Node名を指定する。

    例)  '{"ha-mode":"nodes","ha-params":["rabbit@vm-1st", "rabbit@vm-2nd"]}'


policy が作成できたか確認します。
vm-1st のターミナルから以下のコマンドを実行します。
rabbitmqctl list_policies

Listing policies ...
/       myPolicy        all     ^myQueue$       {"ha-mode":"all"}       0
作成できているようです。
policy の削除
policy を指定して以下のコマンドを実行します。
rabbitmqctl clear_policy myPolicy

Cluster 内の Node で QueueMirroring が同期しているか確認します。
vm-1st のターミナルから以下のコマンドを実行します。
rabbitmqctl list_queues name slave_pids synchronised_slave_pids

Listing queues ...
myQueue [<rabbit@vm-1st.3.24159.1>]     [<rabbit@vm-1st.3.24159.1>]
"myQueue" の slave に vm-1st があり、同期している slave にも vm-1st があるようなので
問題ないと思います。

では、メッセージを送信してみて HomeNode が落ちてもメッセージが消えてしまわないか確認したいと思います。
vm-1st からメッセージを送信します。
vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqadmin publish routing_key=myQueue payload="Hello?"

HomeNode になっている vm-2nd の RabbitMQ サーバーを停止します。
vm-2nd のターミナルから以下のコマンドを実行します。
vm-2nd

systemctl stop rabbitmq-server.service

この状態で HomeNode でない vm-1st で Queue がどういう状態になっているか確認します。
vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqadmin list queues name durable node messages

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-1st | 1        |
+---------+---------+---------------+----------+
メッセージが消えずに残っているのが確認できると思います。
もう一点、HomeNode が vm-2nd から vm-1st に変わっているのも確認できると思います。

ここで vm-2nd の RabbitMQ サーバーが起動すると
vm-1st と vm-2nd から Queue がどう見えているか確認します。
vm-1st と vm-2nd のターミナルでそれぞれ以下のコマンドを実行します。
vm-2nd

systemctl start rabbitmq-server.service

rabbitmqadmin list queues name durable node messages 

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-1st | 1        |
+---------+---------+---------------+----------+

vm-1st

rabbitmqadmin list queues name durable node messages

+---------+---------+---------------+----------+
|  name   | durable |     node      | messages |
+---------+---------+---------------+----------+
| myQueue | True    | rabbit@vm-1st | 1        |
+---------+---------+---------------+----------+
HomeNode が vm-1st に変更されたままになっているのが確認できると思います。
HomeNode が落ちたタイミングで別の Node が HomeNode になり
メッセージを引き継ぐ形になっているようです。
そのため HomeNode が落ちてもメッセージが消えることは、ないようです。

HomeNode が変更されたので QueueMirroring の同期を確認します。
vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqctl list_queues name slave_pids synchronised_slave_pids

Listing queues ...
myQueue [<rabbit@vm-2nd.3.293.0>]       []
slave として、vm-2nd が追加されているのは、確認できますが同期がうまくいっていないようです。
vm-1st のターミナルから以下のコマンドを実行して同期の設定をします。
vm-1st

rabbitmqctl sync_queue myQueue

もう一度、QueueMirroring の同期を確認します。
vm-1st のターミナルから以下のコマンドを実行します。
vm-1st

rabbitmqctl list_queues name slave_pids synchronised_slave_pids

Listing queues ...
myQueue [<rabbit@vm-2nd.3.293.0>]       [<rabbit@vm-2nd.3.293.0>]
今度は、問題なく同期できているのが確認できると思います。
QueueMirroring の同期
デフォルトの QueueMirroring の同期設定は、手動になっているようです。
policy 作成時に ha-sync-mode を "automatic" にすると自動同期にすることができるようです。
rabbitmqctl set_policy myPolicy "^myQueue$" '{"ha-mode":"all", "ha-sync-mode":"automatic"}'


おわりに

なかなかいい感じに MultiMaster のような形で冗長化できたのではと思います。
後は、Cluster 内の Node に仮想の IP を設定して、アプリからアクセスする感じでしょうか?
参考URL
http://www.rabbitmq.com/clustering.html
http://www.rabbitmq.com/ha.html