달력

32024  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

애플리케이션 서버의 성능과 확장성을 개선하기 위한 Java HotSpot VM vl.4.x Turbo-Charging (2)

 

 
 



J2SE 1.3의 Sun HotSpot JVM에 있는 heap 크기와 GC 튜닝에 잘 알려진 스위치들은 다음과 같다. 스위치에 대한 더 자세한 것은 JVM HotSpot VM options을 참조한다.

일반 스위치들
-server
-Xmx, -Xms
-XX:NewSize=, -XX:MaxNewSize=
-XX:SurvivorRatio=

J2SE 1.4.1에서 2개의 새로운 가비지 컬렉터를 활성화시키기 위한 스위치는 다음과 같다.


Parallel Collector
-XX:+UseParNewGC : 이 플래그는 신세대에 병렬 가비지 컬렉션을 실행시키게 한다. 구세대에 CMS collector와 함께 활성화할 수 있다.

 

-XX:ParallelGCThreads=n : 이 스위치는 JVM이 신세대 GC를 실행할 때 몇개의 병렬 GC 쓰레드를 사용할 것인지 지정해준다. 기본값은 시스템의 CPU 개수와 같다. 그렇지만 어떤 경우에는 이 수를 변경해 성능을 개선할 수 있다는 것을 관찰할 수 있다. 예를 들면 다중 CPU 컴퓨터에서 여러 JVM 인스턴트를 실행할 때이다. 이런 경우, 각 JVM이 사용하는 병렬 GC 쓰레드수를 이 스위치를 사용해 전체 CPU수보다 적게 지정해줘야 할 것이다.

-XX:+UseParallelGC : 이 플래그도 신세대에 병렬 가비지 컬렉션 정책을 사용하게 하지만, 이것은 구세대에서 CMS collector를 같이 사용하지 못한다. 이는 매우 큰 신세대 heap을 사용하는 기업환경용 애플리케이션에 적합하다.

Concurrent Collector
-XX:+UseConcMarkSweepGC : 이 플래그는 구세대에 동시 수행 가비지 컬렉션을 사용하게 한다.

-XX:CMSInitiatingOccupancyFraction=x : CMS collection을 동작시키는 구세대 heap의 threshold 비율을 지정한다. 예를 들어 60으로 지정하면, CMS collector는 구세대의 60%가 사용될 때마다 실행될 것이다. 이 threshold는 실행시 기본값으로 계산되며, 대개의 경우 구세대의 heap이 80~90%가 사용될 때만 CMS collector가 실행된다. 이 값은 튜닝을 통해 대부분의 경우 성능을 개선할 수 있다. CMS collector는 스위핑하고 메모리를 반환할 때 애플리케이션 쓰레드를 중지시키지 않기 때문에, 많은 메모리를 요구하는 애플리케이션의 경우 신세대로부터 넘어오는 객체를 위한 여유 메모리 공간을 최대한 확보할 수 있도록 해준다. 이 스위치가 최적화되지 않았을 때, CMS collection은 수행에 실패하는 경우가 생길 수도 있으며, 이 경우 기본인 stop-the-world mark-compact collector를 실행하게 된다.
다음과 같이 다른 스위치를 사용해 성능을 튜닝할 수도 있다.

-XX:MaxTenuringThreshold=y : 이 스위치는 신세대의 객체가 얼마만큼의 시간이 지나야 구세대로 승진되는지 지정할 수 있다. 기본값은 31이다. 꽤 큰 신세대와 ‘생존 공간’은 오래된 객체는 구세대로 승진되기까지 생존 공간 사이에 31번 복사가 이뤄진다. 대부분의 통신 애플리케이션에서는 호출 또는 세션마다 만들어진 80~90%의 객체는 생성 즉시 죽고, 10~20%정도가 호출이 끝날 때까지 생존한다. XX:MaxTenuringThreshold=0는 애플리케이션의 신세대에 배정한 모든 객체가 한번의 GC 주기를 생존하면 신세대의 생존 공간 사이에 복사되지 않고 곧바로 구세대로 옮겨진다. 구세대에서 CMS collector를 사용할 때, 이 스위치는 두 가지로 도움이 된다.

- 신세대 GC는 10~20%의 장기 객체를 생존 공간 사이에 여러 번 복사할 필요가 없고 곧바로 구세대로 승진시킨다.
- 추가적으로 이 객체는 구세대에서 대부분의 동시발생적인 컬렉션을 행할 수 있다. 이것은 GC 순차 비용을 추가적으로 감소시킨다.

이 스위치가 사용되면 -XX:SurvivorRatio를 128 정도의 매우 큰 값으로 정하길 권한다. 이는 생존 공간이 사용되지 않고, 매 GC 주기마다 신생에서 곧바로 구세대로 승진되기 때문이다. 생존 비율을 높임으로써 신세대의 heap은 대부분 신생에 배정된다.

-XX:TargetSurvivorRatio=z : 이 플래그는 객체가 구세대로 승진되기 전에 사용해야 하는 생존 공간 heap의 희망 비율이다. 예를 들어 z를 90으로 지정하면 생존 공간의 90%를 사용해야 신세대가 꽉 찼다고 생각해 객체가 구세대로 승진된다. 이것은 객체가 승진되기까지 신세대에서 더 오래 머무를 수 있게 한다. 기본값은 50이다.

 

JVM은 GC에 관련된 많은 유용한 정보를 파일에 로그할 수 있다. 이 정보는 애플리케이션과 JVM을 GC 시각에서 튜닝하고 측정하는 데 사용할 수 있다. 이 정보를 로그하기 위한 스위치는 다음과 같다.

verbose:gc : 이 플래그는 GC 정보를 기록하게 한다.
-Xloggc=filename : 이 스위치는 ‘verbose:gc’로 인한 정보를 표준 출력 대신 기록할 로그 파일명을 지정한다.
-XX:+PrintGCTimeStamps : 애플리케이션의 시작을 시점으로 GC가 언제 실행되었는지 출력한다.
-XX:+PrintGCDetails : GC에 대해 GC하기 전과 후의 신세대와 구세대의 크기, 총 heap 크기, 신세대와 구세대에서 GC가 실행되기까지의 시간, GC 주기마다 승진된 객체 크기와 같은 자세한 정보를 준다.
-XX:+PrintTenuringDistribution : 신세대에서 배정된 객체의 나이 분포를 알려준다. 앞서 설명한 -XX:NewSize, -XX:MaxNewSize, -XX:SurvivorRatio, -XX:MaxTenuringThreshold=0의 튜닝은 바로 이 스위치에서 나오는 데이터를 사용해서 객체가 구세대로 승진할 정도로 시간이 지났는지 판단하게 된다.

 

다음의 커맨드라인 스위치를 사용해 나타나는 GC 관련 출력을 살펴보자,

java -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:+UseParNewGC -XX:+UseConc MarkSweepGC -Xmx512m -Xms512m -XX:NewSize=24m -XX:MaxNew Size=24m -XX:SurvivorRatio=2 <app-name>
 
신세대 GC
311.649: [GC 311.65: [ParNew Desired survivor size 4194304 bytes, new threshold 3 (max 31)
- age 1: 1848472 bytes, 1848472 total
- age 2: 1796200 bytes, 3644672 total
- age 3: 1795664 bytes, 5440336 total
: 21647K->5312K(24576K), 0.1333032 secs] 377334K->362736K(516096K), 0.1334940 secs]
 
앞서의 GC 스냅샷은 신세대의 GC에 대한 설명이며, 이는 다음 정보를 제공한다.

- 이 GC를 실행할 때, 총 JVM heap은 516096K이며 신세대의 heap 크기는 24576K이다.
- 이 GC가 실행될 때, 구세대의 heap 크기는 516096K-24576K=491520K이다.
- 이 GC 주기의 마지막에 애플리케이션의 신세대에 배정받은 객체의 ‘나이 분포’를 설명한다. 이 GC 주기는 new threshold=3으로 설정돼 있으며, 이는 다음 GC 주기가 되기 전에 최대 3번 시간이 지나야 한다는 뜻이다.

- Desired survivor size가 4194304바이트라는 것은 SurvivorRatio=2를 통해 결정되었다. 이는 Eden/Survivor space=2로 지정한 것이다. 신세대의 heap 크기가 24MB이면, 24MB=2*Survivor space+Survivor space+Survivor space 또는 Survivor space=8MB를 뜻한다. 기본값인 TargetSurvivorRatio=50의 희망 생존 크기는 8*.5=4MB이다.
- 애플리케이션 시작을 시점으로 이 GC가 실행된 시간은 311.649초이다.
- 이 GC가 신세대에서 만든 멈춤 시간은 0.1334940초이다.
- GC를 하기 전에 사용중이던 신세대의 크기는 21647K이다.
- GC를 한 후 사용되는 신세대의 크기는 5312K이다.
- 신세대에서 377334K-362736K=14598K의 데이터가 GC되었다.
- 21647K-5312K-14598K=1737K가 구세대로 승진되었다.
- GC를 하기 전에 사용되고 있던 총 heap 크기는 377334K이었다.
- GC를 한 후에 사용되고 있는 총 heap 크기는 362736K이다.
- GC를 하기 전에 구세대에 사용중인 heap 크기는 377334K-21647K= 355687K이었다.
- GC를 한 후 구세대의 사용하는 heap 크기는 362736K-5312K= 357424K이다.
- 이 GC에 구세대로 승진한 객체의 총 크기는 357424K-355687K=1737K이다.
- ParNew 표기는 신세대에 Parallel collector를 사용했다는 것을 나타낸다. 기본인 Copying collector를 사용하면 ParNew 대신 DefNew로 표기한다.

verbose:gc 로그 형식에서 사용된 관례는 특정 세대에 대한 보고가 있다면 그 세대의 이름이 우선시되어 보고될 것이다.

[GC [gen1 info1] [gen2 info2] info]

info는 GC의 일반적인 것이고, 신세대와 구세대를 합한 것이다. info<n>는 gen<n>에 한정돼 있다. 그러므로 각 괄호의 배치를 잘 봐야 할 것이다.
 

구세대 GC

CMS Collector
구세대에서 concurrent mark-sweep collector를 사용하는 것을 스냅샷한 것이다. 앞서 언급했듯이 이 부분은 4단계로 나눠진다.

513.474: [GC [1 CMS-initial-mark: 335432K(491520K)] 340897K (516096K), 0.0482491secs]

- stop-the-world인 initial mark 단계는 0.048초 걸렸다.
- 애플리케이션 시작 시점에서 513.474초에 시작되었다.
- 사용중이었던 구세대의 heap 크기는 335432K이다.
- 신세대에서 사용하는 heap을 포함한 총 사용중인 heap 크기는 491520K이다.
- 구세대의 heap 총 크기는 340897K이다.
- 신세대 heap을 포함한 총 heap 크기는 516096K이다.
- 이 단계는 메모리를 재순환하지 않는다.
- ‘[GC’ 접두사는 stop-the-world 단계를 뜻한다.

513.523: [CMS-concurrent-mark-start]
514.337: [CMS-concurrent-mark: 0.814/0.814 secs]
514.337: [CMS-concurrent-preclean-start]
514.36: [CMS-concurrent-preclean: 0.023/0.023 secs]

이 concurrent mark 단계는 일초(0.814 + 0.023초)도 걸리지 않았다. 그러나 GC와 함께 애플리케이션도 동시에 실행되었다. 이 단계 또한 어떠한 가비지도 컬렉션되지 않았다.

514.361: [GC 514.361: [dirty card accumulation, 0.0072366 secs]
514.368: [dirty card rescan, 0.0037990 secs]
514.372: [remark from roots, 0.1471209 secs]
514.519: [weak refs processing, 0.0043200 secs] [1 CMS-remark: 335432K(491520K)] 352841K(516096K), 0.1629795 secs]

- stop-the-world인 remark 단계는 0.162초 걸렸다.
- 구세대에 사용중이던 heap 크기는 335432K이다.
- 구세대의 총 heap 크기는 491520K이다.
- 신세대를 포함한 총 사용중이던 heap 크기는 352841K이다.
- 신세대를 포함한 총 heap 크기는 516096K이다.
- 이 단계에서 어떠한 메모리도 재순환되지 않는다.
- 이미 말했듯이 ‘[GC’ 표기는 stop-the-world 단계라는 것을 알려준다.

514.525: [CMS-concurrent-sweep-start]
517.692: [CMS-concurrent-sweep: 2.905/3.167 secs]
517.693: [CMS-concurrent-reset-start]
517.766: [CMS-concurrent-reset: 0.073/0.073 secs]

이 concurrent sweep은 3초 걸렸다. 그러나 여러 프로세스가 있는 시스템에서 이 단계는 GC와 함께 애플리케이션 쓰레드가 실행될 수 있다. 4개의 CMS 단계에서 이 단계만이 heap을 스위핑하고 컬렉션한다.

Default Mark-Compact Collector
만약 CMS collector 대신에 구세대에서 기본인 mark-compact collector가 사용되었다면 GC 스냅샷은 다음과 같을 것이다.

719.2: [GC 719.2: [DefNew: 20607K->20607K(24576K), 0.0000341 secs]719.2: [Tenured: 471847K->92010K(491520K), 2.6654172 secs] 492454K->92010K(516096K), 2.6658030 secs]

- GC가 시작한 시작은 애플리케이션 시작 시점으로 719.2초였다.
- DefNew 표기는 신세대에서 기본인 copying collector가 사용되었음을 알려준다.
- ‘[GC’ 표기는 JVM이 stop-the-world GC를 사용했다는 것을 나타낸다. 구세대의 GC에서, 애플리케이션에서 System.gc()를 통해 시스템에 GC를 요청할 수 있는데, 이것은 ‘Full GC’로 표기된다.
- 신세대의 총 heap 크기는 24576K이다.
- 신세대에서 컬렉션은 단지 시도만 이뤄졌다. 구세대에서 잠재적인 데이터를 모두 흡수할 수 있다는 보장이 없어 컬렉션할 수 없다는 것을 알았기 때문이다. 그 결과 신세대의 컬렉션은 일어나지 않았고, 신세대에서는 어떠한 메모리 재순환도 이뤄지지 않았다고 보고했다(순간의 ms 내에 종료했다는 것을 주시한다). 이는 신세대에서 컬렉션이 이뤄지지 못한다는 것을 결정하는 것 외에는 별달리 한 것이 없기 때문이다.

[DefNew: 20607K->20607K(24576K), 0.0000341 secs]

신세대에서 승진되는 것을 흡수하지 못해 구세대가 꽉 찼기 때문에 GC가 필요하다는 알게 되었다. 그래서 full mark-compact collection이 실행되었다.

- 보유하고 있는 표기는 구세대에 full mark-compact GC가 실행되었다는 것을 나타낸다. ‘구세대’는 ‘Tenured generation’이라고도 한다.
- GC를 하기 전에 구세대의 사용중인 heap 크기는 471847K이었다.
- GC 후의 구세대의 사용 heap 크기는 92010K이다.
- GC가 실행될 때의 구세대의 총 heap 크기는 491520K이었다.
- GC 전에 신세대와 구세대의 합인 총 사용중인 heap 크기는 492454K이었다.
- GC 후에 신세대와 구세대의 합인 총 사용 heap 크기는 92010K이다.
- GC를 통해 컬렉션된 총 크기는 492454K-92010K=399837K이다.
- JVM의 총 heap 크기는 516096K이다.
- 이 GC로 총 2.6658030초동안 애플리케이션이 중지되었다.

 

verbose:gc 로그로부터 데이터 마이닝을 통해 애플리케이션
모델링하기


이 로그를 통해 가비지 컬렉션 시각에서 애플리케이션과 JVM 행동에 대한 다양한 정보가 파생될 수 있다. 이들은 다음과 같다.

- 신세대와 구세대에서의 평균 GC 멈춤 시간(Average GC pauses) : JVM에서 가비지 컬렉션이 이뤄지는 동안 애플리케이션이 중지되는 평균 시간이다.
- 신세대와 구세대에서의 평균 GC 빈도수(Average GC frequency) : 신세대와 구세대에서 실행된 가비지 컬렉터의 주기수이다. 이 값은 각 GC가 로그에 기록된 시간을 보면 구할 수 있다.
- GC 순차 비용(GC sequential overhead) : 가비지 컬렉션이 이뤄지면서 애플리케이션이 중지된 시스템 시간의 비율이다. Avg. GC pause*Avg. GC frequency*100%로 계산할 수 있다.
- GC concurrent overhead : 애플리케이션과 함께 가비지 컬렉션이 일어난 시스템 시간의 비율이다. Avg. concurrent GC time (sweeping phase) * Avg. concurrent GC frequency / no. of CPUs로 계산할 수 있다.
- 신세대와 구세대에서 각 GC에서 재순환된 메모리 : 각 GC에서 컬렉션된 총 가비지
- 할당 비율 : 애플리케이션의 신세대에서 데이터가 할당받는 비율을 말한다. 만약 heap이 occupancy_at_start_of_current_gc=x , occupancy_at_end_of_ previous_gc = y, GC 빈도수가 초당 1이면, 할당 비율은 약 초당 x-y이다.
- 승진 비율(Promotion rate) : 데이터가 구세대로 승진되는 비율. ‘신세대 GC’에서 설명한 것처럼 GC당 승진된 객체의 크기가 계산되었고, 예를 들어 신세대 GC 빈도수가 초당 1일 때 그 스냅샷에 의하면 승진 비율은 초당 1737K이다.
- 호출당 애플리케이션에 의해 할당된 총 데이터 : 이것은 앞서와 같이 할당 비율을 구하고, 애플리케이션 서버의 로드가 호출 비율이라고 할 때, 할당 비율/호출 비율(Allocation Rate/Call rate)로 계산될 수 있다. 즉 호출 비율은 요청 또는 오는 호출을 서버가 처리하는 비율을 말한다.
- 모든 데이터는 단기 데이터(short term)와 장기 데이터(long term data)를 나눌 수 있다 : 장기 데이터는 신세대 GC에서 생존해서 구세대로 승진한 것을 말한다. 승진 비율/호출 비율(Promotion Rate/Call Rate)로 계산할 수 있다.
- 호출당 단기 데이터(Short Term) : 단기 데이터는 매우 빨리 소멸하며, 신세대에서 컬렉션할 수 있다. 이것은 총 데이터-장기 데이터(Total Data-Long Term Data)로 계산할 수 있다.
- 호출당 총 활성화 데이터 : JVM heap 크기를 결정하는 데 매우 중요한 정보이다. 예를 들어 이 SIP 애플리케이션의 경우와 같이, 초당 100 호출의 로드에 초당 50K의 장기 데이터가 최소 40초 이상 지속되면 구세대의 최소 메모리 크기는 50K*40s*100=200M이어야 한다.
- ‘메모리 누출(Memory leaks)’ : 이것은 감지할 수 있으며 로그가 보여주는 각 GC를 모니터링에서 나오는 ‘out of memory’ 에러로 보다 쉽게 이해할 수 있을 것이다.

이런 종류의 정보는 GC의 시각에서 애플리케이션과 JVM 행동을 보다 잘 이해하고 가비지 컬렉션의 실행 성능을 튜닝하는 데 사용할 수 있다.

 

PrintGCStats(http://developer.java.sun.com/servlet/Turbo1EntryServlet에서 다운받을 수 있다)는 ‘verbose:gc’ 로그를 마이닝하는 쉘 스크립트이고 신세대와 구세대의 GC 중단 시간(총, 평균, 최대, 표준편차)의 통계를 낸다. 이는 GC 순차적 오버헤드, GC 동시발생 오버헤드, 데이터 배정, 승진 비율, 총 GC와 애플리케이션 시간과 같은 다른 중요한 GC 매개변수도 계산한다. 통계 외에도 PrintGCStats는 사용자가 설정한 주기에 맞춰 애플리케이션 실행에 대해 GC를 시간에 따라 분석한다.

 

입력
이 스크립트의 입력은 HotSpot VM이 다음의 하나 이상의 플래그를 사용했을 때 나오는 출력이다.

-verbose:gc : 최소의 출력을 생성한다. 제한적인 통계를 내지만, 모든 JVM에서 사용할 수 있다.
-XX:+PrintGCTimeStamps : 시간 단위의 통계를 활성화한다. 예를 들어 할당 비율과 주기 등이 있다.
-XX:+PrintGCDetails : 더 많은 통계 정보를 얻을 수 있다.
J2SE 1.4.1 이후부터 다음 명령어를 권한다.
java -verbose:gc -XX:+PrintGCTimeStamps -XX:+ PrintGCDetails ...

 

사용법
PrintGCStats -v ncpu=<n> [-v interval=<seconds>] [-v verbose=1] <gc_log_file >
·ncpu
Java 애플리케이션이 실행된 컴퓨터의 CPU 개수이다. 사용할 수 있는 CPU 시간과 GC ‘로드’ 요소를 계산하는 데 사용된다. 기본값이 없기 때문에 명령어에서 필히 지정해줘야 한다(기본값 1은 틀릴 때가 많다).

·interval
시간 단위 분석을 위해서 매 주기의 마지막에 통계를 출력한다 : -XX:+PrintGCTimeStamps의 출력이 필요하다. 기본값은 0(비활성화)

·verbose
만약 non-zero이면 통계 요약 후 추가적으로 각 아이템은 다른 줄에 출력한다.

출력 통계치
표 1은 PrintGCStats로부터의 통계치를 설명한 것이다.

 
표 1. PrintGCStats이 출력한 통계 요약
항목명 정의
  gen0(s) 신세대의 GC 시간을 초단위로 나타낸다.
  cmsIM(s) CMS의 initial mark 단계의 멈춤 시간을 초단위로 나타낸다.
  cmsRM(s) CMS의 remark 단계의 멈춤 시간을 초단위로 나타낸다.
  GC(s) 모든 stop-the-world GC의 멈춤 시간을 초단위로 나타낸다.
  cmsCM(s) CMS concurrent mark 단계를 초단위로 나타낸다.
  cmsCS(s) CMS concurrent sweep 단계를 초단위로 나타낸다.
  alloc(MB) 신세대에 객체 할당한 용량(MB)
  promo(MB) 구세대에 승진된 객체 용량(MB)
  elapsed_time(s) 애플리케이션이 실행되며 경과된 총 시간(초)
  tot_cpu_time(s) 총 CPU 시간 = CPU 개수 * 경과 시간
  mut_cpu_time(s) 애플리케이션이 사용한 총 CPU 시간.
  gc0_time(s) 신세대를 GC할 때 총 멈춰진 시간
  alloc/elapsed_time(MB/s) 경위 시간당 할당 비율(MB/초)
  alloc/tot_cpu_time(MB/s) 총 CPU 시간당 할당 비율(MB/초)
  alloc/mut_cpu_time(MB/s) 애플리케이션 총 시간당 할당 비율(MB/초)
  promo/gc0_time(MB/s) GC 시간당 승진 비율(MB/초)
  gc_seq_load(%) stop-the-world GC에 사용된 총 시간의 비율
  gc_conc_load(%) 동시발생적 GC에 사용된 총 시간의 비율
  gc_tot_load(%) GC 시간(순차적과 동시발생적)의 총 비율

- Solaris 8 이후로 사용할 수 있는 alternate thread library를 사용해라.
Solaris 8에서 LD_LIBRARY_PATH=/usr/lib/lwp:/usr/lib을 설정해서 사용할 수 있고, Solaris 9에서는 기본 라이브러리로 설정돼 있다. 이는 Java 쓰레드와 커널 쓰레드 사이에 ‘one-to-one threading model’을 사용한다. 이 라이브러리를 사용해서 대부분의 경우에 처리량이 5~10% 또는 그 이상으로 성능이 향상됐다.

- prstat -Lm -p <jvm process id>로 light-weight-process(LWP) 단위로 한 프로세스의 자원 사용을 분석하고 확장성과 성능 면에서의 병목 지점을 찾을 수 있게 한다. 예를 들어 애플리케이션의 확장성에 있어 병목 지점이 GC인지 확인하는 데 도움을 준다. 대부분의 경우, 애플리케이션이 자체적으로 제대로 쓰레드화하지 못해 큰 시스템으로 확장되지 못한다. 이 명령어는 시스템 자원 사용과 주어진 프로세스에서 LWP 단위의 활동을 보고한다. 더 자세한 사항은 man prstat에서 참조한다. 이 명령어의 결과 출력물의 단점은 보고된 LWP id가 어떤 Java 쓰레드인지 확인하기 어렵다는 것이다.

- ThreadAnalyser는 Java 프로세스를 분석하는 쉘 스크립트이고, Solaris의 LWPIDs 대신 prstat의 출력물에 있는 ‘쓰레드명’을 생성한다. Java 애플리케이션의 시스템 사용을 ‘쓰레드명’ 단위로 확인할 수 있게 해준다. 이 스크립트를 앞서 언급한 alternate thread library와 함께 사용하는 것이 가장 이상적이다.

ThreadAnalyser는 이미 실행돼 있는 Java 프로세스에 붙일 수 있거나(파일로 전환된 프로세스의 stderr 출력에 접근할 수 있는 한) Java 프로세스를 시동시킬 수 있는 스크립트 파일을 사용해 동작시킬 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
사용 예
Java 프로세스를 시작하고 표준 에러를 java <app_name>>/ tmp/java.out 2>&1 으로 파일로 전환시킨다. Java 프로세스의 PID를 주의하고 다음과 같이 ThreadAnalyser(http://developer.java.sun.com/servlet/Turbo2EntryServlet에서 다운받을 수 있다.)을 붙인다.

ThreadAnalyser -f /tmp/java.out -p PID <interval>
또는
ThreadAnalyser -f /tmp/java.out <interval>


이 컴퓨터에 다른 Java 프로세스가 없으면 ps(1)을 사용해 PID를 구한다. ThreadAnalyser로 Java 프로세스를 실행하고자 한다면 다음과 같이 한다.

ThreadAnalyser -s <app_start_script_file> <interval>

이것은 이 시스템에 다른 Java 프로세스가 동작하고 있지 않을 때만 사용할 수 있다. app_start_script_file이 Java 프로세스을 실행하는 실행 스크립트 파일명이다. Interval은 스크립트로부터 출력되는 정보 갱신 주기이고, 기본값은 5이다.

보다 많은 옵션과 사용법은 ThreadAnalyser 스크립트의 README 부분을 읽어보기 바란다.
 

man prstat 명령어를 입력하면 다음과 같은 출력의 각 열이 의미하는 것을 자세히 알 수 있다.



- 실행되는 Java 애플리케이션의 모든 쓰레드의 덤프(dump)를 얻으려면, SIGQUIT 신호를 JVM 프로세스에 보낸다. 이는 kill - QUIT <JVM process pid> 명령어로 신호를 보낼 수 있다.
- JVM 프로세스 안의 각 LWP의 hex와 symbolic stack trace를 얻으려면 명령어 pstack <JVM process id>을 사용하면 된다. 이 출력은 LWP id와 Java 애플리케이션 쓰레드 id와의 관계를 알려준다. 만약 alternate thread library를 JVM에서 사용하면, 출력물은 프로세스의 각 LWP와 해당 묶여 있는 Java 애플리케이션 쓰레드의 일대일 관계를 보여준다. 명령어 man pstack을 입력하면 보다 자세한 것을 알 수 있다.
- JVM에 있는 -Xrunhprof 플래그를 사용해 불필요한 객체 보유(때로는 막연하게 ‘메모리 누수’라 부른다.)를 확인할 수 있다.
- UDP 기반의 애플리케이션에서는 DatagramSocket.connect (InetAddress address, int port)와 DatagramSocket.disconnect()의 호출 횟수를 최소화한다. J2SE 1.4 버전 이상의 JVM부터는 이것들이 네이티브한 호출이기 때문에, 여러 다른 이점을 가져다 주지만, 잘못된 사용은 애플리케이션의 성능 저하를 야기한다. API를 잘못 사용하면 애플리케이션의 애플리케이션 ‘시스템 시간’ 시점에서 매우 비싼 비용을 지불하게 된다.

 

다음에 보여줄 결과값은 2개의 새로운 GC 정책과 앞서 언급한 튜닝 기법을 사용해 얻은 것이다. 이 사례를 위해서 Ubiquity의 Application Services Broker(ASB)인 SIP Application Server가 사용되었다. 지난 호에 언급했듯이 ASB는 GC 시점에서 JVM을 광범위하게 사용하는 일반적인 통신 애플리케이션 서버를 대표한다.

 

실험적인 플랫폼
4개의 900MHz UltraSPARC Ⅲ 프로세서를 장착하고, Solaris 8이 설치된 Sun Fire V480을 사용했다. 물론 논의된 튜닝 기술들은 Linux와 Windows, Solaris(인텔 아키텍처)와 같은 다른 플랫폼에도 적용된다.
구세대 heap의 크기는 ASB 서버의 최대 로드에 기반해서 결정되었고, JVM은 튜닝이 적용되었다. 신세대 heap의 크기는 12MB에서 128MB 사이에 경험을 통해 계속적으로 변경해보았다. GC 순차적 오버헤드가 가장 낮고 신세대에 허용할 수 있는 GC 멈춤 정도를 갖는 최적의 크기는 24MB였다. 24MB 이상의 신세대는 GC 멈춤이 증가했고 성능의 차이가 없었다. 24MB 이하는 신세대의 GC 빈도수가 많아지면서 GC 순차적 오버헤드가 급증하고 짧은 생명주기를 갖은 데이터도 구세대로 승진되고 만다. 그림 1은 신세대의 heap 크기에 따른 다양한 GC 순차적 오버헤드와 GC 멈춤, GC 빈도수를 보여준다.

512MB의 구세대와 24MB의 신세대에서 여러 가지의 GC 정책과 튜닝 기술을 사용해 나온 결과값을 보여준다.

그림 1. 신세대의 다양한 GC 순차적 오버헤드와 GC 멈춤, GC 빈도수를 신세대의 heap 크기에 비례해서 보여준다.

단계 1 : J2SE 1.4.1에서 신세대와 구세대의 기본 GC를 사용할 때 가장 좋은 결과다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=2 <application>

구세대의 평균 GC pause: 3 secs.
신세대의 평균 GC pause: 110 ms.
GC sequential overhead: 18.9%

단계 2 : J2SE 1.4.1에서 구세대의 GC를 CMS 컬렉터로 설정하고 나온 가장 좋은 값이다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=128 -XX:+UseConcMarkSweepGC -XX:MaxTenuring Threshold=0 -XX:CMSInitiatingOccupancyFraction=60 <application>

-XX:MaxTenuringThreshold=0로 설정하면 성능이 증가한다. -XX:CMSInitiatingOccupancyFraction의 적절한 값은 60이다. 이보다 작으면 더 많은 CMS 가비지 컬렉션이 일어나고 많으면 CMS 컬렉션의 효율이 떨어진다.

구세대의 평균 GC 멈춤(stop-the-world init mark와 동시발생 컬렉션의 remark phase) : 115ms
신세대의 평균 GC 멈춤 : 100ms
GC 순차적 오버헤드 : 8.6%

단계 3 : J2SE 1.4.1에서 구세대는 CMS 컬렉터를 신세대는 새로운 패러럴 컬렉터를 사용할 때 가장 좋은 값이다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=128 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=0 -XX:CMSInitiatingOccupancyFraction=60 <application>

구세대의 평균 GC 멈춤(stop-the-world init mark와 동시발생 컬렉션의 remark phase) : 142ms
신세대의 평균 GC 멈춤 : 50ms
GC 순차적 오버헤드 : 5.9%

 
표 2. 사례를 통해 얻은 값을 정리
Summary Table 구세대의 평균
GC 멈춤(ms)
신세대의 평균
GC 멈춤(ms)
순차적
오버헤드(%)
  기본 컬렉터3000 110 18.9%
  CMS 컬렉터1151008.6%
  CMS와 패러럴 컬렉터142 50 5.9%
 

결과
CMS 컬렉터를 사용함으로써 구세대에서 GC 멈춤은 2000% 감소하고 GC 순차적 오버헤드는 220% 감소했다. GC 순차적 오버헤드의 감소는 애플리케이션 처리량의 성능 향상에 직접적으로 영향을 준다. 패러럴 컬렉터를 사용해 신세대에서 GC 멈춤은 100% 감소했다. 4개의 CPU가 장착된 시스템에서 4배 성능의 패러럴 컬렉터를 기대했을지도 모르겠다. 그러나 패러럴 컬렉션에 연관된 간접 비용에 의해, 가속도는 선형을 그리지 않지만 JVM이 선형을 그리도록 연구하고 있다.

기업에 적용
성능 튜닝 기술과 소개된 새로운 GC 정책은 통신업체에만 국한된 것이 아니고 웹 서버와 포털 서버, 애플리케이션 서버와 같이 비슷한 요구사항을 갖는 대기업용으로도 적용될 수 있다.

미래에는...
썬 JVM에서 하위 레벨의 명령어 플래그를 없애려고 연구중이다. 신세대와 구세대의 크기와 GC 정책 등을 설정하기보다는 최고 멈춤 시간 또는 지연, JVM의 메모리 사용량, CPU 사용법 등과 같은 상위 레벨의 요구사항을 설정하면 JVM이 자동적으로 그 설정에 맞게 애플리케이션을 위해 여러 하위 레벨의 값과 정책을 변경한다.

HotSpot JVM에 JVM의 조작 면에서 상위 레벨 단의 성능 정보를 제공할 수 있는 가볍고 항상 실행될 수 있는 것을 포함하려고 한다. 향후 출시되는 JVM에서 사용할 수 있겠지만, 현재로서는 툴과 기술은 아직 실험적이다.

Java Specification Request(JSR) 174는 JVM을 모니터링하고 관리하는 API 스펙이고, JSR 163은 시간과 메모리에 대한 프로파일링의 지원으로 실행되는 JVM의 프로파일링 정보를 추출해내는 API 스펙이다. 이 API는 프로파일에 영향을 주지 않는 구현이 되도록 설계될 것이다. 이 API는 프로파일링의 상호 운용성과 향상된 가비지 컬렉션 기술과 최대한 많은 JVM에서도 안정적으로 동작하는 구현을 고려할 것이고, Java 애플리케이션, 시스템 관리 툴, RAS 관련 툴에 JVM의 상태를 모니터할 수 있는 능력을 부여하며, 많은 런타임 컨트롤을 관리할 수 있게 할 것이다.

통신 애플리케이션 서버의 특수한 요구사항을 충족하기 위한 연구가 Sun ONE Application Server 7에서도 진행되고 있다. 통신업체의 경우 서비스와 데이터에 더 높은 가용성이 요구되는 것과 서비스 업그레이드를 제외하면 대기업과 유사하다. Sun ONE Application Server Enterprise Edition 7은 상태 확인 저장소와 세션, 빈 상태의 높은 유용성을 주는 Clustra ‘Always ON’ 기술이 포함돼 있다. Sun ONE Application server 7는 JAIN과 OSA/Parlay 프레임웍 기반의 호출 프로세싱 서버를 구축하는 데 사용할 수 있다. EJB 컨테이너 향상을 위해 SLEE 구현은 Sun ONE Application server에 통합될 수 있다. 추후 EJB를 사용할 때 지속적인 유용성과 어느 정도의 실시간, QoS 능력을 제공하기 위해 JSR 117 API 뿐만 아니라 통합된 SIP 스택도 포함될 수 있다.

통신 업체 사이에 Java의 인기가 증가하면서 썬 JVM은 그들의 요구 사항에 맞추어 발전해가고 있다. 대형 서버의 요구사항을 충족시키는 데 필요한 해결책으로 64비트 JVM과 대형 하드웨어에서(4G에 제한되지 않은) 대용량의 heap 크기를 설정할 수 있다. 여러 CPU 시스템에서 패러럴 컬렉터는 신세대에서 멈춤을 줄일 수 있고 동시발생 컬렉션은 구세대에 mark-compact collector로 인해 생긴 큰 GC 멈춤을 숨길 수 있다. 이로 인해 GC 순차적 오버헤드를 상당히 감소시켜서, 다수의 CPU가 장착된 시스템에서 애플리케이션의 성능과 확장성을 높여줄 수 있다.

JVM의 가비지 컬렉션은 verbose:gc의 로그를 보고 모니터하고, 분석하고 튜닝할 수 있다. GC 로그의 정보는 JVM의 크기를 조정하고 애플리케이션의 최적의 성능을 내기 위한 스위치와 매개변수를 설정하기 위해 분석된다.

머지않아 Sun ONE Application Server 7 Enterprise Edition은 최적의 통신 애플리케이션 서버로 구현될 것이다.

참고로 이글은 썬의 Alka Gupta 씨와 Ubiquity Software Corporation의 CTO인 Michael Doyle 씨가 쓴 ‘Turbo-Charging the Java HotSpot VM, 1.4.X to Improve the Performance and Scalability of Application Servers’를 정리한 것이다.

 

Posted by tornado
|