Java의 heap dump를 이용한 OOM 원인 분석 with Eclipse MAT

kyle·2025년 3월 18일
0

1. 배경

1-1. 문제 상황 및 원인

우리 회사는 백준이나 프로그래머스와 같은 알고리즘 온라인 저지 서비스를 제공한다.
온라인 저지 서비스는 사용자의 채점 요청을 받는 API 서버와, 실제 채점이 이루어지는 채점 서버로 구성되어 있다.

그런데, 채점 서버의 운영 환경에서 OOM이 발생하며 다운되는 현상이 발생하였다.

일시적인 현상이라 생각해 서버를 재시작했지만, 재시작하자마자 바로 OOM이 또 발생하였다.
이로 인해, 결국 사용자의 요청 데이터가 문제라는 것을 알아냈다.

  • API 서버에서는 사용자의 요청 데이터를 Kafka를 통해 채점 서버로 넘겨준다.
  • 특정 요청은 OOM을 발생시켰고 채점 서버가 다운된다.
  • 채점 서버를 재시작해도 Kafka에서 직전에 실패한 메시지가 retry 되면서 OOM이 또 발생한다.

사용자의 요청 데이터를 확인해보니, 아래와 같은 문제 코드가 포함되어 있었다.

def solution(n):
    ...(생략)
    
    while queue:
        print(1)   
        
    ...(생략)

BFS 문제에 대한 파이썬 답안인데, while 문 안에서 큐의 데이터를 제거하는 로직이 없다보니 무한루프를 돌게 되었다. 그로 인해, print(1)이 계속 반복된다.
알고리즘 온라인 저지는 사용자가 어떤 출력을 했는지 보여줘야 하기 때문에, 채점 서버에서는 사용자 출력에 대해 print.log 파일로 저장하고, 이를 다시 읽어서 응답 데이터로 만든다.

예를 들면 아래와 같다.

print.log 파일을 다시 읽는 과정에서, 내용이 너무 길어 이를 BufferedReader가 문자열로 heap에 저장하는 과정에서 OOM이 발생하게 된 것이다.

예를 들면, print.log가 아래와 같이 끝없이 문자열 '1'로 채워져 있다고 보면된다.

출시 전에 무한루프도 출력초과도 꼼꼼하게 QA를 했다고 생각하였다. 해당 문제의 시간제한이 다른 문제들보다 넉넉하다보니 무한루프가 더 오래 지속되어, 기존에는 나올 수 없던 용량의 파일이 생성된 것으로 보인다. 더욱 꼼꼼하게 확인하지 못해 스스로 아쉬운 부분이었다.

1-2. 해결

의외로 해결은 간단하다.

시간제한이 넉넉한 문제들은 결국 print.log 파일의 용량이 굉장히 커질 수 있다는 것이므로, 생성되는 파일의 최대 용량을 제한하면 된다.

채점 서버는 국제정보올림피아드(IOI)에서 공식적으로 제공하는 오픈소스인 isolate격리 환경으로 사용하고 있다. 격리 환경이란 사용자의 답안 코드를 실행하는 독립된 환경을 의미하는데, 이를 이용해 시간과 메모리를 제한할 수 있고 악성 코드에 대한 보안도 가능하다.

isolate의 매뉴얼에는 격리 환경에서 생성하는 파일의 용량도 제한할 수 있는 --fsize라는 옵션이 존재한다. 이를 통해, print.log가 최대 10MB까지만 생성될 수 있도록 설정하여 문제를 해결했다.


2. heap dump 적용하기

장애 상황이 해결된 후, 팀원 중 한 분께서 아래와 같이 heap dump를 적용해보길 추천하셨다. 👍

이참에, heap dump에 대해 조금 더 알아보고 적용하여, 앞으로 일어날 수 있는 OOM 상황에 잘 대비해야겠다는 생각이 들었다.

2-1. heap dump란?

Java의 heap dump는 JVM이 실행 중인 애플리케이션의 힙 메모리 상태를 스냅샷(snapshot) 형태로 저장한 파일을 말한다. (.hprof 확장자를 가진다)

heap dump를 아래와 같이 활용할 수 있다.

  1. 메모리 상태 파악 : heap dump는 JVM의 힙 영역 전체를 기록하므로, 객체 생성이나 참조 관계 혹은 메모리 사용량 등을 확인하는데 사용된다. 이를 통해 메모리 누수(memory leak)나 불필요한 객체가 과도하게 생성되는 문제를 진단하는 데 유용하다.

  2. 디버깅 및 성능 최적화 : 애플리케이션이 비정상 종료되거나 성능 문제가 발생했을 때, heap dump 파일을 분석하여 문제의 원인을 파악할 수 있다. 어떤 객체가 지속적으로 메모리에 남아 있는지, GC(Garbage Collection)가 어떻게 동작하는지 등을 살펴보고 튜닝도 진행할 수 있다.

  3. 분석 도구 : Eclipse MAT(Memory Analyzer Tool), VisualVM 등과 같은 도구를 사용하여 시각적으로 분석할 수 있다. 실제로, 나는 Eclipse MAT을 사용하였다.

2-2. OOM 발생 시, heap dump 파일 생성하기

애플리케이션 실행 명령어에 heap dump와 관련된 옵션을 추가할 수 있다.

> java \
    -Xms1G \
    -Xmx1G \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=./heapdump/scoring_dump_$(date +%Y-%m-%d_%H:%M:%S).hprof \
    -jar \
    app.jar
  • -XX:+HeapDumpOnOutOfMemoryError : OOM이 발생하면 heap dump 파일이 생성되도록 지정하는 옵션이다.
  • -XX:HeapDumpPath={경로} : heap dump 파일이 생성되는 경로를 지정하는 옵션이다. 리눅스의 date를 이용해 현재 시각이 파일명에 들어갈 수 있도록 하였다. (ex. scoring_dump_2025-03-18_01:41:39.hprof)

2-3. heap dump 파일 생성을 감지하고 자동으로 S3에 전송하기

채점 서버는 AWS EC2 내에 배포되어 있기 때문에, OOM이 발생하면 EC2 인스턴스 내에 heap dump 파일이 생성되므로 이를 분석하기 힘들다.
따라서, heap dump 파일이 생성되면 이를 감지하고 자동으로 S3에 전송하도록 해야한다.

이를 위해, aws-cliinotify-tools라는 리눅스 라이브러리가 필요하다.

  1. aws-cli는 다음 명령어들을 통해 설치할 수 있다.

    $ sudo apt-get install unzip
    $ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o  "awscliv2.zip"
    $ unzip awscliv2.zip
    $ sudo ./aws/install
    • AWS EC2 OS 사양에 맞게 압축 파일을 다운받아야 한다.

    이후 자격증명을 확인한다.

    $ aws configure
    • Access Key, Secret Key, Region Name을 각각 알맞게 입력한다.
    • aws configure 대신, S3 관련 권한이 있는 IAM Role을 EC2 인스턴스 자체에 부여하는 방법도 있다.
  2. inotify-tools는 다음 명령어를 통해 설치할 수 있다.

    $ sudo apt-get install inotify-tools
    • inotify-tools는 리눅스에서 파일 시스템 이벤트를 모니터링하는 라이브러리이다.
    • 해당 라이브러리에 존재하는 inotifywait 명령어를 통해, 리눅스의 파일 또는 폴더의 변화를 실시간으로 감지할 수 있다.

이후, dump.sh라는 쉘 스크립트 파일을 EC2 내에 생성하고 아래와 같이 작성한다.

{}로 된 부분은 따로 알맞게 채워야 한다.

#!/bin/bash

LOCAL_PATH="{heap dump 파일이 존재하는 폴더의 절대경로}"
REMOTE_PATH="s3://{S3 버킷 이름}/heapdump"

inotifywait -m -e close_write "$LOCAL_PATH" --format '%f' | while read DUMP_FILE; do
    if [[ "$DUMP_FILE" == *.hprof ]]; then
    	sudo chmod 644 "$LOCAL_PATH/$DUMP_FILE"
        aws s3 cp "$LOCAL_PATH/$DUMP_FILE" "$REMOTE_PATH/$DUMP_FILE"
        echo "$DUMP_FILE S3 전송 완료"
    fi
done
  • inotifywait의 옵션을 살펴보면 다음과 같다. (더 많은 옵션은 여기 참고)
    • -m : monitor 모드를 의미하며 백그라운드로 실행하도록 한다.
    • -e close_write : 파일에 대한 쓰기가 완료되어 파일 핸들이 닫히는(close_write) 이벤트를 감지한다.
    • --format '%f' : 감지된 이벤트가 발생한 파일의 이름을 출력하도록 지정한다. 그리고 이 파일명은 뒤이어 등장하는 DUMP_FILE이라는 변수에 할당된다.
  • S3에 전송하기 위해서는 파일을 읽어야 하므로, chmod 644를 통해 모든 사용자에게 읽기 권한을 부여한다.
  • aws-cli를 이용해 S3의 특정 버킷으로 heap dump 파일을 전송한다.

추가적으로 dump.sh가 EC2 인스턴스가 재부팅 되더라도 항상 실행될 수 있도록, systemd를 통해 데몬으로 실행하는 것이 좋다.

일단, systemd 등록을 위한 스크립트를 작성한다.

$ sudo vim /etc/systemd/system/heapdump-script.service

내용은 아래와 같이 작성한다.

[Unit]
Description=Monitor OOM and upload heap dump file to AWS S3
After=network.target

[Service]
Type=simple
User=ubuntu
ExecStart=/bin/bash {dump.sh 파일이 존재하는 폴더의 절대경로}/dump.sh
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
  • Restart=always를 통해 무조건 재실행 할 수 있도록 지정한다.
  • RestartSec=3은 프로세스가 꺼졌을 때 3초 뒤에 재실행 하라는 의미이다.

작성한 스크립트를 systemd에 등록하고 시작한다.

$ sudo systemctl daemon-reload
$ sudo systemctl enable heapdump-script.service
$ sudo systemctl start heapdump-script.service
$ sudo systemctl status heapdump-script.service

아래와 같이 로그가 나타나면 정상적으로 스크립트가 실행된 것이다.

이제부터 heap dump 파일이 생성되면 리눅스에서 자동으로 감지하여 S3로 전송하는 스크립트를 실행할 것이다.

2-4. OOM 상황을 재현하고 테스트 해보기

OOM 상황에서 heap dump 파일이 실제로 잘 생성되고, S3에도 잘 전송되는지 확인해보자.

  • isolate--fsize 옵션을 잠시 지우고 애플리케이션을 실행한다.
  • 문제가 되었던 사용자 요청 데이터로 채점 요청을 한다.
  • OOM이 발생하며 애플리케이션이 다운된다.

이후, ls 명령어로 확인해보니 정상적으로 heap dump 파일이 생성되었다.

그리고 S3 버킷에도 마찬가지로 heap dump 파일이 정상적으로 전송되었다.

2-5. cron을 이용해 오래된 heap dump 파일 자동 제거하기

EC2 인스턴스에 생성되는 heap dump 파일은 생성되는 즉시 S3로 전송된다. 따라서, 로컬에 오랫동안 보관하고 있으면 스토리지를 낭비하므로, cron을 이용해 주기적으로 제거하면 좋다.

cron은 리눅스에서 시간 기반 작업을 자동으로 예약 및 실행할 수 있게 해주는 스케줄러이다.
cron을 설정하여 수정된지 일주일이 지난 heap dump 파일을 자동으로 제거해보자.

먼저, cron을 설정하기 위한 에디터를 띄워야한다.

$ crontab -e
  • nano 혹은 vim 중에 선택하라고 나오는데, 편한 에디터를 선택하면된다.

이후, cron 문법을 이용해 특정 작업을 예약한다.

0 0 * * * /usr/bin/find {heap dump 파일이 존재하는 폴더의 절대경로} -type f -name "*.hprof" -mtime +7 -delete
  • 0 0 * * * : 매일 자정에 작업을 실행한다.
  • -type f : 파일만 검색한다.
  • -name "*.hprof" : 파일 이름이 .hprof로 끝나는 것들만 검색한다.
  • -mtime +7 : 현재 시각으로부터 7일(일주일)보다 오래전에 수정된 파일만 검색한다.
  • -delete : 찾은 파일들을 제거한다.

앞으로 생성되는 모든 heap dump 파일은 7일 뒤에 자동으로 삭제될 예정이므로, 따로 스토리지 용량에 대해 신경쓰지 않아도된다.


3. Eclipse MAT을 이용한 heap dump 분석하기

heap dump 파일을 분석하는 도구는 여러 개가 있다. 그 중에서 Eclipse MAT이 가장 다루기 편하고 자료도 많기 때문에, 이를 사용하고자 한다.

3-1. 다운로드 및 설치 (MacOS 기준)

https://eclipse.dev/mat/download/ 에서 설치파일을 다운받고 실행한다.

아래와 같은 화면이 나오면 MemoryAnalyzer를 Applications로 이동시키면 설치가 완료된다.

3-2. heap dump 파일 불러오기

Open a Heap Dump를 눌러 heap dump 파일을 불러온다.

혹시나 heap dump 파일을 불러오는 과정에서 아래와 같은 에러를 만날 수 있다. 이는 heap dump 파일이 너무 커서 Eclipse MAT의 기본 메모리 용량을 넘을때 발생한다.

이를 해결하기 위해서는, 아래 파일을 찾아 -Xmx 부분을 수정하여 Eclipse MAT이 사용할 수 있는 최대 메모리 용량을 늘려야한다.

/Applications/MemoryAnalyzer.app/Contents/Eclipse/MemoryAnalyzer.ini

  • 기본값은 1024m인데, 2048m로 늘려주었다.

재시작 후, 다시 heap dump 파일을 불러오면 정상적으로 결과가 나타난다.

3-3. heap dump 파일 분석하기

아까 S3에 저장했던 scoring_dump_2025-03-18_01:41:39.hprof을 분석해보자.

  • Overview 화면의 아래 쪽 메뉴에서 다양한 정보들을 살펴볼 수 있다.

히스토그램(Histogram)은 각 클래스별 인스턴스 수와 총 메모리 사용량을 보여주는 지표이다. 비정상적으로 많은 인스턴스가 생성된 클래스나, 한 클래스에 의해 지나치게 많은 메모리가 사용되는 경우, 문제를 의심해 볼 수 있다.

  • String 객체가 비정상적으로 많음을 알 수 있다.
  • 이를 통해, 많은 양의 문자열이 메모리에 로딩되는 것이 OOM의 원인임을 파악할 수 있다.

도미네이터 트리(Dominator Tree)는 메모리에서 해제되지 못하고 남아있는 객체(=메모리 누수 후보)를 보여주는 지표이다. 메모리에서 가장 많은 영역을 차지하는 객체를 보여준다.

  • KafkaListener 스레드가 거의 929MB에 달하는 객체들을 직간접적으로 참조하고 있음을 알 수 있다. 이는 채점 서버가 API 서버로부터 Kafka를 통해 전달되는 메시지를 소비할 때, OOM이 발생하기 때문이다.
  • 해당 스레드가 참조하는 객체들 중에서도 문자열 리스트가 많은 비율을 차지하고 있는 것으로 보아, 역시나 많은 양의 문자열로 인해 OOM이 발생함을 파악할 수 있다.

누수 의심 리포트(Leak Suspects)는 Eclipse MAT에서 생성하는 일종의 보고서로, 메모리 누수가 의심되는 객체나 패턴을 분석해 보여주는 지표이다. 스택트레이스(Stacktrace)도 함께 보여주어, 누수 후보를 중심으로 객체들이 왜 해제되지 않는지 원인을 추적하는데 도움을 준다.

  • 어떤 누수 후보가 얼마나 메모리를 차지하는지 시각화를 통해 보여준다.
  • 그리고 아래 쪽에는 해당 누수 후보에 관한 설명이 작성되어 있다.

여기서 See stacktrace with involved local variables를 누르면 아래와 같은 화면이 등장한다.

  • 스택 프레임에 따른 지역변수들과 메서드 호출 위치를 보여준다.
  • PrintLog의 27번째 라인의 실행으로 인해 OOM이 발생함을 알 수 있다.
  • 실제로 PrintLog의 27번째 라인을 보면, logs라는 매개변수를 join하는 과정임을 알 수 있다.

    logs는 print.log를 BufferedReader로 읽어와 ArrayList에 담은 데이터이므로, 결국 print.log가 너무 커서 OOM이 발생한다라고 최종 결론을 내릴 수 있다.

4. 정리

실서비스에서의 장애를 바탕으로 heap dump 적용의 필요성을 느껴, 직접 적용까지 해보았다.
적용 순서를 정리하면 아래와 같다.

  1. 실행 중인 애플리케이션에서 모종의 이유로 OOM이 발생한다.
  2. 당시 heap 스냅샷을 찍은 heap dump 파일이 생성된다.
  3. inotifywait은 이를 감지하고 S3로 heap dump 파일을 전송하는 스크립트를 실행한다.
  4. 개발자는 원인 파악을 위해 S3에서 heap dump 파일을 수동으로 다운로드 받는다.
  5. Eclipse MAT을 이용해 다양한 지표를 참고하여 원인을 분석한다.
  6. 일주일이 지난 heap dump 파일은 자동으로 로컬에서 삭제된다.

기존에는 단순히 로그만 보고 코드를 따라가면서 원인을 "추측"하는 쪽에 가까웠다면,
이제는 Eclipse MAT의 다양한 지표를 보면서 더욱 확실한 근거로 원인을 "분석"하는 쪽에 가까워진 것 같다.

OOM은 최대한 발생하지 않는 것이 좋겠지만, 설령 발생하더라도 이전보다는 더욱 빠르고 정확하게 해결할거라 기대해본다.

0개의 댓글

관련 채용 정보