JVM 기반의 서비스를 운영하면서 만약 OOME(OutOfMemoryError) 같은 메모리 관련 이슈가 발생하면 어떻게 문제를 분석해야 할까요? 어떤 객체가 메모리를 많이 잡아먹고 있는지 코드를 하나하나 분석하며 추측해볼 수도 있겠지만, 수만에서 수백만, 수억 라인이 되는 대규모 서비스의 코드를 일일이 분석하기란 불가능에 가까울 것입니다.
그렇기에 서비스를 운영할 때는 문제를 더 쉽고 빠르게 파악할 수 있도록 모니터링 툴을 활용하고 있습니다. 위와 같은 메모리 관련 이슈가 발생했다면 JVM의 메모리가 어떻게 관리되고 있는지 확인하고 싶을 것입니다. 다양한 상용 툴이 있지만 Oracle에서 지원하는 오픈소스 기반의 VisualVM을 활용해 설명해보도록 하겠습니다.
VisualVM
모니터링을 위한 Controller를 만들고 'loop' 요청을 보내면 약 20억번 반복하며 ArrayList에 객체를 추가합니다. heap 메모리 할당 과정을 보기 위해서 천만 번에 한 번씩 300ms 동안 Thread를 중단하는 코드를 추가하였습니다.
@RestController
public class MonitorController {
private List list = new ArrayList();
private Object lock = new Object();
@GetMapping("/loop")
public void loop() throws InterruptedException {
for (int i = 0; i < 2_000_000_000; i++) {
if (i % 10_000_000 == 0) Thread.sleep(300);
list.add(new Object());
}
}
}
//monitor.http
GET http://localhost:8080/loop
loop 요청을 보내면 'java.lang.OutOfMemoryError: Java heap space' 가 발생합니다. VisualVM을 활용하여 메모리 상황이 어떤지 확인해보도록 하겠습니다.
heap 메모리의 Max size는 약 4GB입니다. heap 메모리 사용량이 우상향하다가 할당한 Heap size가 4GB가 되었을 때 OOME가 발생했습니다.
이 때, CPU의 사용량과 GC Activity는 일정 비율을 유지하다 OOME가 발생하기 직전 GC 비율이 올라가고, CPU 사용량이 급격히 증가했습니다. 할당하려는 heap size가 Max heap size에 도달하게 되면서 GC가 메모리 확보를 위해 과다 실행된 것입니다.
각 메모리의 영역에서 GC가 일어나다가 결국 old generation 영역이 가득차고 GC가 더 이상 작동하지 못하며 메모리 공간을 확보하지 못했습니다.
Heap Dump
이 때, 어떤 객체가 비용이 높은지 분석하기 위해서 heap 메모리 데이터를 복사하는 heap dump 후, VisualVM이 제공해주는 gui 기반의 분석 결과를 보겠습니다.
대시보드 summary에는 전체 heap size, classes 개수 등 heap data 정보, 시스템 환경 정보, 모듈 정보, 시스템 설정 정보와 함께 인스턴스의 수와 크기에 따른 클래스, 사이즈별 인스턴스를 제공하고 있습니다.
요약 정보를 보더라도, 위에서 생성한 Object 클래스의 인스턴스 수와 사이즈가 굉장히 높은 것을 확인할 수 있습니다. 인스턴스의 수는 약 6억 3천만개나 됩니다. 과도하게 많이 생성되거나 사이즈가 너무 큰 클래스를 튜닝 또는 리팩토링하여 해당 문제를 해결해볼 수 있을 것입니다.
JMX
VisualVM은 JMX(Java Management eXtension) API를 활용해 각종 기능들을 제공합니다. JMX는 jdk 1.5부터 JDK에 내장된 표준 API로, JVM 기반의 application 서비스, 자원, 객체 등을 모니터링 할 수 있는 인터페이스를 제공합니다. 이를 통해 JVM 기반 application의 표준화된 정보를 획득할 수 있습니다.
JMX의 아키텍처는 3가지로 구성됩니다.
1. Instrumentation
JMX 기술을 사용해 리소스를 관리하려면 java 언어로 리소스를 측정해야 합니다. 이 측정을 하기 위해 MBeans(Managed Beans)라 불리는 Java 객체를 사용합니다. MBeans는 JMX 명세에 정의된 인터페이스와 패턴을 따르도록 하고 있는데, 이를 통해 표준화된 방법으로 자원을 측정할 수 있도록 합니다.
일단 특정 자원이 MBeans에 의해 측정되면, JMX agent를 통해 관리될 수 있습니다. MBeans는 작동할 JMX agent에 대해 분리되어 있어 의존적이지 않습니다. 이를 통해 더 유연하게 동작할 수 있습니다.
2. JMX Agent
JMX agent는 자원들을 곧바로 통제할 수 있고, 이를 다루는 remote management application에서 사용가능하도록 합니다. 보통 해당 자원이 있는 서버와 동일한 곳에서 관리되지만, 반드시 그래야 하는 것은 아닙니다.
JMX agent의 핵심 컴포넌트는 'MBean server' 입니다. JMX agent에서는 통신 어댑터 또는 커넥터를 통해 외부 management application과 통신할 수 있는 MBeans service를 갖고 있습니다.
3. Remote Management
JVM 외부에 있는 management application이 JMX agent와 연동할 수 있도록 어댑터와 커넥터의 인터페이스를 제공하여, 이를 구현한 여러 프로토콜 기반의 application에서 동작할 수 있도록 해줍니다.
성능 테스트를 수동으로 해야할까?
앞서 보여준 예시에서는 application을 local에서 실행시키고 IDE(IntelliJ)에서 http request를 직접 요청하여 테스트하였습니다. 만약 여러 client에 의해 다수의 request가 들어온다면 일일이 수동으로 요청을 보내야 할까요? 요청 수와 값 등을 지정하여 정해진 시나리오대로 트래픽을 발생시켜 부하 테스트를 할 수 있는 자동화된 툴이 있습니다. 다음 글에서는 JMeter나 nGrinder와 같은 테스팅 툴에 대해 살펴보도록 하겠습니다.
[참고자료]
https://visualvm.github.io/documentation.html
https://docs.oracle.com/javase/tutorial/jmx/overview/architecture.html
'java' 카테고리의 다른 글
[Java] Thread Dump (0) | 2023.03.01 |
---|---|
[Java] 테스팅 툴 - JMeter (1) | 2023.02.21 |
[Java] 동시성 처리 - Lock Algorithms(락 알고리즘) (0) | 2023.02.19 |
[Java] Garbage Collection(GC) 변천사 (0) | 2023.02.14 |
[Java] Java Virtual Machine(JVM) (0) | 2023.02.12 |