개발/개발관련

[개발관련] 정말 중요한 메모리

mabb 2024. 2. 7. 21:44
반응형

벌써 개발자로 전직을 한 지 만으로 14개월이 되었다. 그중 현 회사에서는 6개월 간 정말 다양한 경험을 할 수가 있었고
 최근에는 유독 메모리와 싸움을 벌이고 있다. 처음 메모리에 관심을 가지게 된 것은 소중한 데이터 수집 프로세스가 killed 된 것을 알게 되었을 때였다. 도대체 누가 왜 나의 프로세스를 죽였을까. 이 프로세스가 무엇을 그리 잘못했을까 궁금하여 메모리 사용량을 유심히 관찰하였으나 엄청나게 메모리를 많이 사용하지 않는데도 불구하고 주기적으로 프로세스가  killed 되는 것을 알게 되었다. killed의 주기가 정확하게 10분이라는 것이 의아하여 crontab을 확인해 보니 10분마다 프로세스를 종료시키는 스케줄링이 등록되어 있었다. 소스와 나뿐, 히스토리를 묻기 어려운 상황에 '아 뭔가 안정적인 운영을 위해 주기적으로 프로세스를 재시작해주는구나' 생각하고 넘어갔었다. 다음 메모리 문제를 마주한 것은 크롬 브라우저였다. 상시 실행되어야 할 대시보드에는 하얀색 화면에 Out Of Memory라는 글자가 기재되어 있었다. 톰캣을 의심했다. 웹이니까 톰캣에 잘못이 있을 것이라 생각하였는데 서버에는 이상이 없었다. OOM은  클라이언트 단의 브라우저에서 발생한 것이었다. 크롬의 작업 관리자와 개발자 도구의 타임라인 할당 기능을 통해 확인해 본 바 JavaScript에서 자원이 제대로 해제되지 않는다는 것을 알게 되었다. am4차트 라이브러리의 객체가 제대로 GC 되지 않고 있었다. 내부적으로 DOM을 참조하기 때문인 것으로 파악했고, 친절하게도 해당 라이브러리 매뉴얼에는 자원을 해제하기 위한 방법이 명시되어 있었다. 자사 소스에도 해당 방법이 적용되어 있었으나 다소 잘못 적용되어 있어 제대로 자원을 해제하지 못하고 있어 조치를 해주었었다.

아름다운 파란색 줄무늬
개운해진 모습

이번에 마주한 메모리 문제는 현재 진행형이다. 이틀 째 원인을 파악중이나 아직 근본 원인에 접근하지 못하여 오늘 밤에는 꼭 원인을 찾길 기원하며 머리를 식힐 겸 글을 쓰고 있다. 서버의 메모리 사용량이 90%에 임박한 케이스들이 보이고 있다. 그중 서버가 멈추는 등  문제가 발생하는 케이스도 있어 원인을 파악하는 중이다. 몇 개월 간 메모리 90%의 상태를 유지한 케이스도 있기에 이것이 정말 문제인가 메모리 누수의 관점에서 확인해 볼 필요가 있다. 그리고 서버의 한정된 메모리 안에서 우리의 솔루션이 지속가능하기 위한 권장 사양에 대한 정리가 필요하다고 생각했다. 데이터를 수집하는 대상 규모에 따라 똑같은 프로세스가 600MB에서 12GB에 육박하기까지 메모리 사용량에 차이가 발생했다. 5분 주기로 수행하는 배치에 의해 heap사이즈가 순간적으로 늘어나고, GC가 수행되더라도 JVM이 확보한 heap 메모리가 OS에 즉각적으로 회수되지는 않는 것으로 보였다. Xmx 설정이 없어 힙 사이즈의 적정 크기는 어느 정도이어야 할지 Xmx30m으로 매우 작게 힙사이즈를 설정해 보았다. 그러자 힙사이즈는 30mb 언더로 유지가 되었으나 DB세션이 제대로 클로즈되지 않는 등의 이상 증상이 발생하기 시작하였다. 예상한 것은 JVM의 OOM이 발생하는 것이었으나 프로세스가 죽지 않고 유지되었다. jstat으로 확인해 본 결과 배치를 수행하는 5분 주기로 FGC가 실행되는 것으로 보였다. 아직 JVM과 GC에 대한 지식이 부족하여 어떠한 원리인지 파악이 필요하다. 약 20여 개의 스케줄 된 배치 프로세스들이 각각의 JVM에서 실행된다는 것이 메모리 측면에서 큰 비용이라는 것을 이번에 알게 된 것 같다. 더군다나 적절한 Xmx 설정이 없으니 default로 heap이 가질 수 있는 최대 사이즈가 물리 메모리의 약 1/4라는 인터넷에서 확인한 정보에 의하면 언제든지 OOM이 발생해도 이상하지 않은 상황인 것이다. heap사이즈가 늘어나는 시점에 어떤 객체가 생기고 있는 것일까 궁금하여 jstat과 heap dump를 활용하여 보았다. 놀랍게도 가장 많은 메모리를 차지하는 것은 int []이었다. int []이 약 400MB 가까이 heap 메모리를 차지하였다가 GC 되었다. length가 긴 int 배열의 용량이 얼마나 큰 지는 프로그래머스 코테 연습을 하면서 익히 알게 되었었다. int가 4byte인데 Integer.MAX_VALUE만큼의 int 배열을 선언해 보니 메모리 부족이라는 문구가 발생했었다. 대략 4*21억 바이트인 것이다. 이렇게까지 긴 int []은 없었지만 개수가 많았다. regex, char []과 String이 그다음을 이었다.

*첨부된 이미지의 수치는 글과 다름.

heap에 생겨났던 객체들 jmap -histo {PID}

이렇게 heap 사이즈가 늘어나버리고, 늘어난 heap은 OS에 회수되지 않고, 하나의 배치 프로세스는 제법 큰 메모리를 점유하게 되는 것 같다. 소스 상에서 생성하는 int[], 정규표현식, 그리고 문자열을 유심히 확인할 필요가 있겠다. 그런데 여기서 어딘가 또 문제가 있다는 것을 알게 되었다. 

jstat -gc, jconsole, 그래도 heap GC는 잘 되는 거 같은데

heap + non-heap < RES
RES가 굉장히 크다. heap + non-heap = RES 라고 생각하였는데 다른 무언가가 더 있는 것이다. 

RES 1.9g

native_memory를 확인해보았다. 의아하다. 뭔가 숫자가 딱딱 들어맞질 않는다. Total committed와 RES가 다르다. JVM이 사용하고 있는 전체 메모리와 운영체제의 top 명령에서 확인할 수 있는 RES가 왜 다른 것일까. 왜 OS에서는 JVM의 용량을 더 크게 알고 있는 것일까. 세탁기가 다 돌아간 관계로 빨래를 널고 원인 파악에 들어가 보아야겠다. 오늘 저녁에는 Java 스트림을 연습할 계획이었는데 메모리 이슈를 파악하는 것도 재미있는 구석이 있는 것 같다. 정말 메모리는 중요하고 컴퓨터의 한정된 자원을 잘 활용하는 것, 문제가 발생하였을 때 근본 원인을 파악하여 조치하는 것 또한 정말 중요하다고 생각한다. 현 직장을 다니면서 네트워크와 운영체제에 대한 기본기, 그리고 영어와 수학, 무엇이든 원리를 제대로 깊이 있게 알고 넘어가는 것이 여러 가지 문제 해결의 양분이 된다는 것을 느끼고 있다. 조급할 시간에 천천히 하나라도 제대로 익히는 개발자가 되어야겠다!

해당 시점의 native_memory이 아니라 차이가 있지만, Total과 RES에 차이가 있었다.

반응형