2021. 9. 3. 07:09ㆍ카테고리 없음
* 본 포스트는 https://www.rabbitmq.com/clustering.html 를 번역한 것입니다.
[Clustering Guide]
Overview
RabbitMQ 브로커는 한 개 이상의 Erlang 노드들이 모여 하나의 논리적 집합체를 이룬 것이다. 각각의 노드는 RabbitMQ 어플리케이션을 각각 실행시키지만, users, vhosts, queues, exchanges, bindings, 그리고 runtime parameters는 공유한다. 우리는 이런 노드들의 모음을 cluster라고 한다.
What is Replicated?
RabbitMQ 브로커가 운영되는데 필요한 모든 데이터와 state는 클러스터의 모든 노드에 복제된다. 하지만 복제되지 않는 것 하나가 있는데, 그건 message queue들이다. message queue들은 기본 설정에 의하면, 한 노드에만 존재한다. 한 노드에 존재하지만 클러스터 내의 모든 노드에서 visible하고 reachable하다. 만약 클러스터 내의 모든 노드들에 queue를 복제하려면 high availability 문서를 확인하면 된다.
Hostname Resolution Requirements
RabbitMQ 노드들은 서로를 도메인 이름으로 부른다. 짧은 도메인 이름이든 fully-qualified (FQDNs)이든. 그래서 각 노드에서는 같은 클러스터의 모든 멤버들의 hostname을 알 수 있어야 한다. 뿐만 아니라, rabbitmqctl 같은 커맨드 라인툴이 사용되는 장비의 이름도 당연히 알고 있어야 한다.
Hostname resolution은 OS가 제공하는 기본적인 방법들 중 어떤 것을 이용해도 좋다.
- DNS records
- Local host files (e.g. /etc/hosts)
Cluster Formation
클러스터링 하는 방법은 여러가지가 있습니다.
- rabbitmqctl 로 수동적으로 하는 법
- config file에 클러스터 노드를 적어서 선언적으로 하는 법
- (plugin) rabbitmq-autocluster
- (plugin) rabbitmq-clusterer
클러스터의 구성요소는 다이내믹하게 변경될 수 있습니다. 모든 RabbitMQ 브로커는 하나의 노드에서 시작한다. 이런 노드들을 모아서 클러스터로 만들어도 되고, 다시 각각의 브로커로 되돌려 놓을 수도 있는 것이다.
Failure Handling
RabbitMQ broker는 각각의 노드의 실패를 용인한다. 각 노드의 스타트업과 중단이 각 노드의 사정에 따라 언제든 수행할 수 있다. 단, 중단의 순간에 클러스터를 구성하는 노드 중 하나만에라도 그 사실을 알릴 수는 있어야 한다.
Disk and RAM Nodes
노드는 disk 모드나 RAM 모드이다. 거의 대부분은 disk 모드이길 원할 것이다. RAM 모드는 high queue, exchange, or binding이 마구 휘젓는(churn) 클러스터의 성능을 향상시키기 위한 정도에 사용될 만한 특별한 모드이다. 잘 모르겠을 때는, disk 노드만 사용하면 된다.
[Clustering Transcript]
다음은 세 개의 장비 rabbit1, rabbit2, rabbit3을 가지고 RabbitMQ 클러스터를 만들고 설정하는 설명서이다.
사용자가 세 개의 장비 모두에 로그인 해있고, RabbitMQ가 각 장비에 설치되어 있으며, rabbitmq-server와 rabbitmqctl 스크립트가 사용자의 PATH에 포함되어 있다고 가정하고 보면 좋겠다.
이 transcript는 하나의 호스트에서만 운영하기 위해 수정될 수도 있다. 아래에 자세히 설명한다.
How Nodes (and CLI tools) Authenticate to Each Other: the Erlang Cookie
RabbitMQ 노드들과 CLI 툴(e.g. rabbitmqctl)들은 서로 통신할 때 상대와 통신해도 되는지를 판단하기 위해 쿠키를 사용한다. 두 노드가 커뮤니케이션 하기 위해서는 공유하는 비밀키(여기서는 그게 Erlang cookie.)가 있어야 한다. Erlang cookie는 단순히 alphanumeric 한 문자열이다. 길이는 상관없다. 클러스터 노드들은 같은 쿠키를 가져야만 한다.
Erlang VM는 RabbitMQ 서버가 시작될 때 자동으로 랜덤 쿠키 파일을 생성한다. 클러스터 노드들이 같은 쿠키를 갖게할 가장 쉬운 방법은 노드에 쿠키 파일이 생기면 클러스터 내의 노드에 쿠키를 복붙하는 것.
Unix 시스템이면 쿠키는 보통 /var/lib/rabbitmq/.erlang.cookie 나 $HOME/.erlang.cookie 에 위치할 것이다.
다른 방법으로는 rabbitmq-server와 rabbitmqctl 스크립트에서 erl 콜을 할 때 "-setcookie [cookie]" 옵션을 주는 것이다.
쿠키가 같지 않으면 RabbitMQ는 "Connection attempt from disallowed node" 나 "Could not auto-cluster" 같은 에러로그를 남길 것이다.
Starting independent nodes
클러스터는 이미 존재하는 RabbitMQ 노드를 클러스터 설정으로 재설정하여 생성한다. 따라서, 클러스터링의 첫번째 스텝은 각각의 노드를 시작시키는 것이다.
rabbit1$ rabbitmq-server -detached
rabbit2$ rabbitmq-server -detached
rabbit3$ rabbitmq-server -detached
이렇게 하면 세 개의 독립적인 RabbitMQ 브로커가 각각의 노드 위에 생성된다. cluster_status 명령어로 이 사실을 확인할 수 있다.
rabbit1$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit1 ...
[{nodes, [{disc, [rabbit@rabbit1]}]}, running_nodes, [rabbit@rabbit1]}]
...done.
rabbit2$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit2 ...
[{nodes, [{disc, [rabbit@rabbit2]}]}, running_nodes, [rabbit@rabbit2]}]
...done.
rabbit3$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit3 ...
[{nodes, [{disc, [rabbit@rabbit3]}]}, running_nodes, [rabbit@rabbit3]}]
...done.
노드 이름은 case-sensitive 하다.
Creating the cluster
위의 세 노드를 하나의 클러스터로 엮기 위해서는 rabbit@rabbit2와 rabbit@rabbit3가 rabbit@rabbit1에 조인하라는 명령어를 줘야 한다.
먼저 rabbit@rabbit2가 rabbit@rabbit1 클러스터에 조인하도록 한다. 그러기 위해서, rabbit@rabbit2에서 RabbitMQ 어플리케이션을 중지시키고 rabbit@rabbit1 클러스터에 조인하도록 하고, RabbitMQ 어플리케이션을 재시작한다. 클러스터에 조인하는 것은 조인하는 노드를 리셋하는 것이기 때문에, 클러스터 조인과 동시에 조인하기 전에 그 노드에 있던 리소스와 데이터를 제거하게 된다.
// 그 리소스와 데이터가 어떤 걸까? 얼마나 중요한?
rabbit2$ rabbitmqctl stop_app
Stopping node rabbit@rabbit2 ...done.
rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1
//rabbit1에 조인하는게 아니라 그중에서도 rabbit에 조인하는 것을 보니 한 노드에서 여러 클러스터 컨텍스트가 있을 수 있다는 의미인 것 같다.
Clustering node rabbit@rabbit2 with [rabbit@rabbit1] ...done.
rabbit2$ rabbitmqctl start_app
Starting node rabbit@rabbit2 ...done.
두 노드 중 어느 곳에서든 cluster_status 명령어를 쓰면 두 노드가 하나의 클러스터를 이룬 것을 확인할 수 있다.
rabbit1$ rabbitmqctl cluster_status
Cluster status of node rabit@rabbit1 ...
[{nodes, [{disc, [rabbit@rabbit1, rabbit@rabbit2]}]}, {running_nodes, [rabbit@rabbit2, rabbit@rabbit1]}] ...done.
rabbit2$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabibt2 ...
[{nodes, [{disc, [rabbit@rabbit1, rabbit@rabbit2]}]}, {running_nodes, [rabbit@rabbit1, rabbit@rabbit2]}] ...done.
이번엔 rabbit@rabbit3 을 같은 클러스터에 조인시킨다. 방법은 위와 같이 해도 되지만, 이번에는 rabbit2에 조인시킴으로써 클러스터 내의 어떤 노드에 조인시켜도 위와 결과는 같다는 것을 확인해보자. 클러스터 내의 노드 하나에만 붙으면 그 노드가 속한 클러스터에 조인할 수 있다는 것.
rabbit3$ rabbitmqctl stop_app
Stopping node rabbit@rabbit3 ...done.
rabbit3$ rabbitmqctl join_cluster rabbit@rabbit2
Clustering node rabbit@rabbit3 with rabbit@rabbit2 ...done.
rabbit3$ rabbitmqctl start_app
Starting node rabbit@rabbit3 ...done.
rabbit1$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit1 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit3,rabbit@rabbit2,rabbit@rabbit1]}] ...done. rabbit2$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit2 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit3,rabbit@rabbit1,rabbit@rabbit2]}] ...done. rabbit3$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit3 ... [{nodes,[{disc,[rabbit@rabbit3,rabbit@rabbit2,rabbit@rabbit1]}]}, {running_nodes,[rabbit@rabbit2,rabbit@rabbit1,rabbit@rabbit3]}] ...done.
위의 설명대로하면 클러스터가 운영되고 있는 중에도 새 노드를 클러스터에 추가할 수 있다.
Restarting cluster nodes
클러스터에 속한 노드들은 언제든 중단되도 괜찮다. 심지어 장비가 갑자기 멈춰도 괜찮다. 두 경우 모두, 한 노드가 멈춰져도 나머지 노드들은 영향받지 않고 계속해서 운영된다. 그리고 다시 클러스터에 조인하는 노드들은 나머지 노드들을 자동으로 "catch up" 한다.
rabbit@rabbit2와 rabbit@rabbit3을 내리면서 각 스텝에서 클러스터의 status를 확인해보자.
rabbit1$ rabbitmqctl stop Stopping and halting node rabbit@rabbit1 ...done. rabbit2$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit2 ... // rabbit1이 클러스터에 포함은 되어있지만, running은 안함. [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit3,rabbit@rabbit2]}] ...done. rabbit3$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit3 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit2,rabbit@rabbit3]}] ...done. rabbit3$ rabbitmqctl stop Stopping and halting node rabbit@rabbit3 ...done. rabbit2$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit2 ... // 1이랑 3이 클러스터에 포함은 되어 있지만, running은 안함. [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit2]}] ...done.
이번엔 다시 노드를 살리면서 클러스터 status를 확인한다.
rabbit1$ rabbitmq-server -detached //1번 살리기 rabbit1$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit1 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit2,rabbit@rabbit1]}] ...done. rabbit2$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit2 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit1,rabbit@rabbit2]}] ...done. rabbit3$ rabbitmq-server -detached rabbit1$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit1 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit2,rabbit@rabbit1,rabbit@rabbit3]}] ...done. rabbit2$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit2 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}] ...done. rabbit3$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit3 ... [{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2,rabbit@rabbit3]}]}, {running_nodes,[rabbit@rabbit2,rabbit@rabbit1,rabbit@rabbit3]}] ...done.
클러스터링의 몇가지 중요한 사항이 있다.
- 클러스터의 모든 노드가 내려갈 때, 마지막으로 내려가는 노드는 다시 온라인으로 돌아올 때에 가장 먼저 들어와야 한다. 그렇게 하지 않으면, 다른 노드들이 30초 동안은 마지막으로 나간 노드가 온라인으로 돌아오길 기다리지만, 결국엔 fail 한다. 마지막으로 내려간 노드가 다시 돌아올 수 없다면, forget_cluster_node 명령어를 이용해서 클러스터에서 제거할 수 있다. rabbitmqctl manpage를 참고해서 더 알아보면 좋다.
- 전원이 나가는 경우처럼 갑자기 동시에 컨트롤 할 수 없게, 모든 클러스터 노드들이 중지된다면, 모든 노드들은 이렇게 생각한다: "나보다 뒤에 내려간 노드가 있을거야". 이런 경우에는 force_boot 명령어를 어떤 노드에서 실행해서 그 노드가 bootable 하도록 만들어 줄 수 있다. 이 또한 rabbitmqctl manpage를 참고하도록.
Breaking up a cluster
클러스터에서 노드를 빼려면 제대로 제거해야 한다. rabbit@rabbit3을 클러스터로 부터 제거해서 독립적인 operation 할 수 있도록 해보자. 그러기 위해선 rabbit@rabbit3을 중지시키고, 노드를 리셋하고, RabbitMQ 어플리케이션을 재시작한다.
rabbit3$ rabbitmqctl stop_app Stopping node rabbit@rabbit3 ...done. rabbit3$ rabbitmqctl reset Resetting node rabbit@rabbit3 ...done. rabbit3$ rabbitmqctl start_app Starting node rabbit@rabbit3 ...done.
Note that it would have been equally valid to list rabbit@rabbit3 as a node.
이것은 클러스터에서 제거하고자 하는 노드가 아닌 곳에서도 할 수 있다. 제대로 일하지 않는 노드를 빼야만 할 때 유용할 것이다.
rabbit1$ rabbitmqctl stop_app Stopping node rabbit@rabbit1 ...done. rabbit2$ rabbitmqctl forget_cluster_node rabbit@rabbit1 Removing node rabbit@rabbit1 from cluster ... ...done.
하지만 rabbit1은 아직 자신이 rabbit2에 클러스터 되어 있는 줄 안다. rabbit1이 시작하려고 하면 에러를 낸다. 그래서 reset을 하고 시작해야 할 것이다.
rabbit1$ rabbitmqctl start_app Starting node rabbit@rabbit1 ... Error: inconsistent_cluster: Node rabbit@rabbit1 thinks it's clustered with node rabbit@rabbit2, but rabbit@rabbit2 disagrees rabbit1$ rabbitmqctl reset Resetting node rabbit@rabbit1 ...done. rabbit1$ rabbitmqctl start_app Starting node rabbit@mcnulty ... ...done.
이제는 세 노드 모두 독립적이다.
rabbit1$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit1 ... [{nodes,[{disc,[rabbit@rabbit1]}]},{running_nodes,[rabbit@rabbit1]}] ...done. rabbit2$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit2 ... [{nodes,[{disc,[rabbit@rabbit2]}]},{running_nodes,[rabbit@rabbit2]}] ...done. rabbit3$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit3 ... [{nodes,[{disc,[rabbit@rabbit3]}]},{running_nodes,[rabbit@rabbit3]}] ...done.
rabbit2는 아직 클러스터에 남아 있는 상태와 다름 없다. 그에 반해, rabbit1, rabbit3는 완전히 초기화되어서 독립적인 브로커라고 볼 수 있다. rabbit2도 클러스터에 속했던 상태를 재설정하고 싶다면 다른 노드들처럼 해줘야 한다.
rabbit2$ rabbitmqctl stop_app Stopping node rabbit@rabbit2 ...done. rabbit2$ rabbitmqctl reset Resetting node rabbit@rabbit2 ...done. rabbit2$ rabbitmqctl start_app // rabbitmqctl start_app이랑 rabbitmq-server -detached 다른건가? Starting node rabbit@rabbit2 ...done.
* rabbitmqctl start_app starts the RabbitMQ application. This command is typically run after performing
other management actions that required the RabbitMQ application to be stopped, e.g. reset.
This command instructs the RabbitMQ node to start the RabbitMQ application.
ref. https://www.rabbitmq.com/man/rabbitmqctl.1.man.html
* rabbitmq-server [-detached]
description: Running rabbitmq-server in the foreground displays a banner message, and reports on
progress in the startup sequence, concluding with the message "broker running", indicating that the
RabbitMQ broker has been started successfully. To shut down the server, just terminate the process or
use rabbitmqctl(1).
ref. https://www.rabbitmq.com/man/rabbitmq-server.1.man.html
Upgrading clusters
RabbitMQ의 메이저 버전이나 마이너 버전이나 Erlang 버전을 업그레이드 할 때, 클러스터 전체가 내려가 있어야 한다. 한 클러스터는 mixed 버전을 운영할 수 없기 떄문이다. 패치버전을 업그레이드 할 때는 상관없다. 패치버전은 한 클러스터 내에서 달라도 된다. (예외: 3.0.0과 3.0.x 시리즈 이상 버전은 안된다.)
Connecting to Clusters from Clients
클라이언트는 클러스터 내의 어떤 노드에도 연결할 수 있다. 근데 만약에 클라이언트가 연결한 노드가 죽고 클러스터의 나머지 노드들은 살아있다면, 클라리언트는 끊어진 커넥션을 인지해서 살아남은 다른 노드에 연결을 할 수 있어야 한다. 일반적으로 클라이언트 어플리케이션 내에 노드들의 hostname이나 IP 주소를 적어 놓는 것은 바람직하지 않다. 그러면 클라이언트가 변화에 바로 적응하지도 못하고, 클러스터 구성에 변경사항이 생겼을 때 수정해서 컴파일하고 재배포해야 하기 때문이다.
그보다는 더 추상적인 방법을 쓸 것을 추천한다. 매우 짧은 TTL 설정을 가진 다이나믹 DNS 서비스나 평범한 TCP 로드밸런서, 혹은 some sort of mobile IP achieved with pacemaker or similar technologies(이게 뭔지 모르겠다)가 될 수 있겠다. 클러스터 노드로 연결하는 것을 다루는 것은 RabbitMQ 의 능력을 넘기 때문에 이런 문제를 해결할 구체적인 솔루션을 택하길 바란다.
[Clusters with RAM nodes]
RAM 노드는 메타데이터를 메모리에만 갖고 있는다. RAM 노드는 disc 노드가 쓰는 만큼 디스크에 쓰지 않기 때문에, 퍼포먼스가 디스크보다 좋다. 하지만, persistent 큐 데이터는 항상 디스크에 저장되기 때문에, RAM 노드를 씀으로써 얻는 퍼포먼스 향상은 adding/removing queues, exchanges or vhosts 같은 리소스 매니지먼트에만 영향이 있다. publishing과 consuming의 속도 향상이 되는것은 아니다.
RAM node의 사용은 심화된 use case라고 볼 수 있다. 처음 클러스터를 구성하고 나서 그냥 사용하면 안된다. disc 노드를 충분히 포함시켜서 redundancy requirement를 처리할 수 있어야 한다. 그래서 추가로 RAM 노드를 넣어서 스케일링한다.
RAM 노드만 있는 클러스터는 취약하다. 클러스터가 멈추는 경우에 다시 시작시킬 수 없기도 하고, 모든 데이터를 잃기 때문이다. RabbitMQ는 RAM 노드로만 이루어진 클러스터링을 하지 못하게 대부분의 경우에 막지만, 완전히 막지는 못한다.
// 해보니 클러스터의 유일한 RAM 노드를 다른 노드에서 rabbitmqctl forget_cluster_node 해주니, 남은 disc 노드로만 클러스터가 구성되는 상태가 되기는 한다.
아래 예제는 disc 노드 하나와 RAM 노드 하나를 간단하게 보여준다. 이런 클러스터는 안 좋은 설계이다.
Creating RAM nodes
클러스터에 노드를 조인시킬 때 그 노드를 RAM 노드로 선언할 수 있다.
전에 클러스터링 한 것 처럼 rabbitmqctl join_cluster 를 쓰지만 --ram 플래그도 같이 넘겨주면 된다.
rabbit2$ rabbitmqctl stop_app Stopping node rabbit@rabbit2 ...done. rabbit2$ rabbitmqctl join_cluster --ram rabbit@rabbit1 //노드2를 멈추고 2를 ram 노드로 해서 노드1과 클러스터링 Clustering node rabbit@rabbit2 with [rabbit@rabbit1] ...done. rabbit2$ rabbitmqctl start_app // 클러스터링 설정한 후 다시 시작시킴 Starting node rabbit@rabbit2 ...done.
클러스터 status 를 확인하면 RAM 노드인 것을 확인할 수 있다.
rabbit1$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit1 ... [{nodes,[{disc,[rabbit@rabbit1]},{ram,[rabbit@rabbit2]}]}, {running_nodes,[rabbit@rabbit2,rabbit@rabbit1]}] ...done. rabbit2$ rabbitmqctl cluster_status Cluster status of node rabbit@rabbit2 ... [{nodes,[{disc,[rabbit@rabbit1]},{ram,[rabbit@rabbit2]}]}, {running_nodes,[rabbit@rabbit1,rabbit@rabbit2]}] ...done.
Changing node types
노드1과 노드2의 노드 타입을 바꿔보자. change_cluster_node_type 명령어를 사용하고, 노드를 먼저 중지시켜야 한다.
rabbit2$ rabbitmqctl stop_app Stopping node rabbit@rabbit2 ...done. rabbit2$ rabbitmqctl change_cluster_node_type disc Turning rabbit@rabbit2 into a disc node ... ...done. Starting node rabbit@rabbit2 ...done. rabbit1$ rabbitmqctl stop_app Stopping node rabbit@rabbit1 ...done. rabbit1$ rabbitmqctl change_cluster_node_type ram Turning rabbit@rabbit1 into a ram node ... rabbit1$ rabbitmqctl start_app Starting node rabbit@rabbit1 ...done.
* 주의 *
"Non clustered nodes can only be disc nodes." - 클러스터링하기 전에 독립노드를 RAM으로 바꾸려고 하니 나오는 메시지