brunch

You can make anything
by writing

C.S.Lewis

by 강진우 Jan 23. 2019

G1 GC 적용과 JVM Upgrade

ElasticSearch

오늘은 ElasticSearch (이하 ES)에서 GC 방식을 G1 GC로 변경하는 것과 JVM의 버전을 업그레이드하는 이야기를 해볼까 합니다. 서비스 중인 클러스터에서 어떤 문제가 발생해서 G1 GC로의 변경을 고민했는지, 그리고 진행 시 고려해야 할 사항들, 변경 후의 효과 등등에 대해서 이야기해 보겠습니다.


문제의 시작


먼저 어떤 문제가 발생했는지에 대해 살펴보겠습니다. 사용자의 데이터를 검색하는 애플리케이션에서 ES로 검색 혹은 색인 요청을 보낼 때 간헐적으로 초 단위의 타임아웃이 발생하는 이슈가 있었습니다. 원인 분석을 하다 보니 아래와 같이 Old GC가 초 단위로 발생하는 것을 확인할 수 있었습니다.

GC TIme와 힙 메모리 변화량 그래프

위 그림에서 볼 수 있듯이 Old GC가 발생하는 순간에 상당히 많은 양의 메모리가 반환되고 그로 인해 초 단위의 GC Pause Time이 발생하는 것을 볼 수 있습니다. 이 문제를 해결하기 위해서는 앞서 이야기한 것처럼 GC의 방식을 바꾸거나 힙 메모리의 양을 줄이거나 두 가지의 방법이 있습니다. 

사실 너무 많은 양의 힙 메모리를 사용하고 있는 것도 문제가 될 수 있기 때문에 필요한 만큼만 힙 메모리를 할당하는 것이 좋습니다. 

위 예제에서는 Old GC가 발생할 때 거의 20GB에 육박하는 많은 양의 메모리를 비우기 때문에 초 단위의 GC Pause Time이 발생하고 이로 인해 초 단위의 Stop-The-World 현상이 발생합니다. 만약 힙 메모리를 줄이면 한 번에 비워야 하는 메모리의 양이 줄어들기 때문에 GC Pause Time이 확연히 줄어들게 됩니다. 하지만, 힙 메모리를 줄였을 경우에는 순간적으로 사용자의 요청이 급증할 때 OOM이 발생할 수 있는 잠재적인 문제가 있습니다. 또한 자연스러운 사용량 증가에 따라 결국 어느 시점에서는 줄였던 힙 메모리를 다시 늘려서 재시작해주어야 하는 문제도 있습니다. 그래서 조금 더 근본적으로 해결하고자 GC 방식을 바꿔보기로 했습니다.


G1 GC와 CMS GC의 차이점


G1 GC로의 변경에 대해서 이야기하기 전에 G1 GC와 CMS GC의 차이점에 대해서 간략하게 살펴보겠습니다. 두 GC 방식에 어떤 차이점이 있는지를 알아야 G1 GC로 갔을 때의 장점을 이해할 수 있기 때문입니다.

GC 동작 원리나 G1 GC에 대한 자세한 이야기는 https://d2.naver.com/helloworld/37111 을 참고하시기 바랍니다. 
CMS GC와 G1 GC의 차이

위 그림에서 볼 수 있는 것처럼 CMS GCG1 GC의 가장 큰 차이는 힙 메모리를 나눠서 사용한다는 것입니다. 

CMS GC는 힙 메모리를 하나의 큰 영역으로 보고 전체적으로 쓰고 지우 고를 반복 합니다. 

따라서 한 번에 많은 양의 메모리를 지우게 되는 경우가 생깁니다. 

하지만 G1 GC는 힙 메모리를 N개의 영역으로 나눈 뒤 각각의 영역에 대해 GC를 진행합니다.

그래서 CMS GC에 비해 대규모의 메모리 삭제 작업이 일어날 경우가 줄어듭니다. 즉 큰 영역을 한 번에 썼다 지웠다 할 것이냐, 작은 영역을 여러 번 썼다 지웠다 할 것이냐의 차이가 있게 됩니다. G1 GC에서는 힙 메모리의 전체 용량이 몇이냐에 따라 영역의 크기 및 개수가 결정됩니다. 

자세한 내용은 https://medium.com/naukri-engineering/garbage-collection-in-elasticsearch-and-the-g1gc-16b79a447181 을 참고하시기 바랍니다.

루씬 버그


하지만 아직 G1 GC로 가기 전에 마지막 관문이 남아 있습니다. 바로 루씬 라이브러리와 G1 GC와의 호환성 문제입니다. 공식 문서에서도 루씬 라이브러리와의 문제 때문에 G1 GC 적용은 가급적 하지 않기를 권고합니다. (https://www.elastic.co/guide/en/elasticsearch/guide/current/_don_8217_t_touch_these_settings.html)

그리고 어떤 버그들이 있는지에 대해서는 https://wiki.apache.org/lucene-java/JavaBugs 에서 확인할 수 있습니다. 대부분의 버그들은 수정이 되었지만 아직 확실하게 수정되지 않은 버그가 하나 남아 있습니다. 바로 JDK-8038348 (https://bugs.openjdk.java.net/browse/JDK-8038348) 입니다.

JDK-8038348 버그

8 버전에서는 일부 백 포트 된 , 즉 패치된 버전들이 있지만 지금 사용하고 있는 JVM 버전이 패치가 된 버전인지를 추가로 확인을 해봐야 합니다. 하지만 다행스럽게도 9 버전부터는 확실하게 수정이 된 것으로 보입니다. 그래서 G1 GC로의 변경과 함께 JDK 버전도 상위 버전으로 과감하게 가기로 결정했습니다. 그럼 어떤 JDK 버전을 사용 가능한지 확인해 봐야 할 차례입니다. 

https://www.elastic.co/kr/support/matrix#matrix_jvm JDK 버전 Matrix는 이 페이지에서 확인할 수 있습니다. 이번에 변경하려고 하는 버전은 6.4.2 이기 때문에 Oracle/OpenJDK 10 버전과 호환이 됩니다.

JVM Version Matrix

이제 모든 준비는 끝났습니다. G1 GC + JVM 10 Upgrade로의 여행을 시작해 보겠습니다.


G1 GC 변경


G1 GC로의 GC 방식 변경은 의외로 간단합니다. jvm.options 파일에서 GC 설정 부분을 아래와 같이 수정해 줍니다.

## GC configuration
# CMS GC 설정 부분이며 주석처리합니다.
## START
#-XX:+UseConcMarkSweepGC 
#-XX:CMSInitiatingOccupancyFraction=75
#-XX:+UseCMSInitiatingOccupancyOnly
## END
# G1 GC 설정 부분
## START
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
## END

사실 위 설정은 아주 간단하고 기초적인 설정이며 G1 GC와 관련된 세팅은 몇 가지 설정값이 더 있습니다. 이 설정이 모든 G1 GC에 어울리는 설정은 아닙니다. 이렇게 변경할 수 있다고 참고만 하시면 됩니다.


JVM 10 Upgrade


JVM 업그레이드는 오라클 공식 홈페이지에서 OpenJDK를 다운로드하는 것으로 시작합니다.

JDK 다운로드

https://jdk.java.net/10/

그리고 다운로드한 압축 파일을 원하는 곳에 복사하고 압축을 풀어 줍니다.

[root@elasticsearch java]# tar xvfz ./openjdk-10.0.2_linux-x64_bin.tar.gz
jdk-10.0.2/bin/appletviewer
jdk-10.0.2/bin/idlj
... (중략) ...
jdk-10.0.2/man/man1/xjc.1
jdk-10.0.2/release
[root@elasticsearch java]#

압축을 풀면 jdk-10.0.2 디렉터리가 생성됩니다. jdk-10.0.2/bin 디렉터리에 들어가서 java를 실행시켜서 버전이 잘 나오는지 확인해 보겠습니다.

[root@elasticsearch bin]# ./java -version
openjdk version "10.0.2" 2018-07-17
OpenJDK Runtime Environment 18.3 (build 10.0.2+13)
OpenJDK 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode)

그다음엔 시스템에 미리 설치되어 있던 java 바이너리를 바꿔 줍니다.

[root@elasticsearch bin]# alternatives --install /usr/bin/java java /usr/local/java/bin/java 2
[root@elasticsearch bin]# alternatives --config java
here are 2 programs which provide 'java'.
  Selection    Command
-----------------------------------------------
*+ 1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-1.el7_6.x86_64/jre/bin/java)
   2           /usr/local/java/bin/java
Enter to keep the current selection[+], or type selection number: 2
[root@elasticsearch local]# java -version
openjdk version "10.0.2" 2018-07-17
OpenJDK Runtime Environment 18.3 (build 10.0.2+13)
OpenJDK 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode)

이제 시스템의 기본 java10.0.2로 바뀐 것을 볼 수 있습니다. 이렇게 GC 방식을 변경해 주고 JVM 버전을 높인 후 재시작해 주면 됩니다.


GC 패턴의 변화


위 작업을 해 주고 난 후의 GC 패턴입니다.

GC 패턴과 힙 메모리 사용 패턴의 변화

이틀간의 데이터인데 Old GC는 1회도 발생하지 않았고, 힙 메모리의 사용 패턴이 들쭉날쭉한 패턴으로 바뀐 것을 볼 수 있습니다. 작업 이후에는 GC Pause Time이 발생하지 않았기 때문에 애플리케이션에서 발생하던 간헐적인 초 단위의 타임아웃도 발생하지 않았습니다.


마치며


그럼 G1 GC로의 변경은 항상 좋은 점만 있을까요? 안타깝게도 그렇지 않습니다. 저희도 위와 같이 G1 GC로의 변경 이후 Old GC가 발생하지 않아서 일부 GC Pause Time 문제를 겪고 있는 클러스터들에 일괄 적용했으나 오히려 더 잦은 Pause 현상으로 인해 다시 CMS GC로 복귀를 한 경우도 있습니다. 복귀한 클러스터의 경우 로그 수집 및 분석 용도의 클러스터이고 꽤 잦은 대량의 어그리게이션이 발생하는 클러스터였습니다. 아마도 어그리게이션 시 발생하는 많은 양의 메모리 요청과 G1 GC의 방식이 잘 맞지 않아서 발생하는 것이라고 추측만 할 뿐입니다.

G1 GC는 분명히 CMS GC보다 Old GC에 대한 효율이 좋아서 Old GC로 인한 Stop-The-World 문제를 해결하는데 도움을 주긴 하지만 언제나 더 효율적인 만병통치약은 아닙니다. 반드시 적용 전에 충분한 테스트를 진행하고 하시길 바랍니다. 감사합니다.


참고 자료


https://medium.com/naukri-engineering/garbage-collection-in-elasticsearch-and-the-g1gc-16b79a447181

https://sematext.com/blog/java-9-elasticsearch-benchmark/


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari