달력

12022  이전 다음

  •  
  •  
  •  
  •  
  •  
  •  
  • 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 (1)

 

 
 
 

Java 프로그래밍 언어는 많은 데이터 중심적인 애플리케이션용 선택 언어로 널리 받아들여지고 있다. Java의 성공은 언어의 초기 단계부터 설계된 다음과 같은 중요 기능을 토대로 만들어졌기 때문이다.

- WORA : Write Once Run Anywhere
- 자동 메모리 할당과 컬렉션
- 객체 지향 디자인
- 상속

그러나 결정적으로 소스 코드를 컴파일해서 만들어진 Java 바이트 코드를 하위단인 JVM이 실행시키는 구조는 시간을 다투는 환경에서 Java를 선택하는 데 있어 고민을 안겨주었던 것도 사실이다. 전통적으로 가비지 컬렉션(GC)은 사용자의 애플리케이션을 일시적으로 중단시키고 시스템이 메모리를 재순환시킨다. 이런 GC 중단은 밀리초에서 심한 경우에는 수초일 수도 있다.

통신 회사 장비 공급자는 통신 회사가 기업 고객과 엄밀한 수준의 서비스 협약이 돼 있을 뿐더러 부동산 시장에서도 기본적인 수준의 신뢰성과 반응 시간을 가지고 있어야 한다는 것을 잘 알고 있다. 수화기를 든 다음 10 또는 20초를 기다려야 신호음이 난다는 것은 용납될 수 없다.

통신 회사들은 여러 가지 이유로 자사의 환경에 Java를 사용하길 원한다.


특히 이미 존재하는 광범위하고 풍부한 애플리케이션을 생성할 수 있는 능력있는 수백만의 프로그래머들과 회사들이 Java를 기본 기술로 표준화하고 있다. 이는 새로운 서비스와 기술을 많이 구할 수 있다는 것이다. 거기에다가 J2EE에는 보편적인 표준 환경과 기하급수적으로 확장할 수 있는 잠재력이 있다. 그렇지만 예측할 수 없는 ‘중단’과 그로 인해 애플리케이션의 반응이 지연되는 것 때문에 어떤 회사라도 자사 네트웍의 핵심 기술로 Java를 사용하는 것이 타당한지 묻게 한다.

통신 회사는 수익을 얻기 위해서 완전한 통화와 서비스 세션에 의존한다. 불완전하거나 버림받은 세션은(예를 들어 통화와 인스턴트 메시지) 네트웍 자원과 전혀 사용되지 않는 대역폭을 제공하는 비용을 낭비하는 것이다.

이러한 환경에서 통신 회사뿐만 아니라 차세대의 통신 기반을 선도하는 3rd generation(3G) 회사들에게도 표준으로 채용된 SIP 프로토콜이 이상적인 선택일 것이다. SIP는 UDP와 TCP를 기반으로 신호를 보내고, 패킷(UDP) 재전송이 내장돼 있는 프로토콜이다. 만약 UDP 요청 패킷이 특정 시간 내(주로 500ms)에 반응이 없으면 재전송된다. 네트웍 불능이나 패킷 손실 등 다른 에러들에 대한 계산도 돼 있어야 한다.

즉 JVM은 500ms에 근접한 시간만큼 애플리케이션을 중단시키면 안된다는 의미다. 왜냐하면 네트웍 패킷 지연(가는 데만 50~100ms)과 CPU의 프로세싱 시간이 계산되면 재전송이 일어나기까지 시간이 많지 않는다. 계단식(Cascade) 실패는 주로 애플리케이션 서버에 높은 로드에 대한 대비를 하지 않은 불완전한 디자인이나 단일 쓰레드, 애플리케이션 쓰레드를 모두 중지시키는 ‘Stop-the-world 가비지 컬렉션’ 정책을 사용하는 JVM(J2SE 1.4 버전 이하)을 사용할 때 일어난다. 그리고 이 JVM들은 단일 쓰레드이고, 여러 CPU가 있더라도 오직 하나의 CPU에서만 가비지 컬렉션을 실행시킨다.

Sun HotSpot JVM(JDKTM 1.3+)에 사용된 알고리즘은 메모리 재사용과 객체 노화에 효과적이다. JVM heap은 객체의 나이에 따라 ‘신세대’와 ‘구세대’로 나뉘어진다.

신세대는 ‘에덴’과 2개의 ‘생존 공간(Survivor)’으로 나눠졌다. 대부분의 애플리케이션에서는 2/3의 객체는 ‘단기 객체’로 빨리 죽고, ‘신세대’에서 컬렉션할 수 있다. 주로 신세대의 객체는 총 heap 크기에 비해 작다. 따라서 신세대에서 짧은 멈춤이 빈번하게 일어나지만 한번의 컬렉션에 더 많은 메모리를 컬렉션할 수 있다. 그러나 신세대에서 여러 번 생존 공간에 있게 되면, 그것은 ‘오래된’ 또는 ‘장기 객체’로 인식하고 ‘구세대’로 ‘승진(promoted)’ 또는 ‘보유(tenured)’하게 된다. 비록 대개 ‘구세대’의 크기가 크기만, 결국은 모두 사용하게 되면 컬렉션이 필요하게 된다. 이로 인해 구세대에서 빈도수는 낮지만, 장시간의 멈춤이 일어난다. 보다 자세한 것은 ‘Tuning Garbage Collection with the 1.3.1 Java Virtual Machine’이라는 Sun HotSpot JVM 가비지 컬렉션 테크놀로지를 참조한다.

Ubiquity의 SIP 애플리케이션 서버인 Application Services Broker(ASB)는 100% 순수 Java 기술이며, JVM과 가비지 컬렉션에 대한 완벽한 테스트 환경을 제공한다. SIP 애플리케이션 서버로서, ASB는 신호가 오는 네트웍으로부터 서비스 요청을 받은 다음 수정하고, 최종적으로 답변해주는 책임이 있다. SIP 트래픽을 신호가 오는 네트웍으로부터 서비스 요소(SIP Servlets)로 연결해 책임을 이행한다. 이 서비스 요소는 특정 신호 메시지에 관심이 있다고 ASB에 등록한다. ASB는 대규모 호출을 다양한 로드 분산으로 생성한다. 이는 클러스터 기반으로 구현되어 한 대나 여러 대에서나, 여러 프로세서에서 잘 확장된다. 이런 때 JVM 디자인이 한계점까지 압력을 받으며, 이런 상태를 관찰하면서 다중 쓰레드의 JVM을 개선시키는 데 많은 통찰력을 얻게 된다.

예를 들어 Ubiquity ASB는 호출되는 하나의 프로세스당(일반적으로 한 호출은 하나의 세션이다) 약 220KB의 가비지를 생성한다. 10%의 데이터는 40초 가까이 생존하고, 나머지 90%는 곧바로 죽어버린다. 이 10%의 데이터는 단시간이지만 평균 초당 100번을 호출한다고 하면 대단히 많은 메모리를 사용하게 된다. 40초 안에 최소한 88MB의 활성 데이터를 갖게 된다. 만약 가비지 컬렉션이 5분에 한번씩 ‘구세대’에서 일어난다면 heap의 크기는 최소한 660MB가 돼야 한다. 이전의 가비지 컬렉션 엔진이 스캔하기에 매우 커 컬렉션하는 데 100~150ms 정도 걸린다.

통신 애플리케이션 서버는 애플리케이션을 멈추는 시간을 엄격히 제한해줄 수 있는 결정적인 GC 모델을 요구하고 여러 프로세스에서도 잘 확장돼야 한다.

J2SE 1.2와 1.3에서 GC는 단일 쓰레드이며, stop-the-world 형식이었다. 이 JVM의 가비지 컬렉션으로 인해 생기는 멈춤은 애플리케이션에 지연을 더해 처리량과 확장성으로 본 성능이 떨어지게 된다. 이런 아키텍처는 한대의 컴퓨터의 여러 CPU에서 확장하기 어럽게 만든다. 가비지 컬렉션이 여러 프로세서로 분산되지 않기 때문에, 매우 다중 쓰레드인 애플리케이션이 JVM에서 실행될 때, 애플리케이션 레벨의 작업을 이행하기에 충분한 프로세서를 갖고 있음에도 불구하고 JVM으로부터 컨트롤을 얻기 위해 기다리며, JVM은 다중 CPU 환경에서 많은 압력을 받는다. 다중 프로세서 시스템에서 단일 쓰레드 GC의 영향은 병렬 애플리케이션일수록 크게 나타난다. 만약 GC를 제외하고 애플리케이션이 완벽하게 확장한다고 가정하면, GC가 일어나는 동안 작업을 수행할 수 있는 다른 프로세서가 유휴 상태에 있게 됨으로써, GC가 확장성을 저해하는 원인(Bottleneck)이 되는 것이다.

이런 내용에서 Java를 Turbo-charge한다는 것은 많은 CPU 사용과 많은 메모리에 접근, 많은 동시 발생 소켓 연결의 처리를 의미한다.

J2SE 1.4는 새로운 기능과 버퍼 관리, 네트웍 확장성, 파일 I/O의 성능을 개선한 논블록킹 new I/O API를 구현했다.
- 새로운 네트웍 I/O 패키지는 접속할 때마다 쓰레드를 배정하는 방식을 제거함으로써 서버에 동시 접속할 수
  있는 수를 극적으로 증가시켰다.
- 새로운 파일 I/O는 읽기와 쓰기, 복사, 이동 작업에서 현재의 파일 I/O보다 2배 가까운 속도를 낸다.
  새 것은 file locking과 memory-mapped files, 동시 여러 읽기/쓰기 작업을 지원한다.


J2SE 1.4의 64비트의 JVM은 4G 이상 큰 heap을 가질 수 있을 뿐더러 300G 이상 가질 수도 있다. 만약 가비지의 생성과 배당하는 비율이 일정하다면 heap이 클수록 GC로 인한 멈춤은 더욱 빈도수가 낮아지지만 그 시간은 길어질 것이다.

애플리케이션의 효율성과 확장성을 정하는 중요한 요소는 ‘GC 순차 비용(GC sequential overhead)’이다. 이는 애플리케이션의 실행 시간 중에 애플리케이션이 멈춘 상태에서 JVM에서 GC가 실행되는 시간의 비율을 뜻한다. 다음과 같이 계산할 수 있다.

Avg. GC pause * Avg. GC frequency * 100 %.
‘GC 빈도수’는 주기적인 GC나 한 단위의 시간에 일어나는 GC수이다. ‘신세대’와 ‘구세대’의 평균 GC 멈춤과 빈도수가 매우 다르기 때문에 GC 순차 비용을 구하는 계산이 다른데, 즉 둘을 합해서 애플리케이션의 총 GC 순차 비용을 구할 수 있다. GC 순차 비용은 다음과 같이 계산될 수 있다.

Total GC time/Total wall clock run time.

총 GC 시간은 다음과 같이 계산할 수 있다.

Avg. GC pause * total no. of GCs

분명 적은 GC 순차 비용은 애플리케이션의 높은 처리량과 확장성을 의미한다.

J2SE 1.4.1은 JVM이 더 많은 CPU와 메모리를 사용할 수 있게 해, 애플리케이션의 성능과 확장성을 높일 수 있게 디자인한 두 개의 새 가비지 컬렉터를 소개했다. 이 컬렉터는 통신 업체의 도전과제를 충족할 수 있게 해준다.

- 최대의 처리량을 위해 시스템의 자원을 확장성과 최적화로 사용하기 위해, 시스템의 GC 순차 비용은 10% 이상
  될 수 없다.
- 클라이언트와 서버 간에 정해진 프로토콜에 정의된 지연에 부합되고 서버의 양호한 답변 시간을 보장하기
  위해서, 애플리케이션에서 발생되는 모든 GC 멈춤은 200ms 이상이 되면 안된다.


J2SE 1.4.1에서 소개한 두 개의 새로운 컬렉터는 Parallel Collector와 Concurrent mark-sweep(CMS) Collector이다.

-Parallel collector: Parallel collector는 신세대를 대상으로 구현되었다. 이는 다중 쓰레드이며, stop-the-world이다. 이 컬렉터는 다중 프로세서 컴퓨터에서 더 좋은 성능을 내도록 다중 쓰레드에서 GC를 할 수 있게 한다. 비록 모든 애플리케이션 쓰레드를 중지시키지만 시스템의 모든 CPU를 사용해 주어진 양의 GC를 더욱 빠르게 처리할 수 있다. 신세대 부분에서의 GC 멈춤을 많이 감소시킨다. 따라서 Parallel collector는 여러 CPU뿐만 아니라 더 많은 메모리에 애플리케이션이 확장할 수 있게 해준다.

-Concurrent mark-sweep(CMS) collector: CMS collector는 구세대를 대상으로 구현되었다. CMS collector는 애플리케이션과 함께 ‘mostly concurrently’하게 실행되어, 때로는 ‘mostly-concurrent garbage collector’로 불린다. GC 멈춤 시간을 짧게 하기 위해서 애플리케이션을 위해 사용할 처리 능력을 이용한다. CMS collection은 Initial mark와 Concurrent marking, Remark, Concurrent sweeping의 4단계로 나뉜다.

‘initial mark’와 ‘remark’ 단계는 CMS collector가 모든 애플리케이션 쓰레드를 일시적으로 중지시키는 stop-the-world 단계이다. initial mark 단계는 시스템의 ‘roots’에서 곧바로 접근가능하게 모든 객체를 기록한다. ‘concurrent marking’ 단계에서 모든 애플리케이션 쓰레드는 재시작되고 concurrent marking 단계가 초기화된다. ‘remark’ 단계에서 애플리케이션 쓰레드는 다시 중지되고 마지막 마킹을 끝낸다. ‘concurrent sweeping’ 단계에서 애플리케이션 쓰레드는 다시 시작되며 heap을 동시 스위핑하며 표기되지 않은 모든 객체는 회수된다. initial mark와 remark 단계는 상당히 짧다. 구세대의 크기가 1G라고 해도 200ms 이하의 시간이 걸린다. concurrent sweeping 단계는 mark-compact collector 만큼의 시간이 걸릴 것이지만 애플리케이션 쓰레드가 중지되지 않기 때문에, 멈춤이 숨겨져 있다.

 
‘mostly concurrent’인 CMS collector는 JVM이 큰 heap과 여러 CPU에 확장할 수 있게 해, 지연과 mark-compact stop-the-world collector로 발생한 처리량 문제를 해결한다.

표 1은 J2SE 1.4.1에 있는 여러 컬렉터의 기능을 비교해본다.
그림 1과 2, 3은 다른 여러 가비지 컬렉터를 그림으로 나타낸 것이다. 초록 화살표는 다중 CPU에서 실행되는 다중 쓰레드 애플리케이션을 뜻한다. 빨간 화살표는 GC 쓰레드를 뜻한다. GC 쓰레드의 길이는 GC 멈춤 시간을 대략적으로 나타낸다.

표 1.다른 여러 가비지 컬렉터의 기능 요약
신세대 컬렉터구세대 컬렉터
Copying collector collector
Default
Stop-the-
world
Single
threaded
All J2SEs
Mark-compact collector
Default
Stop-the-world
Single threaded
All J2SEs
Parallel collectorConcurrent mark-sweep collector
Stop-the-
world
Multi-
threaded
J2SE 1.4.1+
Mostly-concurrent
Single threaded
J2SE 1.4.1+

그림 1과 2, 3이 보여주듯, Parallel collector를 신세대에서 concurrent mark-sweep collector를 구세대에서 같이 사용하면 중지 시간과 GC 순차 비용을 줄일 수 있다. 이 두 컬렉터는 애플리케이션이 더 많은 프로세서와 메모리에 확장할 수 있도록 도와준다.

Posted by tornado

댓글을 달아 주세요