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

'Disk Scan' 주기가 짧을 때 발생하는 I/O Wait 현상은?

by 엉짱 2026. 4. 2.
반응형

'Disk Scan' 주기가 짧을 때 발생하는 I/O Wait 현상은?

엔터프라이즈 메시지 브로커(ActiveMQ 등)를 운영하는 인프라 환경에서, CPU 사용률은 높지 않은데 시스템 전체가 늪에 빠진 것처럼 느려지고 프로듀서(Producer) 애플리케이션에 타임아웃이 빗발치는 원인 불명의 장애를 마주할 때가 있습니다. 시스템 모니터링 대시보드를 열어보면 CPU의 usr(사용자)나 sys(시스템) 수치는 평온하지만, 유독 %iowait (I/O Wait) 수치만 비정상적으로 치솟아 있는 것을 발견하게 됩니다.

이러한 I/O 병목의 배후에는 브로커의 스토리지 엔진이 디스크의 상태를 점검하기 위해 백그라운드에서 실행하는 '디스크 스캔(Disk Scan)' 작업이 자리 잡고 있는 경우가 많습니다. 디스크 공간을 빨리 확보하겠다는 욕심으로 이 스캔 주기를 너무 짧게 설정하면, 인프라의 처리량(TPS)을 근본적으로 파괴하는 침묵의 살인자가 됩니다.

이 가이드에서는 브로커 내부의 디스크 스캔 메커니즘을 이해하고, 과도한 스캔 주기가 어떻게 시스템의 I/O 자원을 고갈시켜 메인 라우팅 스레드를 멈춰 세우는지 상세히 해부합니다.


1. 메시지 브로커 스토리지의 'Disk Scan' 목적

ActiveMQ의 KahaDB나 Artemis의 저널 스토리지 엔진은 한 번 디스크에 기록한 메시지 파일을 방치하지 않습니다. 브로커는 파일 시스템을 주기적으로 스캔하며 다음과 같은 핵심적인 '유지보수(Housekeeping)' 작업을 수행합니다.

  • 가비지 컬렉션(GC) 대상 탐색: 수백 개의 저널 파일(db-*.log) 중, 컨슈머가 메시지를 모두 소비하여 100% 쓸모없어진 파일(가비지)을 찾아내어 삭제하거나, 유효한 메시지만 모아 압축(Compaction)할 대상을 선정하기 위해 파일들의 메타데이터를 스캔합니다.
  • 만료 메시지(TTL) 정리: 지정된 수명(Time-To-Live)이 지난 메시지들을 찾아내어 디스크에서 제거하기 위해 B-Tree 인덱스나 페이징 디렉토리를 순회합니다.
  • 디렉토리 폴링(Directory Polling): 클러스터링 환경이나 특정 파일 기반의 배포 환경에서, 외부로부터 새로운 파일이 추가되었는지 혹은 설정 파일이 변경되었는지 감지하기 위해 디스크를 정기적으로 읽어 들입니다.

이러한 스캔 작업은 설정 파일(예: cleanupInterval)에 정의된 타이머 주기에 따라 백그라운드 스레드에서 반복적으로 실행됩니다.


2. I/O Wait 현상의 시스템 공학적 실체

스캔 주기의 위험성을 논하기 전에 I/O Wait가 정확히 무엇인지 짚고 넘어가야 합니다.

CPU 연산 속도는 디스크의 물리적인 읽기/쓰기 속도보다 수만 배 이상 빠릅니다. 스레드가 디스크에서 저널 파일을 스캔하기 위해 I/O 요청(시스템 콜)을 운영체제(OS)에 넘기면, CPU는 하드웨어(디스크 컨트롤러)가 데이터를 메모리로 올려줄 때까지 아무 일도 하지 못하고 기다려야 합니다.

이때 OS 스케줄러는 해당 스레드를 대기(Blocked) 상태로 전환하는데, CPU가 연산할 준비가 되어 있고 남는 코어가 있음에도 불구하고 오직 '디스크의 응답을 기다리느라' 쉬고 있는 상태의 비율을 측정한 지표가 바로 I/O Wait(%iowait)입니다. 즉, 이 수치가 높다는 것은 디스크가 시스템 전체의 발목을 심각하게 잡고 있다는 치명적인 경고입니다.


3. Disk Scan 주기가 극단적으로 짧아질 때의 연쇄 재난

디스크 공간을 조금이라도 더 빨리 회수(Cleanup)하기 위해 기본 30초인 스캔 주기를 1초나 2초 단위로 극단적으로 줄여버리면, 인프라 내부에서는 다음과 같은 3단계 연쇄 재난이 발생합니다.

A. 메타데이터(Inode) 경합 폭발

디스크 스캔은 단순히 파일의 본문을 읽는 것이 아니라, 수천 개의 파일에 대한 크기, 수정 시간 등의 메타데이터(Inode)를 읽어 들이는 무거운 작업입니다. 주기가 짧아지면 디스크 컨트롤러는 1초마다 수천 개의 파일 헤더를 뒤지기 위해 물리적 디스크 헤드를 쉴 새 없이 움직여야(랜덤 I/O) 합니다. 디스크의 IOPS(초당 입출력 횟수) 대역폭이 순식간에 스캔 작업 하나만으로 100% 점유되어 버립니다.

B. 페이지 캐시(Page Cache) 스래싱(Thrashing)

OS는 디스크 I/O 성능을 높이기 위해 남는 RAM 공간을 '페이지 캐시'로 활용하여 자주 쓰는 데이터를 올려둡니다.
하지만 백그라운드 스레드가 거대한 저널 파일 디렉토리를 1초마다 무식하게 스캔하기 시작하면, OS는 스캔을 위해 읽어 들인 쓸모없는 과거의 파일 데이터들로 페이지 캐시를 가득 채워버립니다. 이 과정에서 정작 브로커가 실시간으로 라우팅해야 할 '진짜 중요한 B-Tree 인덱스 캐시'가 메모리 밖으로 밀려나는 캐시 스래싱 현상이 발생합니다.

C. 메인 라우팅 스레드의 기아 상태 (Starvation) - 최종 장애

이것이 가장 치명적인 결과입니다. 프로듀서가 새로운 메시지를 보내면, 브로커의 메인 스레드는 이를 저널 파일 끝에 안전하게 동기화(fsync)해야만 클라이언트에게 성공(ACK) 응답을 줄 수 있습니다.
그러나 디스크 컨트롤러의 작업 대기열(I/O Queue)은 이미 1초마다 실행되는 맹목적인 스캔 스레드의 읽기 요청들로 가득 차 있습니다. 메인 스레드의 쓰기 요청은 이 줄의 맨 뒤에 서서 하염없이 기다리게 됩니다.
결과적으로 1밀리초면 끝날 메시지 저장이 수 초 이상 지연되며, 프로듀서 애플리케이션들은 타임아웃 예외를 뿜어내고 서비스 전체가 마비됩니다.


4. 아키텍처를 살리는 스토리지 최적화 튜닝 가이드

이러한 I/O Wait의 늪에서 벗어나려면, 맹목적인 스캔을 멈추고 시스템의 리듬에 맞게 설정 파일(broker.xml 또는 activemq.xml)을 조율해야 합니다.

  1. 스캔 주기(cleanupInterval)의 현실화:
    디스크 공간 회수를 1초라도 빨리 해야 할 만큼 인프라 스펙이 쪼들린다면 디스크 용량 자체를 증설하는 것이 맞습니다. 스토리지 정리 주기(cleanupInterval)는 브로커의 트래픽 볼륨에 따라 최소 30초에서 5분(300000ms) 사이의 넉넉한 값으로 설정하십시오. 청소는 뜸하게 하되, 한 번 할 때 확실하게 하도록 여유를 주는 것이 메인 트래픽을 살리는 길입니다.
  2. 물리적 디스크 파티션의 격리 (I/O Isolation):
    디스크 스캔이 불가피하게 잦아야 하는 환경(예: 만료 메시지가 비정상적으로 많은 도메인)이라면, 스캔의 대상이 되는 페이징 디렉토리나 대용량 메시지 디렉토리를 순차 쓰기가 생명인 주 저널 디렉토리(journal-directory)와 물리적으로 전혀 다른 디스크(별도의 SSD/NVMe 볼륨)로 분리하십시오. 이렇게 하면 백그라운드 스캔이 메인 쓰기 I/O를 간섭할 수 없습니다.
  3. OS 레벨의 모니터링 체계 구축:
    단순히 브로커의 JMX 지표만 보지 말고, 리눅스의 iostat -dx 1 명령어를 통해 물리 장치별 I/O 큐 길이(aqu-sz), 디스크 대기 시간(await), 그리고 디스크 활용률(%util)을 상시 모니터링해야 합니다. %util이 지속적으로 80%를 넘고 await가 수십 밀리초 이상으로 튄다면, 스토리지 엔진의 백그라운드 작업 주기를 즉각적으로 늘려야 하는 신호입니다.

결론적으로 스토리지 엔진의 디스크 스캔은 브로커의 건강을 유지하기 위해 반드시 필요한 위생 작업이지만, 주기가 비정상적으로 짧아지는 순간 인프라의 동맥을 막아버리는 독약이 됩니다. 하드웨어의 I/O 처리 한계를 명확히 인지하고, 백그라운드 유지보수 작업이 결코 메인 비즈니스 트래픽을 방해하지 않도록 정교한 타임 튜닝을 적용하시기 바랍니다.

반응형