Memory
Tuning
Test
JVM 메모리 설정 변경을 통한 GC Throughput 성능 테스트
2014 @ S/W Archtecture Team
테스트 개요
자바 웹 어플리케이션에 할당할 수 있는 메모리의 한계가 정
해져 있는 상황에서 전체 자바 메모리의 조절을 통해 얼마만
큼의 성능 향상이 가능한지 알아본다.
동일 부하를 주며 JVM 옵션을 변경하며 결과를 측정하였고,
부하의 기준은 이 웹 어플리케이션의 peak time 이라고 가정
한다.
어플리케이션 설치 환경
• AWS c3.xlarge (4core, 7.5G memory)
• RHEL-6.4_GA-x86_64
• JDK 1.6.0_45
• haksvn-0.3.2
• Subversion, mysql 14.14 Distrib 5.1.73, apache-tomcat-
7.0.47
이 서버는 여러 어플리케이션이 설치되며 어느 정도의 부하를
견뎌야 하므로 비용한계상 4core 를 선택하였으며 , JDK 는 현
재 회사 프로젝트에서 가장 널리 쓰이는 1.6 버전으로 하였다.
haksvn의 내장 h2 DB는 수십 개의 커넥션을 버틸 수 없기에
mysql 을 설치하여 활용하였다.
부하 발생기 설치 환경
• AWS c3.large (2core, 3.75G memory)
• Windows_Server-2012-RTM-English-64Bit
• Apache-jmeter-2.11
100 명의 사용자로 하기로 결정하였기에 싱글 프로세서는 좀
그렇고 최소한의 멀티코어 서버로 선택하였다.
목표 성능
• GC Throughput 95% 이상
general guide line 을 따라 95퍼센트로 목표를 설정한다.
근거 데이터는 없으나 HostSpot JVM 엔지니어 및 IBM 튜닝
가이드 등에서 제시한 기준이다. max gc pause
duration/interval 은 고려하지 않는다.
해당 어플리케이션 허용 메모리
• 512M ~ 2G
가장 많이 쓰는 1~1.5G 기준으로 +/- 버퍼를 두고 내가 정한
기준이다.
부하 내역 (jMeter script 참조)
• Number of Threads: 100
• Ramp-up period: 60 seconds
• Thread Loop Count: 20
초기 1분 동안 해당 스크립트 실행 후 종료한다. WAS 시작 후
처음부터 부하가 들어가니 쓰레드 생성에 많은 CPU 가 소요
되어 정상적인 결과를 얻지 못하였다. 처음에 약한 부하를 주
고 종료 후, CPU 가 안정되는 것을 보고 본 테스트를 시작한
다.
2014 @ S/W Archtecture Team 2
JVM Options
-Xmx256m -Xms256m
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/home/ec2-user/haksvn/logs/gc.log
테스트 결과
GC Throughput 은 원하는 결과에 한 참 미치지 않는다. 10분
정도의 기간에 100여 번의 full gc 가 발생하였다는 것은 절대
적인 메모리 부족일 가능성이 높다.
• Live Data에 비해 Old generation 부족
• Promote Data에 비해 Survivor space 부족
• Perm generation 부족
등의 이유가 있을 것이며 측정한 수치를 통해 확인해본다.
TEST CASE 01
JVM 옵션에 아무것도 손 대지 않은 상황이며, 기본적인 메모리 및 GC 상황을 확인하기 위해 조금 모자란다 싶은 정도로 heap size
를 설정한다. 추가로 GC 관련한 프린트 옵션을 넣는다. Garbage collector 는 디폴트 설정인 parallel gc 가 선택될 것이며 지금 서
버의 프로세서 수인 4개로 parallel 동작하게 된다.
Result
jMeter Throughput 184.3 /sec
Total Time 9m 38s
GC Throughput 79.1%
Number of Full GC 95
Number of GC 1883
Avg Full GC pause 0.376s
Avg GC pause 0.045s
2014 @ S/W Archtecture Team 3
Perm Generation
GC Log를 통해 최대 필요한 Perm generation 용량을 산출할
수 있다. Full gc 이전 용량 중 최대값을 찾으면 67.334M 가 나
온다. 지금 아무런 옵션을 주지 않았으므로 perm 최대치는 기
본 값인 64M에 64bit인 경우 30% 추가가 되므로 83.2M 가 나
온다. permgen 부족은 낮은 gc throughput의 주 원인이 아님
을 알 수 있다.
일반적인 가이드라인에 따르면 permgen 용량은 사용량의 1.2
배~1.5배 범위로 잡아야 한다. permgen 은 변화가 크지 않으
며 지금이 worst case 임의 감안하면 가이드라인 중 최소 기준
으로 잡는다.
최소/최대 사이즈의 명시적 선언을 위해서 다음과 같은 옵션
을 추가한다.
-XX:PermSize=82M -XX:MaxPermSize=82M
Live Data & Old Generation
전체 heap과 oldgen 의 적정 용량을 확인하기 위하여 현재 상
황에서의 live data의 크기를 알아야 한다. “Java Performance”
에서 권장하는 범위는 전체 heap 크기는 live data의 3~4배
이며 oldgen 은 2~3배 이다.
Live data는 full gc 이 후 남은 oldgen 크기로 측정할 수 있는
데 최대수치를 기준으로 한다.
현재 전체 heap 크기인 256M은 권장 범위에 한 참 미치지 않
는다. 지금 oldgen 크기인 170M도 최소 권장보다 적다. 전체
heap 크기를 현재 크기에 2배인 512M으로 잡는다면 권장 범
위 안에 포함되며, default newRatio 는 2 이므로
Heap size * ( New Ratio / New Ratio+1 )
계산하면 341M이 나와 위 범위에 포함된다.
-Xmx512M -Xms512M
TEST CASE 01
[PSYoungGen: 3056K->0K(76160K)]
[PSOldGen: 169371K->122923K(174784K)]
172427K->122923K(250944K)
[PSPermGen: 68886K->68881K(69568K)]
, 0.3871880 secs]
[Times: user=0.36 sys=0.00, real=0.39 secs]
Max x1.2 x1.5
Size 67M 82M 101M
[PSYoungGen: 6496K->0K(79104K)]
[PSOldGen: 170654K->134903K(174784K)]
177150K->134903K(253888K)
[PSPermGen: 68883K->68883K(69504K)]
, 0.3599300 secs]
[Times: user=0.33 sys=0.00, real=0.36 secs]
Max x2 x3 x4
Size 161M 320M 480M 641M
2014 @ S/W Archtecture Team 4
JVM Options
-Xmx512m –Xms512m
-XX:PermSize=82M -XX:MaxPermSize=82M
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/home/ec2-user/haksvn/logs/gc.log
JMap Check
위 옵션 설정 후, JVM이 예상한 메모리를 할당했는지 jmap 을
통하여 보면 정확하게 계산대로 배분하였음을 알 수 있다. 처
음에는 설정대로 되어있지만 AdaptivePolicy에 의하여 어느정
도 시간이 지난 후에는 비율이 변경될 것이다.
테스트 결과
GC Throughput 은 경이적으로 좋아졌다. 기본 가이드 라인에
만 따라도 기본적인 성능은 나온다는 것을 알 수 있다. 하지만
목표치인 95%를 만족시키지는 않는다. 여기서 조금 더 손을
보아야 한다. 먼저 full gc 가 1분 마다 발생하는데 Live Data를
측정해서 적정 oldgen 크기를 잡은 상태이므로 survivor 크기
부족에 의한 overflow promote가 발생하여 oldgen 이 가득
차는 것이 이유일 수 있다. 로그를 보며 분석을 해본다.
TEST CASE 02
TEST CASE 01 결과를 분석하며 계산한 JVM 옵션을 추가하여 테스트를 수행한다. 전체 메모리 용량 및 그에 따른 oldgen 을 늘렸
으며, permgen을 적정 크기로 명시적으로 선언하였다. Garbage Collector 는 기존과 동일한 4개의 parallel gc 이다.
Result
jMeter Throughput 197.0 /sec
Total Time 10m 19s
GC Throughput 93.35%
Number of Full GC 10
Number of GC 827
Avg Full GC pause 0.437s
Avg GC pause 0.045s
MaxHeapSize = 536870912 (512.0MB)
…
PS Old Generation
capacity = 357957632 (341.375MB)
…
PS Perm Generation
capacity = 85983232 (82.0MB)
2014 @ S/W Archtecture Team 5
Survivor Overflow
GC Log에서 adaptiveSizePolicy 로그를 확인해보면 overflow
여부를 알 수 있다. 현재 overflow가 자주 발생하고 있으며 이
것은 불필요한 promote가 발생하여 full gc를 유발 시킬 뿐 아
니라 survivor  oldgen 메모리 복제만으로도 성능저하를 유
발시킨다.
overflow 발생 시의 필요 survivor 크기와 eden 의 크기 최대
치는 아래와 같다.
eden과 survivor 크기는 수치로 줄 수 없고 비율로 주어야 하
므로 다음 식에 맞춰서 적정 크기를 구해야 한다.
Young = eden + survivor*2
survivor = young/survivorRatio
eden = (young/survivorRatio * surviorRatio-2
위 식에 맞추어 계산하기 쉽게 8의 배수로 크기를 조절하면
아래의 값이 나온다.
• Young Generation Size: 320M
• Eden Size: 192M
• Survivor Size: 64M
• Survivor Ratio: 5
위 수치에 맞추어 다음과 같이 옵션을 추가한다.
-XX:NewSize=320m
-XX:MaxNewSize=320m
-XX:SurvivorRatio=5
-XX:InitialSurvivorRatio=5
수치 적용을 위해서는 JVM에서 기본으로 제공하는
adaptiveSizePolicy를 off 시켜야 한다.
-XX:-UseAdaptiveSizePolicy
young 이 늘어난 만큼 전체 heap 크기를 늘려준다. 전체
512M이었을 때 newRatio=2 이므로 young 크기는 150M 정
도였으므로 170M이 더 늘어났다. 전체 heap을 170M씩 더 늘
려준다.
-Xmx682m -Xms682m
TEST CASE 02
602.010: [GCAdaptiveSizePolicy::
compute_survivor_space_size_and_thresh:
survived: 7503904
promoted: 1818880
overflow: true
AdaptiveSizeStart: 602.058
collection: 284
…
Desired survivor size 9043968 bytes, new threshold 1
(max 15)
Survivor Eden
Max Desired Size 57M 182M
2014 @ S/W Archtecture Team 6
JVM Options
-Xmx682m –Xms682m
-XX:NewSize=320m -XX:MaxNewSize=320m
-XX:SurvivorRatio=5 -XX:InitialSurvivorRatio=5
-XX:-UseAdaptiveSizePolicy
-XX:PermSize=82M -XX:MaxPermSize=82M
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/home/ec2-user/haksvn/logs/gc.log
JMap Check
계산한 수치대로 정확하게 반영되었음을 확인할 수 있다.
테스트 결과
GC Throughput은 이전과 큰 차이를 보이지 않는다. gc 횟수
는 줄어들었으나 성능에 차이가 없는 것으로 보아 또 다른 곳
을 손봐야 한다. overflow를 없애서 full gc 횟수를 줄였으며
minor gc 횟수도 줄어들었으나 minor gc의 효율이 좋지 않다.
overflow 로 넘어가던 메모리 복제가 사라지고 from  to
간의 복제가 더 늘어났을 거라고 추정이 가능하다.
TEST CASE 03
TEST CASE 02 결과를 분석하며 계산한 JVM 옵션을 추가하여 테스트를 수행한다. Survivor overflow 에 따른 서바이버 및 그에 따
른 young과 전체 heap 을 증가시켰으며 자동 설정 옵션을 동작하지 않게 하였다.
Result
jMeter Throughput 208.4 /sec
Total Time 8m 41s
GC Throughput 93.11%
Number of Full GC 1
Number of GC 614
Avg Full GC pause 0.415s
Avg GC pause 0.058sMaxHeapSize = 715128832 (682.0MB)
…
Eden Space:
capacity = 201326592 (192.0MB)
…
From Space:
capacity = 67108864 (64.0MB)
2014 @ S/W Archtecture Team 7
Tenuring Threshold
로그에서는 threshold 가 남지 않는다. adaptivePolicy가 적용
되어있던 로그에서는 찾아볼 수 있으나, 해당 옵션을 disabled
시킨 로그에서는 해당 정보가 사라졌다. Parallel GC에서는 동
작하지 않는다는 정보도 있지만, 적용 전에 있었던 것으로 보
아 확신할 수 없다. 현재 기본 값이 initial 은 7이며 max는 15
로 잡혀있으므로 일반적인 가이드라인에 따라 프로세서 수로
맞춰본다.
-XX:InitialTenuringThreshold=4
-XX:MaxTenuringThreshold=4
TEST CASE 03
2014 @ S/W Archtecture Team 8
JVM Options
-Xmx682m –Xms682m
-XX:NewSize=320m -XX:MaxNewSize=320m
-XX:SurvivorRatio=5 -XX:InitialSurvivorRatio=5
-XX:-UseAdaptiveSizePolicy
-XX:InitialTenuringThreshold=4
-XX:MaxTenuringThreshold=4
-XX:PermSize=82M -XX:MaxPermSize=82M
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/home/ec2-user/haksvn/logs/gc.log
테스트 결과
GC Throughput이 조금 좋아진 것을 확인할 수 있다. gc log를
분석해 보면 전체 promote 크기가 늘어났음을 알 수 있는데
이를 통해 tenuringthreshold 설정이 적용되었음을 확인할 수
있다. 불필요한 survivor 복제가 줄어들었으나 너무 이른
promote 발생으로 oldgen 이 이전보다 빨리 차오르나 minor
gc 성능을 보았을 때 괜찮은 trade off 이다. minor gc의 횟수
를 줄인다면 좀 더 좋은 결과를 받을 수 있을 것이다.
TEST CASE 04
TEST CASE 03 에서 설정한 메모리 옵션은 그대로 두고 tenuringthreshold 만 현재 서버의 프로세서에 맞추어 추가하여 테스트를
진행한다.
Result
jMeter Throughput 213.7 /sec
Total Time 8m 26s
GC Throughput 94.32%
Number of Full GC 1
Number of GC 608
Avg Full GC pause 0.363s
Avg GC pause 0.047s
2014 @ S/W Archtecture Team 9
Young Generation
GC Log를 통해 현재 필요한 survivor 크기를 구할 수 있다. 최
대 survived 크기를 조사해 보면 16M을 넘지 않는다. 현재
64M으로 잡혀있는 survivor space를 16M으로 줄인다.
minor gc의 횟수를 줄이기 위해서는 전체 younggen 크기를
늘려야 하며 survivor 크기는 16M으로 정해졌으니 eden의 크
기를 늘린다.
기존 younggen은 320M으로 잡혀있으니 2배 정도 늘려준다
고 생각하며 TEST CASE 002 의 방법을 따라 적정 크기를 산출
해보면 아래와 같이 나온다.
• Young Generation Size: 704M
• Eden Size: 672M
• Survivor Size: 16M
• Survivor Ratio: 44
위 수치에 맞추어 다음과 같이 옵션을 추가한다.
-XX:NewSize=704m
-XX:MaxNewSize=704m
-XX:SurvivorRatio=44
-XX:InitialSurvivorRatio=44
young 이 늘어난 만큼 전체 heap 크기를 늘려준다. 320M에
서 704M으로 늘어났으므로 384M씩 더 늘려준다.
-Xmx1066m –Xms1066m
TEST CASE 04
142.608: [GCAdaptiveSizePolicy::
compute_survivor_space_size_and_thresh:
survived: 2392064 promoted: 3700256 overflow: false
[PSYoungGen: 202208K->2336K(262144K)]
486579K->290321K(632832K), 0.0162330 secs]
2014 @ S/W Archtecture Team 10
JVM Options
-Xmx1066m –Xms1066m
-XX:NewSize=704m -XX:MaxNewSize=704m
-XX:SurvivorRatio=44 -XX:InitialSurvivorRatio=44
-XX:-UseAdaptiveSizePolicy
-XX:InitialTenuringThreshold=4
-XX:MaxTenuringThreshold=4
-XX:PermSize=82M -XX:MaxPermSize=82M
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/home/ec2-user/haksvn/logs/gc.log
JMap Check
계산한 수치대로 정확하게 반영되었음을 확인할 수 있다.
테스트 결과
GC Throughput은 전과 비교해 많이 좋아졌으며 목표치인
95% 을 달성하였다. Full GC 는 한 번도 발생하지 않아서 인
위적으로 툴을 통해 한 번 발생시켰다. 쓸 수 있는 2G 한도 내
에서 절반 정도 사용하며 목표치를 달성하였으니 나쁘지 않은
결과라고 생각한다. 분석 결과를 분석하면 이 어플리케이션은
거의 모든 것이 eden을 늘릴 수록 성능이 향상됨을 알 수 있
다. 대부분의 객체가 짧은 생명 주기를 가지며 굳이 old 로 갈
필요가 없는 것들이었다. 만약 동일 heap 크기를 가지고 다른
튜닝 옵션들을 제외하면 어떤 결과가 나올까.
TEST CASE 05
minor gc 횟수를 줄이기 위해서 young generation 크기를 늘렸고 survivor영역은 최대치 만큼 주고 나머지는 모두 eden 으로 설
정하고 테스트를 진행한다.
Result
jMeter Throughput 218.5 /sec
Total Time 8m 17s
GC Throughput 97.93%
Number of Full GC 1 (강제발생)
Number of GC 173
Avg Full GC pause 0.361s (강제발생)
Avg GC pause 0.057s
MaxHeapSize = 17782016 (1066.0MB)
…
Eden Space:
capacity = 704643072 (672.0MB)
…
From Space:
capacity = 16777216 (16.0MB)
2014 @ S/W Archtecture Team 11
JVM Options
-Xmx1066m –Xms1066m
-XX:PermSize=82M -XX:MaxPermSize=82M
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/home/ec2-user/haksvn/logs/gc.log
테스트 결과
GC Throughput은 목표치인 95%가 나오나 이전보다는 조금
떨어진 것을 확인할 수 있다. full gc도 한 번 발생하였으며 전
체 gc 횟수도 많아졌다. 하지만 겨우 2% 차이이다.
TEST CASE 06
마지막 05번 테스트와 동일한 heap 크기로 설정하고 JVM 자동 설정 옵션을 키고 비교를 해본다. 힘들게 크기를 측정하여 비율을
맞춘 것과 차이가 있는지 테스트 해본다.
Result
jMeter Throughput 214.6 /sec
Total Time 8m 17s
GC Throughput 96.16%
Number of Full GC 1
Number of GC 355
Avg Full GC pause 0.386s
Avg GC pause 0.056s
2014 @ S/W Archtecture Team 12
JVM Options
-Xmx1g –Xms1g
-XX:+UseConcMarkSweepGC
-XX:+CMSIncrementalMode
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintTenuringDistribution
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/home/ec2-user/haksvn/logs/gc.log
JMap Check
parallel gc의 oldgen에 해당하는 영역이 940M으로 잡히며
eden과 survivor 영역이 굉장히 적게 잡혀있음을 확인할 수 있
다.
테스트 결과
GC Throughput은 굉장히 안 좋게 나왔다. 적은 young 영역으
로 인해 많은 gc 가 발생하였음을 알 수 있으며, 어플리케이션
의 특성에 따라 단순히 garbage collector 를 변경한다고 성능
이 좋아지는 것은 아님을 알 수 있다. 이 결과는 아무런 설정
없는 256M의 Parallel GC 보다 안 좋은 결과이다.
TEST CASE 07
여태까지 테스트는 Garbage Collector 를 바꾸지 않고 진행하였다. 만약 효율이 좋다는 CMS 로 설정을 하면 어떤 결과가 나올까.
CMS로 옵션 최적화를 진행하지 않고 기본 세팅으로 테스트를 해본다. 메모리는 이전 테스트의 1066M과 비슷한 1G로 설정을 하
였다.
Result
jMeter Throughput 202.8 /sec
Total Time 8m 42s
GC Throughput 78.52%
Number of Full GC 0
Number of GC 1693
Avg Full GC pause N/A
Avg GC pause 0.066s
MaxHeapSize = 1073741824 (1024.0MB)
…
Eden Space:
capacity = 69795840 (66.5625MB)
…
From Space:
capacity = 8716288 (8.3125MB)
…
concurrent mark-sweep generation:
capacity = 986513408 (940.8125MB)
2014 @ S/W Archtecture Team 13
비교 요약
Case 01 Case 02 Case 03 Case 04 Case 05 Case 06 Case 07
256M 512M
682M
young 320M
682M
young 320M
threshold 4
1066M
young 704M
threshold 4
1066M
1G
CMS
jMeter
Throughput
184.3 /sec 197.0 /sec 208.4 /sec 213.7 /sec 218.5 /sec 214.6 /sec 202.8 /sec
Total Time 9m 38s 10m 19s 8m 41s 8m 26s 8m 17s 8m 17s 8m 42s
GC
Throughput
79.1% 93.35% 93.11% 94.32% 97.93% 96.16% 78.52%
Number of
Full GC
95 10 1 1 1 (강제발생) 1 0
Number of
GC
1883 827 614 608 173 355 1693
Avg Full GC
pause
0.376s 0.437s 0.415s 0.363s
0.361s (강제
발생)
0.386s N/A
Avg GC
pause
0.045s 0.045s 0.058s 0.047s 0.057s 0.056s 0.066s
이 테스트에서 목표치로 잡은 GC Thoughput 기준으로 보았을 경우, Parallel GC 로 적정 young 비율을 조정한 것이 가장 좋은 결
과가 나왔다. 최적화하지 않은 CMS 사용은 오히려 좋지 않은 결과를 보였으며 목표치를 달성한 것보다 더 세심한 조정은 시간과
근성의 부족으로 하지 못하였다.
2014 @ S/W Archtecture Team 14
결론
한정된 자원 안에서 최적화된 어플리케이션이 돌아가고 있다
는 가정 하에 이런 테스트를 진행해 보았다. GC에 관련한 성
능 요구가 없는 프로젝트만 진행해왔기에 많은 것들이 아직
부족하다는 것을 알 수 있었다.
참고 자료들을 찾아보면서 gc throughput, full gc max pause
time, full gc frequency 등이 성능 요구사항에 있다는 것을 알
게 되었고 이런 것들이 요구사항에 없는 것이 참 다행이라고
느낄 수 있었다.
만약 어플리케이션 성능이 기대치에 미치지 않는다면 프로파
일링을 통하여 크리티컬 로직을 튜닝하는 것이 훨씬 효율이
좋을 것이다.
테스트 환경에서 짧은 시간으로 테스트를 진행하였으나, 실환
경이라면 수 일에 걸친 로그를 수집하고 분석하고 적용 후, 또
수 일 동안 수집하여 검증을 해야 할 것이다. 그런 노력과 시
간을 들일 바에는 기본 가이드라인에 따라 전체 heap 크기만
을 수정하는 것이 가장 효율적인 선택일 것이다.
현재 테스트는 어플리케이션 성능향상이 목표가 아닌, 오로지
메모리 설정 변경만을 통한 성능 변화를 알기 위한 것이다. 성
능향상을 위해서라면 WAS 설정, OS 설정 등 모든 것을 다 살
펴봐야 할 것이다.
2014 @ S/W Archtecture Team 15

JVM Memory And GC Tuning Test

  • 1.
    Memory Tuning Test JVM 메모리 설정변경을 통한 GC Throughput 성능 테스트 2014 @ S/W Archtecture Team
  • 2.
    테스트 개요 자바 웹어플리케이션에 할당할 수 있는 메모리의 한계가 정 해져 있는 상황에서 전체 자바 메모리의 조절을 통해 얼마만 큼의 성능 향상이 가능한지 알아본다. 동일 부하를 주며 JVM 옵션을 변경하며 결과를 측정하였고, 부하의 기준은 이 웹 어플리케이션의 peak time 이라고 가정 한다. 어플리케이션 설치 환경 • AWS c3.xlarge (4core, 7.5G memory) • RHEL-6.4_GA-x86_64 • JDK 1.6.0_45 • haksvn-0.3.2 • Subversion, mysql 14.14 Distrib 5.1.73, apache-tomcat- 7.0.47 이 서버는 여러 어플리케이션이 설치되며 어느 정도의 부하를 견뎌야 하므로 비용한계상 4core 를 선택하였으며 , JDK 는 현 재 회사 프로젝트에서 가장 널리 쓰이는 1.6 버전으로 하였다. haksvn의 내장 h2 DB는 수십 개의 커넥션을 버틸 수 없기에 mysql 을 설치하여 활용하였다. 부하 발생기 설치 환경 • AWS c3.large (2core, 3.75G memory) • Windows_Server-2012-RTM-English-64Bit • Apache-jmeter-2.11 100 명의 사용자로 하기로 결정하였기에 싱글 프로세서는 좀 그렇고 최소한의 멀티코어 서버로 선택하였다. 목표 성능 • GC Throughput 95% 이상 general guide line 을 따라 95퍼센트로 목표를 설정한다. 근거 데이터는 없으나 HostSpot JVM 엔지니어 및 IBM 튜닝 가이드 등에서 제시한 기준이다. max gc pause duration/interval 은 고려하지 않는다. 해당 어플리케이션 허용 메모리 • 512M ~ 2G 가장 많이 쓰는 1~1.5G 기준으로 +/- 버퍼를 두고 내가 정한 기준이다. 부하 내역 (jMeter script 참조) • Number of Threads: 100 • Ramp-up period: 60 seconds • Thread Loop Count: 20 초기 1분 동안 해당 스크립트 실행 후 종료한다. WAS 시작 후 처음부터 부하가 들어가니 쓰레드 생성에 많은 CPU 가 소요 되어 정상적인 결과를 얻지 못하였다. 처음에 약한 부하를 주 고 종료 후, CPU 가 안정되는 것을 보고 본 테스트를 시작한 다. 2014 @ S/W Archtecture Team 2
  • 3.
    JVM Options -Xmx256m -Xms256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintAdaptiveSizePolicy -Xloggc:/home/ec2-user/haksvn/logs/gc.log 테스트결과 GC Throughput 은 원하는 결과에 한 참 미치지 않는다. 10분 정도의 기간에 100여 번의 full gc 가 발생하였다는 것은 절대 적인 메모리 부족일 가능성이 높다. • Live Data에 비해 Old generation 부족 • Promote Data에 비해 Survivor space 부족 • Perm generation 부족 등의 이유가 있을 것이며 측정한 수치를 통해 확인해본다. TEST CASE 01 JVM 옵션에 아무것도 손 대지 않은 상황이며, 기본적인 메모리 및 GC 상황을 확인하기 위해 조금 모자란다 싶은 정도로 heap size 를 설정한다. 추가로 GC 관련한 프린트 옵션을 넣는다. Garbage collector 는 디폴트 설정인 parallel gc 가 선택될 것이며 지금 서 버의 프로세서 수인 4개로 parallel 동작하게 된다. Result jMeter Throughput 184.3 /sec Total Time 9m 38s GC Throughput 79.1% Number of Full GC 95 Number of GC 1883 Avg Full GC pause 0.376s Avg GC pause 0.045s 2014 @ S/W Archtecture Team 3
  • 4.
    Perm Generation GC Log를통해 최대 필요한 Perm generation 용량을 산출할 수 있다. Full gc 이전 용량 중 최대값을 찾으면 67.334M 가 나 온다. 지금 아무런 옵션을 주지 않았으므로 perm 최대치는 기 본 값인 64M에 64bit인 경우 30% 추가가 되므로 83.2M 가 나 온다. permgen 부족은 낮은 gc throughput의 주 원인이 아님 을 알 수 있다. 일반적인 가이드라인에 따르면 permgen 용량은 사용량의 1.2 배~1.5배 범위로 잡아야 한다. permgen 은 변화가 크지 않으 며 지금이 worst case 임의 감안하면 가이드라인 중 최소 기준 으로 잡는다. 최소/최대 사이즈의 명시적 선언을 위해서 다음과 같은 옵션 을 추가한다. -XX:PermSize=82M -XX:MaxPermSize=82M Live Data & Old Generation 전체 heap과 oldgen 의 적정 용량을 확인하기 위하여 현재 상 황에서의 live data의 크기를 알아야 한다. “Java Performance” 에서 권장하는 범위는 전체 heap 크기는 live data의 3~4배 이며 oldgen 은 2~3배 이다. Live data는 full gc 이 후 남은 oldgen 크기로 측정할 수 있는 데 최대수치를 기준으로 한다. 현재 전체 heap 크기인 256M은 권장 범위에 한 참 미치지 않 는다. 지금 oldgen 크기인 170M도 최소 권장보다 적다. 전체 heap 크기를 현재 크기에 2배인 512M으로 잡는다면 권장 범 위 안에 포함되며, default newRatio 는 2 이므로 Heap size * ( New Ratio / New Ratio+1 ) 계산하면 341M이 나와 위 범위에 포함된다. -Xmx512M -Xms512M TEST CASE 01 [PSYoungGen: 3056K->0K(76160K)] [PSOldGen: 169371K->122923K(174784K)] 172427K->122923K(250944K) [PSPermGen: 68886K->68881K(69568K)] , 0.3871880 secs] [Times: user=0.36 sys=0.00, real=0.39 secs] Max x1.2 x1.5 Size 67M 82M 101M [PSYoungGen: 6496K->0K(79104K)] [PSOldGen: 170654K->134903K(174784K)] 177150K->134903K(253888K) [PSPermGen: 68883K->68883K(69504K)] , 0.3599300 secs] [Times: user=0.33 sys=0.00, real=0.36 secs] Max x2 x3 x4 Size 161M 320M 480M 641M 2014 @ S/W Archtecture Team 4
  • 5.
    JVM Options -Xmx512m –Xms512m -XX:PermSize=82M-XX:MaxPermSize=82M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintAdaptiveSizePolicy -Xloggc:/home/ec2-user/haksvn/logs/gc.log JMap Check 위 옵션 설정 후, JVM이 예상한 메모리를 할당했는지 jmap 을 통하여 보면 정확하게 계산대로 배분하였음을 알 수 있다. 처 음에는 설정대로 되어있지만 AdaptivePolicy에 의하여 어느정 도 시간이 지난 후에는 비율이 변경될 것이다. 테스트 결과 GC Throughput 은 경이적으로 좋아졌다. 기본 가이드 라인에 만 따라도 기본적인 성능은 나온다는 것을 알 수 있다. 하지만 목표치인 95%를 만족시키지는 않는다. 여기서 조금 더 손을 보아야 한다. 먼저 full gc 가 1분 마다 발생하는데 Live Data를 측정해서 적정 oldgen 크기를 잡은 상태이므로 survivor 크기 부족에 의한 overflow promote가 발생하여 oldgen 이 가득 차는 것이 이유일 수 있다. 로그를 보며 분석을 해본다. TEST CASE 02 TEST CASE 01 결과를 분석하며 계산한 JVM 옵션을 추가하여 테스트를 수행한다. 전체 메모리 용량 및 그에 따른 oldgen 을 늘렸 으며, permgen을 적정 크기로 명시적으로 선언하였다. Garbage Collector 는 기존과 동일한 4개의 parallel gc 이다. Result jMeter Throughput 197.0 /sec Total Time 10m 19s GC Throughput 93.35% Number of Full GC 10 Number of GC 827 Avg Full GC pause 0.437s Avg GC pause 0.045s MaxHeapSize = 536870912 (512.0MB) … PS Old Generation capacity = 357957632 (341.375MB) … PS Perm Generation capacity = 85983232 (82.0MB) 2014 @ S/W Archtecture Team 5
  • 6.
    Survivor Overflow GC Log에서adaptiveSizePolicy 로그를 확인해보면 overflow 여부를 알 수 있다. 현재 overflow가 자주 발생하고 있으며 이 것은 불필요한 promote가 발생하여 full gc를 유발 시킬 뿐 아 니라 survivor  oldgen 메모리 복제만으로도 성능저하를 유 발시킨다. overflow 발생 시의 필요 survivor 크기와 eden 의 크기 최대 치는 아래와 같다. eden과 survivor 크기는 수치로 줄 수 없고 비율로 주어야 하 므로 다음 식에 맞춰서 적정 크기를 구해야 한다. Young = eden + survivor*2 survivor = young/survivorRatio eden = (young/survivorRatio * surviorRatio-2 위 식에 맞추어 계산하기 쉽게 8의 배수로 크기를 조절하면 아래의 값이 나온다. • Young Generation Size: 320M • Eden Size: 192M • Survivor Size: 64M • Survivor Ratio: 5 위 수치에 맞추어 다음과 같이 옵션을 추가한다. -XX:NewSize=320m -XX:MaxNewSize=320m -XX:SurvivorRatio=5 -XX:InitialSurvivorRatio=5 수치 적용을 위해서는 JVM에서 기본으로 제공하는 adaptiveSizePolicy를 off 시켜야 한다. -XX:-UseAdaptiveSizePolicy young 이 늘어난 만큼 전체 heap 크기를 늘려준다. 전체 512M이었을 때 newRatio=2 이므로 young 크기는 150M 정 도였으므로 170M이 더 늘어났다. 전체 heap을 170M씩 더 늘 려준다. -Xmx682m -Xms682m TEST CASE 02 602.010: [GCAdaptiveSizePolicy:: compute_survivor_space_size_and_thresh: survived: 7503904 promoted: 1818880 overflow: true AdaptiveSizeStart: 602.058 collection: 284 … Desired survivor size 9043968 bytes, new threshold 1 (max 15) Survivor Eden Max Desired Size 57M 182M 2014 @ S/W Archtecture Team 6
  • 7.
    JVM Options -Xmx682m –Xms682m -XX:NewSize=320m-XX:MaxNewSize=320m -XX:SurvivorRatio=5 -XX:InitialSurvivorRatio=5 -XX:-UseAdaptiveSizePolicy -XX:PermSize=82M -XX:MaxPermSize=82M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintAdaptiveSizePolicy -Xloggc:/home/ec2-user/haksvn/logs/gc.log JMap Check 계산한 수치대로 정확하게 반영되었음을 확인할 수 있다. 테스트 결과 GC Throughput은 이전과 큰 차이를 보이지 않는다. gc 횟수 는 줄어들었으나 성능에 차이가 없는 것으로 보아 또 다른 곳 을 손봐야 한다. overflow를 없애서 full gc 횟수를 줄였으며 minor gc 횟수도 줄어들었으나 minor gc의 효율이 좋지 않다. overflow 로 넘어가던 메모리 복제가 사라지고 from  to 간의 복제가 더 늘어났을 거라고 추정이 가능하다. TEST CASE 03 TEST CASE 02 결과를 분석하며 계산한 JVM 옵션을 추가하여 테스트를 수행한다. Survivor overflow 에 따른 서바이버 및 그에 따 른 young과 전체 heap 을 증가시켰으며 자동 설정 옵션을 동작하지 않게 하였다. Result jMeter Throughput 208.4 /sec Total Time 8m 41s GC Throughput 93.11% Number of Full GC 1 Number of GC 614 Avg Full GC pause 0.415s Avg GC pause 0.058sMaxHeapSize = 715128832 (682.0MB) … Eden Space: capacity = 201326592 (192.0MB) … From Space: capacity = 67108864 (64.0MB) 2014 @ S/W Archtecture Team 7
  • 8.
    Tenuring Threshold 로그에서는 threshold가 남지 않는다. adaptivePolicy가 적용 되어있던 로그에서는 찾아볼 수 있으나, 해당 옵션을 disabled 시킨 로그에서는 해당 정보가 사라졌다. Parallel GC에서는 동 작하지 않는다는 정보도 있지만, 적용 전에 있었던 것으로 보 아 확신할 수 없다. 현재 기본 값이 initial 은 7이며 max는 15 로 잡혀있으므로 일반적인 가이드라인에 따라 프로세서 수로 맞춰본다. -XX:InitialTenuringThreshold=4 -XX:MaxTenuringThreshold=4 TEST CASE 03 2014 @ S/W Archtecture Team 8
  • 9.
    JVM Options -Xmx682m –Xms682m -XX:NewSize=320m-XX:MaxNewSize=320m -XX:SurvivorRatio=5 -XX:InitialSurvivorRatio=5 -XX:-UseAdaptiveSizePolicy -XX:InitialTenuringThreshold=4 -XX:MaxTenuringThreshold=4 -XX:PermSize=82M -XX:MaxPermSize=82M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintAdaptiveSizePolicy -Xloggc:/home/ec2-user/haksvn/logs/gc.log 테스트 결과 GC Throughput이 조금 좋아진 것을 확인할 수 있다. gc log를 분석해 보면 전체 promote 크기가 늘어났음을 알 수 있는데 이를 통해 tenuringthreshold 설정이 적용되었음을 확인할 수 있다. 불필요한 survivor 복제가 줄어들었으나 너무 이른 promote 발생으로 oldgen 이 이전보다 빨리 차오르나 minor gc 성능을 보았을 때 괜찮은 trade off 이다. minor gc의 횟수 를 줄인다면 좀 더 좋은 결과를 받을 수 있을 것이다. TEST CASE 04 TEST CASE 03 에서 설정한 메모리 옵션은 그대로 두고 tenuringthreshold 만 현재 서버의 프로세서에 맞추어 추가하여 테스트를 진행한다. Result jMeter Throughput 213.7 /sec Total Time 8m 26s GC Throughput 94.32% Number of Full GC 1 Number of GC 608 Avg Full GC pause 0.363s Avg GC pause 0.047s 2014 @ S/W Archtecture Team 9
  • 10.
    Young Generation GC Log를통해 현재 필요한 survivor 크기를 구할 수 있다. 최 대 survived 크기를 조사해 보면 16M을 넘지 않는다. 현재 64M으로 잡혀있는 survivor space를 16M으로 줄인다. minor gc의 횟수를 줄이기 위해서는 전체 younggen 크기를 늘려야 하며 survivor 크기는 16M으로 정해졌으니 eden의 크 기를 늘린다. 기존 younggen은 320M으로 잡혀있으니 2배 정도 늘려준다 고 생각하며 TEST CASE 002 의 방법을 따라 적정 크기를 산출 해보면 아래와 같이 나온다. • Young Generation Size: 704M • Eden Size: 672M • Survivor Size: 16M • Survivor Ratio: 44 위 수치에 맞추어 다음과 같이 옵션을 추가한다. -XX:NewSize=704m -XX:MaxNewSize=704m -XX:SurvivorRatio=44 -XX:InitialSurvivorRatio=44 young 이 늘어난 만큼 전체 heap 크기를 늘려준다. 320M에 서 704M으로 늘어났으므로 384M씩 더 늘려준다. -Xmx1066m –Xms1066m TEST CASE 04 142.608: [GCAdaptiveSizePolicy:: compute_survivor_space_size_and_thresh: survived: 2392064 promoted: 3700256 overflow: false [PSYoungGen: 202208K->2336K(262144K)] 486579K->290321K(632832K), 0.0162330 secs] 2014 @ S/W Archtecture Team 10
  • 11.
    JVM Options -Xmx1066m –Xms1066m -XX:NewSize=704m-XX:MaxNewSize=704m -XX:SurvivorRatio=44 -XX:InitialSurvivorRatio=44 -XX:-UseAdaptiveSizePolicy -XX:InitialTenuringThreshold=4 -XX:MaxTenuringThreshold=4 -XX:PermSize=82M -XX:MaxPermSize=82M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintAdaptiveSizePolicy -Xloggc:/home/ec2-user/haksvn/logs/gc.log JMap Check 계산한 수치대로 정확하게 반영되었음을 확인할 수 있다. 테스트 결과 GC Throughput은 전과 비교해 많이 좋아졌으며 목표치인 95% 을 달성하였다. Full GC 는 한 번도 발생하지 않아서 인 위적으로 툴을 통해 한 번 발생시켰다. 쓸 수 있는 2G 한도 내 에서 절반 정도 사용하며 목표치를 달성하였으니 나쁘지 않은 결과라고 생각한다. 분석 결과를 분석하면 이 어플리케이션은 거의 모든 것이 eden을 늘릴 수록 성능이 향상됨을 알 수 있 다. 대부분의 객체가 짧은 생명 주기를 가지며 굳이 old 로 갈 필요가 없는 것들이었다. 만약 동일 heap 크기를 가지고 다른 튜닝 옵션들을 제외하면 어떤 결과가 나올까. TEST CASE 05 minor gc 횟수를 줄이기 위해서 young generation 크기를 늘렸고 survivor영역은 최대치 만큼 주고 나머지는 모두 eden 으로 설 정하고 테스트를 진행한다. Result jMeter Throughput 218.5 /sec Total Time 8m 17s GC Throughput 97.93% Number of Full GC 1 (강제발생) Number of GC 173 Avg Full GC pause 0.361s (강제발생) Avg GC pause 0.057s MaxHeapSize = 17782016 (1066.0MB) … Eden Space: capacity = 704643072 (672.0MB) … From Space: capacity = 16777216 (16.0MB) 2014 @ S/W Archtecture Team 11
  • 12.
    JVM Options -Xmx1066m –Xms1066m -XX:PermSize=82M-XX:MaxPermSize=82M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintAdaptiveSizePolicy -Xloggc:/home/ec2-user/haksvn/logs/gc.log 테스트 결과 GC Throughput은 목표치인 95%가 나오나 이전보다는 조금 떨어진 것을 확인할 수 있다. full gc도 한 번 발생하였으며 전 체 gc 횟수도 많아졌다. 하지만 겨우 2% 차이이다. TEST CASE 06 마지막 05번 테스트와 동일한 heap 크기로 설정하고 JVM 자동 설정 옵션을 키고 비교를 해본다. 힘들게 크기를 측정하여 비율을 맞춘 것과 차이가 있는지 테스트 해본다. Result jMeter Throughput 214.6 /sec Total Time 8m 17s GC Throughput 96.16% Number of Full GC 1 Number of GC 355 Avg Full GC pause 0.386s Avg GC pause 0.056s 2014 @ S/W Archtecture Team 12
  • 13.
    JVM Options -Xmx1g –Xms1g -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintAdaptiveSizePolicy -Xloggc:/home/ec2-user/haksvn/logs/gc.log JMapCheck parallel gc의 oldgen에 해당하는 영역이 940M으로 잡히며 eden과 survivor 영역이 굉장히 적게 잡혀있음을 확인할 수 있 다. 테스트 결과 GC Throughput은 굉장히 안 좋게 나왔다. 적은 young 영역으 로 인해 많은 gc 가 발생하였음을 알 수 있으며, 어플리케이션 의 특성에 따라 단순히 garbage collector 를 변경한다고 성능 이 좋아지는 것은 아님을 알 수 있다. 이 결과는 아무런 설정 없는 256M의 Parallel GC 보다 안 좋은 결과이다. TEST CASE 07 여태까지 테스트는 Garbage Collector 를 바꾸지 않고 진행하였다. 만약 효율이 좋다는 CMS 로 설정을 하면 어떤 결과가 나올까. CMS로 옵션 최적화를 진행하지 않고 기본 세팅으로 테스트를 해본다. 메모리는 이전 테스트의 1066M과 비슷한 1G로 설정을 하 였다. Result jMeter Throughput 202.8 /sec Total Time 8m 42s GC Throughput 78.52% Number of Full GC 0 Number of GC 1693 Avg Full GC pause N/A Avg GC pause 0.066s MaxHeapSize = 1073741824 (1024.0MB) … Eden Space: capacity = 69795840 (66.5625MB) … From Space: capacity = 8716288 (8.3125MB) … concurrent mark-sweep generation: capacity = 986513408 (940.8125MB) 2014 @ S/W Archtecture Team 13
  • 14.
    비교 요약 Case 01Case 02 Case 03 Case 04 Case 05 Case 06 Case 07 256M 512M 682M young 320M 682M young 320M threshold 4 1066M young 704M threshold 4 1066M 1G CMS jMeter Throughput 184.3 /sec 197.0 /sec 208.4 /sec 213.7 /sec 218.5 /sec 214.6 /sec 202.8 /sec Total Time 9m 38s 10m 19s 8m 41s 8m 26s 8m 17s 8m 17s 8m 42s GC Throughput 79.1% 93.35% 93.11% 94.32% 97.93% 96.16% 78.52% Number of Full GC 95 10 1 1 1 (강제발생) 1 0 Number of GC 1883 827 614 608 173 355 1693 Avg Full GC pause 0.376s 0.437s 0.415s 0.363s 0.361s (강제 발생) 0.386s N/A Avg GC pause 0.045s 0.045s 0.058s 0.047s 0.057s 0.056s 0.066s 이 테스트에서 목표치로 잡은 GC Thoughput 기준으로 보았을 경우, Parallel GC 로 적정 young 비율을 조정한 것이 가장 좋은 결 과가 나왔다. 최적화하지 않은 CMS 사용은 오히려 좋지 않은 결과를 보였으며 목표치를 달성한 것보다 더 세심한 조정은 시간과 근성의 부족으로 하지 못하였다. 2014 @ S/W Archtecture Team 14
  • 15.
    결론 한정된 자원 안에서최적화된 어플리케이션이 돌아가고 있다 는 가정 하에 이런 테스트를 진행해 보았다. GC에 관련한 성 능 요구가 없는 프로젝트만 진행해왔기에 많은 것들이 아직 부족하다는 것을 알 수 있었다. 참고 자료들을 찾아보면서 gc throughput, full gc max pause time, full gc frequency 등이 성능 요구사항에 있다는 것을 알 게 되었고 이런 것들이 요구사항에 없는 것이 참 다행이라고 느낄 수 있었다. 만약 어플리케이션 성능이 기대치에 미치지 않는다면 프로파 일링을 통하여 크리티컬 로직을 튜닝하는 것이 훨씬 효율이 좋을 것이다. 테스트 환경에서 짧은 시간으로 테스트를 진행하였으나, 실환 경이라면 수 일에 걸친 로그를 수집하고 분석하고 적용 후, 또 수 일 동안 수집하여 검증을 해야 할 것이다. 그런 노력과 시 간을 들일 바에는 기본 가이드라인에 따라 전체 heap 크기만 을 수정하는 것이 가장 효율적인 선택일 것이다. 현재 테스트는 어플리케이션 성능향상이 목표가 아닌, 오로지 메모리 설정 변경만을 통한 성능 변화를 알기 위한 것이다. 성 능향상을 위해서라면 WAS 설정, OS 설정 등 모든 것을 다 살 펴봐야 할 것이다. 2014 @ S/W Archtecture Team 15