Java Out of Memory, 99% Memory In Use

PEPPERMINT100·2023년 4월 4일
1

서론

어느 날 갑자기 운영 중인 서버가 멈췄다. 다운 타임은 약 40분 정도 였고, 해당 건으로 많은 CS가 들어왔다. 심각한건 알아차리지 못했다. 변명을 해보자면 일단 휴가 중으로 한국이 아닌 일본에서 여행 중이긴 했다.

조치는 다른 개발자분이 인스턴스를 늘려주었고, 메모리 문제가 생긴 인스턴스들을 타겟그룹에서 제외함으로서 급한 불을 껐다. 몇 일 후 휴가에서 복귀했고 해당 내용을 살펴보았다.

사실 여행 중이든 아니었든 서버 장애에 대한 알림 시스템이 아쉽게 되어 있었다. 물론 AWS SNS을 통해 계속 보내주는 장애 관련 메일을 무시해왔던 안일함도 분명 있었다.

해당 이슈가 왜 발생했는지 디버깅하는 과정부터 후속 조치를 어떻게 했는지에 대해 간단히 적어볼까 한다.

JVM Heap 메모리

신입 때(2년전에..) 강남에 있는 코딩 부트캠프 회사 인턴 면접을 본 적이 있다. CTO님의 무한 자료구조 꼬리물기 질문을 버텨낸 후 당시 내가 쓴 이력서를 바탕으로 면접이 진행되었다.

그 때 정말 어리석게도 JVM의 구조를 이해한다고 적었다. 구글에서 떠도는 내용을 적당히 외우고 적당히 까먹은 이 후라 어버버대며 제대로 답하지 못했다.

이 후에도 JVM 구조를 내가 실무에서 볼 일이 있을까? 싶었지만 그럴 일이 발생했다.

일단 문제는 사용중인 빈스톡의 헬스가 떨어졌고 빈스톡 단 에러는 99% memory in use 였다. 바로 단순하게 자바 어플리케이션에서 메모리 누수가 일어났던 것으로 판단했다.

이 때도 6~8시간 정도가 지나면 배포중인 인스턴스 하나의 메모리 에러가 발생하여 재시작 혹은 인스턴스 추가를 해주어야 하는 상황이었다. 일단 예상되는 부분이 있어 먼저 그 부분을 수정한 후 분석을 해보았다.

힙 덤프 남기기

메모리 분석을 위해선 힙 덤프를 떠야했다. 일반적으로 가변적으로 늘었다 줄었다 하는 메모리 공간인 힙이 문제가 되는 경우가 많고, 이 힙 메모리의 현 상황이 어떤지 캡쳐하여 파일로 남길 수 있었다.

어떻게 남기는지에 대해서 삽질을 조금 했는데, 과정은 아래와 같다.

참고로 환경은 Amazon Linux 2 이며(Cent OS 기반), 빈스톡의 톰캣 플랫폼을 통해 웹 서버를 배포중에 있다.

$ sudo [path-to-java-home]/bin/jps
$ sudo [path-to-java-home]/bin/jhsdb jmap
 --binaryheap --dumpfile [덤프파일이름] --pid [자바프로세스 id]

먼저 JAVA_HOME의 위치를 찾아야 한다. 환경 변수에 등록 되어 있다면 바로 jps만 실행하면 되지만 등록이 안되어 있을 경우 java home까지의 경로를 찾아서 bin 폴더 내부에 jps를 실행해준다.

jps를 사용하면 Java 프로그램이 실행중인 프로세스의 pid를 찾을 수 있다.

그리고 이 pid로 jmap을 통해 heap dump 파일을 생성 할 수 있다.

참고로 보통 힙 덤프파일은 용량이 커 서비스 중인 인스턴스에서 바로 열면 서비스에 영향이 갈 수 있으므로 안전하게 로컬로 파일을 옮긴 후 분석 툴을 통해 분석해야 한다.

파일을 가져오는 방법은 FileZila와 같은 툴을 이용해도 되고 scp 커맨드로 가져와도 된다.

분석 툴

분석 툴에는 여러가지가 있지만 나는 이클립스사에서 만든 MAT를 사용하였다. 여기에서 다운로드 받으면 되는데, 최신 버전은 실행을 위해 자바 17 이상을 요구하므로 낮은 버전을 다운로드 받아서 사용했다.

그리고 생성한 힙 덤프 파일을 열고 분석해보았다. 파일을 열 때 Leak Support까지 볼거냐 라고 물어는데 체크하고 실행한다.

결과는

저 파란 부분이 사용 중인 메모리이고 회색 부분이 remainder 메모리이다. 그렇다 문제가 없었다.

이유를 생각해보니 덤프를 떴을 때의 상태는 사실 메모리가 정상이었기 때문에 문제가 없다고 나오는것으로 판단했다.

HeapDumpOnOutOfMemoryError

그렇다면 문제가 있는 상황을 캡쳐해야 했다. 다행히도 의심되어 수정한 코드가 원인이었는지 더 이상 메모리 문제는 발생하지 않았다.

하지만 문제가 있는 상황이 일어나지 않으니 해당 상황의 힙 메모리를 분석하는 것도 불가능했다.

위 문제를 키워드로 찾아보니 OOM 에러가 뜰 때 덤프를 남기는 JVM 옵션이 있었다.

옵션은 아래와 같다.

**-XX:+HeapDumpOnOutOfMemoryError # OOM 에러시 힙덤프를 내리는 옵션을 활성화 한다.
-XX:HeapDumpPath=/usr/local/tomcat9/logs # 생성되는 힙덤프의 위치를 지정한다.
-XX:OnOutOfMemoryError=/home/test/tomcatRestart.sh # OOM 에러시 특정 쉘 스크립트를 실행한다.**

이제 위 옵션을 추가해야 했는데, 빈스톡 환경이라 적용하는 과정이 달랐다.

Beanstalk 콘솔 → Configuration → Software → Edit에 JVM Options가 있다. 이곳에 , 가 아닌 스페이스로 나누어 옵션을 나열해주면 정상적으로 적용된다.

적용된 이 후에는 ps aux | grep java 커맨드를 인스턴스에서 실행하여 잘 적용되었는지 확인할 수 있다.

테스트

이제 OOM 상태시 덤프파일이 잘 남겨지나 확인을 해야했다. 스트레스 테스트를 위해 stress 를 다운받았다. 그런데 Amazon Linux 2 에서는 CentOS yum 패키지가 지원하는 패키지들을 바로 사용할 수 없다.

sudo amazon-linux-extras install epel -y
sudo yum install stress -y

위 커맨드를 통해 stress 를 설치해준다.

이 후

stress --vm <프로세스 수> --vm-bytes <사용할 크기>

이 커맨드를 통해 메모리에 부하를 가했다.

결과는?

힙덤프가 남지 않았다. 사실 이 stress를 통해서는 OS의 메모리를 채우는 것이고 Java의 힙메모리에서 OOM에러를 띄우는 것은 아니었다.

결국 인스턴스의 Health가 떨어졌고 CPU 사용량을 기준으로 오토스케일링 정책이 있었기에 새로운 인스턴스만 추가되는 결과를 낳았다.

잘못 생각했던 것이고 원하는 결과를 위해선 Java Application에 부하를 줘야 한다. 결국 아래 자바 코드를 실행하는 것으로 테스트를 다시 시도했다.

public void heapOutOfMemoryTest() {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            byte[] bytes = new byte[1024 * 1024];
            list.add(bytes);
        }
    }

바로 OOM 에러가 떴고, 위 JVM Options에서 HeapDumpPath 에 지정한 폴더에 가보니 덤프가 남아있었다.

성공

다시 분석

위 덤프 파일을 로컬에 가져 온 후 MAT를 통해 다시 분석했다. Leak Support를 보니

그치 이거지, 사용 중인 메모리가 대부분을 차지하고 있었다.

쭉 내려서 디테일을 보니

바이트를 포함한 ArrayList가 문제를 일으키고 있었고, 그 아래에는 아예 어플리케이션 코드 스택트레이스 정보도 포함하고 있었다.

결론

사실 정확히 힙 덤프를 통해서 어떤 부분이 문제였다는 확인하지 못했다. 다행히도 2주 째 메모리 에러가 안나고 있었기에 수정한 부분이 문제였던 것으로 예상이 된다.

이 후엔 운영중인 서버에 모두

**-XX:+HeapDumpOnOutOfMemoryError # OOM 에러시 힙덤프를 내리는 옵션을 활성화 한다.
-XX:HeapDumpPath=/usr/local/tomcat9/logs # 생성되는 힙덤프의 위치를 지정한다.**

위 옵션을 적용했고, 서버 헬스 상태가 좋지 않으면 슬랙으로 메시지를 보내도록 CloudWatch 트리거를 통해 알림을 설정하였다.

또 오토 스케일링 되는 조건을 조금 조정해야 할 것으로 보인다. CPU 사용 중이 아닌 JVM의 메모리 사용량으로 조건을 걸 수 있는지 봐야겠다.

또 추가로

**-XX:OnOutOfMemoryError=/home/test/tomcatRestart.sh**

이 옵션도 추가하여 OOM 에러로 죽은 톰캣 서버를 재기동 시키고 파일을 S3로 업로드 해놓으면 좋을 것 같다.

다 해놓고 싶지만 다른 업무가 너무 많다 ㅠ

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.

0개의 댓글