RabbitMq: ackMode = None

트러블슈팅: ackMode = ‘NONE’

상황

부하테스트를 여러번 시도 했었는데, 이상한 현상이 동일하게 발견됐습니다.

  • API 서버는 RabbitMQ로 초당 수백 개의 메시지를 쏟아내고 있었습니다.

  • RabbitMQ는 Consumer의 처리량과 관계없이 메시지를 소비하고 있었습니다.

  • Consumer 서버의 CPU 사용률은 20% 미만으로 매우 여유로웠고, 스레드들도 분명히 일을 하고 있었습니다.

  • 문제 발생: 처음에는 50개 이상의 스레드가 정상적으로 동작하는 듯 보이다가, 특정 시점 이후 메시지를 처리하는 스레드가 갑자기 2개로 급격하게 줄어들었습니다.

    • 증상 1: Consumer 서버에서 2개의 스레드만 처리를 하게됨

    • 증상 2: RabbitMq에서 분당 소비되는 메시지 수가 급격하게 줄음

    • 증상 3: RabbitMq의 분당 컨텍스트 스위칭 횟수가 급격하게 줄음

이 미스터리를 풀기 위해 두가지 가설을 세우고 검증하는 과정을 거쳤습니다.

가설 1: CPU 라이브락(Livelock)

  • 내용: busyWait와 같은 CPU 집약적 코드가 너무 많은 스레드와 만나, OS 스케줄러가 스레드 교체에만 모든 자원을 낭비하는 '라이브락' 상태에 빠졌을 것이다.

vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 1  0      0 250348  25144 681148    0    0    78    26  255    1  2  0 98  0  0  0
 0  0      0 250348  25144 681148    0    0     0     0  192  296  0  1 100  0  0  0
 0  0      0 250348  25144 681148    0    0     0     0  270  367  1  0 99  0  0  0
 0  0      0 250348  25144 681148    0    0     0     0 1486 1242  1  1 98  0  1  0
 0  0      0 250348  25144 681148    0    0     0    12  196  311  1  0 100  0  0  0
 1  0      0 248584  25144 681148    0    0     0     0  287  292  5  1 95  0  0  0
 0  0      0 248584  25144 681148    0    0     0     0  208  295  1  0 99  0  0  0
 0  0      0 248584  25144 681148    0    0     0    24  179  290  0  0 100  0  0  0
 0  0      0 248584  25144 681148    0    0     0     0  162  274  1  0 100  0  0  0
 0  0      0 248584  25144 681148    0    0     0     0  183  287  0  0 100  0  0  0
 0  0      0 248584  25144 681152    0    0     0     0  215  315  2  0 98  0  0  0
 0  0      0 248584  25144 681152    0    0     0     0  151  274  0  0 100  0  0  0
 0  0      0 248584  25144 681152    0    0     0     0  164  289  0  0 99  0  0  0
 0  0      0 248584  25144 681152    0    0     0     0  160  266  0  0 100  0  0  0
 ......
  • 기각: vmstat 지표 확인 결과, 실제 CPU는 대부분 유휴(Idle) 상태였음이 보여 이 가설은 폐기.

가설 2: TCP Zero-Window로 인한 네트워크 마비

  • 내용: ackMode=NONE으로 인해 Consumer가 메시지를 감당 못 하여 TCP 수신 버퍼가 가득 찼고, 이로 인해 RabbitMQ에 메시지 전송 중단을 요청하여 모든 것이 멈췄을 것이다.

  • 기각: RabbitMQ의 Ready 큐에 메시지가 쌓인 적이 없습니다. 메시지는 계속 끊임없이 Consumer 애플리케이션으로 전달되고 있었습니다.

가설 3: 메시지가 Heap에 쌓이고, GC가 자주 실행

  • 내용: 메시지들은 Consumer 애플리케이션으로 성공적으로 전달되며 많은 객체들이 JVM 힙 메모리(Heap Memory)에 쌓이고, GC에 부담을 줬을 것이다.

  • 기각:

    • GC Pressure 그래프는 0%를 기록하며 GC로 인한 CPU 부하가 전혀 없음을 보여주었습니다.

    • Pause Durations 그래프 역시 GC로 인한 멈춤 시간이 10~30ms 수준으로 매우 짧고 안정적이어서, 시스템이 GC 때문에 멈췄다고 보기 어려웠습니다.

    • JVM Heap 그래프는 완만하게 사용량이 증가할 뿐, 시스템을 붕괴시킬 만한 급격한 메모리 적체 현상은 보이지 않았습니다.

AckMode = MANUAL

모든 가설이 기각된 지금, 매우 이상한 상황에 놓였습니다.

  • CPU는 충분히 여유롭습니다 (라이브락 X)

  • 메시지는 Consumer 애플리케이션까지 잘 전달되고 있습니다 (네트워크 마비 X)

  • 메모리와 GC는 안정적입니다 (GC 문제 X)

  • 분명히 50개 이상의 스레드가 일을 할 준비가 되어 있습니다.

하지만 현실에서는 단 2개의 스레드만이 일을 처리하고 있었습니다. 모든 자원이 정상인데도 애플리케이션이 제 성능을 내지 못하는 이 상황은, 문제의 원인이 하드웨어나 OS 같은 외부 환경이 아닌 애플리케이션과 프레임워크의 내부 동작 방식에 있다고 생각이 듭니다.

지금까지 기각된 모든 가설들의 공통된 전제는 ackMode=NONE 이었습니다. 이 설정은 RabbitMQ와 Consumer 사이의 '흐름 제어(Flow Control)'를 완전히 제거해 버립니다.

그렇다면, 문제의 원인이 CPU, 네트워크, 메모리 같은 자원의 문제가 아니라, 흐름 제어가 부재한 이 특수한 상태에서 스프링 AMQP 프레임워크(SimpleMessageListenerContainer)가 예상치 못하게 동작하는 것일지도 모른다고 생각하게 됐습니다.

어쩌면 프레임워크가 스스로를 보호하기 위해, 혹은 비정상적인 상황에 대응하기 위해 스레드 수를 극단적으로 줄여버리는 것일 수 있습니다.

이 가설을 검증하는 가장 확실한 방법은 ackMode=NONE 설정을 MANUAL로 변경하여, 시스템에 흐름 제어를 도입해보는 것이었습니다.

결과 1: 병목 지점의 이동

가장 먼저 눈에 띈 긍정적인 변화는 메시지가 쌓이는 위치였습니다.

  • 변경 전 (ackMode=NONE): RabbitMQ의 Ready 큐는 항상 0이었습니다. 모든 메시지는 Consume 서버로 보내지고 있었습니다.

  • 변경 후 (ackMode=MANUAL): 이제 메시지들은 RabbitMQ의 Ready 큐에 차곡차곡 쌓이기 시작했습니다. 이제 메시지 백로그는 Consumer의 메모리가 아닌, 안정적이고 영속성이 보장되는 RabbitMQ에 의해 관리되었고, 제대로된 모니터링이 가능해졌습니다.

결과 2: 모든 스레드의 활성화

ackMode를 변경하자, 이전의 가장 큰 미스터리였던 '2개의 스레드만 일하던 현상'이 거짓말처럼 사라졌습니다.

  • 변경 전: ackMode=NONE이 유발한 내부 마비와 '패닉 리셋' 때문에, 50개 이상의 스레드 중 단 2개만 동작하는 기현상이 발생했습니다.

  • 변경 후: 명시적인 흐름 제어가 도입되자, 스프링 컨테이너의 '비상 재시작' 메커니즘이 더 이상 발동하지 않았습니다. 설정된 50개 이상의 모든 Consumer 스레드가 깨어나 동시에 RabbitMQ에 메시지를 요청하며 바쁘게 움직이기 시작했습니다.

위 과정으로 인해 유추할 수 있는게 버퍼 문제라고 생각이 됩니다.

ackMode=MANUAL로의 전환 실험은 매우 성공적이었습니다. '2개 스레드 문제'의 정확한 이유는 설명하지 못하지만, 원인은 ackMode 였던것을 찾아냈습니다.

하지만, 흐름 제어 문제를 해결하자마자 Consumer 서버의 CPU 사용률이 100%에 도달하는 상황이 발생하였고, 새로운 도전 과제가 생기게 됐습니다.

Last updated