'Message Expiry Scan' 작업이 CPU를 점유할 때 튜닝 방법은?
엔터프라이즈 아키텍처에서 메시지의 유효기간(TTL, Time-To-Live)을 설정하는 것은 매우 훌륭한 시스템 방어 패턴입니다. 1시간이 지난 주식 호가 데이터나, 5분이 지난 일회성 인증 코드(OTP)는 이미 비즈니스적 가치를 상실했으므로 큐(Queue)에 계속 머물며 메모리를 차지할 이유가 없습니다.
따라서 메시지 브로커(ActiveMQ, Artemis 등)는 설정된 TTL이 만료된 메시지를 스스로 찾아내어 폐기(혹은 Expiry Queue로 이동)하는 백그라운드 청소부 스레드를 쉼 없이 가동합니다. 이를 'Message Expiry Scan(메시지 만료 스캔)' 작업이라고 부릅니다.
하지만 평화롭던 브로커의 CPU 사용률이 주기적으로 100%를 치고, 그 순간마다 프로듀서와 컨슈머의 응답 지연(Latency)이 폭증한다면 이 성실한 청소부를 가장 먼저 의심해야 합니다. 이 가이드에서는 Expiry Scan 작업이 왜 시스템의 CPU 자원을 고갈시키는지 그 원리를 파헤치고, 이를 우아하게 통제하기 위한 파라미터 튜닝 방법과 아키텍처적 대안을 상세히 해부합니다.

1. Expiry Scanner의 무거운 동작 원리
Expiry Scan은 마법처럼 만료된 메시지만 쏙쏙 골라내는 기능이 아닙니다. 이 백그라운드 스레드는 설정된 타이머 주기에 맞춰 잠에서 깨어난 뒤, 브로커 내부에 존재하는 모든 큐를 돌며 메모리와 디스크에 쌓여있는 메시지들의 타임스탬프 메타데이터를 하나하나 순차적으로 뒤져보는(Linear Scan) 무식하고 무거운 작업을 수행합니다.
메시지가 큐의 맨 앞에 있든 맨 뒤에 있든, 스캐너는 큐의 내부 링크드 리스트나 페이징 구조체를 탐색해야 합니다. 메시지가 만료되었다고 판정되면 브로커는 이 메시지를 큐에서 빼내어 삭제하거나 Expiry Queue로 라우팅하는 추가적인 디스크 I/O와 트랜잭션까지 발생시킵니다.
2. CPU 스파이크(Spike)가 발생하는 치명적 조건
평소에는 눈에 띄지 않던 이 스캔 작업이 브로커의 메인 라우팅 스레드의 목을 조르는 치명적인 CPU 병목으로 돌변하는 데에는 두 가지 전제 조건이 있습니다.
A. 딥 큐(Deep Queue) 현상의 방치
컨슈머가 제때 메시지를 가져가지 않아 큐에 수십만에서 수백만 건의 메시지가 적체되어 있는 상태(Deep Queue)입니다. 스캐너 스레드가 이 거대한 산더미를 뒤지기 시작하면, CPU는 끝없는 메타데이터 파싱 연산에 빠지게 됩니다. 심지어 디스크로 페이징(Paging)된 메시지들까지 스캔하려 들면 메모리와 디스크 간의 스래싱(Thrashing) 현상까지 동반됩니다.
B. 극단적으로 짧은 스캔 주기
메시지를 1초라도 빨리 지우겠다는 생각으로 스캔 주기를 1초 단위로 설정해 둔 경우입니다. 수백만 건의 큐를 스캔하는 데 2초가 걸리는데 1초마다 스캔을 지시하면, 스캐너 스레드들이 꼬리를 물고 중첩되어 CPU 코어를 완전히 점유해버리는 재난이 발생합니다.
3. 코어 튜닝 파라미터 1: 스캔 주기(Scan Period)의 현실화
이 문제를 해결하는 가장 직접적이고 확실한 방법은 브로커 설정 파일(broker.xml 또는 activemq.xml)에서 스캔의 주기를 시스템의 체력에 맞게 대폭 늘려주는 것입니다.
ActiveMQ Artemis 환경의 튜닝:
Artemis에서는 address-setting 블록 내에서 주소를 세밀하게 제어할 수 있습니다.
message-expiry-scan-period: 스레드가 깨어나는 주기입니다. 기본값(통상 30000ms, 즉 30초)도 트래픽이 많을 때는 부담스럽습니다. 이를 비즈니스 허용 범위 내에서60000(1분) 혹은300000(5분) 수준으로 과감히 상향 조정하십시오.-1설정 (스캔 완전 비활성화): 만약 특정 큐에서는 메시지가 만료되더라도 굳이 브로커가 백그라운드에서 지워줄 필요가 없고, 나중에 컨슈머가 메시지를 꺼내갈 때(On-Demand) 만료 여부를 판별해서 버려도 무방하다면 이 값을-1로 설정하여 해당 큐에 대한 스캔 작업 자체를 완전히 꺼버리는 것이 최고의 성능 튜닝입니다.
ActiveMQ Classic 환경의 튜닝:
최상단 <broker> 태그 내의 expireMessagesPeriod 속성을 조절합니다. (예: expireMessagesPeriod="60000")
4. 코어 튜닝 파라미터 2: 스레드 우선순위와 스캔 한도 설정
스캔 주기를 늦췄음에도 한 번 스캔이 돌 때마다 발생하는 순간적인 CPU 튀어오름이 부담스럽다면, 스캐너의 작업 강도 자체를 조절해야 합니다.
- Artemis의
message-expiry-thread-priority:
스캐너 스레드가 CPU를 선점하지 못하도록 스레드 우선순위를 낮출 수 있습니다. 자바 스레드 기본 우선순위는 5입니다. 이 값을 3이나 2로 낮춰주면, CPU 코어에 여유가 있을 때만 스캔 작업을 수행하고 메인 라우팅 스레드(프로듀서/컨슈머 처리)에 CPU 자원을 양보하게 됩니다. - 스캔 배칭(Batching): 브로커 버전과 종류에 따라, 한 번의 스캔 사이클에서 만료 처리할 최대 메시지 수량을 제한하는 옵션을 지원합니다. 만료된 메시지가 10만 개 발견되더라도 한 번에 1천 개씩만 잘라서 폐기하고 다음 사이클로 넘기도록 유도하면 CPU의 급격한 점유를 방어할 수 있습니다.
5. 근본적인 아키텍처 관점의 대안 (Best Practices)
단순한 파라미터 튜닝을 넘어, 스캐너가 아예 무거운 작업을 하지 않도록 인프라 파이프라인을 개선하는 것이 근본적인 해결책입니다.
A. 짧은 TTL 전용 큐의 분리 격리
수명이 10초에 불과한 실시간 틱(Tick) 데이터와, 수명이 7일인 주문 데이터를 같은 큐에 섞어 쓰지 마십시오. 만료가 잦은 짧은 TTL의 메시지들은 반드시 별도의 전용 큐(혹은 Topic)로 분리하고, 해당 큐에 대해서만 적극적인 스캔을 허용하거나 혹은 철저히 메모리 기반의 비영속(Non-Persistent) 모드로 전환하여 디스크 I/O를 발생시키지 않도록 격리해야 합니다.
B. TTL 스캔 대신 'TTL 필터링'으로의 발상 전환
브로커 서버의 CPU가 만성적으로 부족하다면 백그라운드 스캐너(message-expiry-scan-period="-1")를 완전히 꺼버리십시오.
대신, 큐에 무한정 쌓이도록 내버려 둔 뒤 컨슈머 애플리케이션 단에서 메시지를 폴링(Polling)으로 꺼내올 때 타임스탬프를 확인하고 애플리케이션 코드로 직접 폐기(Discard)하게 만드는 역발상이 필요합니다. 이 경우 브로커 서버의 CPU 오버헤드는 0에 수렴하게 되며, 무한히 스케일 아웃이 가능한 수많은 컨슈머 서버의 CPU로 만료 처리 연산을 분산(Offloading)시키는 우아한 마이크로서비스 아키텍처가 완성됩니다.
결론적으로 'Message Expiry Scan'은 시스템을 깨끗하게 유지하는 유용한 기능이지만, 깊은 큐(Deep Queue) 환경에서는 CPU를 잡아먹는 괴물로 돌변합니다. 비즈니스의 데이터 유효기간 요건을 면밀히 분석하여 스캔 주기를 최대한 늦추고, 아키텍처 레벨에서 브로커의 청소 부담을 컨슈머에게 덜어주는 전략을 과감하게 도입하시기 바랍니다.
'1. 개발 > 1.8. ActiveMQ' 카테고리의 다른 글
| 'Blob Message' 전송 시 외부 저장소(FTP/HTTP)와의 연동 원리는? (1) | 2026.04.04 |
|---|---|
| KahaDB의 인덱스 파일(db.data) 크기 제한과 분할 방법은? (0) | 2026.04.04 |
| 'memoryUsage' 임계치 도달 시 컨슈머에게 우선권을 주는 설정은? (0) | 2026.04.04 |
| 디스크 조각화(Fragmentation)가 KahaDB 읽기 성능에 미치는 영향? (0) | 2026.04.04 |
| 'Zero-copy' 전송 기술이 Artemis 저장소 읽기에서 어떻게 쓰이나? (1) | 2026.04.03 |