Elasticsearch를 위한 JVM 필수 요소: 메트릭, 메모리 및 모니터링

Elasticsearch는 Apache Lucene을 기반으로 구축된 Java 기반 검색 및 분석 엔진이자 벡터 데이터베이스이며, Elasticsearch Platform의 핵심입니다. 지원되는 플랫폼에서 Elasticsearch를 실행하려면 Java 가상 머신(JVM)이 필요합니다. JVM은 Platform 독립적인 런타임 환경을 제공하며, 기존 운영 체제 위에서 가상 환경으로 Elasticsearch를 실행할 수 있습니다. JVM은 기본 운영 체제와 하드웨어를 추상화하므로 Java 애플리케이션을 모든 플랫폼에서 실행할 수 있습니다.

JVM의 메모리 관리와 가비지 컬렉션(GC)을 통한 객체 정리를 이해하는 것은 java.lang.OutOfMemoryError (Exit code 127)와 같은 문제를 해결하는 핵심입니다. 이러한 문제는 JVM의 메모리가 부족하거나 Exit code 137이 발생할 때 나타나며, 이는 일반적으로 호스트의 OOM(out-of-memory) 킬러가 과도한 메모리 사용으로 인해 JVM 프로세스를 종료했음을 의미합니다. 이 블로그에서는 메모리 사용 패턴을 검토하고 이러한 JVM 문제를 해결하는 방법을 안내합니다.

또한 JVM의 역할과 기존 Elasticsearch API를 사용하여 이를 상호 연관시키는 방법을 설명하여 Elasticsearch JVM 상태에 대한 귀중한 인사이트를 제공하고, 사용자 고유의 사용 사례에 따라 추가로 튜닝할지 결정하는 데 도움을 드리겠습니다. 

Elasticsearch JVM 문서에서 언급했듯이, 제품과 함께 전송되는 JVM 옵션의 기본값은 지원되는 모든 프로덕션 환경에서 효율적으로 작동하며, 이러한 기본값은 검색 및 색인 작업에 대한 대부분의 사용 사례를 처리할 수 있습니다. JVM 옵션이나 설정에 대한 값을 변경하거나 사용자 지정 값을 추가하는 것은 권장되지 않습니다.

워크로드에 도움이 될 만한 변경 사항이 있다고 생각되시면, 변경하기 전에 Elastic 지원 에 문의해 주십시오.

JVM이란 무엇입니까?

Java 가상 머신은 Java Development Kit(JDK)에 번들로 포함된 Java Runtime Environment(JRE)의 필수 구성 요소입니다.

JVM 순서도

JVM은 Java 애플리케이션 실행을 돕는 컴퓨터 프로그램과 유사합니다. 물리적 기계로 존재하는 것은 아니지만, Java 코드를 지원되는 운영 체제가 이해할 수 있는 명령어로 번역하여 기계처럼 작동합니다. 또한 JVM은 메모리 관리, 가비지 수집 처리, 애플리케이션 보안 유지와 같은 중요한 작업을 수행합니다.

다음으로, JVM 메모리 관리와 가비지 컬렉션에 대해 살펴보겠습니다. 이는 메모리 성능이나 OOM 관련 문제를 해결할 때 Elasticsearch에서 자주 확인하는 주요 항목입니다.

메모리 관리와 가비지 컬렉션을 이해하려면 먼저 JVM이 시작될 때 모든 객체가 상주하는 힙 영역(또는 힙 메모리)의 논리적 표현을 이해해야 합니다.

JVM 힙 메모리 이해: Young 및 Old 세대

힙 영역 또는 힙 메모리는 객체가 수명 주기와 연령에 따라 상주하는 메모리 풀의 조합입니다. JVM 힙 메모리는 주로 Young Generation과 Old Generation으로 구성됩니다. Young Generation은 아래 그림과 같이 Eden 및 Survivor 영역으로 더 세분화됩니다. Java 객체는 연령과 참조를 기반으로 이러한 영역에 할당된 다음 Young 영역에서 Old 영역으로 이동합니다. 

Eden 스페이스의 힙 사용량 대부분은 빠르게 회수됩니다. 해제되지 않은 힙은 서바이버 스페이스(S0 또는 S1) 중 하나로 이동하며, 일반적으로 지수적 감쇠 패턴에 따라 회수됩니다. 그 이후에도 유지되면 Tenured 스페이스로 이동합니다.

총 JVM 힙 메모리 = Young generation(Eden 및 Survivor 영역 S0 및 S1) + Old generation(Tenured)

힙 영역의 논리적 표현(사용 중인 가비지 수집기 종류와는 무관함)
힙 영역의 논리적 표현(사용 중인 가비지 수집기 종류와는 무관함)

Young generation 영역/지역
Java 애플리케이션이 시작되면 새로운 객체를 생성하고 이 새로운 객체들을 힙 메모리라고 하는 영역에 할당합니다. Young generation의 Eden 공간은 애플리케이션이 새로 생성된 객체를 할당하는 첫 번째 장소입니다. 이 영역은 새로운 객체 할당을 위해 예약되어 있습니다. 그리고 이 영역이 가득 차면, 적격 객체들은 가비지 컬렉션(reaped)되거나 다른 영역으로 승격됩니다.

마이너 가비지 수집이 발생하면 참조된 개체는 Survivor 영역 S0라는 다른 하위 섹션으로 이동합니다. 그리고 참조되지 않은 개체는 삭제되어 Eden의 공간을 확보합니다. 이 영역에는 가비지 수집 프로세스에서 살아남은(survived) 개체가 포함되어 있으므로 Survivor 영역(Survivor region)이라는 이름이 붙었습니다. 

Eden 스페이스가 다시 가득 차면 동일한 주기가 반복됩니다. 이번에는 Eden과 S0에서 모두 살아남은 객체가 S1 영역으로 이동하며, 이 과정은 계속됩니다. 

Eden, S0 및 S1에서 가비지 수집을 통과한 개체는 나이 계산기에 따라 올드 제너레이션(old generation)으로 승격됩니다.

Old Generation 영역
Old Generation (Tenured Generation이라고도 함)은 Young Generation(Minor GC)에서 여러 번의 가비지 컬렉션 이벤트를 거쳐 살아남은 장기 생존 객체의 집합입니다. Survivor 공간에서 특정 횟수의 주기를 거쳐 살아남은 객체는 알고리즘에 의해 Old Generation으로 이동하며, 이를 객체 승격(object promotion)이라고도 합니다.

GC 비율 및 JVM 힙 그래프

스택 모니터링에서 나타나는 위와 같은 톱니 모양의 GC 패턴은 마이너 가비지 컬렉션이 발생할 때 GC가 메모리에서 불필요한 객체를 제거하므로 JVM 힙 메모리가 감소하고 있음을 보여줍니다. 몇 번의 주기가 지나면 객체가 누적되고, GC가 다시 작동하여 해당 객체를 정리합니다. 

가비지 수집(Garbage collection) 메서드

Elasticsearch는 지원되는 번들 JDK 버전과 함께 전송/수집되며, 자세한 내용은 릴리즈 노트에 문서화되어 있습니다. v6.4까지 Elasticsearch는 JDK9에서 더 이상 사용되지 않는(deprecated) CMS(Concurrent Mark Sweep) 가비지 컬렉터를 사용했습니다. 현재 Elasticsearch는 기본 GC 방식으로 G1(Garbage-First) 가비지 컬렉터를 사용합니다. 또한 CMS GC 방식과 비교했을 때 더 나은 성능을 제공합니다. G1GC의 내부 메커니즘은 복잡하며 여러 단계를 포함하지만, 기본적인 내용을 살펴보겠습니다. 

G1GC를 사용하려면 Elasticsearch JVM 옵션에서 JVM 플래그 -XX:+UseG1GC 를 사용하십시오. G1GC에서 전체 힙 영역은 동일한 크기의 영역으로 나뉘며 -XX:G1HeapRegionSize=1m, 이는 힙 크기에 따라 1MB에서 32MB까지 다양할 수 있습니다. Eden, Survivor 및 Old 세대는 이러한 영역의 논리적 집합이며 연속적이지 않습니다. 이 외에도 힙 영역 내부에는 Humongous 영역과 비어 있거나 사용 가능한 영역이 있습니다. Humongous 영역은 영역 크기의 절반이 넘는 객체를 할당하는 데 사용됩니다.

G1GC에서의 힙(영역) 할당
G1GC에서의 힙(영역) 할당

G1GC에는 가비지 수집 중에 충족하려고 시도하는 일시 중지 시간 목표(-XX:MaxGCPauseMillis=200)가 있습니다. 이는 가비지 수집 중에 JVM이 이 값보다 긴 일시 중지를 경험하지 않도록 보장하며, 이 옵션이 없는 CMS나 다른 수집기보다 우수합니다. 전반적으로 GC 설정을 기본값으로 유지하는 것이 권장되며, 이를 통해 성능 향상을 위해 GC 일시 중지를 제한 범위 내로 유지할 수 있습니다. 

JVM 상태를 위한 Elasticsearch API

Elasticsearch가 수행하려는 모든 작업은 실행하기 위해 JVM에 메모리 할당을 요청해야 합니다. 위에서 언급했듯이, JVM이 Elasticsearch를 대신하여 메모리 관리를 처리하므로 Elasticsearch는 이에 대해 걱정할 필요가 없습니다. 그러나 관리자는 작업이 예상대로 진행되고 있는지 확인하기 위해 Elasticsearch API를 통해 JVM 상태를 확인하고자 할 수 있습니다.

Elasticsearch JVM에 대해 지정되거나 입력 인수로 제공된 모든 설정을 확인하려면 JVM 속성과 함께 Node 정보 API를 사용하십시오(문서에서도 설명됨).

GET _nodes/_all/jvm 

이 API는 JVM 입력 인수, 메모리 풀, 힙 크기를 포함한 Node 구성 세부 정보를 제공합니다. 

또한 특정 노드의 가동 시간, 모든 메모리 풀에서 사용된 힙, 또는 가비지 컬렉션에 대한 통계와 같은 Elasticsearch JVM 메트릭을 확인하려면 node statistics API를 사용할 수 있습니다. 이 API는 언급된 각 메트릭에 대한 상세 정보를 제공합니다.

GET _nodes/stats/jvm
or
GET _nodes/stats/jvm?filter_path=nodes.*.jvm

참고: Elasticsearch는 Node stats API에서 얻은 heap_percent 값을 기반으로 결정을 내리는 것을 권장하지 않습니다. 당사의 UI는 메모리 압력을 계산하며, 사용자는 이 블로그에 설명된 계산을 기반으로 필요한 조치를 취해야 합니다.

기본 제공 JDK 도구를 사용하여 JVM 메트릭 확인

고급 사용자로서 Elasticsearch API를 사용하지 않고 실행 중인 JVM에 대한 실시간 통계를 확인하려면 번들 Java 를 사용하고 jstat 을 실행하여 실시간 통계를 확인하십시오. 이를 수행하려면 먼저 Java 홈 디렉터리 또는 bin 디렉터리와 Elasticsearch 프로세스의 PID를 찾으십시오. bin 디렉터리에서 jstat 도구를 사용하십시오:

./jstat -gcutil <PID> 2000

여기서 2000은 통계를 가져와 CLI에 출력하기까지의 간격(밀리초)입니다. 이 명령은 모든 영역에서 GC가 얼마나 자주 발생하는지, 그리고 객체에 의해 메모리 풀이 얼마나 빨리 채워지는지(객체 생성 및 승격률)를 파악하는 데 유용합니다. 

이는 위에서 언급한 API에 대한 대안이 아니라, 개발자 수준의 디버깅을 위한 것입니다.

여기까지입니다!

이번 블로그에서는 Java 가상 머신(JVM)의 기본 개념, 다양한 메모리 풀, 가비지 컬렉션의 중요성, 그리고 Elasticsearch JVM 통계를 확인하는 방법을 다루었습니다. 다음 블로그에서는 중요한 JVM 설정과 Elasticsearch 내에서 현재 메트릭을 폴링하는 방법을 논의하겠습니다.  

문의 사항이 있으시거나 추가 지원이 필요하시면 언제든지 저희에게 연락해 주시기 바랍니다. 언제나 기꺼이 도와드리겠습니다!

이 게시물에서 설명된 모든 기능이나 성능의 출시와 일정은 Elastic의 단독 재량에 따라 결정됩니다. 현재 제공되지 않는 기능이나 성능은 예정된 시간에 출시되지 않을 수도 있으며 아예 제공되지 않을 수도 있습니다.