Skip to content

4장 메시지 발행에서 성능 절충

Junggeun Lee edited this page Feb 14, 2020 · 1 revision

이 장에서 다루는 내용

  • RabbitMQ에서 메시지 배달 보장
  • 메시지 배달 보장과 성능의 절충점

메시지 브로커는 빠른 성능이나 처리량도 중요하지만, 신뢰할 수 있는 메시지 전달도 중요한 지표 중 하나다.

예 ) 은행업무

AMQP 스팩은 메시지를 발행할 때 트랜잭션을 제공하고 있으며, 메시지를 디스크에 저장하는 경우 일반적인 메시지 발행보다 높은 수준의 안정적인 메시징 환경을 제공할 수 있다.

RabbitMQ has additional function

  • delivery confirmations
  • highly available(HA) queues that span multiple servers.

한편 빠른 속도 보장을 위하여 선택과의 절충을 생각해 봐야 한다.

4.1 발행 속도와 배달 보장의 균형 잡기

RabbitMQ 서버를 리부팅해도 메시지가 유지되도록 하는 등의 배달보장을 위한 일부 옵션들은 너무 느리고 적합하지 않을 수 있다. 한편 결제 / 결제취소와 같은 미션 크리티컬 애플리케이션에는 빠른 성능보다는 꼭 고려해야할 옵션일 수 있다.

4/Screen_Shot_2020-02-04_at_5.21.21_AM.png

RabbitMQ에서 배달 보장을 위하여 설계된 각 메커니즘은 성능에 영향을 미친다. 당장 느끼기에는 차이가 적어 보일 수 있지만 이것들이 조합하여 실제 사용될 때는 메시지 처리량에 상당한 영향을 끼칠 수 있다. 그러므로 충분한 자체적인 성은 벤치마크을 통하여 성능과 배달 보장간에 적절한 균형을 결정하는 것이 좋다.

올바른 솔루현을 위하여 고성능과 배달 보장 사이의 적절한 균형을 찾기 위하여 아래의 질문들을 염두해두자.

  • 발행 시에 메시지를 큐에 넣는 것이 얼마나 중요한가?
  • 메시지를 라우팅할 수 없는 경우, 발행자에게 메시지를 다시 돌려 보내야 하는가? (또는 알려야 하는가?)
  • 메시지를 라우팅할 수 없는 경우, 차후에 조정하는 다른 곳으로 메시지를 보내야 하는가?
  • RabbitMQ 서버에 장애가 발생할 때, 메시지가 손실돼도 괜찮은가?
  • RabbitMQ가 새로운 메시지를 처리할때 요청한 모든 메시지가 라우팅되었고 디스크에 저장하는 작업이 정상적으로 수행되었는지 발행자에게 확인해야 하는가?
  • 발행자가 메시지를 batch message deliveries(다수의 메시지를 한꺼번에 배송하는것)하고 나면 RabbitMQ로 부터 모든 요청들이 라우팅 되었고 디스크에 저장되었는지 확인 해야하는가?
  • 위와 같이 다수의 메시지를 발생 하고 라우팅과 디스크에 저장하는 batch 작업중에 메시지가 목적지 queue에 원자단위의 커밋이 필요한가?
  • 발행자가 높은 성능과 메시지 처리량을 달성할 수 있으며, 신뢰할 수 있는 메시지 배달에 수용가능한 trade-off 가 있는가?
  • 메시지 발행의 어떤 다른 관점이 메시지 처리량과 성능에 영향을 끼칠것인가?

4장에서는 이와 같은 질문들에 대하여 어떤 기능과 옵션들을 제공하고 있는지 알아보고 적절한 수준의 메시지 배달 보장과 성능의 절충점을 찾기위한 정보를 제공한다. 모든 환경에 완벽한 조합은 찾을 수 없지만 가장 적절한 조합을 찾을 수 있을 것이다.

예를 들어 HA 큐와 mandatory 라우팅을 조합하거나 delivery-mode를 2로 하고 트랜잭션 발행을 선택해 메시지를 디스크에 저장할 수 있다. 적절한 조합을 찾기 위해 각기 다른 기술을 조합해 보는 것을 추천한다.

+mandatory 플래그(2장) : 메소드 프레임(Basic.Publish)에서 지정. RabbitMQ가 메시지를 라우팅해야 하는지 혹은 라우팅할 수 없는지를 확인할 수 있게 하며 라우팅할 수 없다면 RabbitMQ는 Basic.Return 프래임을 반환한다.

4.1.1 배달 보장을 사용하지 않는 환경

완벽한 세상에서는 추가 구성이나 설정 없이 메시지가 안정적으로 전달된다.

  • 올바른 exchange, routing 정보와 함께 Basic.Publish 를 통하여 메시지가 간단히 발행하면 RabbitMQ는 메시지를 수신하고 적절한 queue에 전달된다.
  • 네트워크 이슈가 없고, 서버 장비는 안정적이며 충돌이 나지 않는다.
  • 운영 시스템은 결코 RabbitMQ broker의 실행 상태에 영향을 줄 수 있는 문제를 발생시키지 않는다.
  • 이상적인 환경의 애플리케이션 환경이라면 소비자 애플리케이션은 결코 처리속도가 느려질 수 있는 서비스와 연동되어서 발생되는 성능 제약을 받지 않는다.
  • Queue는 메시지를 백업하지 않고 메시지는 발행되면 바로 처리될 정도로 빠르게 동작한다.
  • 발행은 어떤 식으로든 제한되지 않는다.

하지만 불행하게도 장애는 매번 일어나고 완벽한 환경은 결코 일어날 일이 없는 일들이 정기적으로 발생된다.

미션크리티컬한 애플리케이션이 아니라면 일반적인 RibbitMQ의 기본 설정만으로도 적절한 수준의 안정적인 메시징 환경을 구축할 수있다.

예) 웹 서버 collectd 의 통계 수집 데몬은 모니터링 데이터를 RabbitMQ에 발행해 Graphite 및 Rocksteady 소비자에게 제공한다.

4/Screen_Shot_2020-02-05_at_10.33.12_PM.png

Graphite의 콜렉터 서비스인 carbone, Grafana

https://www.44bits.io/ko/post/monitoring-system-with-graphite-and-grafana

모니터링 애플리케이션 Rocksteady , Nagios

https://code.google.com/archive/p/rocksteady/

https://www.slideshare.net/sprdd/ss-25995750

https://system-monitoring.readthedocs.io/en/latest/nagios.html

4/Screen_Shot_2020-02-05_at_10.44.44_PM.png

위와 같은 시스템에서는 추가 메시지 배달 보장 설정 하지 않아도 간단한 설정만으로 충분하다.(메시지 손실 가능)

RabbitMQ 와 애플리케이션의 연결이 끊어지면 다시 연결하여 발생이나 소비하면 된다.

4.1.2 mandatory 플래그를 설정한 메시지를 라우팅할 수 없을 때

메시지가 항상 배달되도록 보장하려면 mandatory를 설정한다.

mandatory 플래그는 Basic.Publish RPC 명령과 함께 전달 되는 인수이다.

메시지를 라우팅 할 수 없으면 클라이언트(publisher)에게 Basic.Return RPC를 통해 메시지와 함께 클라이언트에게 돌려보낸다.

mandatory 플래그는 메시지 라우팅 실패를 알리는데 사용된다. 만일 메시지 라우팅이 정상적으로 처리되면 클라이언트에 별도로 알리지 않는다.

메시지발행시 mandatory옵션을 전달하면 만일 메시지가 라우팅 되지 못하는 경우 RabbitMQ는 Basic.Return 을 돌려준다.

4/Screen_Shot_2020-02-06_at_5.39.07_AM.png

파이썬의 rabbitpy 라이브러리에서 클라이언트는 Basic.Return을 자동으로 수신하며, 채널 범위에서 수신하면 MessageReturnException을 발생시킨다.

파이썬이외의 라이브러리의 경우, 메시지를 발행할때 RabbitMQ로 부터 Basic.Return RPC 호출을 전달받아 실행할 콜백 메소드를 등록해야 사용할 수 있다.

https://www.rabbitmq.com/publishers.html#unroutable

4.1.3 트랜잭션보다 가벼운 발행자 확인

https://www.rabbitmq.com/confirms.html#publisher-confirms

RabbitMQ의 발행자 확인은 AMQP 스펙의 확장기능이다.

RabbitMQ 관련 확장을 지원하는 클라이언트 라이브러리에서만 지원한다.

메시지를 전달하기 전에 메시지 발행자는 Confirm.Select RPC 요청을 전달하고 배송 확인이 enable되었는지 Confirm.SelectOk 응답을 기다린다.

그때부터 발행자가 보내는 메시지 마다 RabbitMQ는 수신확인(Basic.Ack)를 보내거나 부정 수신 확인(Basic.Nack)으로 응답하며, 둘다 메시지를 확인하도록 메시지의 offset을 지정하는 정수 값을 포함한다.(확인번호)

확인번호는 Confirm.Select RPC 요청 다음에 수신된 순서에 따라 메시지를 참조한다.

4/Screen_Shot_2020-02-08_at_9.12.42_AM.png

Basic.Ack 는 발행된 모든 메시지가 소비자에게 바로 소비되거나 메시지가 큐와 디스크에 저장될 때 발행자에게 전송된다.

메시지를 라우팅 할 수 없을때 메시지 브로커는 Basic.Nack RPC 보내게 되는데 발행자는 해당 메시지를 어떻게 할지 결정한다.

발행자 확인은 트랜잭션과 함께 사용할 수 없으며, AMQP TX 프로세스의 대안으로서 가볍고 상대적으로 성능이 뛰어나다. 추가적으로 발행자 확인은 Basic.Publish에 대한 응답을 비동기로 처리하므로 확인이 오는 시점을 보장할 수 없다. 그러므로 발행자확인을 사용하도록 설정한 애플리케이션을 메시지를 보낸 후 언제라도 확인을 받을 수 있어야 한다.

4.1.4 라우팅할 수 없는 메시지를 위한 alternate exchange 사용하기

https://www.rabbitmq.com/ae.html

alternate exchange는 처음 익스체인지를 선언할때 명시되며, RabbitMQ에서 exchange가 메시지를 라우팅할 수 없으면, 새로운 exchange가 메시지를 라우팅할 기존의 exchange를 대신하게 된다.

4/Screen_Shot_2020-02-09_at_5.29.38_AM.png

(내가 이해한게 맞나?) mandatory 플래그를 true로 하더라도 altervate exchange가 설정된 exchange로 메시지를 보내면 라우팅이 실패되더라도 발생자에게 basic.Return을 보내지 않는다. 만일 데드 레터 큐와 alternate exchange와 제대로 바인딩되지 않은 경우 메시지는 큐에 추가되지 않고 유실된다.

alternate exchange를 사용하려면, 기본 익스체인지를 설정할 때 {} 인수로 alternate-exchange 를 추가한다.

라우팅 할 수 없는 메시지는 unroutable-message 큐에 저장된다.

4.1.5 트랜잭션으로 배치 처리하기

배달 확인 기능이 있기 전에, 메시지 전달을 보장하기 위한 유일한 방법은 Transaction이었다. AMQP transaction 이나 TX를 통해 트랜잭션을 제공한다.

트랜잭션을 시작하기 위하여 발행자는 RabbitMQ로 TX.Select RPC를 요청을 보내고 RabbitMQ는 Tx.Select로 응답한다.

한번 트랜젹션이 열리면 발행자는 하나 이상의 메시지를 보낼 수 있다.

4/Screen_Shot_2020-02-13_at_4.24.17_AM.png

만일 존재하지 않는 exchange같은 오류로 인하여 commit(route)을 할 수 없다면 RabbitMQ는 TX.CommitOK 응답을 보내기 전에 Basic.Return 응답을 리턴한다.

발행자가 트랜잭션을 중지(Abort)하려는 경우 Tx.Rollback 요청을 보내고 TX.RollbackOK 응답을 기다린다.

원자성(Atomicity)은 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력이다. 예를 들어, 자금 이체는 성공할 수도 실패할 수도 있지만 보내는 쪽에서 돈을 빼 오는 작업만 성공하고 받는 쪽에 돈을 넣는 작업을 실패해서는 안된다. 원자성은 이와 같이 중간 단계까지 실행되고 실패하는 일이 없도록 하는 것이다.

RabbitMQ는 단일 queue에만 원자 트랜잭션을 지원한다. 발행자가 두개 이상의 큐에 영향을 주는 경우 더 이상 트랜잭션은 원자적이지 않다.

delivery-mode 2를 설정하여 메시지가 디스크에 저장하는 메시지가 원자 트랜잭션은 발행자의 성능에 영향을 준다. I/O 부하가 많은 서버에서 RabbitMQ 가 TX.CoimmitOK를 보내기 전에 쓰기가 완료될때까지 기다리는 경우, 클라이언트는 트랙잭션을 사용하는 경우보다 오래 기다리게 된다.

RabbitMQ의 트랜잭션은 발행자 확인과 비슷한 방식으로 동작하고 발행자에서 메시지 전달 확인 순서를 제어 할 수 있다.

메시지 발행확인 목적이라면 트랜잭션보다 더 빠르며 단순한 발행자 확인을 추천한다.

그러나 많은 경우에 메시지가 큐에 있는 동안 손실되지 않는 거이 중요한데, 이것은 HA큐로 보장할 수 있다.

4.1.6 HA 큐를 사용해 노드 장애 대응하기

안정적인 메시지 전달을 보장하기 위하여 RabbitMQ의 HA 큐가 중요한 역할을 한다.

HA 큐도 AMQP 스팩이 아닌 RabbitMQ 팀이 만든 확장 기능이며 큐를 여러 서버에 중복해 복사복을 저장하는 기능을 제공한다.

HA 큐는 AMQP API 또는 웹 기반 관리자 UI로 설정할 수 있다.

RabbitMQ 클러스터에 대한 내용은 2부 7장에서 소개한다.

4/Screen_Shot_2020-02-13_at_5.44.48_AM.png

메시지가 HA큐로 설정된 큐에 발행되면 HA 큐를 담당하는 클러스터의 각 서버로 메시지가 전송된다.

클러스터의 노드가 메시지를 소비하면 다른 노드의 모든 메시지 복사본이 즉시 제거된다.

HA 큐를 개별 노드로 지정하려면 x-ha-policy: all 대신 nodes를 인수로 저장하고 x-ha-nodes에 큐의 노드 목록을 지정한다.

HA 큐에는 단일 기본 서버 노드와 보조 노드가 있다.

기본 노드가 실패하면 보조 노드 중 하나가 기본 노드의 역할을 대신한다.

다운된 노드가 다시 추가되거나 새 노드가 클러스터에 추가되더라도 기존 노드의 큐에 이미 존재하는 메시지는 포함되지 않는다. 대신 이전에 발행된 모든 메시지가 소비되면 모든 새 메시지가 수신되고 동기화된다.

4.1.7 HA 큐 트랜잭션

HA 큐는 트랜잭션 또는 발행자 확인을 사용하는 경우, 메시지가 HA 큐에 모든 활성 노드에 있는 것으로 확인될 때까지 RabbitMQ는 성공 응답을 보내지 않는다. 이로 인해 발행자 애플리케이션에 대한 응답이 지연될 수 있다.

4.1.8 delivery-mode 2를 사용해 메시지를 디스크에 저장하기

RabbitMQ가 메시지를 처리하는 동안 특정 이유로 노드가 다운될 경우, 메시지를 발행할때 디스크에 저장하도록 설정하지 않았다면 메시지는 영원히 손실된다.

Basic.Properties의 옵션인 delivery-mode

  • 1 (기본값) : 메시지를 디스크에 저장하지 않고 항상 메모리에 저장
  • 2 : 메시지를 디스크에 저장. RabbitMQ 서버가 재시작된 후 다시 시작하면 메시지가 큐에 남아있게 된다.

delivery-mode 2 에 추가적으로 RabbitMQ 서버가 다시 시작한 우에도 메시지가 남아있게 하려면 큐를 만들 때 durable로 선언되어야 한다. 내구성(durable) 큐에 대해서는 5장에 자세히 다룬다.

I/O 성능이 좋지 못한 경우 극단적인 성능 문제가 발생 될 수 있다.

일반적으로 대부분의 웹 애플리케이션에서 쓰기의 비율은 낮고 읽기의 비율이 높다.

4/Screen_Shot_2020-02-14_at_5.34.23_AM.png

4/Screen_Shot_2020-02-14_at_5.38.41_AM.png

  1. RabbitMQ로 발행한 delivery-mode 2의 메시지는 디스크에 보관된다.
  2. 메시지 포인터는 큐 데이터 구조에 저장된다.
  3. 메시지가 더 이상 큐에 없으면 디스크에서 제거된다.

4/Screen_Shot_2020-02-14_at_5.42.49_AM.png

I/O 부하가 많게 되면 OS는 저장장치와 데이터를 주고 받는 동안 I/O 작업을 하는 포로세스를 block한다. RabbitMQ가 이러한 대기가 많아지면 메시지 처리량이 큰폭으로 감소한다.

메시지를 디스크에 저장하는 것은 궁극적으로 메시지의 배달을 보장하는 가장 중요한 방법 중 하나지만 가장 비용이 많이 드는 방법이기도 하다.

4.2 RabbitMQ 푸시백

RabbitMQ 2.0 버전 이전

에는 발행자가 너무 빨리 메시지를 발행해 RabbitMQ 를 압도하기 시작할때 Channel.Flow RPC 메소드를 통해 메시지를 차단하고 또 다른 channel.Flow 명령을 받을때까지 메시지를 더 이상 보내지 않도록 지시할 수 있다.

하지만 상당히 비효율적이므로 결국 문제가 발생될 수 있다.

  • Channel.Flow 를 처리하지 않거나 잘못 처리하는 발행자 애플리케이션의 경우.
  • RabbitMQ가 Channel.Flow를 요청했을 때 발행자가 이를 듣고 있다는 보장이 없다.

4/Screen_Shot_2020-02-14_at_7.41.37_PM.png

RabbitMQ 3.2 버전 이전

Channel.Flow 사용을 중단하고 TCP Back-Pressure 메커니즘으로 문제 해결

4/Screen_Shot_2020-02-14_at_7.42.10_PM.png

  • 발행자에게 더 이상 정중히 요청하지 않고 TCP 소켓 하위 수준의 데이터 수신을 중지

  • 내부적으로 RabbitMQ는 Credit이라는 개념을 사용하여 발행자에게 언제 푸시백을 할것인지 관리

  • 새로운 연결이 생성되면 이 연결에 미리 사용할 수 있는 Credit의 양이 할당되고 RabbitMQ가 각 RPC 명령을 수신하면 크레딧이 감소

  • 연결 Credit은 RabbitMQ가 연결의 소켓에서 값을 읽어야 하는지 판단하기 위해 평가

  • 연결에 남은 크레딧이 없으면 크레딧이 생길 때까지 무시한다.

RabbitMQ 3.2 부터

RabbitMQ 팀은 AMQP 스팩을 확장

  • 연결에 대한 Credit이 임곗값에 도달했을때 전송되는 알람을 추가하고 클라이언트에 연결이 차단됐다는 사실을 알림(비동기 메소드)
    • Connection.Blocked : RabbitMQ가 발행자 클라이언트를 차단 되었을 때 알림
    • Connection.Unblocked : 해당 블록이 제거되었을 때 알림
  • 대부분의 주요 클라이언트 라이브러리는 이 기능을 구현.

4.2.1 rabbitpy 로 연결 상태 확인하기

Connection.Block 알림을 지원하는 RabbitMQ 버전에 연결되면 rabbitpy는 알림을 수신하고 연결이 차단됐다는 내부 플래그를 설정한다.

4.2.2 연결 상태 확인을 위한 관리자 API 사용하기

RabbitMQ 3.2 이전 버전을 사용하는 경우 애플리케이션은 웹 기반 관리 API를 사용해 연결 상태를 지속적으로 polling 해 확인 할 수 있다.

매우 직관적인 방법이지만 너무 자주 사용하면 RabbitMQ에 원치 않는 로드가 발생할 수 있다.

경우에 따라 API 요청을 반환하는 데 몇 초가 걸릴 수 있다.

RabbitMQ에서 관리자 API는 연결, 채널, 큐 또는 외부에 노출된 다른 객체의 상태를 질의하기 위한 RESTful URL을 제공한다. http://localhost:15672/api/

관리자 API에서 차단된 상태는 연결 자체가 아닌 연결 내부의 채널에 적용된다.

채널 상태를

curl -i -u guest:guest 'http://localhost:15672/api/channels?sort=message_stats.publish_details.rate&sort_reverse=true&columns=name,message_stats.publish_details.rate,message_stats.deliver_get_details.rate'

curl -i -u guest:guest 'http://localhost:15672/api/channels

API는 결과를 JSON 직렬화된 객체로 반환한다.

4.3 요약

발행자의 역할과 행동을 정의

  • 발행자가 메시지를 디스크에 저장해야 하는가?
  • 애플리케이션의 다양한 구성 요소는 발행된 메시지가 수신됐는지 보장해야 하는가?
  • TCP Back-Pressure (배압)으로 애플리케이션이 차단되거나 RabbitMQ에 메시지를 발행하는 동안 연결이 차단된 경우 어떻게 되는가?
  • 메시지가 얼마나 중요한가? 메시지 처리량을 높이기 위해 배달 보장을 희생할 수 있는가?

이러한 질문은 올바를 애플리케이션 아키텍처를 만드는 데 도움이 된다.

이를 위해 RabbitMQ는 다양한 옵션을 제공한다.

성능과 높은 안정성 사이의 균형을 유지하고 메시지에 적합한 수준의 설정을 결정해야한다.