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 그래프는 완만하게 사용량이 증가할 뿐, 시스템을 붕괴시킬 만한 급격한 메모리 적체 현상은 보이지 않았습니다.
모든 가설이 기각된 지금, 매우 이상한 상황에 놓였습니다.
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