Garbage Collection 튜닝
- 등록일
- 2012.04.05
- |
- 코멘트
- 94624
여러 가비지 컬렉션(Garbage Collection, 이하 GC) 알고리즘의 동작 과정을 알아본 "Java Garbage Collection" 글과 JVM이 GC를 수행하는 과정을 확인하는 방법을 알아본 "Garbage Collection 모니터링 방법" 글에 이어 마지막으로 Java의 GC 튜닝에 대해서 살펴보겠습니다. GC 튜닝을 글로 설명하는 데에는 어느 정도 한계가 있지만, 마음 편하게 읽고 "지금까지 GC 튜닝에 대해서 잘못 생각하고 있던 부분이 있었구나"라는 생각을 갖기만 해도 이 글의 목적은 달성한 것입니다.
이 글은 이전의 글에서 다룬 내용을 이해하고 있다고 전제하며 작성했기 때문에 이 글을 더 잘 이해하려면 앞의 두 글을 먼저 읽는 것이 좋습니다.
GC 튜닝을 꼭 해야 할까?
본격적으로 GC 튜닝을 살펴보기 전에 다음 질문에 대해 생각해 보자.
"모든 Java 기반의 서비스에서 GC 튜닝을 해야 할까?"
결론부터 이야기하면 모든 Java 기반의 서비스에서 GC 튜닝을 진행할 필요는 없다.
GC 튜닝이 필요 없다는 이야기는 운영 중인 Java 기반 시스템의 옵션과 동작이 다음과 같다는 의미이다.
- -Xms 옵션과 –Xmx 옵션으로메모리크기를지정했다.
- -server 옵션이포함되어있다.
- 시스템에 Timeout 로그와같은로그가남지않는다.
다시 말하면, 메모리 크기도 지정하지 않고 Timeout 로그가 수도 없이 출력된다면 여러분의 시스템에서 GC 튜닝을 하는 것이 좋다.
그런데 한 가지 꼭 명심해야 하는 점이 있다. GC 튜닝은 가장 마지막에 하는 작업이라는 것이다.
GC 튜닝을 하는 이유가 무엇인지 근본적인 원인을 생각해 보자. Java에서 생성된 객체는 가비지 컬렉터(Garbage Collector)가 처리해서 지운다. 생성된 객체가 많으면 많을수록 가비지 컬렉터가 가 처리해야 하는 대상도 많아지고, GC를 수행하는 횟수도 증가한다. 즉, 여러분이 운영하고 만드는 시스템이 GC를 적게 하도록 하려면 객체 생성을 줄이는 작업을 먼저 해야 한다.
"티끌 모아 태산"이라는 말이 있듯이, String대신 StringBuilder나 StringBuffer를 사용하는 것을 생활화하는 것부터가 시작이라고 보면 된다. 그리고, 로그를 최대한 적게 쌓도록 하는 것이 좋다. 하지만 어쩔 수 없는 현실도 있다. 경험상 XML과 JSON 파싱은 메모리를 가장 많이 사용한다. 아무리 String을 최대한 사용 안 하고 Log 처리를 잘 하더라도, 10~100 MB짜리 XML이나 JSON를 파싱하면 엄청난 임시 메모리를 사용한다. 그렇다고 XML과 JSON을 사용하지 않기는 어렵다. 그냥 현실이 그렇다는 것만 알아주기 바란다.
만약 애플리케이션 메모리 사용도 튜닝을 많이 해서 어느 정도 만족할 만한 상황이 되었다면, 본격적으로 GC 튜닝을 시작하면 된다. 필자는 GC 튜닝의 목적을 두 가지로 나눈다. Old 영역으로 넘어가는 객체의 수를 최소화하는 것과 Full GC의 실행 시간을 줄이는 것이다.
Old 영역으로 넘어가는 객체의 수 최소화하기
JDK 7부터 본격적으로 사용할 수 있는 G1 GC를 제외한, Oracle JVM에서 제공하는 모든 GC는 Generational GC이다. 즉, Eden 영역에서 객체가 처음 만들어지고, Survivor 영역을 오가다가, 끝까지 남아 있는 객체는 Old 영역으로 이동한다. 간혹 Eden 영역에서 만들어지다가 크기가 커져서 Old 영역으로 바로 넘어가는 객체도 있긴 하다. Old 영역의 GC는 New 영역의 GC에 비하여 상대적으로 시간이 오래 소요되기 때문에 Old 영역으로 이동하는 객체의 수를 줄이면 Full GC가 발생하는 빈도를 많이 줄일 수 있다. Old 영역으로 넘어가는 객체의 수를 줄인다는 말을 잘못 이해하면 객체를 마음대로 New 영역에만 남길 수 있다고 생각할 수 있지만, 그렇게는 할 수는 없다. 하지만 New 영역의 크기를 잘 조절함으로써 큰 효과를 볼 수는 있다.
Full GC 시간 줄이기
Full GC의 실행 시간은 상대적으로 Minor GC에 비하여 길다. 그래서 Full GC 실행에 시간이 오래 소요되면(1초 이상) 연계된 여러 부분에서 타임아웃이 발생할 수 있다. 그렇다고 Full GC 실행 시간을 줄이기 위해서 Old 영역의 크기를 줄이면 자칫 OutOfMemoryError가 발생하거나 Full GC 횟수가 늘어난다. 반대로 Old 영역의 크기를 늘리면 Full GC 횟수는 줄어들지만 실행 시간이 늘어난다. Old 영역의 크기를 적절하게 '잘' 설정해야 한다.
GC의 성능을 결정하는 옵션
"Java Garbage Collection" 글을 마치며 언급한 바와 같이, GC 옵션은 "누가 이 옵션을 썼을 때 성능이 잘 나왔대. 우리도 이렇게 적용하자."라고 생각하면 안된다. 왜냐하면, 서비스마다 생성되는 객체의 크기도 다르고 살아있는 기간도 다르기 때문이다.
아주 단순하게 생각해서, A, B, C, D, E라는 조건에서 어떤 작업이 수행되는 것과 A, B라는 조건에서 어떤 작업이 수행되는 것을 비교하면 어떤 조건에서 수행되는 작업이 더 빠를까? 일반적으로 그냥 생각해도 A, B 조건에서 수행되는 작업이 더 빠를 것이다.
Java의 GC 옵션도 마찬가지다. 이런 저런 옵션을 많이 설정한다고 시스템의 GC 수행 속도가 월등히 빨라지진 않는다. 오히려 더 느려질 확률이 높다. 두 대 이상의 서버에 GC 옵션을 다르게 적용해서 비교해 보고, 옵션을 추가한 서버의 성능이나 GC 시간이 개선된 때에만 옵션을 추가하는 것이 GC 튜닝의 기본 원칙다. 절대로 잊지 말자!
다음 표는 성능에 영향을 주는 GC 옵션 중 메모리 크기와 관련된 옵션이다.
표 1 GC 튜닝 시 기본적으로 학인해야 하는 JVM 옵션
구분 | 옵션 | 설명 |
힙(heap) 영역 크기 | -Xms | JVM 시작 시 힙 영역 크기 |
-Xmx | 최대 힙 영역 크기 | |
New 영역의 크기 | -XX:NewRatio | New영역과 Old 영역의 비율 |
-XX:NewSize | New영역의 크기 | |
-XX:SurvivorRatio | Eden 영역과 Survivor 영역의 비율 |
이 중에서 필자가 GC 튜닝을 할 때 자주 사용하는 옵션은 -Xms 옵션, -Xmx 옵션, -XX:NewRatio 옵션이다. 특히 -Xms 옵션과 -Xmx 옵션은 필수로 지정해야 하는 옵션이다. 그리고 NewRatio 옵션을 어떻게 설정하느냐에 따라서 GC 성능에 많은 차이가 발생한다.
간혹 Perm 영역의 크기는 어떻게 설정해야 하는지 문의하는 분들이 있다. Perm 영역의 크기는 OutOfMemoryError가 발생하고, 그 문제의 원인이 Perm 영역의 크기 때문일 때에만 -XX:PermSize 옵션과 -XX:MaxPermSize 옵션으로 지정해도 큰 문제는 없다.
GC의 성능에 많은 영향을 주는 또 다른 옵션은 GC 방식이다. 다음 표는 GC 방식에 따라서 지정할 수 있는 옵션이다(JDK 6.0 기준).
표 2 GC 방식에 따라 지정 가능한 옵션
구분 | 옵션 | 비고 |
Serial GC | -XX:+UseSerialGC | |
Parallel GC | -XX:+UseParallelGC -XX:ParallelGCThreads=value |
|
Parallel Compacting GC | -XX:+UseParallelOldGC | |
CMS GC | -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=value -XX:+UseCMSInitiatingOccupancyOnly |
|
G1 | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC |
JDK 6에서는 두 옵션을 반드시 같이 사용해야 함 |
G1 GC를 제외하고는, 각 GC 방식의 첫 번째 줄에 있는 옵션을 지정하면 GC 방식이 변경된다. GC 방식 중에서 특별히 신경쓸 필요가 없는 방식은 Serial GC다. Serial GC는 클라이언트 장비에 최적화되어 있기 때문이다.
이 외에도 GC의 성능에 영향을 주는 옵션은 많이 있다. 하지만 여기에 명시한 옵션만 제대로 지정하더라도 큰 효과를 볼 수 있다. 옵션이 많다고 GC 수행 시간이 좋아지는 것은 절대 아니다.
GC 튜닝의 절차
GC를 튜닝하는 절차도 대부분의 성능 개선 작업과 크게 다르지 않다. 다음은 필자가 사용하는 GC 튜닝 절차이다.
1. GC 상황 모니터링
GC 상황을 모니터링하며 현재 운영되는 시스템의 GC 상황을 확인해야 한다. "Garbage Collection 모니터링 방법" 글에 다양한 GC 모니터링 방법을 소개했으니 참조하기 바란다.
2. 모니터링 결과 분석 후 GC 튜닝 여부 결정
GC 상황을 확인한 후에는, 결과를 분석하고 GC 튜닝 여부를 결정해야 한다. 분석한 결과를 확인했는데 GC 수행에 소요된 시간이 0.1~0.3초 밖에 안 된다면 굳이 GC 튜닝에 시간을 낭비할 필요는 없다. 하지만 GC 수행 시간이 1~3초, 심지어 10초가 넘는 상황이라면 GC 튜닝을 진행해야 한다.
그런데, 만약 Java의 메모리를 10GB 정도로 할당해서 사용하고 있고 메모리의 크기를 줄일 수 없다면 필자가 GC 튜닝에 대해서 안내해 줄 수 있는 방법이 없다. GC 튜닝 전에 시스템의 메모리를 왜 높게 잡아야 하는지에 생각해 봐야만 한다. 만약 메모리를 1GB나 2GB로 지정했을 때 OutOfMemoryError가 발생한다면, 힙 덤프를 떠서 그 원인을 확인하고, 문제점을 제거해야만 한다.
참고
힙 덤프는 현재 Java 메모리에 어떤 객체와 어떤 데이터가 있는지 확인하기 위한 메모리의 단면 파일이라고 생각하면 된다. 이 파일은 JDK에 포함되어 있는 jmap이라는 명령으로 생성할 수 있으다. 파일을 생성하는 도중에는 Java 프로세스가 멈추기 때문에 시스템을 운영하고 있을 때에는 이 파일을 생성하면 안 된다.
힙 덤프에 대한 자세한 설명은 "자바 개발자와 시스템 운영자를 위한 트러블 슈팅 이야기"(이상민 저, 한빛미디어, 2011)를 참조한다.
3. GC 방식/메모리 크기 지정
GC 튜닝을 진행하기로 결정했다면 GC 방식을 선정하고 메모리의 크기를 지정한다. 이때 서버가 여러 대이면 여러 대의 서버에 GC 옵션을 서로 다르게 지정해서 GC 옵션에 따른 차이를 확인하는 것이 중요하다.
4. 결과 분석
GC 옵션을 지정하고 적어도 24시간 이상 데이터를 수집한 후에 분석을 실시한다. 운이 좋으면 해당 시스템에 가장 적합한 GC 옵션을 찾을 수 있다. 그렇지 않다면 로그를 분석해 메모리가 어떻게 할당되는지 확인해야 한다. 그 다음에 GC 방식/메모리 크기를 변경해 가면서 최적의 옵션을 찾아 나간다.
5. 결과가 만족스러울 경우 전체 서버에 반영 및 종료
GC 튜닝 결과가 만족스러우면 전체 서버의 GC 옵션을 적용하고 마무리 한다.
다음 절부터는 각 단계에 해야 하는 작업을 자세히 살펴볼 것이다.
GC 상황 모니터링 및 결과 분석하기
운영 중인 WAS(Web Application Server)의 GC 상황을 확인하는 가장 좋은 방법은 jstat 명령어를 사용하는 것이다. jstat 명령어는 "Garbage Collection 모니터링 방법" 글에서 설명했으므로 어떤 데이터를 봐야 하는지만 설명하겠다.
다음 예제는 GC 튜닝을 안 한 어떤 JVM의 상황이다(참고로 운영 서버의 상황은 아니다).
$ jstat -gcutil 21719 1s
S0 S1 E O P YGC YGCT FGC FGCT GCT
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
이 중에서 YGC와 YGCT의 값을 확인한다. 두 값을 YGCT/YGC와 같이 나누면 0.050초(50ms)라는 값이 나온다. 즉, Young 영역에서 GC가 수행되는데 평균 50ms가 소요되었다는 말이다. 이 정도면 Young 영역의 GC는 신경쓰지 않아도 된다.
이번에는 FGCT와 FGC의 값을 확인한다. 두 값을 FGCT/FGC와 같이 나누면 19.68초라는 값이 나온다. 평균 19.68초가 소요되었다는 말이다. 세 번의 GC에서 모두 19.68초가 걸렸을 수도 있고, 두 번의 GC는 1초가 소요되고 한 번의 GC는 58초가 소요됐을 수도 있다. 그러나 어떤 경우이던 GC 튜닝이 필요한 경우라고 판단할 수 있다.
이렇게 GC의 상황을 jstat으로 간단하게 확인할 수도 있지만, –verbosegc 옵션으로 로그를 남겨 분석하는 것이 가장 좋다. 로그를 남기는 방법과 분석하는 툴에 대한 내용은 "Garbage Collection 모니터링 방법" 글에서 설명했다. 필자는 -verbosegc 로그를 분석하는 도구 중 HPJMeter를 가장 좋아한다. 사용법도 간단하고 분석하는 방법도 어렵지 않기 때문이다. HPJmeter를 사용하면, GC를 수행한 시간의 분포와 얼마나 자주 GC가 발생하는지를 쉽게 확인할 수 있다.
GC가 수행되는 시간을 확인했을 때 결과가 다음의 조건에 모두 부합한다면 GC 튜닝이 필요 없다.
- Minor GC의처리시간이빠르다(50ms내외).
- Minor GC 주기가빈번하지않다(10초내외).
- Full GC의처리시간이빠르다(보통1초이내).
- Full GC 주기가빈번하지않다(10분에 1회).
위에서 괄호에 있는 값은 절댓값은 아니고 서비스의 상황에 따라 달라질 수 있는 값이다. Full GC 처리 속도가 0.9초가 나와도 만족하는 서비스가 있고, 그렇지 않은 서비스도 있기 때문이다. 따라서, 이와 같은 값을 확인하고 서비스의 특성에 따라 GC 튜닝 작업을 진행할지 결정한다.
한 가지 주의할 점은, GC 상황을 확인 할 때 Minor GC와 Full GC의 시간만 보면 안 된다는 점이다. GC가 수행되는 횟수도 확인해야 한다. 만약 New 영역의 크기가 너무 작게 잡혀 있다면 Minor GC가 발생하는 빈도도 매우 높을 뿐만 아니라(1초에 한번 이상인 경우도 있음), Old 영역으로 넘어가는 객체의 개수도 증가하게 되어 Full GC 횟수도 증가한다. 따라서 jstat명령의 –gccapacity 옵션을 적용하여 각 영역을 얼마나 점유하여 사용하는지도 확인해야 한다.
GC 방식/메모리 크기 지정
GC 방식 지정
GC 방식은 Oracle JVM을 기준으로 총 5가지가 있다. 그러나 JDK 7이 아니라면 Parallel GC, Parallel Compacting GC, CMS GC의 3개 중에 하나를 선택해야 한다. 이 중에서 어떤 방식을 선택해야 한다는 공식이나 원칙은 없다.
그렇다면, 어떻게 정해야 할까? 가장 좋은 방법은 3가지를 다 적용해 보는 것이다. 하지만, 한가지 확실한 것은 CMS GC가 다른 Parallel GC보다 빠르다는 것이다. 그렇다면 그냥 CMS GC만 적용하면 되겠지만, CMS GC가 항상 빠른 것은 아니다. 일반적인 CMS GC의 Full GC는 빠르지만, Concurrent mode failure가 발생하면 다른 Parallel GC보다 느리다.
Concurrent mode failure에 대해서 좀 더 알아 보자.
Parallel GC와 CMS GC의 가장 큰 차이점은 Compaction 작업 여부이다. Compaction 작업은 메모리 할당 공간 사이에 사용하지 않는 빈 공간이 없도록 옮겨서 메모리 단편화를 제거하는 작업이다.
Parallel GC 방식에서는 Full GC가 수행될 때마다 Compaction 작업을 진행하기 때문에 시간이 많이 소요된다. 하지만, Full GC가 수행된 이후에는 메모리를 연속적으로 지정할 수 있어 메모리를 더 빠르게 할당할 수 있다.
반대로 CMS GC는 Compaction 작업을 기본으로 수행하지 않는다. Compaction 작업을 수행하지 않기 때문에 당연히 속도가 빠르다. 하지만, Compaction 작업을 수행하지 않으면 디스크 조각 모음을 실행하기 전의 상태처럼 메모리에 빈 공간이 여기저기 생긴다. 그렇기 때문에 크기가 큰 객체가 들어갈 수 있는 공간이 없을 수 있다. 예를 들어, Old 영역에 남아 있는 크기가 300MB인데도 10MB짜리 객체가 연속적으로 들어갈 공간이 없을 수도 있다는 말이다. 그럴 때 Concurrent mode failure라는 경고가 발생하면서 Compaction 작업을 수행한다. 그런데, CMS GC를 사용할 때에는 Compaction 시간이 다른 Parallel GC보다 더 오래 소요된다. 그래서 오히려 더 문제가 될 수 있다. Concurrent mode failure에 대한 더 자세한 설명은 오라클 엔지니어가 쓴 "Understanding CMS GC Logs " 글을 참조한다.
결론적으로, 운영 중인 시스템에 가장 적합한 GC 방식을 찾아 내야 한다.
결론적으로, 운영 중인 시스템 특성에 따라 적합한 GC 방식이 다르므로 해당 시스템에 가장 적합한 방식을 찾아야 한다. 운영 중인 서버가 6대 정도 있다면, 2대씩 각 옵션을 동일하게 지정하고 -verbosegc 옵션을 추가한 후 결과를 분석하는 방법을 추천한다.
메모리 크기 지정
메모리 크기와 GC 발생 횟수, GC 수행 시간의 관계는 다음과 같다.
- 메모리크기가크면
- GC 발생횟수는줄어든다.
- GC 수행시간은길어진다.
- 메모리크기가작으면
- GC 수행시간은적어진다.
- GC 발생횟수는증가한다.
메모리 크기를 크게 설정할 것인지, 작게 설정할 것인지에 대한 정답 역시 없다. 서버 자원이 좋은 시스템이라 메모리를 10GB로 설정해도 Full GC가 1초 이내에 끝난다면 10 GB로 지정해도 된다. 하지만, 대부분의 서버는 그렇지 못하다. 메모리를 10GB 정도로 설정하면 Full GC 시간이 10~30초 정도 소요된다. 물론 이 시간은 객체의 크기가 어떻게 되어 있느냐에 따라서 달라진다.
그렇다면 메모리 크기를 얼떻게 설정해야 할까? 필자는 보통 500MB로 설정하라고 이야기한다. 그렇다고 WAS의 메모리를 –Xms500m 옵션과 –Xmx500m 옵션으로 지정하라는 이야기는 절대 아니다. GC 튜닝 이전에 현재 상황을 모니터링한 결과를 바탕으로 Full GC가 발생한 이후에 남아 있는 메모리의 크기를 봐야 한다. 만약 Full GC 후에 남아 있는 메모리가 300MB 정도라면 300MB(기본 사용) + 500MB(Old 영역용 최소) + 200 MB(여유 메모리)를 감안하여 1GB 정도로 지정하는 것이 좋다. 즉, Old 영역을 위해서 500MB 이상 여유가 있는 공간을 지정해야 한다는 말이다. 그래서 3대 정도의 운영 서버가 있다면, 서버 한대는 1GB로, 다른 한대는 1.5 GB로, 또 다른 한대는 2GB 정도로 지정한 후 결과를 지켜 본 다음 결정한다.
이렇게 지정하면, 이론적으로 생각 했을 때에는 당연히 1GB > 1.5GB > 2GB 순서로 GC가 빠르고, 결국 1GB일 때 GC가 제일 빠를 것이다. 하지만 그렇다고 1GB일 때 Full GC가 1초 걸리고, 2GB일 때 2초 걸린다고 보장할 수 없다. 서버의 성능에 따라 다르고 객체의 크기에 따라서 시간이 달라지기 때문이다. 그러므로 측정 데이터 셋을 최대한 많이 만들어 모니터링을 통해서 확인하는 것이 가장 좋은 방법이다.
메모리 크기를 지정할 때 지정해야 하는 것이 한 가지 더 있다. 바로 NewRatio다. NewRatio는 New 영역과 Old 영역의 비율이다. –XX:NewRatio=1로 지정하면 New 영역:Old 영역이 1:1이 된다. 만약 1GB라면 New 영역:Old 영역은 500MB:500MB가 된다. NewRatio가 2이면 New 영역:Old 영역이 1:2가 된다. 즉, 값이 커지면 커질수록 Old 영역의 크기가 커지고 New 영역의 크기가 작아진다.
별 것이 아닌 것처럼 생각할 수 있지만, NewRatio 값은 GC의 전반적인 성능에 많은 영향을 준다. New 영역의 크기가 작으면 Old 영역으로 넘어가는 메모리의 양이 많아져서 Full GC도 잦아지고 시간도 오래 걸린다.
단순하게 생각해서 NewRatio 값을 1로 주면 최고의 상황이 될 수 있다고 볼 수도 있지만, 꼭 그렇지만은 않다. 오히려 NewRatio의 값이 2나 3일 때의 전반적인 GC 상황이 좋을 수 있다. 필자의 경험으로도 그렇다.
GC 튜닝을 가장 빨리 진행하는 방법은 무엇일까? 성능 테스트로 결과를 비교하는 것이 가장 빠른 검토 결과를 얻을 수 있는 방법이다. 운영 서버마다 옵션을 다르게 지정하고 상황을 모니터링하려면, 적어도 하루에서 이틀 정도 데이터가 쌓인 후에 보는 것이 바람직하다. 하지만 성능 테스트를 통해서 GC 튜닝을 할 때는 운영 상황과 동일하게 부하를 주도록 준비하는 과정이 필요하다. 그리고 부하를 주는 URL과 같은 요청 비율도 운영과 동일해야 한다. 그러나 이렇게 정확하게 부하를 주는 것은 전문 성능 테스터도 쉽지 않고, 준비하는 데 오히려 더 많은 시간이 소요될 수 있다. 시간이 오래 걸리더라도 운영에 적용하고 기다리는 것이 더 간단하고 편하다.
GC튜닝 결과 분석
GC 옵션을 적용하고, -verbosegc 옵션을 지정한 다음에 tail 명령어로 로그가 제대로 쌓이고 있는지 확인해야 한다. 만약 옵션을 잘못 지정해서 로그가 안 쌓이면, 시간만 허비하기 때문이다. 로그가 잘 쌓이고 있다면, 하루 혹은 이틀 정도의 데이터가 축적된 후 결과를 확인해 보자. 로그를 로컬 PC로 옮긴 다음에 HPJMeter로 분석하는 것이 가장 쉽다.
분석할 때에는 다음의 사항을 중심으로 살펴보는 것이 좋다. 순서는 필자 나름의 기준에 따른 우선 순위다. GC 옵션을 결정하는 데 가장 큰 비중을 차지하는 것은 1번 항목인 Full GC 수행 시간이다.
- Full GC 수행시간
- Minor GC 수행시간
- Full GC 수행간격
- Minor GC 수행간격
- 전체 Full GC 수행시간
- 전체 Minor GC 수행시간
- 전체 GC 수행시간
- Full GC 수행횟수
- Minor GC 수행횟수
운이 좋아서 한 번에 가장 적합한 GC 옵션을 찾으면 좋지만, 그렇지 못한 경우가 대부분이다. 한 번에 끝내려다가 잘못하면 서비스에 OutOfMemoryError가 발생할 수 있으니 조심해서 GC 튜닝을 진행하는 것이 좋다.
튜닝 사례
지금까지 뜬 구름 잡는 이론적인 이야기만 했다면, 이제 실제 튜닝 사례로 보면서 어떻게 GC를 튜닝하는지 알아보자.
튜닝 사례 1
아래 예는 S 서비스의 GC 튜닝 사례이다. 신규로 개발된 S 서비스는 Full GC를 수행하는데 시간이 오래 소요되고 있이다.
먼저 jstat –gcutil의 결과를 보자.
S0 S1 E O P YGC YGCT FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993
왼쪽에 있는 Perm 영역까지의 정보는 처음 GC 튜닝을 할 때에는 중요하지 않다. 오른쪽에 있는 YGC부터의 값이 중요하다.
Minor GC와 Full GC가 한 번 수행될 때 평균 얼마나 소요되었는지 계산하면 다음과 같다.
표 3 S 서비스의 Minor GC와 Full GC 평균 소요 시간
GC 종류 | GC 수행횟수 | GC 수행 시간 | 평균 |
Minor GC | 54 | 2.047 | 37ms |
Full GC | 5 | 6.946 | 1,389ms |
Minor GC를 수행하는데 37ms면 양호한 상황이다. 하지만, Full GC가 평균 1.389초 걸렸다는 것은 DB Timeout을 1초로 한 시스템에서는 GC가 발생할 때 많은 Timeout이 발생할 수 있다는 말이 된다. 이런 상황의 시스템은 GC 튜닝을 해야 한다.
이 상태에서 무작정 GC 튜닝을 시작하면 안 되고 메모리를 어떻게 사용하고 있는지 살펴봐야 한다. 메모리 사용량은 jstat –gccapacity 옵션으로 확인한다. 이 서버에서 확인한 결과는 다음과 같다.
NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5
주요한 값만 살펴 보면 다음과 같다.
- New 영역사용크기: 212,992 KB
- Old 영역사용크기: 1,884,160 KB
즉, 전체 할당된 메모리의 크기는 Perm 영역을 제외하고 2GB이며, New 영역:Old 영역이 1:9이다. jstat보다 더 상세하게 상황을 확인하기 위해서 -verbosegc 로그를 추가하고, 3개의 인스턴스에 다음과 같이 3가지 옵션을 지정했다. 다른 옵션은 별도로 추가하지 않았다.
- NewRatio=2
- NewRatio=3
- NewRatio=4
하루 정도 지난 이후 이 시스템의 GC 로그를 확인했다. 운이 좋게도 이 시스템은 NewRatio를 지정한 이후에 Full GC가 한번도 발생하지 않았다.
왜 그랬을까? 그 이유는 해당 시스템에서 생성되는 객체는 대부분 금방 소멸되는 객체이며, 객체가 생성된 이후에 Old 영역으로 넘어가지 않고, New 영역에서 모두 사라져 버리기 때문이다.
이 상황에서는 다른 옵션은 변경할 필요도 없다. 따라서, NewRatio 값 중에서 가장 좋은 값을 선택하면 된다. 가장 좋은 값은 어떻게 판단할까? 각 NewRatio의 Minor GC 평균 응답 시간을 분석하면 된다.
각 옵션별 평균 응답시간은 다음과 같다.
- NewRatio=2 : 45 ms
- NewRatio=3 : 34 ms
- NewRatio=4 : 30 ms
New 영역의 크기는 가장 작지만 GC시간이 짧은 NewRatio=4이 가장 좋은 옵션이라는 결론을 내렸으며, GC 옵션을 적용한 이후에 이 서버에서는 Full GC가 발생하지 않았다.
참고로 해당 서비스의 JVM이 시작하고 며칠이 지난 후에 수행한 jstat –gcutil 결과는 다음과 같다.
S0 S1 E O P YGC YGCT FGC FGCT GCT
8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219
서버에 요청이 많지 않아서 GC가 자주 발생하지 않았다고 생각할 수도 있다. 하지만, Minor GC가 2,424 번 수행될 동안 Full GC는 한 번도 수행되지 않았다.
튜닝 사례 2
이번 사례는 A 서비스의 사례이다. A 서비스에 GC 튜닝을 진행하게된 이유는 사내에 운영 중인 APM(Application Performance Manager)에서 주기적으로 JVM이 오랫동안(8초 이상) 동작하지 않는다는 것을 발견했기 때문이다. 원인을 찾던 중 Full GC 시간이 오래 소요되는 것을 보고 GC 튜닝을 진행하기로 했다.
튜닝의 첫 단계로 -verbosegc 옵션을 추가했으며, 결과는 다음과 같았다.
그림 1 GC 튜닝 전의 Duration 그래프
위의 그래프는 HPJMeter가 분석 후 자동으로 제공하는 그래프 중 Duration 그래프이다. JVM이 시작되었을 때부터의 시간이 X축이며, 각 GC의 응답 시간이 Y축이다. CMS로 표시된 것은 Full GC이며, Parallel Scavenge로 표시된 것은 Minor GC 결과다.
CMS GC가 제일 빠르다고 했는데, 결과를 보면 15초까지 소요된 것도 있다. 왜 이러한 결과가 나왔을까? 앞에서 CMS가 Compaction 단계를 거치면 오히려 더 느려진다고 설명한 것을 기억한다면 그 이유를 이해할 수 있을 것이다. 게다가 해당 서비스는 메모리를 –Xms1g –Xmx4g로 지정해 놓았고, 4GB까지 메모리를 할당해서 사용하고 있었다.
일단 CMS GC대신 Parallel GC로 변경했다. 메모리 크기는 2GB로 변경하고, NewRatio를 3으로 지정했다. 이렇게 지정하고 몇 시간 후의 jstat –gcutil 결과는 다음과 같다.
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890
4GB일 때 15초에 비하면 Full GC 시간이 회당 3초 정도로 빨라지긴 했지만, 3초도 빠른 것은 아니다. 그래서 다음과 같이 6개의 케이스를 만들어 변경해 보았다.
- Case1 : -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2
- Case2 : -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3
- Case3 : -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3
- Case4 : -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2
- Case5 : -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3
- Case6 : -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3
이 중 어떤 것이 가장 빠르게 나왔을까? 당연히 할당된 메모리의 크기가 작을 수록 유리한 결과가 나왔다. 가장 GC개선율이 높은 Case6의 Duration 그래프는 다음과 같다. 가장 느린 응답 속도가 1.7초 정도이며, 평균 1초 이내의 양호한 상황으로 바뀌었다.
그림 2 Case6를 적용한 후의 Duration 그래프
이 결과를 보고 해당 서비스의 GC 옵션을 모두 Case 6으로 변경했다. 하지만, 이것이 원인이 되어 그날 저녁에 OutOfMemoryError가 발생하는 문제가 있었다. 여기서 그 원인을 자세하게 이야기하기는 어려우나, 대량으로 데이터를 처리하는 배치 작업때문에 JVM의 메모리가 부족해졌다. 관련된 문제를 해결하는 작업은 지금 진행 중이다.
GC 튜닝을 진행할 때에는 단순히 짧은 시간 동안 쌓인 GC 로그만 분석하고 전체 서버에 적용하는 것은 매우 위험하다. 서비스가 어떻게 운영되고 있는지에 대한 분석도 같이 진행되어야 장애 없이 GC 튜닝이 가능하다는 것을 잊지 말기 바란다.
지금까지 간단하게 두 건의 GC 튜닝 사례로 GC튜닝을 어떻게 진행하는지 살펴 보았다. 다시 이야기하지만, 필자가 튜닝 사례에서 지정한 GC 옵션은 동일한 기능을 수행하는 서비스의 동일한 CPU 및 운영체제 버전, 동일한 JDK 버전을 갖는 서버에는 일괄적으로 지정해도 된다. 하지만, 여러분이 지금 운영 중인 서비스에 이 옵션을 그대로 적용해서는 절대 안 된다.
마치며
필자는 힙 덤프를 떠서 메모리를 일일이 분석하면서 GC 튜닝을 진행하지 않고, 경험 위주의 GC 튜닝을 애용한다. 치밀하게 메모리 상태를 분석한다면 더 좋은 GC 튜닝 결과가 나올 수도 있다. 하지만 이러한 분석은 시스템에서 메모리가 사용되는 패턴이 거의 일정하고 동일한 경우에는 도움이 되지만, 서비스의 사용량이 많고 사용자의 패턴이 아주 다양한 경우에는 여러 경험치를 녹여서 튜닝 작업을 하는 것이 더 바람직하다고 본다.
아직 필자가 어떤 운영 서버에도 적용하지 않았지만, 몇몇 서버에 G1 GC 옵션을 지정하여 성능 테스트를 진행한 적이 있다. G1 GC는 그 어떤 GC 방식 보다 빠르지만, 이 옵션을 적용하려면 JDK 7으로 업그레이드해야 하한다. 그리고 안정성도 아직은 보장하기 어렵다. 어떤 치명적인 버그가 있는지 아직 아무도 모르기 때문에 해당 옵션을 적용하라고 말하기에는 아직 시기상조다.
JDK 7이 안정화되고(그렇다고 불안하다는 이야기는 아니다) JDK 7에 최적화된 WAS가 나와서 G1 GC를 안정적으로 적용할 수 있는 날이 온다면, GC 튜닝을 안 해도 되는 날이 올 수도 있지 않을까 생각한다.
GC 튜닝에 대해서 더 많은 사항들을 알고 싶다면, sideshare와 같은 사이트에 공유되는 자료가 많은 도움이 될 것이다. 가장 추천할 만한 자료는 트위터의 엔지니어인 Attila Szegedi가 작성한 "Everything I Ever Learned About JVM Performance Tuning @Twitter" 자료다. 시간이 되면 한번 읽어 보기 바란다.
- 글쓴이
- 이상민NHN 성능엔지니어링랩
- 소개
- 2009년부터 NHN에 근무하고 있다. 주요 업무는 장애 진단 지원, 사내 강의, APM 기술 지원등을 하고 있으며, 홈페이지로는 tuning-java.com 및 GodOfJava.com 이 있다. 집필서로는 전 직장에서 집필한 "자바 성능을 결정짓는 코딩 습관과 튜닝 이야기"와 현재 직장 출퇴근 버스에서 집필한 "자바 개발자도 쉽고 즐겁게 배우는 테스팅 이야기", "자바 개발자와 시스템 운영자를 위한 트러블 슈팅 이야기" 와 현재 퇴고 중인 "자바 기본서"가 있다.
관련글
댓글
6- aeei옵션 열기좋은 글 감사합니다.2019-11-12 11:04답글0공감/비공감공감0비공감0
- Surya Baby옵션 열기Wow this article is very informative…Universal Garbage Collection log analyzer that parses any format of Garbage collection logs and generates WOW graphs & AHA metrics. Inbuilt intelligence has ability to discover any sort of memory problems.Excellence & Simplicity Devops tools for cloud. <a href="http://gceasy.io/">Gceasy</a>2019-02-04 10:56
- Surya Baby옵션 열기Wow this article is very informative…Universal Garbage Collection log analyzer that parses any format of Garbage collection logs and generates WOW graphs & AHA metrics. Inbuilt intelligence has ability to discover any sort of memory problems.Excellence & Simplicity Devops tools for cloud. <a href="http://gceasy.io/">Gceasy</a>2019-02-04 10:56