안정성 가이드

안정성 가이드 (Reliability Guide)

메시징이 아무리 잘 돌아도 네트워크·노드·애플리케이션이 실패하면 이야기가 달라져요. 이 가이드는 RabbitMQ가 신뢰할 수 있는 전달(reliable delivery)을 실현하는 데 쓰는 기능들을 전반적으로 훑어줘요. 인정·확인, 내구성·복제, 재전송과 중복 처리까지 데이터 안전성의 큰 그림을 잡아볼게요.

출처: Reliability Guide | RabbitMQ

본문

이 가이드는 RabbitMQ와 (일부) 지원 프로토콜의 데이터 안전성·실패 처리 관련 기능 개요를 제공해요. 이 기능들은 애플리케이션 개발자와 운영자가 신뢰할 수 있는 전달을 달성하도록 도와줘요. 즉, 다양한 종류의 실패를 만나더라도 메시지가 항상 전달되도록 보장하는 거예요.

데이터 안전성은 RabbitMQ 노드·퍼블리셔·컨슈머의 공동 책임이에요. 그래서 이 가이드는 메시징 기반 시스템의 각 부분에 중요한 주제를 개관해요. 이 가이드는 주로 개관이고, 각 주제는 전용 가이드에서 더 자세히 다뤄요. 다음 가이드가 데이터 안전성·복원력 주제를 자세히 논의해요.

실패의 종류

메시징 기반 시스템은 정의상 분산되어 있고, 다른 때로는 미묘한 방식으로 실패할 수 있어요. 네트워크 연결 문제와 혼잡이 가장 흔한 실패 유형일 거예요. 네트워크만 실패하는 게 아니라, 방화벽이 유휴로 간주한 연결을 끊을 수 있고 네트워크 실패 감지에는 시간이 걸려요. 연결 실패 외에도 서버·클라이언트 애플리케이션은 언제든 하드웨어 실패(또는 소프트웨어 크래시)를 겪을 수 있어요. 게다가 클라이언트 애플리케이션이 계속 돌아가도, 로직 오류가 채널이나 연결 오류를 일으켜 클라이언트가 새 채널·연결을 만들고 문제에서 복구하게 할 수 있어요.

이 실패 목록은 당연히 완전하지 않아요. 예측 가능한 시간 안에 응답하지 못하는 생략(omission) 실패, 성능 저하, 시스템 리소스를 고갈시키는 악성·버기 애플리케이션 같은 더 미묘한 실패는 다루지 않아요. 그런 실패는 모니터링·지표·헬스 체크로 감지할 수 있어요.

연결 실패와 재전송

클라이언트와 RabbitMQ 노드 사이의 네트워크 연결이 실패하면 클라이언트는 브로커에 새 연결을 맺어야 해요. 이전 연결에서 열린 채널은 자동으로 닫혔을 테니 다시 열어야 해요. 일반적으로 연결이 실패하면 연결이 예외(또는 유사한 언어 구조)를 던져 클라이언트가 알게 돼요. 대부분의 클라이언트 라이브러리는 연결 실패에서 자동으로 복구하는 기능을 제공해요. 이렇게 정해진 복구가 맞지 않는 경우에는 연결 실패 이벤트 핸들러를 정의해서 직접 복구를 구현할 수 있어요. Java·.NET 클라이언트 가이드 같은 클라이언트 문서를 참고하세요.

연결이 실패하면 메시지가 클라이언트와 서버 사이에서 이동 중일 수 있어요. 양쪽에서 디코딩·인코딩되는 중이거나, TCP 스택 버퍼에 쌓여 있거나, 와이어 위에서 이동 중일 수 있죠. 그런 경우 이동 중인 메시지는 전달되지 않아요. 재전송해야 해요. 인정이 서버·클라이언트에게 언제 재전송할지를 알려줘요.

인정은 양방향으로 쓸 수 있어요. 컨슈머가 서버에 전달을 받았고/처리했음을 알리게, 그리고 서버가 퍼블리셔에게 같은 것을 알리게요. 이 둘을 컨슈머 인정과 퍼블리셔 확인이라 불러요. TCP가 패킷이 연결 상대에게 전달됨을 보장하고 전달될 때까지 재전송하지만, 이는 네트워크 계층의 실패만 다뤄요. 인정과 확인은 메시지가 상대 애플리케이션에 **수신되었고 처리되었음(acted upon)**을 나타내요. 인정은 메시지 수신과 동시에, 수신자가 그 메시지에 대한 완전한 책임을 떠맡는 소유권 이전을 신호해요.

따라서 인정은 의미론이 있어요. 소비 애플리케이션은 메시지로 해야 할 일을 다 할 때까지 인정해서는 안 돼요. 데이터 스토어에 기록하거나, 전달하거나, 다른 연산을 수행하기 전까지요. 일단 인정하면 브로커는 그 전달을 삭제로 표시할 자유가 생겨요. 마찬가지로 브로커는 메시지에 대한 책임을 떠맡은 다음에야 확인해요. 자세한 내용은 인정과 확인 가이드에 있어요.

인정을 쓰면 적어도 한 번(at least once) 전달이 보장돼요. 인정이 없으면 게시·소비 중 메시지 손실이 가능하고 기껏해야 한 번(at most once) 전달만 보장돼요.

일부 네트워크 실패에서 패킷 손실은 끊긴 TCP 연결을 운영체제가 감지하는 데 꽤 오랜 시간(예: Linux 기본 설정에서 약 11분)이 걸리게 할 수 있어요. AMQP 0-9-1은 하트비트 기능을 제공해 애플리케이션 계층이 끊긴 연결(그리고 완전히 응답 없는 피어)을 신속히 알아차리게 해요. 하트비트는 "유휴" TCP 연결을 종료할 수 있는 일부 네트워크 장비에 대한 방어도 돼요. 하트비트 가이드 참고.

내구성과 복제

RabbitMQ(애플리케이션이 아니라) 쪽에서 메시지 손실을 피하려면, 큐와 메시지가 RabbitMQ 노드 재시작·노드 하드웨어 실패를 견딜 수 있어야 해요. RabbitMQ가 지원하는 일부 메시징 프로토콜에서는 애플리케이션이 큐·메시지의 내구성을 제어해요. 따라서 중요한 데이터에는 내구성 큐(또는 아래 다루는 복제 큐 타입)를 사용하고, 메시지는 퍼블리셔가 영속으로 게시하는 게 아주 중요해요.

노드 클러스터는 중복성을 제공하고 단일 노드 실패를 견딜 수 있어요. RabbitMQ 클러스터에서 모든 정의(익스체인지·바인딩·사용자 등)는 클러스터 전체에 복제돼요. quorum 큐·스트림·슈퍼스트림(파티셔닝된 스트림)은 복제 데이터 구조예요. 노드 하나가 리더 복제본을 호스팅하고, 다른 복제본은 팔로워예요. 리더가 실패하면 팔로워 중 하나가 새 리더로 선출돼요. 큐 상태 변경(인큐, 전달·인정 추적)은 리더 복제본에서 일어나지만, 일부 연산은 팔로워에서도 수행될 수 있어요.

큐·스트림은 리더 복제본이 어느 노드에 있든 모든 노드에서 보이고 접근 가능해요. 리더 재선출 동안 quorum 큐는 새 리더가 선출될 때까지 in-flight 메시지 전달이 일시 중지돼요. 리더 선출이 성공하면 이는 클라이언트에게 투명하게 일어나요. 독점 큐는 연결의 생명주기에 묶여 있어 절대 복제되지 않고 정의상 노드 재시작에도 살아남지 못해요.

실패한 노드에 연결된 컨슈머는 평소처럼 복구해야 해요. 다른 노드에 연결된 컨슈머는 큐의 새 리더 복제본이 선출되면 RabbitMQ가 자동으로 다시 등록해요. 그런 컨슈머는 복구(재연결·재구독)를 수행할 필요가 없어요.

중복 처리와 멱등성

확인(confirms)을 쓸 때, 채널·연결 실패에서 복구하는 프로듀서는 브로커로부터 인정을 받지 못한 메시지를 재전송해야 해요. 여기서 메시지 중복 가능성이 생겨요. 브로커가 보낸 확인이 (네트워크 실패 등으로) 프로듀서에게 도달하지 못했을 수 있으니까요. 따라서 컨슈머 애플리케이션은 중복 제거를 수행하거나 수신 메시지를 멱등(idempotent) 방식으로 처리해야 해요.

어떤 상황에서는 프로듀서가 자기 메시지가 큐로 라우팅되고 있음을 확인하는 게 중요할 수 있어요(항상은 아니에요 — pub-sub 시스템에서는 프로듀서가 그냥 게시하고, 관심 있는 컨슈머가 없으면 메시지가 버려지는 게 맞아요). 메시지가 알려진 단일 큐로 라우팅되도록 하려면 프로듀서가 대상 큐를 선언하고 직접 게시하면 돼요. 메시지가 더 복잡하게 라우팅될 수 있지만 프로듀서가 적어도 하나의 큐에 도달했는지 알아야 한다면, basic.publishmandatory 플래그를 설정해 적절히 바인딩된 큐가 없을 때 (응답 코드와 텍스트 설명을 담은) basic.return이 클라이언트로 돌아가게 할 수 있어요. 퍼블리셔 가이드 참고.

프로듀서는 또, 클러스터 노드에 게시할 때 익스체인지에 바인딩된 대상 큐 중 하나 이상이 클러스터에 미러가 있으면, 복제본과 큐 리더 복제본 사이의 흐름 제어 때문에 노드 간 네트워크 실패에 직면해 지연이 발생할 수 있다는 점도 알아야 해요. 노드 간 하트비트 가이드 참고.

네트워크 실패(또는 노드 실패) 시 메시지가 재전송될 수 있고, 컨슈머는 과거에 본 전달을 처리할 준비가 되어 있어야 해요. 컨슈머 구현을 명시적 중복 제거가 아니라 멱등적으로 설계하는 걸 권장해요. 메시지가 컨슈머에게 전달된 뒤 재큐잉되면, RabbitMQ가 자동으로든 같은 또는 다른 컨슈머가든, 다시 전달될 때 RabbitMQ는 그 메시지에 redelivered 플래그를 설정해요. 이는 컨슈머가 이 메시지를 이전에 봤을 수도 있다는 힌트예요. 원래 전달이 네트워크·컨슈머 앱 실패로 어떤 컨슈머에게도 도달하지 못했을 수 있으므로 보장되진 않아요.

redelivered 플래그가 설정되지 않았다면 메시지가 이전에 본 적 없다는 것이 보장돼요. 따라서 재전송된 메시지를 (멱등 처리보다) 중복 제거하는 게 더 비싸다고 느끼는 컨슈머는, redelivered 플래그가 설정된 메시지에 대해서만 중복 제거 처리를 해도 돼요. 컨슈머가 메시지를 처리할 수 없다고 판단하면 basic.rejectbasic.nack 메서드로 거부할 수 있고, 서버에 재큐잉을 요청하거나 요청하지 않을 수 있어요(요청하지 않으면 서버가 데드 레터 처리를 설정했을 수 있어요).

컨슈머가 소비하던 큐가 삭제되면 RabbitMQ는 컨슈머에게 알려요. 그런 컨슈머는 다른 큐에서 소비하거나, 안전하고 적절할 때 원래 소비하던 큐를 재선언하는 등의 복구 조치를 취해야 해요.

연합(Federation)·셔블(Shovel)과 모니터링

RabbitMQ는 신뢰할 수 없는 네트워크(광역망 같은)에 걸쳐 노드를 분산하는 두 플러그인을 제공해요. FederationShovel이에요. 둘 다 네트워크 실패에서 복구하고 필요할 때 메시지를 재전송해요. 둘 다 기본적으로 확인(confirms)과 인정(acknowledgements)을 사용해요. Federation이나 Shovel로 클러스터를 연결할 때는 연합 링크와 셔블이 영구적인(fail-stop) 시나리오를 포함한 노드 실패에서 복구할 수 있도록 하는 게 바람직해요.

Federation은 다운스트림 클러스터 전반에 링크를 자동으로 분산하고 다운스트림 노드 실패 시 마이그레이션해요. 업스트림 노드가 실패할 때 새 업스트림에 연결하려면, 업스트림에 업스트림 URI를 여러 개 지정하거나 충분한 가용성 특성을 가진 로드 밸런서를 통해 연결해야 해요. 셔블은 소스·대상 엔드포인트를 여러 개 쓸 수 있고, 처음 도달 가능한 엔드포인트를 사용해요. 실패한 셔블은 설정 가능한 지연 후 재시작·재시도돼요.

일부 실패 시나리오는 미묘하고 관찰·감지하기 어려워요. 예를 들어 느린 연결 누수는 시간이 지나며 쌓이고, 만성 질환처럼 한동안 눈에 띄지 않을 수 있어요. 모니터링·지표가 많은 유형의 실패를 감지하는 방법이에요. Prometheus 같은 도구로 모은 장기 지표 데이터는 시스템 동작의 불규칙성·문제 패턴을 찾는 데 도움이 돼요. 모니터링 외에도, 헬스 체크시점(point-in-time) 문제, 즉 그 순간 관찰 가능한 문제를 감지하는 또 다른 도구예요. 헬스 체크를 과하게 하면 가양성이 나올 수 있어서 체크가 많다고 항상 좋은 건 아니에요. 모니터링과 헬스 체크 둘 다 전용 가이드에서 다뤄요.

더 알아보기