약 한달 전 새로운 팀으로 옮겼다. 동료들이 반갑게 맞아주며 약 2주 간은 천천히 소스코드도 읽어보고 기존 업무를 파악하라고 한다. 사실 기존 조직에서 여러모로 지쳐있는 상태에서 왔기 때문에 좋은 기회라고 생각했다. 천천히 소스코드도 읽고 그동안 했던 생각도 정리해보며 하루 하루를 나름 알차게 보냈다. 그런데, 얼마 지나지 않은 금요일에 갑작스레 일을 맡게 되었다.


1. 첫 번째 시도


우리 서비스는 RabbitMQ를 사용하고 있다. 대부분의 서비스는 장비 중 한 대에 문제가 생길 때도 정상적인 서비스를 하기 위해 최소한 장비 2대 이상을 클러스터링 해서 사용하는데, 우리 서비스도 L4 스위치와 RabbitMQ 2.3.1 자체의 클러스터링 기능을 이용해 이런 부분을 염두에 두고 있었다. L4 스위치는 클라이언트(Queue 구조에서 Publisher)에서 도메인명으로 MQ 서버에 접근했을 때 연결(Connection)을 분산시키는 역할을 하고 있었고, RabbitMQ 자체의 클러스터링은 두 독립적인 노드가 마치 하나의 MQ 인스턴스처럼 동작하게 하는 역할을 하고 있었다. 그런데, 어느날 시스템 팀에서 메모리 불량으로 MQ 1번 장비가 언제 문제가 생길지 모르니 가급적 신속히 교체 작업을 진행하자고 했다. 옆 자리 과장님은 RabbitMQ가 클러스터링 되어 있음을 알았기 때문에 바로 장비 1대를 중단했다. 별 문제가 없을 것이라 생각했지만 혹시나 해서 MQ를 사용하는 클라이언트의 로그를 보았다. 그런데, 놀랍게도 MQ 관련 오류 메세지가 수도 없이 올라오고 있었다. 과장님은 놀라 바로 중단했던 장비를 즉시 재기동 시켰다. 좀 살펴보던 과장님은 머리가 아팠는지 옆 자리에 있던 내게 도움을 부탁했다. 나도 사실 RabbitMQ를 한번도 써보지 못햇다. 하지만, 어쨌든 함께 문제를 살펴보기 시작했다.


2. 두 번째 시도


RabbitMQ에 대한 지식이 별로 없었음에도 신속히 교체를 해야했기 때문에 내부 동작을 파악하기 위한 충분한 시간이 부족하다 생각했다. 따라서 내부 이해에 근거한 접근보다는 블랙박스 식의 접근으로 문제를 해결하려 시도했다. 다시 말해 RabbitMQ 테스트 장비에서 여러 동작을 해보며 드러나는 결과를 관찰한 것이다. 목적은 문제 없이 장비 한대를 중단하는 것이기 때문에 어떻게 하면 서비스에 지장없이 장비를 중단할 수 있을지 여러 가지 방식으로 테스트 해보았다. 테스트를 하며 알아낸 사실은 MQ 1번 장비(마스터)를 중단하면 선언 된 Queue 등이 사라지며 문제가 생기지만, MQ 2번 장비(슬래이브)를 중단하면 아무 문제도 생기지 않는다는 점이었다. 이 결과를 확인하고 몇몇 문서를 참고한 후 '마스터가 Queue 등에 대한 메타 정보를 갖고 있기 때문에 마스터만 중단되지 않으면 서비스가 된다'라는 전제를 갖게 되었다. 일반적인 마스터-슬래이브 구조에서 마스터는 메타 정보를 관리하고 이에 따라 SPOF(Single Point Of Failure)가 되기 때문에 이 전제는 보편성이 있다고 생각했다. 이 전제에 따라 아래와 같은 교체 계획을 세웠다.


1) MQ2(슬래이브)를 중단하고 L4에서 제거한다.

이때 MQ2에 연결되어 있는 클라이언트는 연결이 끊긴 후 재연결을 시도할 것이고, 이 때 L4에서는 MQ2가 중단된 것을 인지하고 자연스레 MQ1로 연결을 해줄 것이다. MQ1로 연결이 완료되면 MQ1이 혼자 서비스한다.


2) MQ1과 MQ2의 클러스터 관계를 해제한다. 

이로인해 더이상 MQ1과 MQ2는 관계가 없게 된다.


3) MQ2의 클러스터링 설정을 초기화하고 MQ2를 마스터로 변경한다. 또한 Queue 선언 등을 MQ1과 동일하게 해 마스터로써 서비스 할 수 있게 준비시킨다. 즉, 새로운 마스터를 만드는 것이다.


4) MQ2를 다시 L4에 투입한다.


5) MQ1을 L4에서 제거하고 MQ1의 RabbitMQ를 중단한다.

이 때 1번과 같은 이유(재연결, L4)로 자연스레 MQ2로 재연결이 된다.


6) 메모리 교체가 끝나면 MQ1을 다시 MQ2의 슬래이브로 편입시킨다.


위 시나리오를 기반으로 베타환경에서 테스트를 시작했다. 혹시나 해서 여러 번 테스트를 했는데 테스트가 매우 잘 되었다. 어느정도 확신이 생겼고 리얼환경에 적용하기로 했다. 그런데 리얼환경에서 위 시나리오 중 1번 부분을 적용하는 순간 선언 된 Queue을 찾을 수 없다는 오류가 무수히 발생하며 다시 롤백을 해야했다. 


왜 그런것일까? 다시 원인을 살펴보았다. 이런저런 부분을 상세히 살피다보니 원인은 Queue가 선언되는 노드의 위치에 있었다. RabbitMQ에는 Queue를 선언 할 때 해당 Queue가 선언 될 노드를 지정할 수 있다. 리얼환경을 상세히 살펴보니 Queue선언 중 일부는 MQ1에 선언되있었고, 나머지는 MQ2에 선언되어 있었다. 이로인해 MQ2를 중단하자 MQ2에 선언 되있던 Queue선언을 찾을 수 없다는 오류가 발생한 것이다. 그렇다면 베타에서는 왜 문제가 없었지? 살펴보니 베타환경에는 모든 Queue가 MQ1번에 선언되어 있었다. 결국 현상만 보고 판단해 세운 "마스터가 중단되지 않으면 서비스가 가능하다"라는 전제는 완전히 잘못되었던 것이다.



# 그림1. 클러스터가 중단되면 Queue도 없어지는 현상


또 한번의 교체 실패로 머리가 아파왔다. 마음이 급했지만 이 상황을 어떻게 해결할지 침착하게 생각해보았다. 조금 방법을 찾아보면 현재 상황에서 어떻게든 교체를 할 수 있을 것 같았다. 하지만, 간단히 보였던 일이 커진 이유를 생각하자 생각이 바뀌었다. 즉, 근본적인 문제는 클러스터링이 제대로 동작하지 않는다는 점이었다. 이번에는 어떻게든 상황을 넘어갈 수 있지만, 이후 또 비슷한 일이 생겼을 때 지금처럼 일이 어려워 질 것이라 생각했다. 그래서 차라리 이번 기회에 클러스터링을 제대로 구성해보자 마음먹었다.


3. 세 번째 시도


결국 시스템팀에 상황을 얘기하고 잠시 교체를 미루기로 했다. 이번에는 클러스터 노드 중 하나가 중단되도 잘 동작하는 진짜 클러스터링 환경을 구성하기로 마음먹었다. 찾아보니 RabbitMQ 2.6부터 HA(High Availability)가 강화되었음을 확인했다. 


The RabbitMQ team is pleased to announce the release of RabbitMQ 2.6.0. The highlight of this release is the introduction of active-active HA, with queues getting replicated across nodes in a cluster. (생략)

http://www.rabbitmq.com/news.html


HA 관련 문서를 상세히 읽다보니 두 번째 시도에서 겪었던 문제도 이미 언급이 되어 있었다. 기존에도 읽었던 부분이지만 급하게 읽다보니 미처 인지하지 못했던 부분이었다.


You could use a cluster of RabbitMQ nodes to construct your RabbitMQ broker. This will be resilient to the loss of individual nodes in terms of the overall availability of service, but some important caveats apply: whilst exchanges and bindings survive the loss of individual nodes, queues and their messages do not.


To solve these various problems, we have developed active/active high availability for queues. This works by allowing queues to be mirrored on other nodes within a RabbitMQ cluster. The result is that should one node of a cluster fail, the queue can automatically switch to one of the mirrors and continue to operate, with no unavailability of service.

http://www.rabbitmq.com/ha.html


RabbitMQ에서는 위에서 언급 된 HA가 고려 된 Queue를 Mirrored Queue라고 부르는데 단어 그대로 Queue를 여러 노드에 걸쳐 Mirroring 해주는 기능이다. 따라서 한 노드가 중단 되어도 Queue 선언과 Queue에 담긴 메세지가 다른 노드에 이미 복사되어있기 때문에 문제 없이 서비스가 가능하다. 혹시 모르니 테스트 클러스터 환경을 구성해 RabbitMQ 2.6 이상의 최신버전을 설치하고 다양한 방법으로 노드를 중단해보며 여러 테스트를 해보았다. 마스터 노드든 슬래이드 노드든 중단되도 Queue 선언이나 Queue 안에 들어있는 메세지가 잘 보존되고 잘 처리됨을 확인했다. 이 결과에 따라 아래와 같은 교체 계획을 다시 세웠다.


1) 새로운 MQ 장비 2대를 준비하고 해당 장비에 대한 새로운 도메인(L4 바인딩 포함)도 준비한다.

2) MQ 장비에 Mirrored Queue를 제공하는 RabbitMQ 최신버전을 설치하고 클러스터링 한다.

3) 베타환경에서 신규 MQ 도메인으로 연결해 테스트를 진행한다.

4) MQ를 참조하는 모든 프로젝트에서 기존 MQ 도메인을 신규 MQ 도메인으로 바꾸어 배포한다.

5) 기존 MQ 클러스터는 제거한다.


결국 위 계획은 성공했다. 처음 예상했던 것보다 훨씬 많은 시간을 소모했지만 서비스를 중단하지 않고 MQ 클러스터를 교체한 것이다. 현재 위와 같은 구성으로 바뀐지 약 한달이 넘었지만 큰 문제 없이 잘 동작하고 있다. 앞으로는 위와 같이 급하게 장비를 교체 할 일이 생겨도 쉽게 할 수 있을 것이라 기대하고 있다.

# 그림2. 클러스터가 중단되었음에도 Mirrored Queue 때문에 Queue가 유지 됨


4. 관련 설정 및 몇 가지 이야기

 

고가용성을 보장하려면 RabbitMQ의 2.6 이상의 버전을 사용하는 게 좋고, Queue는 선언 시 반드시 HA 설정(Mirrored Queue)을 해줘야 한다. 


4-1) Spring-AMQP(Spring-Rabbit)


아래와 같이 Queue를 선언할 때 x-ha-policy를 all로 주면 된다. 


<rabbit:queue name="hello.string">

<rabbit:queue-arguments>

        <entry key="x-ha-policy" value="all" />

</rabbit:queue-arguments>

</rabbit:queue>


<rabbit:queue name="hello.object">

<rabbit:queue-arguments>

<entry key="x-ha-policy" value="all" />

</rabbit:queue-arguments>

</rabbit:queue>


혹은 코드에서 Queue를 선언한다면 아래와 같이 Parameter를 추가해주면 된다.


Map<String, Object> args = new HashMap<String, Object>();

args.put("x-ha-policy", "all");

channel.queueDeclare("myqueue", false, false, false, args);


4-2) RabbitMQ Management Web UI


Queue를 선언할 때 Mirror 필드의 값을 "Across All Nodes"로 선택해주면 된다.


4-3) Mirrored Queue는 Replication이 아님


문서에 HA가 등장하고 요즘에는 Replication을 기본적으로 지원하는 NoSQL류를 많이 살펴보다보니 Mirrored Queue가 Replication 처럼 동작한다고 착각했었다. 내가 이해한 Mirrored Queue와 Replication의 차이점은 이렇다. 


노드 A가 있고 아직 처리되지 않은 메세지가 10개 있다고 가정해보자. 이때 노드 B가 새롭게 클러스터에 참여한다. 잠시 후 노드 A가 예기치 않게 중단된다. 그럼 이 때 노드 B에 메세지가 있을까? 만약 Replication 이고 노드 B가 참여한 후 노드간에 Replication이 이뤄질 시간이 있었다면 노드 B에는 10개의 메세지가 보존되어 있을 것이다. 하지만, Mirrored Queue는 메세지가 없을 것이다. 왜냐하면 Mirrored Queue는 메세지가 들어올 때 전 노드에 복사(Mirroring) 하는 방식이기 때문이다. 위 10개 메세지가 들어올 당시 노드 B는 클러스터에 없었기 때문에 메세지는 존재하지 않는다.


4-4) 무손실 배포


이번에 새로운 클러스터를 도입하며 고민해본 부분은 어떻게 하면 무정지/무손실로 배포를 할지였다. 우리쪽에 MQ를 이용하는 곳을 생각해 보면 일반적인 MQ 모델처럼 Publisher와 Consumer가 있다. 만약 Publisher와 Consumer를 신규 도메인을 보도록 변경해 그냥 동시 배포한다면, 기존 MQ 클러스터에 쌓여있는 메세지는 더 이상 처리가 안 될 수 있다. 이런 이유로 배포 시 아래와 같은 단계로 배포를 진행했다.


1. Publisher 배포

앞으로 새롭게 유입되는 메세지는 신규 MQ 클러스터에 쌓인다.


2. 기존 MQ 클러스터의 메세지가 모두 처리되었음을 확인

이 시점에서 신규 MQ 클러스터에는 메세지가 쌓이고 있을 것이다.


3. Consumer 배포

Consumer를 배포하고 신규 MQ 클러스터에 쌓인 메세지를 처리하게 한다.


배포 중 새롭게 유입 된 메세지는 약간의 처리 지연 시간이 생기겠지만 메세지 손실 없이 배포가 가능하다.



WRITTEN BY
차민창
르세상스 엔지니어가 사회를 이끌어나가는 상상을 하며!

트랙백  0 , 댓글  3개가 달렸습니다.
  1. 잘보았습니다.
    감사합니다.
  2. 몇 년이 흐른 글이지만 RabbitMQ 구조를 이해하는데 큰 도움이 되는 글인듯 싶네요. 감사합니다.
  3. 나그네 2015.09.10 20:37
    좋은글 감사합니다.
    참고로 ha-sync-mode 옵션을 사용하면 말씀하신 replication이 적용됩니다.
    대신 sync 도중에는 큐가 응답을 할 수 없으므로 사용에 주의해야합니다.
secret