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