본문 바로가기
1. 개발/1.8. ActiveMQ

'Message Paging'이 시작되는 메모리 계산 공식은?

by 엉짱 2026. 3. 25.
반응형

'Message Paging'이 시작되는 메모리 계산 공식은?

대용량 트래픽을 처리하는 메시지 브로커(ActiveMQ Artemis 등)의 가장 중요한 임무는 어떤 상황에서도 서버가 메모리 부족(OOM, Out Of Memory)으로 다운되지 않도록 스스로를 보호하는 것입니다. 시스템 메모리가 한계에 도달했을 때 브로커가 취하는 궁극적인 방어 기제가 바로 메시지를 메모리에서 디스크로 밀어내는 '페이징(Paging)' 모드입니다.

그렇다면 브로커는 도대체 어떤 기준으로 "메모리가 꽉 찼다"고 판단하고 페이징을 시작할까요? 자바(Java) 환경에서 객체의 정확한 메모리 크기를 실시간으로 측정하는 것은 매우 까다로운 작업입니다. 이 가이드에서는 브로커가 페이징의 시작점(임계값)을 결정하기 위해 내부적으로 사용하는 메모리 산정 방식과 아키텍처의 동작 원리를 상세히 분석합니다.


1. 페이징을 촉발하는 두 가지 메모리 임계값

페이징 모드로의 전환은 브로커 설정 파일(broker.xml)에 정의된 메모리 상한선에 도달했을 때 시작됩니다. 이 상한선은 크게 두 가지 레벨로 나뉘어 동작합니다.

  • 주소 레벨 임계값 (max-size-bytes): 특정 큐나 토픽(Address) 하나가 사용할 수 있는 최대 메모리 용량입니다. 특정 큐에 메시지가 집중적으로 쌓여 이 설정값을 초과하면, 해당 큐만 독립적으로 페이징 모드에 돌입하여 디스크에 데이터를 쓰기 시작합니다.
  • 글로벌 레벨 임계값 (global-max-size): 브로커 내부에 있는 모든 큐의 메모리 사용량을 합친 전체 시스템의 절대적인 메모리 상한선입니다. 개별 큐가 자신의 임계값에 도달하지 않았더라도, 전체 합산 용량이 이 글로벌 임계값을 넘어서면 브로커 전체가 메모리 보호를 위해 즉각적인 조치(기본적으로 프로듀서 차단 또는 페이징)를 취하게 됩니다.

결국 페이징이 시작되는 조건은 "현재 쌓여있는 메시지들의 계산된 메모리 총합이 위 두 가지 임계값 중 하나라도 초과하는 순간"입니다.


2. 브로커의 논리적 메모리 산정 방식

Java 가상 머신(JVM) 위에서 동작하는 애플리케이션은 가비지 컬렉터(GC)와 객체 헤더, 메모리 패딩 등으로 인해 메모리에 적재된 메시지 객체의 정확한 물리적 바이트(Byte) 크기를 실시간으로 계산하기가 사실상 불가능합니다.

따라서 ActiveMQ Artemis는 JVM의 실제 힙(Heap) 메모리 사용량을 추적하는 대신, 메시지를 구성하는 요소들의 크기를 합산하는 '결정론적이고 논리적인 크기 추정(Estimation)' 방식을 사용합니다. 계산은 다음 세 가지 요소의 크기를 합산하여 이루어집니다.

A. 메시지 본문 (Payload / Body)
가장 직관적인 영역입니다. 클라이언트가 전송한 텍스트, JSON, 혹은 바이너리 데이터 파일의 실제 바이트 배열 크기입니다.

B. 속성 및 헤더 (Properties & Headers)
메시지에는 본문뿐만 아니라 라우팅을 위한 다양한 메타데이터가 포함됩니다. JMS 메시지 헤더(메시지 ID, 타임스탬프, 만료 시간 등)와 개발자가 임의로 추가한 사용자 정의 속성(Custom Properties)들의 크기를 합산합니다. 문자열 데이터의 경우 인코딩 방식에 따른 바이트 길이가 그대로 반영됩니다.

C. 브로커 내부 고정 오버헤드 (Internal Overhead)
메시지 본문과 속성 외에도, 브로커가 이 메시지를 메모리 큐 자료구조(Linked List 등)에서 관리하기 위해 부가적으로 생성하는 내부 래퍼(Wrapper) 객체들의 크기가 존재합니다. Artemis는 프로토콜마다 조금씩 다르지만, 메시지 하나당 고정된 크기의 오버헤드 바이트를 기본적으로 더하여 보수적으로 메모리를 산정합니다.

브로커는 새로운 메시지가 인입될 때마다 위 세 가지 요소의 크기를 합산하여 큐의 현재 메모리 사용량 카운터에 더합니다. 반대로 메시지가 소비(Consume)되어 삭제되면 그만큼 카운터를 차감합니다. 이 누적된 카운터 값이 설정된 max-size-bytes를 넘어서는 순간, 디스크 페이징 스레드가 깨어나 작동합니다.


3. 프로토콜 변환 오버헤드에 대한 주의사항

현대의 브로커는 AMQP, MQTT, OpenWire, Core 등 다양한 프로토콜을 동시에 지원합니다. 여기서 아키텍트가 반드시 인지해야 할 중요한 함정이 있습니다.

클라이언트가 AMQP 프로토콜로 1KB짜리 메시지를 보냈다고 가정해 보겠습니다. 이 메시지가 브로커 내부로 들어오면, 브로커는 라우팅과 저장을 위해 이 AMQP 메시지를 브로커가 이해할 수 있는 범용적인 내부 코어(Core) 포맷으로 매핑하거나 추가적인 디코딩 객체를 생성해야 할 수 있습니다.

이 과정에서 1KB였던 원본 데이터가 브로커의 힙 메모리상에서는 2KB~3KB의 메모리를 차지하는 구조체로 부풀려질 수 있습니다. 브로커는 이 부풀려진 메모리 객체의 크기를 기준으로 임계값을 계산합니다. 따라서 프로듀서가 보낸 순수 데이터의 총합만으로 페이징 시작 시점을 예측하면, 예상보다 훨씬 일찍 페이징 모드에 진입하는 현상을 목격하게 됩니다.


4. 아키텍처 튜닝 및 운영 가이드

논리적 메모리 계산 방식을 이해했다면, 시스템의 안정성을 극대화하기 위해 다음과 같은 튜닝 전략을 적용해야 합니다.

  1. 보수적인 최대 크기 설정: 브로커의 산정 방식은 추정치일 뿐, JVM의 실제 힙 사용량(GC 오버헤드 포함)을 완벽하게 반영하지 못합니다. 따라서 global-max-size를 설정할 때는 브로커에 할당된 최대 힙 메모리(-Xmx)의 50%를 절대 넘지 않도록 설정해야 실제 물리적인 OOM 장애를 예방할 수 있습니다.
  2. 페이징 임계값과 디스크 I/O 분리: address-full-policyPAGE로 설정하여 페이징이 활성화된 주소는 메모리가 가득 찬 시점부터 극심한 디스크 쓰기를 유발합니다. 페이징 전용 디렉토리(paging-directory)를 저널 디렉토리(journal-directory)와 물리적으로 다른 고속 디스크(SSD/NVMe)로 분리하여 메인 트래픽의 병목을 방지해야 합니다.
  3. 대용량 헤더 지양: 비즈니스 로직상 메시지 본문은 작게 유지하면서 커스텀 헤더에 방대한 양의 데이터를 담아 보내는 안티 패턴(Anti-pattern)을 피해야 합니다. 헤더 크기 역시 메모리 카운터에 고스란히 합산되므로 페이징을 앞당기는 주범이 됩니다.

요약하자면, 브로커는 복잡한 JVM의 메모리를 직접 뜯어보는 대신, 메시지 페이로드와 속성, 그리고 구조적 오버헤드를 합산하는 명확하고 결정론적인 방식으로 페이징 임계치를 계산합니다. 이 동작 원리를 바탕으로 각 큐의 용량(Capacity)을 정교하게 산정하시기 바랍니다.

반응형