블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

어제, 네이버D2에서 열린, NHN nGrinder 개발자인 윤준호님의 성능 강의를 듣고 왔습니다.

몇가지 얻은 인사이트가 있어서 기록해놓습니다.





아마존의 T2 인스턴스 타입


- 서버의 부하에 따라서 컴퓨팅 파워를 높였다 낮췄다가 함.

- 크레딧이란 개념을 써서, 부하가 낮을때는 크레딕을 저축해놓고, 부하가 높아지면 모아놓은 크레딧을 소모하는 방식


성능 테스트시 주의 사항


- 외부 OPEN API등을 사용할때, 성능 테스트시, OPEN API가 성능 테스트를 DDoS 공격으로 인식해서 차단될 수 있음


대용량 파일 (동영상등)에 대한 부하 테스트


- 네트워크 대역폭을 많이 소모하여 운영 시스템 장애로 발전할 수 있음

- 비용도 많이 소모됨

- 이를 방지 하기 위해서 초기 1000 바이트 정도만 읽어들이고, 해당 커넥션 스트림을 닫아 버리는 방법이 있음

- 이렇게 해도 실제 DB나 내부 오퍼레이션에 대한 코드는 테스트가 가능하다. 


성능 테스트시 서버에 대한 모니터링 툴


- pinpoint 등 APM이 있지만, 인프라 모니터링으로 netdata (https://github.com/firehol/netdata) 추천.

데모를 봤는데, 사용법이 매우 쉬운데 반해서 상당히 디테일한 모니터링이 가능




저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

MAC에서 HTTP 패킷 모니터링 하기

성능과 튜닝 | 2016.03.11 00:06 | Posted by 조대협

MAC에서 HTTP 패킷 모니터링 하기


조대협 (http://bcho.tistory.com)


윈도우즈의 경우 fiddler (http://www.telerik.com/fiddler ) 라는 무료 도구가 있고, Mac의 경우에는 HTTPScoope (https://www.tuffcode.com/) 을 추천한다. HTTPScoope의 경우 14일 무료이고, 유료 버전은 15$ (16000원정도)이다.

 

아래는 로컬에서 3000번 포트로 띄운 웹서버의 패킷을 모니터링 하는 방법이다.


Scoop을 사용하는 방법을 보면, Scoop을 설치한 후에, “Preference” 메뉴에서 아래 그림과 같이 Restrict to port 메뉴를 선택해서, 3000 번 포트를 추가한다. (예제가 3000번 포트를 통해서 Listening 하도록 되어 있다.)



그림 HTTPScoop에서 Preference 메뉴를 통하여 리스닝 포트를 변경하기


다음으로 HTTPScoop에서 어느 LAN카드를 통해서 Listening 할지를 정해야 한다.

HTTPScoop 우측 상단에 보면 LAN 카드를 고르는 메뉴가 있는데, 여기서는 로컬에 띄워진 node.js의 패킷을 모니터링 할것이기 때문에, “Local interface (lo0)” 을 선택한다.

선택이 끝났으면, 이제 패킷을 모니터링 하기 위해서 좌측 상단의 “Scoop” 버튼을 클릭하면 127.0.0.1:3000 으로 오고 가는 패킷이 모두 캡춰 된다. 



그림  HTTPScoop에서 설정하기


이제 HTTP 헤더를 모니터링 해보자

Scoop 상에서 모니터링을 하고자 하는 패킷을 선택하면 아래와 같이 디테일이 출력된다.



그림 HTTPScoop에서 HTTP Header 정보를 확인하기

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

http://haydenjames.io/20-top-server-monitoring-application-performance-monitoring-apm-solutions/

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

웹트래픽 디버깅툴-Fiddler

성능과 튜닝 | 2014.01.07 21:14 | Posted by 조대협

지앤선 김지영 대표님이 보내주신 책으로 본 솔루션




피들러 완벽 가이드

저자
에릭 로렌스 지음
출판사
지앤선 | 2013-09-24 출간
카테고리
컴퓨터/IT
책소개
웹 디버깅 툴, 피들러의 가이드. 피들러의 개발자가 직접 피들러...
가격비교


http://fiddler2.com/

마치 Proxy 서버 처럼 중간에 패킷을 snipping 해서 디버깅 할 수 있다. SSL 인증서 변경해서 Man in middle attack emulation해볼 수 도 있고, 여러가지로 유용한 툴인듯.

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

VisualVM을 이용한 JVM 모니터링

성능과 튜닝/JVM | 2013.09.05 23:30 | Posted by 조대협

JVM Monitoring

조대협

 

JVM 1.4 버전대에만 해도 GUI 기반으로 JVM을 모니터링 하는 도구는 거의 없었다. 콘솔로 접속해서 Thread dump GC 로그등을 분석하거나 필요할 경우 JMX 기반의 모니터링 명령어등을 만들어야 했는데, 근래에는 좋은 모니터링 도구들이 많이 나와서, 쉽게 JVM의 상태를 모니터링 할 수 있고, 메모리,CPU 사용률, Thread 현황들을 매우 쉽게 분석할 수 있다.

여기에, 여러개의 VM을 동시에 중앙에서 매우 세세하게 모니터링이 가능하게 되었다.

여기서는 몇가지 자주 사용되는 JVM 모니터링 툴을 소개하고자 한다. 

VisualVM

대표적인 도구로는 오픈소스 기반의 Visual VM이라는 도구가 있다. http://visualvm.java.net/

에서 다운로드 받을 수 있다. 여러개의 VM을 동시 모니터링이 가능할 뿐만 아니라, profiling도 가능하고, eclipse 안에 embed 해서 사용할 수 도 있다.

먼저 사용법을 알아보자. VisualVM을 다운받은 후에, 압축을 풀어놓는다.다음은 원격지에 있는 VM을 모니터링 하는 설정이다. 모니터링할 VM 서버에서 먼저 rmiregistry 서버를 기동한다.

C:\dev\lang\jdk1.7\bin>rmiregistry 1099

 

다음으로는 VM에 대한 정보를 접근할 수 있도록 permission을 허용해야 하는데, ${java.home}/bin 디렉토리에 tools.policy라는 파일을 다음과 같이 만든 후에,

grant codebase "file:${java.home}/../lib/tools.jar" {

   permission java.security.AllPermission;

};

jstatd 라는 데몬을 기동 시킨다.

C:\dev\lang\jdk1.7\bin\> jstatd -p 1099
   -J-Djava.security.policy=tools.policy

위의 설정까지 하면 기본적인 VM에 대한 상태 정보 (메모리)를 모니터링할 수 있는데, 조금 더 상세한 모니터링을 위해서, JVM JMX 설정을 on 한다.

다음은 Tomcat JMX 설정을 하기 위해서 ${tomcathome}/bin/catalina.bat를 수정한 내용이다.

JVM옵션에 다음과 같은 내용을 추가 하면 된다.



set JAVA_OPTS=%JAVA_OPTS% %LOGGING_CONFIG% -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9983


여기 까지 했으면 모든 준비가 끝났다.

주의할점은, 실제 운영이나 개발 환경들에 이러한 설정을 적용할때, 포트가 외부에 오픈 되기 때문에, 보안상 문제가 될 수 있다. 그래서 반드시 OS (Linux) iptable 설정등에서, VisualConsole이 인스톨 되는 서버로 부터의 inbound connection 만 오픈하도록 하는 것이 좋다.

Visual VM을 실행하고, Remote Host Add하고, ip 주소와 위에서 지정한 포트를 지정하면 아래와 같이 VM을 모니터링 할 수 있다.

아래 그림은 편의상 local에 있는 JVM을 모니터링 하도록 설정하였다.



여기서는 전체 VM에 대한 Thread 개수, Class개수와 기본적인 CPU JVM 메모리 상황을 모니터링 할 수 있다.

 

다음은 Thread를 모니터링 하는 화면이다. time frame에 따라서 어떤 thread가 있고,어떤 상태인지를 모니터링이 가능하다.



 

아래는 Thread dump 추출 화면이다. 좌측 메뉴에서 JVM을 선택하고 오른쪽 마우스 버튼을 이용하면 Thread dump 추출 메뉴가 나오는데, 아래 그림과 같이 추출한 Thread dump를 저장해서 보여준다. Bottleneck 분석등에 매우 유용하게 사용할 수 있다. 



다음은 성능 튜닝 부분에서 유용하게 사용할 수 있는 기능인데, profiling 기능을 제공한다.

우측 상자에 profiling하고 싶은 class (패키지명)을 지정해놓고, start를 하게 되면, 각 패키지 또는 클래스에서 소요되는 CPU 사용량을 보여준다. 


이를 이용하면 손쉽게 CPU 소모가 많은 모듈을 찾아낼 수 있다.


 

JConsole

참고로, JVM을 인스톨 하면 default로 인스톨 되는 GUI 기반의 JVM 모니터링 툴이 있는데, ${java.home} jconsole이라는 유틸리티이다.

VMConsole 처럼 permission이나 rmi setting이 필요 없고, JMX를 이용해서 통신을 한다.

기능상으로나 UI 면에서는 개인적으로는 Visual VM이 더 났다고 판단하는데, JConsole의 경우에는 모니터링 대상 VM이 제공하는 MBean(JMX)를 모니터링 할 수 있다.

Tomcat이나 각종 자바 서버들은 MBean을 통해서 서버의 상태나 Configuration  정보를 저장하기 때문에 이 MBean 만 모니터링 하더라도 서버의 상태를 알아내는데 매우 유용하게 사용할 수 있다.

아래 그림은 JConsole을 이용하여, Tomcat MBean을 모니터링 하는 화면이다.

 


 


 2013.9.12 추가. Oracle JVM 7 최신 버전에 multiple JVM을 monitoring 할 수 있는 JMC 라는 것이 포함 되었음

http://www.oracle.com/technetwork/java/javase/2col/jmc-relnotes-2004763.html


 

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

'성능과 튜닝 > JVM' 카테고리의 다른 글

VisualVM을 이용한 JVM 모니터링  (0) 2013.09.05
G1GC Collector  (0) 2009.04.22
새로운 GC Collector G1.  (1) 2009.04.20
JVM 튜닝  (5) 2008.03.12
Sun JVM HeapDump 얻기  (1) 2007.11.28
-XX:PretenureSizeThreshold  (0) 2007.11.10

톰캣 튜닝

조대협


이번에는 톰캣 서버에 대한 튜닝 옵션에 대해서 한번 알아보자.

애플리케이션 관점에서의 튜닝도 중요하지만, 각 솔루션에 대한 특성을 업무 시나리오에 맞춰서 튜닝하는 것도 못지 않게 중요하다. 여기서 톰캣 튜닝을 설명하는 것은 톰캣 자체에 대한 튜닝 옵션을 소개하는 것도 목적이 있지만, 그보다 업무형태에 따라서 어떠한 접근을 해서 톰캣을 튜닝하는지를 소개하기 위함이다.

 

가정

여기서 튜닝 하는 톰캣은 HTTP/JSON형태의 REST 형태로 서비스를 제공하는 API 서버의 형태이다. 여러대의 톰캣을 이용하여 REST 서비스를 제공하며, 앞단에는 L4 스위치를 둬서 부하를 분산하며, 서비스는 stateless 서비스로 공유되는 상태 정보가 없다. 

server.xml 튜닝

톰캣의 대부분 튜닝 패러미터는 ${Tomcat_HOME}/conf/server.xml 파일에 정의된다.

몇몇 parameter를 살펴보도록 하자.

 

Listener 설정

 <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" /> 

이 옵션은 tomcat이 기동할 때, root 사용자이면 기동을 하지 못하게 하는 옵션이다. 서버를 운영해본 사람이라면 종종 겪었을 실수중의 하나가 application server root 권한으로 띄웠다가 다음번에 다시 실행하려고 하면 permission 에러가 나는 시나리오 이다. root 권한으로 서버가 실행되었기 때문에, 각종 config 파일이나 log 파일들의 permission이 모두 root로 바뀌어 버리기 때문에, 일반 계정으로 다시 재 기동하려고 시도하면, config 파일이나 log file들의 permission 이 바뀌어서 파일을 읽어나 쓰는데 실패하게 되고 결국 서버 기동이 불가능한 경우가 있다. 이 옵션은 이러한 실수를 막아 줄 수 있다.

 

Connector 설정

 

protocol="org.apache.coyote.http11.Http11Protocol"

먼저 protocol setting인데, Tomcat은 네트워크 통신하는 부분에 대해서 3가지 정도의 옵션을 제공한다. BIO,NIO,APR 3 가지이다. NIO Java NIO 라이브러리를 사용하는 모듈이고, APR Apache Web Server io module을 사용한다. 그래서 C라이브러리를 JNI 인터페이스를 통해서 로딩해서 사용하는데, 속도는 APR이 가장 빠른것으로 알려져 있지만, JNI를 사용하는 특성상, JNI 코드 쪽에서 문제가 생기면, 자바 프로세스 자체가 core dump를 내면서 죽어 버리기 때문에 안정성 측면에서는 BIO NIO보다 낮다. BIO는 일반적인 Java Socket IO 모듈을 사용하는데, 이론적으로 보면 APR > NIO > BIO 순으로 성능이 빠르지만, 실제 테스트 해보면 OS 설정이나 자바 버전에 따라서 차이가 있다. Default BIO이다.

 

acceptCount="10"

이 옵션은 request Queue의 길이를 정의한다. HTTP request가 들어왔을때, idle thread가 없으면 queue에서 idle thread가 생길때 까지 요청을 대기하는 queue의 길이이다. 보통 queue에 메세지가 쌓였다는 것은 해당 톰캣 인스턴스에 처리할 수 있는 쓰레드가 없다는 이야기이고, 모든 쓰레드를 사용해도 요청을 처리를 못한다는 것은 이미 장애 상태일 가능성이 높다.

그래서 큐의 길이를 길게 주는 것 보다는, 짧게 줘서, 요청을 처리할 수 없는 상황이면 빨리 에러 코드를 클라이언트에게 보내서 에러처리를 하도록 하는 것이 좋다. Queue의 길이가 길면, 대기 하는 시간이 길어지기 때문에 장애 상황에서도 계속 응답을 대기를 하다가 다른 장애로 전파 되는 경우가 있다.

순간적인 과부하 상황에 대비 하기 위해서 큐의 길이를 0 보다는 10내외 정도로 짧게 주는 것이 좋다.

 

enableLookups="false"

톰캣에서 실행되는 Servlet/JSP 코드 중에서 들어오는 http request에 대한 ip를 조회 하는 명령등이 있을 경우, 톰캣은 yahoo.com과 같은 DNS 이름을 IP주소로 바뀌기 위해서 DNS 서버에 look up 요청을 보낸다. 이것이 http request 처리 중에 일어나는데, 다른 서버로 DNS 쿼리를 보낸다는 소리이다. 그만큼의 서버간의 round trip 시간이 발생하는데, 이 옵션을 false로 해놓으면 dns lookup 없이 그냥 dns 명을 리턴하기 때문에, round trip 발생을 막을 수 있다.

 

compression="off"

HTTP message body gzip 형태로 압축해서 리턴한다. 업무 시나리오가 이미지나 파일을 response 하는 경우에는  compression을 적용함으로써 네트워크 대역폭을 절약하는 효과가 있겠지만, 이 업무 시스템의 가정은, JSON 기반의 REST 통신이기 때문에, 굳이 compression을 사용할 필요가 없으며, compression에 사용되는 CPU를 차라리 비지니스 로직 처리에 사용하는 것이 더 효율적이다.

 

maxConnection="8192"

하나의 톰캣인스턴스가 유지할 수 있는 Connection의 수를 정의 한다.

이 때 주의해야 할 점은 이 수는 현재 연결되어 있는 실제 Connection의 수가 아니라 현재 사용중인 socket fd (file descriptor)의 수 이다. 무슨 말인가 하면 TCP Connection은 특성상 Connection 이 끊난 후에도 바로 socket close 되는 것이 아니라 FIN 신호를 보내고, TIME_WAIT 시간을 거쳐서 connection을 정리한다. 실제 톰캣 인스턴스가 100개의 Connection 으로 부터 들어오는 요청을 동시 처리할 수 있다하더라도, 요청을 처리하고 socket close 되면 TIME_WAIT에 머물러 있는 Connection 수가 많기 때문에, 단시간내에 많은 요청을 처리하게 되면 이 TIME_WAIT가 사용하는 fd 수 때문에, maxConnection이 모자를 수 있다. 그래서 maxConnection은 넉넉하게 주는 것이 좋다.

이외에도 HTTP 1.1 Keep Alive를 사용하게 되면 요청을 처리 하지 않는 Connection도 계속 유지 되기 때문에, 요청 처리 수 보다, 실제 연결되어 있는 Connection 수가 높게 된다.

그리고, process당 열 수 있는 fd수는 ulimit -f 를 통해서 설정이 된다. maxConnection 8192로 주더라도, ulimit -f에서 fd 수를 적게 해놓으면 소용이 없기 때문에 반드시 ulimit -f 로 최대 물리 Connection 수를 설정해놔야 한다.

 

maxKeepAliveRequest="1"

HTTP 1.1 Keep Alive Connection을 사용할 때, 최대 유지할 Connection 수를 결정하는 옵션이다. 본 시나리오에서는 REST 방식으로 Connectionless 형태로 서비스를 진행할 예정이기 때문에, Kepp Alive를 사용하지 않기 위해서 값을 1로 준다.

만약에 KeepAlive를 사용할 예정이면, maxConnection과 같이 ulimit에서 fd수를 충분히 지정해줘야 하낟.

 

maxThread="100"

사실상 이 옵션이 가장 중요한 옵션이 아닌가 싶다. 톰캣내의 쓰레드 수를 결정 하는 옵션이다. 쓰레드수는 실제 Active User 수를 뜻한다. 즉 순간 처리 가능한 Transaction 수를 의미한다.

일반적으로 100 내외가 가장 적절하고, 트렌젝션의 무게에 따라 50~500 개 정도로 설정하는 게 일반적이다. 이 값은 성능 테스트를 통해서 튜닝을 하면서 조정해 나가는 것이 좋다.

 

tcpNoDelay="true"

TCP 프로토콜은 기본적으로 패킷을 보낼때 바로 보내지 않는다. 작은 패킷들을 모아서 버퍼 사이즈가 다 차면 모아서 보내는 로직을 사용한다. 그래서 버퍼가 4K라고 가정할때, 보내고자 하는 패킷이 1K이면 3K가 찰 때 까지 기다리기 때문에, 바로바로 전송이 되지 않고 대기가 발생한다.

tcpNoDelay 옵션을 사용하면, 버퍼가 차기전에라도 바로 전송이 되기 때문에, 전송 속도가 빨라진다. 반대로, 작은 패킷을 여러번 보내기 때문에 전체적인 네트워크 트래픽은 증가한다. (예전에야 대역폭이 낮아서 한꺼번에 보내는 방식이 선호되었지만 요즘은 망 속도가 워낙 좋아서 tcpNoDelay를 사용해도 대역폭에 대한 문제가 그리 크지 않다.)

 

Tomcat Lib 세팅

다음으로 자바 애플리케이션에서 사용하는 라이브러리에 대한 메모리 사용률을 줄이는 방법인데, 일반적으로 배포를 할때 사용되는 라이브러리(jar) *.war 패키지 내의 WEB-INF/jar 디렉토리에 넣어서 배포 하는 것이 일반적이다. 보통 하나의 war를 하나의 톰캣에 배포할 때는 큰 문제가 없는데, 하나의 톰캣에 여러개의 war 파일을 동시 배포 하게 되면, 같은 라이브러리가 각각 다른 클래스 로더로 배포가 되기 때문에, 메모리 효율성이 떨어진다.

그래서 이런 경우는 ${TOMCAT_HOME}/lib 디렉토리에 배포를 하고 war 파일에서 빼면 모든 war가 공통 적으로 같은 라이브러리를 사용하기 때문에 메모리 사용이 효율적이고, 또한 시스템 운영 관점에서도 개발팀이 잘못된 jar 버전을 패키징해서 배포하였다 하더라도, lib 디렉토리의 라이브러리가 우선 적용되기 때문에, 관리가 편하다.

반대로 war의 경우, war만 운영중에 재배포를 하면 반영이 가능하지만, lib 디렉토리의 jar 파일들은 반드시 톰캣 인스턴스를 재기동해야 반영되기 때문에, 이 부분은 주의해야 한다.

 

JVM Tuning

Java Virtual Machine 튜닝은 java 기반 애플리케이션에서는 거의 필수 요소이다.

-server

제일 먼저 해야할일은 JVM 모드를 server 모드로 전환하는 것이다. JVM 내의 hotspot 컴파일러도 클라이언트 애플리케이션이나 서버 애플리케이션이냐 에 따라서 최적화 되는 방법이 다르다.

그리고 메모리 배치 역시 클라이언트 애플리케이션(MS 워드와같은)의 경우 버튼이나 메뉴는 한번 메모리에 로드 되면, 애플리케이션이 끝날 때 까지 메모리에 잔존하기 때문에 Old 영역이 커야 하지만, 서버의 경우 request를 받아서 처리하고 응답을 주고 빠져서 소멸되는 객체들이 대부분이기 때문에, New 영역이 커야 한다.

이런 서버나 클라이언트냐에 대한 최적화 옵션이 이 옵션 하나로 상당 부분 자동으로 적용되기 때문에, 반드시 적용하기를 바란다.

 

메모리 옵션

앞에서도 설명하였듯이 JVM 튜닝의 대부분의 메모리 튜닝이고 그중에서도 JVM 메모리 튜닝은 매우 중요하다. 결국 Full GC 시간을 줄이는 것이 관건인데, 큰 요구 사항만 없다면, 전체 Heap Size 1G 정도가 적당하다. 그리고 New Old의 비율은 서버 애플리케이션의 경우 1:2 비율이 가장 적절하다. 그리고 PermSize class가 로딩되는 공간인데, 배포하고자 하는 애플리케이션이 아주 크지 않다면 128m 정도면 적당하다. (보통 256m를 넘지 않는다. 256m가 넘는다면 몬가 애플린케이션 배포나 패키징에 문제가 있다고 봐야 한다.)

그리고 heap size JVM에서 자동으로 늘리거나 줄일 수 가 있다. 그래서 -Xms -Xmx로 최소,최대 heap size를 정할 수 있는데, Server 시스템의 경우 항상 최대 사용 메모리로 잡아 놓는 것이 좋다. 메모리가 늘어난다는 것은 부하가 늘어난다는 것이고, 부하가 늘어날때 메모리를 늘리는 작업 자체가 새로운 부하가 될 수 있기 때문에, 같은 값을 사용하는 것이 좋다.

이렇게 JVM 메모리를 튜닝하면 다음과 같은 옵션이 된다.

-Xmx1024m Xms1024m -XX:MaxNewSize=384m -XX:MaxPermSize=128m

이렇게 하면 전체 메모리 사용량은 heap 1024m (이중에서 new 384m) 그리고 perm 128m 가 되고, JVM 자체가 사용하는 메모리가 보통 300~500m 내외가 되서 java process가 사용하는 메모리 량은 대략 1024+128+300~500 = 대략 1.5G 정도가 된다.

 

32 bit JVM의 경우 process가 사용할 수 있는 공간은 4G가 되는데, 이중 2G는 시스템(OS)이 사용하고 2G가 사용자가 사용할 수 있다. 그래서 위의 설정을 사용하면 32bit JVM에서도 잘 동작한다.

64 bit JVM의 경우 더 큰 메모리 영역을 사용할 수 있는데, 일반적으로 2G를 안 넘는 것이 좋다.(최대 3G), 2G가 넘어서면 Full GC 시간이 많이 걸리기 시작하기 때문에, 그다지 권장하지 않는다. 시스템의 가용 메모리가 많다면 Heap을 넉넉히 잡는 것보다는 톰캣 인스턴스를 여러개 띄워서 클러스터링이나 로드밸런서로 묶는 방법을 권장한다.

 

OutOfMemory

자바 애플리케이션에서 주로 문제가 되는 것중 하나가 Out Of Memory 에러이다. JVM이 메모리를 자동으로 관리해줌에도 불구하고, 이런 문제가 발생하는 원인은 사용이 끝낸 객체를 release 하지 않는 경우이다. 예를 들어 static 변수를 통해서 대규모 array hashmap reference 하고 있으면, GC가 되지 않고 계속 메모리를 점유해서 결과적으로 Out Of Memory 에러를 만들어낸다.

Out Of Memory 에러를 추적하기 위해서는 그 순간의 메모리 레이아웃인 Heap Dump가 필요한데, 이 옵션을 적용해놓으면, Out Of Memory가 나올때, 순간적으로 Heap Dump를 떠서 파일로 저장해놓기 때문에, 장애 발생시 추적이 용이하다.

-XX:-HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid<pid>.hprof

 

GC 옵션

다음은 GC 옵션이다. Memory 옵션 만큼이나 중요한 옵션인데, Parallel GC + Concurrent GC는 요즘은 거의 공식처럼 사용된다고 보면 된다. 이때 Parallel GC에 대한 Thread 수를 정해야 하는데, Thread수는 전체 CPU Core수 보다 적어야 하고, 2~4개 정도가 적당하다.

-XX:ParallelGCThreads=2 -XX:-UseConcMarkSweepGC

GC 로그 옵션

그리고 마지막으로 GC Log 옵션이다. 서버와 JVM이 건강한지 메모리상 문제는 없는지 GC 상황은 어떻게 디는지를 추적하려면 GC 로그는 되도록 자세하게 추출할 필요가 있다. GC로그를 상세하게 걸어도 성능 저하는 거의 없다.

-XX:-PrintGC -XX:-PrintGCDetails -XX:-PrintGCTimeStamps -XX:-TraceClassUnloading -XX:-TraceClassLoading

 

마지막에 적용된 TraceClassLoading은 클래스가 로딩되는 순간에 로그를 남겨준다. 일반적으로는 사용하지 않아도 되나, OutOfMemory 에러 발생시 Object가 아니라 class에서 발생하는 경우는 Heap dump로는 분석이 불가능 하기 때문에, Out Of Memory 에러시 같이 사용하면 좋다.

 

지금까지 간략하게 나마 톰켓 솔루션에 대한 튜닝 parameter 에 대해서 알아보았다. 사실 이러한 튜닝은 일반적인 개발자에게는 힘든 일이다. 해당 솔루션에 대한 많은 경험이 있어야 하기 때문에, 이런 parameter vendor의 기술 지원 엔지니어를 통해서 가이드를 받고, 성능 테스트 과정에서 최적화를 하고 표준화된 parameter를 정해서 사용하는 것이 좋다. Apache Tomcat의 경우에도 오픈소스이기는 하지만, Redhat등에서 기술 지원을 제공한다.

 

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

성능 엔지니어링에 대한 접근 방법

조대협


성능 개선, Performance Tuning, 용량 선정 과 같은 튜닝 관련 용어들은 모든 개발자나 엔지니어에게 모두 흥미가 가는 주제일 것이다. 그 만큼 소프트웨어에서 고성능을 내는 시스템은 만들기도 힘들뿐더러, 고성능 시스템이란 즉 잘 설계되고 구현된 소프트웨어를 뜻하는 것이니 관심을 가지는 것이 당연하지 않을까 싶다.

필자의 경우, 엔터프라이즈 시스템에 대한 약 6년간 장애 해결, 장애 회피 설계, 성능 개선, 고성능 시스템 설계 및 구현에 관련된 일을 해왔다.  특히 장애 해결과 성능 개선 작업은 하고 나면 뿌듯하기는 하지만, 특정한 기술이 필요하기 보다는 문제를 정의하고 접근하는 능력과 끝까지 목표를 달성할 때까지 지루한 작업을 반복적으로 할 수 있는 인내심을 필요로 하는 작업이다

이번 챕터에서는 Performance Engineering의 전반적인 접근 방법과, 용량 산정 방법 그리고 자바 기반의 서버 애플리케이션에 대한 성능 튜닝 및 병목 발견 방법에 대해서 설명하고자 한다.

Performance Engineering 의 정의와 범위

Performance Engineering은 시스템의 목표 성능 (응답 시간과 동시 접속자수)을 정의 하고, 이를 달성하기 위해서, 시스템의 구조를 반복적으로 개선하는 작업을  이야기 한다.

좁게 생각하면, 코드상의 병목을 잡고, 시스템의 설정(Configuration)을 바꿔서 성능을 올리는 튜닝으로 생각할 수 있지만, 성능 목표의 정의에서 부터, 최적의 성능을 내기 위한 디자인 및 구현과 같은 개발 초기의 설계 부분와 개발후의 운영단계에서 모니터링 까지 전과정을 포함한다.

Performance Engineering은 언제 해야 하는가?

Performance Engineering은 전체 소프트웨어 개발 과정에 걸쳐서 크게 아래와 같이 4단계에 걸쳐서 일어난다.



위의 개발 모델은 전형적인 Water fall model이다. 개발프로세스 챕터에서도 설명하였지만, 스크럼과 같은 애자일 방법론을 사용하더라도 큰 범위에서 개발 사이클은 Waterfall 모델과 크게 다르지 않게 된다. (각 단계별을 SPRINT 단위로 수행한다.)

   분석 단계

초기 요구 사항 분석 및 시스템 기획 단계에서는 성능에 대한 목표를 정해야 한다.

목표 응답시간은 어떻게 되는지, 시스템을 사용할 총 사용자수와 동시에 시스템을 사용하는 동시접속자 수가 어떻게 되는지와 같은 성능 목표를 정의한다.

또한 고려해야 하는 사항중의 하나는 성능 모델이다. 시스템에 부하가 어떤 패턴으로 들어오는지를 정의할 필요가 있다.

예를 들어 일반적인 웹컨텐츠 사이트의 경우 사용자가 들어와서 페이지 컨텐츠를 1~3분 내에 읽고 다른 페이지로 이동하다가, 20 여분 후에는 로그아웃하거나 다른 사이트로 이동한다. 즉 한 사용자의 체류 시간은 20분정도되며, 총 평균 20 페이지를 보는 트렌젝션을 발생 시키고 나간다고 할 수 있다. 한글로 만든 사이트이고, 육아나 주부를 대상으로 한 사이트라고 가정하면, 시스템의 부하는 한국 시간으로 아이들이 학교나 유치원을 간후인 10~11시와, 저녁시간대인 10~12시 사이에 몰린다고 가정할 수 있다.

다른 예로 게임 시스템을 예로 들어보자, 주로 초등학생을 타켓으로 한 게임이라면, 방과후 시간인 3~5시 대에 부하가 가장 몰릴 것이며, 게임의 종류에 따라 다르겠지만, 스타크래프트와 같은 게임의 경우 한번 플레이에 40분 정도 소요가 되고, 한 사용자가 하루에 두번정도 게임을 한다 가정 아래, 사용자당 체류 시간은 2시간, 게임 횟수는 2/. 그리고 주요 부하는 3~5시 오후대 라는 성능 모델을 만들 수 있다.

초기 성능 정의는 서비스의 종류(,게임,기업 시스템,쇼핑,뱅킹등) , 서비스를 사용하는 사용자층, 그리고 서비스를 사용하는 지역. 즉 전세계를 서비스하는 시스템이라면 시스템의 부하는 365,24시간 거의 다 걸린다고 봐야 한다. 그러나 한국만을 대상으로 서비스 하는 한국어로 된 사이트인 경우, 새벽 시간에는 일반적으로 로드가 없는 것과 같은 한국의 시간대에 영향을 받을 뿐만 아리나 명절,휴일,휴가와 같은 한국이라는 국가 특성에 따라 시스템의 부하가 영향을 받는다.

   디자인 단계

다음으로는 디자인 단계에서는 목표 성능과 용량을 달성할 수 있는 규모의 시스템으로 설계를 진행한다.

성능 관점에서 시스템 디자인은 항상 Peak Time (최대 성능)에 맞춰서 디자인이 된다. 최대 성능을 기반으로 전체 시스템이 받아낼 수 있는 용량과 응답 시간을 고려해야 한다.

특히 성능과 용량은 애플리케이션 디자인 뿐만 아니라 Technology selection에도 많은 영향을 받는다. 어떤 하드웨어를 사용할 것인지, 어떤 미들웨어나 프레임웍을 사용할 것인지이에 따라 용량과 성능의 차이가 많이 발생하기 때문에, 디자인 단계에서 부터 성능과 용량을 감안해서 시스템을 설계해야 한다.

하드웨어 관점에서는 예전에는 성능 모델을 산정한 후에, Peak Time 기준 (최대 성능 요구)으로 시스템을 설계하고, 하드웨어를 구매 하였으나, 근래에는 클라우드를 이용하여 필요시에만 하드웨어를 탄력적으로 사용하는 Auto Scale Out 모델을 많이 사용한다.

기업 내부의 업무 처럼 (예를 들어 이메일), 부하가 일정하고 예측이 가능한 경우에는 Fixed 된 사이즈의 하드웨어를 사용하도록 설계 하고, 출시 이벤트 행사 사이트와 같이 부하가 갑자기 몰리는 시스템의 경우 클라우드를 고려해보는 것도 권장할만 하다.

또한 빠른 응답 시간이 필요할 경우 SSD 디스크를 사용하거나, RAID 구성도 5보다는 1+0 등을 고려하는 등, 성능 모델에 따라서 적절한 하드웨어 선정과 구성 설계가 필요하다.

미들웨어나 프레임웍 관점에서도 정의된 성능 모델에 따라 적절한 제품군과  설계 구조를 채택해야 한다. 100,000 사용자 정도의 시스템 규모에서는 RDBMS 를 사용해도 성능이나 용량상에 문제가 없다. 그러나 50,000,000 사용자 정도를 지원해야 하는 시스템의 경우 그냥 RDBMS 를 사용할 수 없다. Sharding이나, NoSQL과 같은 다른 차원의 접근이 필요하다.

또한 빠른 응답 시간을 요구하는 경우 Redis Memcached와 같은 Cache 솔루션을 적극적으로 활용하거나, 미들웨어 부분에서는 Tomcat과 같은 일반적은 Web Application Server 보다는 Netty Vertex와 같은 고성능 미들웨어를 고려해볼 수 있다.

이러한 성능이나 용량에 관련된 제품 선정이나 설계는 돌려 보지 않으면 사실 확신을 가지기 어렵다. 그래서 가능하면, Technology selection 후에, 간단한 프로토타입을 구현한후에 시나리오가 단순한 대규모의 성능 및 용량 테스트를 해보는 PoC (Proof Of Concept)과 같은 작업을 이 단계에서 수행하는 것을 권장한다.

   개발단계

개발 단계는 개발프로세스 챕터에서 설명하였듯이, risk 가 높은 부분과 아키텍쳐에 관련되는 부분, 난이도가 높은 부분, 핵심 기능등을 개발 초기의 스프린트에서 개발한다.

초기 스프린트가 끝나고 릴리즈가 되서 성능 테스트가 가능한 QA나 스테이징 환경으로 시스템이 이전되면, Performance Engineering 역량을 이 단계에 집중하여, 시스템의 아키텍쳐와 모듈들이 성능 목표를 달성할 수 있는지 지속적으로 테스트하고 튜닝을 수행한다.

초기 단계에 성능 목표의 달성 가능 여부가 판단되어야, 아키텍쳐 변경이 가능하고, 주요 성능 이슈들을 초반에 발견해야, 발견된 성능 문제들에 대해서는 같은 문제가 발생하지 않도록 디자인 가이드나 코딩 가이드를 개발자들에게 배포하여 성능에 대한 위험도를 줄일 수 있다.

   최종 테스트 단계

앞의 단계에서 성능과 용량을 고려해서 설계가 되었고, 개발 초기 단계에서 성능과 용량 부분을 검증을 제대로 하였다면, 최종 테스트 단계에서는 개발된 최종 시스템에 대한 성능과 용량 부분의 측정과 미세 튜닝 (애플리케이션의 병목을 찾아서 부분적으로 수정하거나, 하드웨어나 미들웨어의 Configuration 하는 수준)을 하는 정도로 마무리가 되어야 한다.

이 과정에서는 실수로 잘못한 설정(configuration) 이나 잘못된 코딩으로 된 부분에 대해서 검증이 이뤄지는데, 이 경우에는 보통 2배에서 크게는 10배까지의 성능 향상이 이루어진다. 이런 경우는 대부분 실수에 의한 것이고 성능이 터무니 없이 낮게 나오기 때문에 찾기가 쉽다.

예를 들어 로그 파일을 NFS와 같은 리모트 디스크에 쓴다던지, Intel 계열의 CPU에서 하이퍼쓰레딩을 ON을 안했다던지와 같이 실수에 의한 경우가 많다.

이런 오류성의 문제들이 해결되면 실제 미세 튜닝에 들어가게 되는데, JVM 튜닝이나 톰캣의 설정 튜닝, SQL 튜닝들이 이루어지는데, 이 미세 튜닝을 통해서는 비약적인 성능향상은 이루어나지 않는다. 보통 20% 내외 정도 성능이 올라간다고 보면 된다.

   운영 단계

마지막으로 시스템이 운영 단계로 넘어가게 되면, 테스트시에 발견되지 않은 성능적인 문제가 있을 수 있기 때문에, 모니터링 도구를 사용하여 지속적으로 성능을 모니터링 하고, 성능상에 문제가 있는 부분을 지속적으로 수정해야 한다. 웹서버의 access로그에서 응답 시간을 모니터링 하거나, 제니퍼(http://www.jennifersoft.com)과 같은 전문적인 APM (Application Performance Monitoring)툴이나, Ganglia와 같은 시스템 모니터링 도구를 사용하면, 시스템의 성능 상태를 잘 알 수 있다.

더불어 용량 부분에 대해서도 운영단에서는 고민을 해야 하는데, 일반적으로 PEAK Time의 시스템 용량이 (CPU) 80% 정도에 다다르면, 시스템 용량 증설을 고려해야 한다.

그리고 업무에 특성에 맞게 미리미리 용량을 준비해놓는게 좋다. 예를 들어 대학의 수강 신청 시스템의 경우, 학기 시작하는 날에 부하가 폭주하기 때문에,  클라우드 기반일 경우 수강신청 전에 시스템 수를 미리 늘려놓는다던지, 클라우드가 아닌 경우, 수강 신청 기간을 앞뒤로 서버를 임대해서 용량을 늘려놓는 등의 대책을 미리 세워놓을 수 있다.

마지막으로, 운영 단계에서 Performance Engineering 관점으로 챙겨야 하는 부분은 운영 로그의 수집이다. 성능 및 용량 목표 설정은 매우 중요한 과정이다. 특히 용량 목표의 경우에는 기존의 업무 시스템의 사용 패턴을 분석 하는 것이 가장 효율적이기 때문에 운영 시스템의 로그를 수집하고 분석하여 운영 중인 업무 시스템의 성능 모델을 분석 및 보유 해놓는 것이 좋다.

시스템 용량 산정 (Capacity Planning)

  더 자세한 설명에 들어가기 앞서서, 성능에 관련된 용어와 함께 시스템의 목표 용량 산정 방법에 대해서 이야기 해보도록 하자.이 용어는 이 글에서 정의하는 의미의 용어이며, 다른 성능 이론에서 언급되는 용어와 다소 다를 수 있다.

l  Response Time (응답 시간) : 사용자가 서버에 요청을 한 시간에서 부터, 응답을 받을 때 까지의 모든 시간을 포함한다. 이 응답시간은 내부적으로 다음과 같이 조금 더 세분하게 분리된다.



Network Time (또는 Latency time). 서버에 요청을 했을때, Request를 보내고 받을 때 소요되는 네트워크 시간을 의미한다.

Transaction Time : 서버에서 실제 트렉젝션이 처리되는 시간을 의미 한다.

Think Time : 사용자가 요청에 대해서 응답을 받은 후에, 웹페이지를 보거나 화면을 보는 등의 작업을 하는 시간의 의미한다.

예를 들어 보면 한국의 사용자가 미국이 페이스북을 사용한다고 했을때, 사용자가 웹 브라우져에서 클릭을 하면, 요청이 서버로 도달할때 까지 걸리는 시간 Network time (Request), 서버가 요청을 받아서 처리를 하고, 응답을 하는 시간 (Transaction Time), 그리고 그 응답이 사용자의 브라우져 까지 도착하는 시간이 Network time (Response) 이다. 이 전체 시간을 합친 것이 Response Time이 된다.

응답을 받은 후에는 사용자가 페이스북 내용을 보는데 소요 되는 시간이 Think Time이 된다.

Think Time 까지 포함하여 다음 요청이 발생하기 까지의 전체 시간을 Request Interval 이라고 한다.

l  Concurrent User (동시 사용자) : 시스템을 현재 사용하고 있는 사용자를 정의한다. 웹사이트를 사용하기 위해서, 현재 브라우져를 열어놓고 웹사이트를 보고 있는 것과 같이 현재 시스템을 사용하고 있는 사용자 수를 의미 한다.



위의 그림을 보자, 5명의 사용자 A~E가 있다고 가정했을 때, 단위 시간 10분동안에 Transaction  Time Think Time중에 있는 사용자는 A,B,C 3명으로 해다 시간 10분간의 Concurrent User 3명이 된다.

l  Active User (액티브 사용자) : 현재 시스템에 트렌젝션을 실행하여 부하를 주고 있는 사용자를 정의한다.

기존에는 Concurrent User Active User간의 차이가 없었다. 이 개념은 웹이 생기면서 구체화된 개념인데, 웹 사이트를 사용하기 위해서 컴퓨터 앞에 앉아 있는다고 하더라도, 웹 페이지가 로딩 되는 순간에만 서버는 부하를 받고, 페이지가 웹 브라우져로딩 된 후에는 부하를 받지 않고 사용자는 로딩된 페이지를 보는데 시간이 발생한다. 이 시간동안에는 서버는 부하를 받지 않는다. 즉 시스템을 사용하기 위해서 웹 사이트를 열어 놓고 있는다 하더라도 지속적으로 서버에 부하를 주는 것이 아니기 때문에 Concurrent  User Active User 의 개념 차이가 발생한다.

Active User는 클릭을 발생시켜서 그 시간 당시에 서버에 트렌젝션을 발생 시키는 사용자를 의미한다.

Active User의 수는 서버에서 순간 실행되고 있는 Thread (쓰레딩 기반의 자바 서버의 경우) Process의 수와 같다. Active User의 수는 실제로 서버가 동시에 처리할 수 있는 트렌젝션의 양을 판단할 수 있는 기준이 되기 때문에 매우 중요한 성능 Factor가 된다.



위의 그림을 보자, 위의 그림에서 특정 순간에 있는 사용자는 총 5 명으로 Concurrent User 5명이지만, Transaction Time 구간중의 있는 사용자는 A,B,C , Active User 3명이 된다.

l  Transaction (트렌젝션) : Transaction이란, 사용자로 부터의 요청을 다루는 단위를 정의 한다. 이 정의가 상당히 중요한데, 성능 모델링이나 성능 테스트 시 이 Transaction의 정의에 따라서 시스템의 성능이 매우 다르게 정의 된다.

예를 들어서 사용자가 웹 페이지를 클릭했을때, 그 페이지에 대한 응답을 받는 것 까지를 하나의 트렌젝션이라고 정의 하자.

이 때, 웹페이지에는 서버에서 생생된 HTML 이외에, 여기서 참고 하는 리소스 즉, 이미지나 동영상, 자바 스크립트들이 들어있을 수 있다. 이 경우 트렌젝션에 대한 응답 시간을 측정할때, HTML 생성 이외에 이러한 리소스들을 로딩 하는 것 까지 하나의 트렌젝션으로 정의 해야 하느냐를 고려해야 한다.리소스에 로딩을 트렌젝션의 범위로 넣게 되면 전체 시스템의 응답 시간은 떨어지게 된다. (리소스를 로딩할 때 까지 기다려야 하니).

이러한 트렌젝션의 정의는 무엇을 판단 기준으로 할것인가에 따라 결정이 되는데, 예를 들어 리소스를 톰캣과 같은 WAS에서 처리하지 않고 앞단의 CDN이나 웹서버에서 처리할 경우 톰캣은 리소스에 대한 트렌젝션 요청을 받지 않기 때문에, 전체 시스템에서 비지니스 로직에 대한 처리 성능을 측정하고자 할 때는 리소스에 대한 로딩 시간을 계산하지 않고 트렌젝션을 정의 한다.  또한 리소스에 대한 로딩은 비지니스 로직에 대한 처리에 비해서 부하가 상대적으로 매우 적고, 일반적으로 브라우져에 캐쉬되기 때문에 보통 서버의 성능 측정시 이러한  리소스 로딩에 대한 부하는 트렌젝션의 단위로 처리하지 않는 경우가 많다.

l  TPS(Transaction Per Second) : 초당 처리할 수 있는 트렌젝션의 양을 정의 한다. 주로 서버의 성능 평가 기준이 된다.

Active 사용자가 순간 Transaction을 처리한다고 하면, 이를 목표 응답시간 (Response Time)으로 나눈 값이 목표 TPS가 된다. 예를 들어, Active User 50 명이고, 개당 Response Time 2초 라고 하면, 이 시스템의 TPS 25 TPS가 된다.
Network time이 미세하다고 판단하여, Network time 0으로 가정하여 계산

l  HPS(Hit Per Second) : 시스템이 처리할 수 있는 모든 웹 request의 초당 처리량이다. TPS가 비지니스 트렌젝션에 대한 처리 시간만을 정의 한다면, HPS는 리소스 (이미지, 자바스크립트)에 대한 request 처리량을 포함하기 때문에, TPS에 비해서 10~20 배 정도 높게 나온다.

l  Peak Time(피크 타임) : 서버가 순간적으로 가장 부하를 많이 받는 순간을 정의 한다. 보통 서버의 용량 산정이나 성능 설계는 이 시간의 부하량을 기준으로 한다

일반적인 업무 시스템의 경우, 출근 9~930분 사이가 가장 부하가 높다. 이 때 Peak (최고 정점)을 찍는 순간의 동시 사용자 수와 기준 응답 시간을 목표로 성능 목표를 정의 하는 것이 일반적이다.

위의 개념을 정리해서 공식화 해보자.

   TPS = (Active User) / (Average Response Time) – F1

   TPS = (Concurrent User) / (Request Interval) – F2

   Active User = TPS * (Average Response Time) – F3

   Active User = (Concurrent User) * (Average Response Time) / (Request Interval) – F4

   Active User = (Concurrent User) * (Average Response Time) / [ (Average Response Time) + (Average Think Time) ] – F5

예를 들어 Concurrent User 300명이고, 목표 응답시간이 3초 이내이며, Think Time 15초 인 시스템의 경우, F5 공식에 따라서 Active User 300*3/(3+15) = 50 이 되며, 시스템의 Thread 또는 적정 Process 양은 50개가 된다. 목표 TPS는 약 16.6 TPS가 된다.

위의 공식은 어디까지나 이론적인 공식이다. Network Latency 값은 가변적이며, Think Time 또한 유동적이다. 그러나 용량 산정에는 어느 정도의 산정 기준이 필요하기 때문에, 이 공식을 사용하면 대략적인 시스템에 대한 요구 용량을 예측할 수 있다.


Performance Engineering 의 절차

그러면 어떤 절차로 성능과 용량을 측정하고 개선하는 절차에 대해서 알아보도록 하자.

성능 목표와 모델의 정의

먼저 주요 업무 패턴이나, 튜닝의 대상이 되는 시나리오에 대한 개별 성능 목표를 정의 한다. 예를 들어 전체 성능 목표가 1,000 동시 사용자에 대해서 응답 시간 1초내의 시스템이 전체 성능 목표라고 가정하고, 전체 성능 목표를 대략 1,000 TPS (Transaction Per Second)라고 하자. 이것이 바로 성능 목표가 된다.

다음으로 성능 모델을 정의 해야 하는데, 해당 시스템의 주요 사용자 시나리오가 여러개 있을 때, 각 시나리오별의 사용 비중을 정의 해야 한다.

예를 들어 사진을 저장하는 클라우드 서비스 시나리오가 있다고 하면, 이 서비스의 주요 사용자 시나리오는

   로그인

   사진 리스트

   사진 업로드

   사진 보기

   사진 다운로드

   로드 아웃

등이 된다. 이 중에서 한 사용자가 실행하는 비율을 따져야 한다. 즉 사용자가 로그인 한후, 리스트 보기를 10, 업로드를 2, 보기를 5, 그리고 다운로드를 1번 한후에 로그 아웃 한다고 하자. 그러면 비율은 다음과 같이 된다. (전체 트렌젝션 횟수 1+10+2+5+1+1 = 20)

성능 모델 :로그인의 비율 5%, 리스트 보기 50%, 업로드 10%, 보기 25%, 로그아웃 5%

이 비율을 기준으로 복합 시나리오 (전체 시나리오를 함께 돌리는) 부하테스트를 수행하였을때, 1000 TPS가 나와야 하고, 각 개별 시나리오에 대해서 최소한, 로그인의 경우 1000 TPS 5% 50 TPS, 리스트 보기는 500 TPS를 상회 해야 한다.

부하 생성

성능 모델이 정의 되었으면, 이 모델에 따라서 부하를 생성해야 한다.

부하 생성 도구는 여러가지가 있다. 대표적인 오픈 소스 도구로는

가장 간단하게 쓸 수 있는 도구로는 Apache AB 라는 명령어 기반의 도구가 있으며, 복잡한 스크립트를 지원할 수 있는 도구로는 grinder apache JMeter 등이 있으며, NHN에서 grinder enhancement해서 만든 (GUI가 지원되는) nGrinder라는 도구가 있다.

근래에는 국내에서는 nGrinder라는 도구가 많이 사용되고 있다.

성능 모델이 단순하고, 테스트 시나리오가 간단할 경우에는 Apache ab 등으로도 가능하지만, 스크립트가 복잡해지는 경우에는 nGrinder와 같은 도구가 유리 하다.

또한 부하 생성에 사용되는 스크립트는 복잡도가 생각보다 높고, 향후 regression(회귀) 테스트에도 재 사용되기 때문에, 반드시 형상 관리 시스템을 통해서 (VCS) 관리 하는 것을 권장한다.

※ 자세한 부하 테스트에 대한 방법은 “4장 테스트의 시스템 테스트 부분을 참고하기 바란다.

 

※ 클라우드 컴퓨팅과 부하 테스트 툴 라이센스 모델에 대해서

 

예전에는 부하 테스트가 사내에서 사내에 있는 시스템을 대상으로 했었기 때문에 큰 문제가 없었다. 그러나 근래 들어서 클라우드 컴퓨팅을 사용하는 사례가 늘어남에 따라, 서비스 시스템이 회사 밖에 즉, 클라우드에 있는 경우가 많아 졌다.

상용 부하 테스트툴의 경우에는 부하 발생기의 위치와 툴 사용자에 대해서 제약을 두는 경우가 있는데, 툴을 구매했다 하더라도, 부하 테스터의 controller (부하 발생기 제외)는 반드시 사내에 있어야 하며, 사용자 역시 그 회사의 내부 직원으로만 한정하는 경우가 있다.

예를 들어, 내가 부하 테스트 도구를 서울에 있는 회사에서 구매하여, 이 툴을 Amazon 클라우드 미국에 설치하고 부하 테스트를 미국 지사 직원을 통해서 진행하는 것이 불가능 하다.

이 경우 부하 테스트 툴의 Controller는 한국 서울 사무소에 설치하고, 부하 생성기만 Amazon에 설치한후 한국 서울 사무소 직원을 통해서만 사용해야 한다.

 

간혹 (이럴리는 없어야 하겠지만) 부하 테스트 툴의 판매 회사 영업 사원이 이러한 사실을 제대로 통보하지 않아서, 툴을 잘 쓰다가 갑자기 영업 사원이 변경되거나, 부하 테스트 툴의 이전을 요청 하였을때, 갑자기 벤더로 부터, 추가 라이센스 구매 요청을 받을 수 있으니, 구매 전에 반드시 구매 조건에 사용 시나리오와  Controller 위치, 사용 주체 및 테스트 대상 시스테들에 대해서 명시적으로 기재 하고 구매 계약을 추진 하는 것이 좋다.

 

 

테스트 및 모니터링

부하 테스트 준비가 되었으면, 부하 테스트를 진행하고 진행중에 주요 성능 Factor에 대해서 지속적으로 모니터링 및 기록을 하여야 한다. 주로 모니터링해야하는 Factor들은 다음과 같다.



   애플리케이션 관점

가장 기본적으로 애플리케이션 즉 시스템의 성능을 측정 해야 한다. 주요 모니터링 Factor는 다음과 같다.

Response Time : Request 별 응답 시간

TPS (Throughput per second) : 초당 요청(Request) 처리량

Factor들이 궁극적으로 성능에 대한 최종 목표 값이 되기 때문에, 가장 중요한 성능 Factor가 되며, 부하 생성 도구를 통해서 손쉽게 측정할 수 있다.

   미들웨어 관점

미들웨어는 애플리케이션이 동작하기 위한 기본적인 솔루션이다.. Apache와 같은 웹서버나 Tomcat과 같은 Web Application 서버 , RabbitMQ와 같은 Message Queue, MySQL과 같은 데이타 베이스 등이 이에 해당한다.

각 성능 시나리오별로, 거쳐 가는 모든 미들웨어들을 모니터링해야 하는데, 이를 위해서는 각 솔루션에 대한 개별적인 깊은 이해가 필요하다.

웹서버의 경우 거의 성능 문제가 되는 부분은 없다. 성능 문제가 발생하는 부분은 대부분 Network outbound io (bandwidth)쪽이 되는 경우가 많다. 웹서버가 설치된 하드웨어의 network out bound bandwidth를 모니터링 하는 것이 유용하다.

대부분의 성능 문제는 실제 애플리케이션 로직이 수행되는 Tomcat과 같은 application server와 데이타 베이스단에서 많이 발생하는데, application server의 경우에는 Thread의 수와 Queue의 길이가 1차 모니터링 대상이 된다.

서버가 용량을 초과 하게 되면, Idle Thread수가 떨어지게 되고, Idle Thread 0이 되면 request message가 앞단의 queue에 저장되게 된다. 그래서 이 두 개를 모니터링 하면 시스템이 병목 상태인지 아닌지를 판단할 수 있다. 이 값들은 JMX (Java Management Extension) API를 이용하여 모니터링 하면 된다.

DB의 경우에는 slow query를 모니터링하면 특히 느리게 수행되는 쿼리들을 잡아서 튜닝할 수 있다. MySQL 5.6의 경우 slow queryhttp://dev.mysql.com/doc/refman/5.6/en/slow-query-log.html

를 사용하면 쉽게 잡아낼 수 있다.

Slow query를 찾았으면, EXPLAIN 명령어를 이용하여 query의 수행 내용을 분석한후 Index등의 튜닝을 수행할 수 있다.

http://dev.mysql.com/doc/refman/5.0/en/using-explain.html

   인프라 관점 : CPU, Memory, Network IO, Disk IO

다음으로 하드웨어 인프라에 대한 부분을 지속적으로 모니터링해줘야 하는데, 이는 하드웨어가 해당 성능을 내기 위해서 용량이 충분한지 그리고 하드웨어 구간에서 병목이 생기지는 않는지, 생긴다면 어느 구간에서 생기는지를 모니터링하여, 해당 병목 구간에 대한 문제 해결을 하기 위함이다.

인프라에 대한 모니터링은 Ganglia Cacti와 같은 전문화된 인프라 모니터링 도구를 사용하거나 top이나 glance, sar와 같은 기본적인 Unix/Linux 커맨드를 사용해서도 모니터링이 가능하다. (부하 테스트주에 top 등을 띄워놓고 모니터링을 하는 것이 좋다. Load Runner와 같은 상용 도구의 경우에는 부하 테스트 툴 자체에서 테스트 대상 시스템에 대한 하드웨어 사용률을 함께 모니터링할 수 있게 제공해준다.)

CPU : 일반적으로 CPU는 대부분 잘 모니터링 한다. 목표 성능을 달성할 시에는 보통 70~80% 정도의 CPU 를 사용하는 것이 좋고, 20~30%의 여유는 항상 가지고 가는 것이 좋다 이유는, 70~80% 정도의 CPU가 사용된 후에, 하드웨어를 물리적으로 늘리는 시간에 대한 여유 시간을 가지기 위함이다. 하드웨어는 특성상 주문을한다고 해도, 바로 그 시간에 증설을 할 수 있는 것이 아니고, CPU 100%가 되는 순간에는 이미 애플리케이션이 CPU 부족으로 제대로 작동을 하지 못하는 경우가 많기 때문에, 항상 여유를 남겨 놓고 성능 목표를 정의 하는 것이 좋다. 그래서 성능 목표를 잡을 때는 “CPU 70%, 500 TPS, 응답시간 1.5초 내외식으로 하드웨어에 대한 사용률을 포함하는 것을 권장한다.

Memory : 다음으로는 Memory 부분이다. Peak Time시에 Memory가 얼마나 사용되느냐가 중요한데, Java Application의 경우 특성상, 전체 JVM 프로세스가 사용할 메모리량을 미리 정해놓기 때문에, 부하 테스트 중에도 메모리 사용량 자체는 크게 변화하지 않는다. 다만 자주 놓치는 점이 swapping status 인데, Unix/Linux는 시스템의 특성상 물리 메모리 이상의 메모리를 제공하기 위해서 virtual memory 라는 개념을 사용하고 swapping space라는 디스크 공간에 자주 사용하지 않는 메모리의 내용을 dump해서 저장한 후 다시 사용할때 memory loading 하는 방식을 사용한다. 그런데 이 메모리의 내용을 디스크에 저장 및 로드 하는 과정 (swapping이라고 함)이 실제 disk io를 발생 시키기 때문에, 실제 메모리 access 성능이 매우 급격하게 떨어진다. 그래서 시스템에서 system에서 swapping이 발생하면 시스템의 성능이 장애 수준으로 매우 급격하게 떨어진다.

부하 테스트 중이나, 운영 중에 swapping이 발생하게 되면 전체 메모리 사용량을 줄이도록 튜닝을 하거나, 반대로 물리 메모리를 늘리는 증설 과정이 필요하다.

Disk IO : Disk IO는 파일 시스템에 파일을 저장하는 시나리오나, Log를 저장하는 모듈 그리고 데이타 베이스와 같이 뒷단에 파일 시스템을 필요로 하는 모듈에서 많이 발생을 한다. Ganglia와 같은 도구를 사용하면, IOPS (Input Out per Second - 초당 read/write등의 IO 발생 횟수)를 통해서 모니터링할 수 있고, 또는 iostat sar와 같은 명령어를 이용하면 iowait 를 통해서 디스크 IO pending이 발생할 경우 디스크 병목이 있는지 없는지를 확인할 수 있다.



Figure 1. iostat

또는 Process Disk IO iotop과 같은 툴을 사용하면 조금 더 상세한 정보를 얻을 수 있다.



Figure 2. iotop

[1]

Disk IO에 대한 Bottleneck은 여러가지 해결 방법이 있다. 먼저 하드웨어 인프라 ㅈ체에서 접근 하는 방식은, 디스크 자체를 SSD로 변경하거나, 버퍼가 크거나 RPM이 높은 디스크로 변경하는 방식, 인터페이스를 SATA에서 SAS SSD와 같은 높은 IO를 제공하는 디스크 인터페이스로 변경, Disk Controller iSCSI에서 FC/HBA와 같은 광케이블 기반의 고속 컨트롤러를 사용하는 방식 또는 RAID 구성을 Stripping 방식으로 변경해서 IO를 여러 디스크로 분산 시키는 방식 등이 있으며, 애플리케이션 차원에서는 데이타 베이스 앞에 memcache와 같은 캐슁을 사용하거나, 로깅의 경우에는 중간에 message queue를 써서 로그를 다른 서버에서 쓰도록 하여 IO를 분산하거나 또는 Back write와 같은 방식으로 로그 메세지가 발생할때 마다 disk writing 하는 것이 아니라 20 30개씩 한꺼번에 디스크로 flushing 하는 방식등을 이용할 수 있다.

또는 조금더 높은 아키텍쳐 레벨로는 디스크 IO가 많이 발생하는 로직의 경우 동기 처리에서 message queue를 사용하는 비동기 방식으로 시스템의 설계를 변경하는 방법을 고민할 수 있다. 예를 들어 사진을 올려서 변환하는 서비스의 경우 파일을 업로드 하는 시나리오와 변경하는 모듈을 물리적으로 분리하여, 파일 업로드가 끝나면, 사용자에게 동기 방식으로 바로 응답을 줘서 응답 시간을 빠르게 하고, 업로드된 파일은 뒷단에서 비동기 프로세스를 통해서 변환 과정을 다 끝낸 후에 사용자에게 변환이 끝나면 알려주는 방법을 사용할 수 있다.

Network IO: Network IO는 특히 고용량의 파일이나 이미지 전송에서 병목이 많이 발생하며, Reverse Proxy, NAT (Network address Translator), Router, Load Balancer 등에서 많이 발생한다. 여러가지 지점과 장비에 대해서 모니터링 해야 하기 때문에, 일반적인 unix/linux command 를 사용하는 방법보다는 Cacti Ganglia와 같은 RRD 툴이나 OpenNMS와 같은 NMS (Network Management System)을 사용하는게 좋다.

그래프를 보면서 추이를 지켜 보는 것이 중요한데, 부하를 넣으면 일정 수준이 되어도, 시스템들의 CPU나 메모리, Disk등의 기타 자원들은 넉넉한데, Network Input/Output이 일정 수준 이상으로 올라가지 않는 경우가 있다. 이 경우는 네트워크 구간의 병목일 가능성이 높다.

특히 소프트웨어 기반의 Load Balancer, 소프트웨어 기반의 NAT 장비에서 많이 발생하는데, 이미지와 같은 정적 컨텐츠는 가급적이면 CDN이나 분리된 Web Server를 이용해서 서비스 하도록 하는 것이 좋다. 클라우드의 경우에는 특히나 소프트웨어 기반의 NAT Load Balancer를 사용해서 문제가 되는 경우가 많은데, NAT의 경우에는 여러개의 NAT를 사용해서 로드를 분산하도록 하고, Load Balancer의 경우에도 충분히 큰 용량을 사용하거나 2개 이상의 Load Balancer를 배포한 후 DNS Round Robine등을 사용하는 방법을 고려 하는 것이 좋다.

개선 (Tuning)

병목을 찾았으면, 해당 병목 문제를 해결 및 반영해야 한다.

튜닝은 병목 구간이 발생하는 부분에 대한 전문적인 지식을 필요로 하지만, 기본적인 접근 방법은 거의 같다고 보면 된다.

   문제의 정의 : 성능 개선의 가장 기본은 문제 자체를 제대로 정의 하는 것이다. “그냥 느려요가 아니라, “성능 목표가 350TPS 1초내의 응답 시간인데, 현재 60 TPS 5초의 응답 시간에 WAS CPU 점유율이 100% 입니다.”와 같이 명확해야 하며, 문제점이 재현 가능해야 한다.

특히 재현 가능성은 매우 중요한 점인데, 테스트 환경이 잘못되었거나, 외부적 요인 예를 들어 부하 테스트 당시 네트워크 회선이 다른 테스트로 인하여 대역폭이 충분히 나오지 않았거나 했을 경우 결과가 그 때마다 다르게 나올 수 있다.

즉 문제 자체를 명확하게 정의할 필요가 있다.

   Break down : 다음으로는 문제가 발생하는 부분이 어떤 부분인지를 판단해야 한다. 시스템은 앞단의 로드밸런서나 미들웨어, 데이타 베이스와 같은 여러 구간에서 발생을 한다. 그렇기 때문에, 성능 저하의 원인이 정확하게 어느 부분인지를 인지하려면, 먼저 성능 시나리오가 어떤 어떤 컴포넌트를 거치는지를 명확하게 할 필요가 있다. 이 과정을 break down이라고 한다. 이 과정을 통해서 전체 성능 구간중, 어느 구간이 문제를 발생 하는지를 정의한다.

   Isolate : 다음으로는 다른 요인들을 막기 위해서, 문제가 되는 구간을 다른 요인으로 부터 분리 (고립) 시킨다. 물론 완벽한 분리는 어렵다. 애플리케이션이 동작하기 위해서는 데이타 베이스가 필수적으로 필요하다. 이 경우에는 데이타 베이스를 분리할 수 는 없다. 그러나 예를 들어 시나리오 자체가 로그인 시나리오이고 Single Sign On을 통해서 로그인 하는 시나리오라서 SSO 시스템과 연동이 되어 있다면, SSO 연동을 빼고 다른 mock up을 넣어서 SSO와의 연결성을 끊고 테스트를 하는 것이 좋다.

이렇게 문제에 대한 다른 요인과의 연관성을 최대한 제거 하는 작업이 isolation이다.

   Narrow down : 문제를 isolation을 시켰으면, 근본적인 문제를 찾기 위해서 문제의 원인을 파 내려간다. Profiling을 하거나, 코드에 디버그 정보를 걸어서 문제의 원인을 분석하는 과정을 narrow down이라고 한다. 특히나 이 narrow down 과정은 분석을 위한 여러가지 기법이나 도구들을 사용해야 하고, 현상에 대한 이해를 하기 위해서는 해당 솔루션이나 기술 분야에 대한 전문성은 필수적으로 필요하다.

   Bottleneck 발견 : Narrow down을 해서 문제의 원인을 계속 파해쳐 나가면 병목의 원인이 되는 근본적인 문제가 판별이 된다.

   해결 : 일단 병목의 원인을 찾으면 해결을 해야 하는데, 찾았다고 모두 해결이 되는건 아니다. 데이타 베이스 index를 걸지 않아서 index를 걸어주면 되는 간단한 문제도 있을 수 있지만, 근본적인 솔루션 특성이나 설계상의 오류로 인해서 문제가 발생하는 경우도 있다. 하드웨어를 늘려서 해결하는 방법도 있지만, 비지니스 시나리오 자체를 바꾸거나 UX 관점에서 해결 하는 방법도 고려할 수 있다. 예를 들어 로그인 화면이 넘어가는데 시간이 많이 걸린다고 했을때, 이 문제가 근본적으로 솔루션의 특성이라면 애플리케이션이나 솔루션 수정으로는 해결이 불가능하다. 이런 경우에는 모래 시계 아이콘이나 progress bar등을 넣어서 UX 관점에서 사용자로 하여금 체감되는 응답 시간에 대해서 느리지 않고 몬가 진행이 되고 있다고 보여주는 형태로 접근을 해서 문제를 해결할 수 도 있다.

간단한 예를 하나 들어보자. Drupal 이라는 웹 CMS 기반의 웹사이트가 있다고 하자. 성능 테스트를 수행하였는데, CPU 점유율이 지나치게 높게 나오고 응답 시간이 느리게 나왔다. 이것이 문제의 정의이다.

성능의 문제점을 찾아내기 위해서, 성능 테스트 시나리오를 검토하였다 성능 테스트 시나리오는 1) 로그인 페이지 로딩, 2) id,password post로 전송 3) 초기 화면으로 redirect 4) 로그 아웃 4가지 과정을 거치고 있었다. 1,2,3,4 과정의 응답시간을 각각 체크해서 보니, 2) 과정에서 성능의 대부분을 차지 하고 있음을 찾아 내었다. 전체적으로 성능이 안나오는 것을 인지한 후, 문제를 여러 구간으로 나누어서 접근 하는 것이 Break down이다.

2) 과정을 분석하기 위해서 성능 테스트를 다시 진행한다. 다른 시나리오가 영향을 주는 것을 방지하기 위해서, 1,3,4 시나리오를 제외 하고, 2 시나리오만 가지고 성능 테스트를 진행한다. 이렇게 문제점을 다른 변수로 부터 분리하여 고립 시키는 것을 isolation이라고 한다.

다음으로 Xhprof 라는 프로파일링 툴을 사용하여 로직중 어느 부분이 가장 성능 문제가 발생하는 지를 profiling 하였다. 대 부분의 성능 저하가 SQL 문장 수행에서 발생함을 찾아내었다. 이렇게 하나의 포인트를 깊게 들어 가면서 범위를 좁혀가는 것을 narrow down이라고 한다.

SQL 수행이 문제가 있음을 정의하고(문제의 정의), 어떤 SQL 문장이 수행되는지(Break down) 각각을 정의한후, 가장 수행 시간이 긴 SQL 문장을 찾아서 원인을 분석하였더니(narrow down) index 가 걸려 있지 않음을 찾아내었다.

해당 테이블에 index를 적용하고, 성능 테스트를 다시 수행하여 성능 목표치를 달성하였음을 해결하였다.

가상의 시나리오지만 성능 튜닝의 접근 방법은 대부분 유사 하다. 관건은 문제를 어떻게 잘 정의하고, 문제가 어떤 요소로 구성이 되어 있으며 각각이 어떤 구조로 동작을 하고 있는지 잘 파고 들어갈 수 있는 문제에 대한 접근 능력과, 점점 솔루션의 아랫부분(low level)로 들어갈 수 있는 전문성이 필요하다.

반복

튜닝이 끝났으면 다시 테스트 및 모니터링항목으로 돌아가서 성능 목표에 도달할때까지 위의 작업을 계속해서 반복해서 수행한다.

Performance Engineering을 위해 필요한 것들

그러면 성능 엔지니어링을 하기 위해서 필요한 것들은 무엇이 있을까? 먼저 도구 적인 측면부터 살펴보자.

   부하 테스트기 : 가장 기초적으로 필요한 것은 부하 발생 도구 이다. HP Load Runner와 같은 상용 도구에서 부터, nGrinder와 같은 오픈 소스 기반의 대규모 부하 발생 도구를 사용할 수 도 있고, SOAP UI같은 micro benchmark 테스트 툴을 이용해서 소규모 (50 사용자 정도)를 발생 시키거나 필요에 따라서는 간단하게 Python등의 스크립트 언어로 부하를 발생시킬 수 도 있다.

   모니터링 도구 : 다음으로는 모니터링 도구이다. 어느 구간이 문제가 있는지 현상이 어떤지를 파악하려면 여러 형태의 모니터링 도구들이 필요하다.

   프로파일링 도구 : 그리고, 문제되는 부분을 발견했을때, 그 문제에 대한 근본적인 원인을 찾기 위해서 프로파일링을 할 수 있는 도구들이 필요하다.

우리가 일반적으로 이야기 하는 프로파일링 도구들은 IDE와 같은 개발툴에서 debug 용도로 사용은 가능하지만, 대부분 대규모 부하 환경에서는 사용이 불가능한 경우가 많다.그래서 그런 경우에는 해당 시스템의 상태에 대한 스냅샷을 추출 할 수 있는 dump 도구들을 많이 사용하는데, unix process의 경우에는 ptrace를 통해서 system call을 모니터링 하거나, pmap을 이용하여 메모리 snapshot등을 추출할 수 도 있고, 자바의 경우에는 thread dump를 추출해서 병목 당시 애플리케이션이 무슨 동작을 하고 있었는지를 찾아낼 수 있다.

다음이 이 글에서 정말 언급하고 싶은 내용인데, 앞에서 도구를 언급했다면 다음은 엔지니어로써의 역량이나 지식적인 부분이다.

   역량 : 당연한 것이겠지만, 기술적인 역량은 필수적이다. netstat를 통해서 TCP 소켓이 FIN_WAIT가 발생하였는데, FIN_WAIT가 의미하는 것이 무엇인지 모르면 아무리 모니터링을 잘해도 소용이 없다. 기본적인 엔지니어로써의 컴퓨터와 프로그래밍, OS등에 대한 넓은 이해는 필수적이다.

   하드웨어 인프라, 미들웨어 , 애플리케이션에 대한 지식 : 다음은 사용하는 특정 솔루션에 대한 전문적인 지식이다. 톰캣의 내부 구조가 어떻게 되어 있으며, JVM의 동작원리가 어떻게 되는지와 같은 특정 지식인데, 사실 이러한 지식은 오랜 경험이나 습득할 시간이 없으면 가지기가 어렵다. 이런 경우는 해당 솔루션 제품 엔지니어를 통해서 지원을 받는 방법도 고려해볼만 하다.

   그리고 경험 : 성능 엔지니어링에 대한 경험인데, 대략 시스템의 상태마 봐도 어느 부분이 의심이 되는지 경험이 많은 엔지니어는 쉽게 접근을 한다. 성능 문제는 넓어보이기는 하지만, 결국 발생되는 패턴이 거의 일정하다. 그리고 특정 솔루션에 대한 지식이 없다하더라도, 문제에 대한 접근 하는 방법이나 모니터링 방법, 툴등은 사용법이 다르다 하더라도 그 의미하는 방법은 거의 비슷하기 때문에, 다른 기술로 구현되어 있는 시스템이라고 하더라도, 경험이 있는 엔지니어는 문제를 접근해서 풀어나가는 방식이 매우 익숙하다.

   마지막으로 인내심 : 그리고 마지막으로 강조하고 싶은 점이 인내심인데, 사실 성능 엔지니어링은 상당히 지루한 작업이다. 반복적인 테스트와 모니터링 및 분석을 거쳐야 하고, 해당 솔루션에 대한 전문적인 지식이 없을 경우에는 보통 제품 문제라고 치부하고 하드웨어 업그레이드로 가는 경우가 많은데, 어짜피 솔루션이라고 해도 소스코드로 만들어진 프로그램이다. 디컴파일을 하건, 덤프를 추출하건, 꾸준히 보고, 오픈 소스의 경우 소스코드를 참고해서 로직을 따라가다 보변, 풀어낼 수 있는 문제가 대부분이다. 결국은 시간과 인내심의 싸움인데, 꾸준하게 인내심을 가지고 문제를 접근하고 풀어나가는 것을 반복하면 문제는 풀린다.


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Apache Tomcat Tuning

성능과 튜닝/WAS 튜닝 | 2013.03.13 23:47 | Posted by 조대협

Tomcat Tuning Guide

 

Tomcat configuration $Tomcat/conf/server.xml

Assumption

This configuration is optimized for REST/HTTP API call. And it doesn’t use any reverse proxy like Apache, NginX etc. We will reside simple L4 switch infront of tomcat groups.

In addition we will not use Tomcat Clustering, Session etc. So the clustering configuration is omitted.

Listener Setting

 <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" /> 

checkedOsUser setting means Unix system user “root” cannot start Tomcat. If user starts tomcat as a root user it makes log file as a root user permission. In that case tomcat user cannot delete the log file.

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> 

This makes detect memory leak.

Connector Setting

protocol="org.apache.coyote.http11.Http11Protocol" 

It makes tomcat use BIO. Tomcat has options for IO (BIO,NIO,APR). APR is fastest IO setting. It uses Apache web server IO module, so it is fastest. But it uses C code (JNI call), it can have a risk to kill tomcat instance. (with core dump). APR is more faster about 10% than BIO. But BIO is more stable. Use BIO. (Default is BIO)

acceptCount="10"

It specifies server request queue length. If message is queued in the request queue, it means server cannot handle incoming message (it is overloaded). It will wait for idle thead and the request message will be pending. This setting reduce total size of request queue to 10. If the queue has been overflowed, client will get a error. It can protect server from high overload and let system manager to know the server has been overloaded.

enableLookups="false"

In Java Servlet Code, user can look up request message origin (IP or URL). For example user in yahoo.com send request to server, and Tomcat try to resolve incoming request IP address. “enableLooksups” option enables return DNS name not a IP address. During this processing Tomcat look up DNS. It brings performance degradation. This option removes DNS look up stage and increase performance.

compression="off" 

We are using REST protocol not a normal web contents like HTML,Image etc. This options allows to compress HTTP message. It consumes computing power but it can reduce network payload. In our environment compression is not required. It is better to save computing power. And in some particular Telco network, compression is not supported.

 connectionTimeout="10000"

It is HTTP Connection time out (client to server). It is milliseconds. (10,000 = 10 sec).

If server cannot make a connection from client til 10 sec. It will throw HTTP time out error. In normal situation, our API response time is under 5 sec. So 10 sec means, server has been overloaded. The reason why I increased the time up to 10 sec is, depends on network condition, connection time will be deferred.

maxConnections="8192"

The maximum number of connection, tomcat can handle. It means tomcat can handle maximum 8192 socket connection in a time. This value is restricted by Unix system parameter “ulimit –f” (You can check up in unix console)

maxKeepAliveRequests="1"

As I mentioned above, this configuration is optimized to REST API request not a common web system. It means client will send REST API call only. It sends the request and get a response. Client will not send request in a short time. It means we cannot reuse the connection from the client. So this setting turn of HTTP Keep Alive. (After response the request from client, tomcat disconnect the connection immediately)

maxThreads="100"

This defines total number of thread in Tomcat. It represents max number of active user at that time. Usually 50~500 is good for performance. And 100~200 is best (it is different depends on use case scenario).

Please test with 100 and 200 values and find value for performance. This parameter also get a impact from DB connection pool setting, even if we have a lot of thread , and the total number of db connection is not enough, the thread will wait to acquire the connection. 

tcpNoDelay="true"

This allows us to use TCP_NO_DELAY in tcp/ip layer. It makes send small packet without delay. In TCP, to reduce small package congestion, it gathers small packet to tcp buffer until it has been filled and send the packet. TCP_NO_DELAY option makes send small packet immediately even though TCP buffer is not full.

 

JVM Tuning

Java Virtual Machine tuning is also very important factor to run Tomcat

The focus of JVM tuning is reducing Full GC time.

-server

This option makes JVM to optimize server application. It tunes HotSpot compiler etc internally. This option is very important and mandatory in server side application

-Xmx1024m –Xms1024m -XX:MaxNewSize=384m -XX:MaxPermSize=128m

This memory tuning options, our infrastructure is using c1.mediuem amazon instance, so the available memory is about 1.7 gb total. Heap size is 1G and let them to have fixed size. It defines max 1Gb, min 1Gb heap size. The NewSize is 384mb (1/3 size of total heap size). 1/3 New Size is best performance usually. Perm size is defines area of memory to load class. 64mb is enough. But we will use 128m first time and tune based on gc log analysis later.

Total physical memory consumption is 1G heap + 128mb perm = 1.128 GB and JVM internally uses memory to run JVM itself. It consumes about 350~500mb. So total estimated required memory is about 1.128GB+500m = 1.5 GB.

As I mentioned, c1.mediuem size has only 1.7GB physical memory. If consumed memory exceeds actual physical memory, it makes disk swapping. If JVM memory is swapped out to disk, the performance is significantly degraded. Please take care swapping is not occurred.

-XX:-HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid<pid>.hprof

These options are for trouble shooting “OOM (Java Out Of Memory Error”. If out of memory error has been occurred. The memory layout will be dumped to disk. The location of dumpfile is specified by “-XX:HeapDumpPath” option

 -XX:ParallelGCThreads=2 -XX:-UseConcMarkSweepGC

These options specify GC strategy. It uses ParallelGC for Minor collection and 2 threads will be used for the Minor GC. And for Old area, concurrent gc will be used. It will reduce Full gc time

-XX:-PrintGC -XX:-PrintGCDetails -XX:-PrintGCTimeStamps -XX:-TraceClassUnloading -XX:-TraceClassLoading

These option specifies GC logging. It logs the GC log detail to stderr (console output). It shows usage trend os Java Heap memory, time stamp etc. (it contains old,new & perm area usage).

Especially, ClassLoading & UnLoading option show what class is loaded and unloaded to memory. It helps us to trace Perm Out of memory error.

 

Reference : http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

Logging

1.     모든 log catalina.out 하나의 파일에 모두 쌓이게 할것 à LogAppender Console Appender로 변경하면 된다.

     Tomcat 자체가 쓰는 로그 (별도로 지정 안하면 원래 Console로 나옴)

     Application에서 LogBack을 이용해서 로깅 되는 로그 (별도로 ConsoleAppender를 개발단에서 정의해줘야 함)

     GC 로그 (별도로 지정 안하면 원래 Console로 나옴)

DB Connection Pool Setting

Please use tomcat dbpcp connection pool. Apache-common connection pool is not updated frequently. Tomcat dbcp connection pool is updated well.

The basic setting guide is “Let number of connection in the pool to keep exact number of connection”. It can be done by set min conn and max conn to same number.

Total number of connections (including read + write) should be around. 70~80. We are using 100 threads in one tomcat instance. The 70~80% will use db connection at the same time.

Library Setting

Developer packaged java lib inside war file (WEB-INF/lib) it can increase usage of perm memory. And sometime can bring confusion about “which lib is actually used”. It means if same lib(jar file) resides in $TOMCAT_HOME/lib and WEB-INF/lib. The lib in $TOMCAT_HOME/lib wil be used and lib is WEB-INF/lib will be ignored.

To solve this problem. Remove common library like my-sql-jdbc driver and dbcp lib jar file from WEB-INF/lib and move it into $TOMCAT_HOME/lib

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Tomcat 7.0 Parameter Tuning

성능과 튜닝/WAS 튜닝 | 2013.03.01 00:51 | Posted by 조대협

server.xml

--

<?xml version='1.0' encoding='utf-8'?>

<Server port="8005" shutdown="SHUTDOWN">

  <!-- Security listener. Documentation at /docs/config/listeners.html -->

  <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" /> root 사용자로 start 하지 못하게 함

  <!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->

  <Listener className="org.apache.catalina.core.JasperListener" />

  <!-- Prevent memory leaks due to use of particular java/javax APIs-->

  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> 메모리 leak detector

  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />

  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />


  <!-- Global JNDI resources

       Documentation at /docs/jndi-resources-howto.html

  -->

  <GlobalNamingResources>

    <!-- Editable user database that can also be used by

         UserDatabaseRealm to authenticate users

    -->

    <Resource name="UserDatabase" auth="Container"

              type="org.apache.catalina.UserDatabase"

              description="User database that can be updated and saved"

              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"

              pathname="conf/tomcat-users.xml" />

  </GlobalNamingResources>


  <Service name="Catalina">


    <Connector port="8080" 

protocol="org.apache.coyote.http11.Http11Protocol" BIO 사용

acceptCount="30" back log를 30으로 제안

enableLookups="false" DNS 사용 금지. 

compression="off" 압축 금지 (REST 사용 및 Mobile 고려)

                connectionTimeout="10000" Connection time out 10초

maxConnections="8192" 

maxKeepAliveRequests="1"

maxThreads="100"

tcpNoDelay="true"

               redirectPort="8443" />

    <Engine name="Catalina" defaultHost="localhost">

      <Realm className="org.apache.catalina.realm.LockOutRealm">

        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"

               resourceName="UserDatabase"/>

      </Realm>


      <Host name="localhost"  

   appBase="webapps" <!-- /bin/apps/ -->

            unpackWARs="true" 

   autoDeploy="false">


        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"

               prefix="localhost_access_log." suffix=".txt"

               pattern="%h %l %u %t &quot;%r&quot; %s %b" />


      </Host>

    </Engine>

  </Service>

</Server>


--

Context에서 auto reload 부분 제외 해야 하고..
dbcp connection pool 튜닝 필요 --> dbcp 말고 tomcat pool 사용
context.xml에 설정
<Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
               maxActive="100" maxIdle="30" maxWait="10000"
               username="javauser" password="javadude" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/javatest"/>

애플리케이션 배포를 쉽게 하기 위해서, 위의 Context.xml은 war/META-INF/context.xml을 사용하는게 좋음


shared lib 처리 필요
JVM은 -server -Xms1024m -Xmx1024m -XX:NewSize=384m -XX:PermSize=128m + ParallelGC, GCThread=3, +PrintGCDetial
디렉토리 처리 (1 server , 1was를 기준으로 함)
tomcat 설치는 /usr/local/tomcat
JVM은 /usr/local/jvm
application은 /applications/war/
log는 /applications/log/access.log catalina.out 기타 application logs
기타 스크립트는 /applications/scripts
설정 파일 /appllications/properties

아니면
/product/j2ee/jdk
/product/j2ee/tomcat

/applications/j2ee/war
/applications/j2ee/logs
/applications/j2ee/scripts
/applications/j2ee/properties

아니면
/product/tomcat
/product/jdk
/product/tomcat7 (link)
/product/jdk1.6 (link)
/applications/tomcat/war
/applications/tomcat/logs/access.log, catalina.out
/applications/tomcat/scripts/deploy/fabfile
/applications/tomcat/scripts/logbackup/fabfile
/applications/tomcat/properties

boot up시 최신 war 파일을 repository에서 읽어오는 부분 추가

튜닝 하려면 한 2일 걸리겠네...




저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

LoadUI 테스트 결과

성능과 튜닝 | 2012.10.30 01:02 | Posted by 조대협

부하테스트툴로 고민중이던 Load UI를 테스트해봤다.

SOAPUI를 만든 업체의 제품이라서 신뢰가 갔는데.. 2012년 제품으로 제품의 성숙도에 의문이 있었다. 지금도 의문은 남아 있다.



전체적으로 Yahoo Pipe와 같이 Mash up의 느낌을 준다.

Wramp up 테스트, agent를 이용한 분산 환경 기반의 테스트등 기본적인 테스트와 Server Side 모니터링을 지원한다.

스크립트 언어로는 Groovy를 지원한다. 그리고 SOAP UI를 PLUG IN하여 부하 발생기로 사용할 수 있으며, Hudson과 같은 CI툴과 통합하여 regression 테스트 까지 지원한다.


전체적으로 기능 자체에는 부족함이 없어보인다.

Java 기반이고 UI는 Java FX를 사용해서 인지, Launch할때 좀 불안정하고, 전체적으로 대규모 테스트시에 UI가 Load Runner의 Windows Client에 비해서 불안정해 보인다.


Web Browser기반의 record&play기능이 없다.

오픈소스 버전과 Pro 버전이 있으며, pro 버전은 10,000USD이다.


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

부하테스트 툴 검토 결과

성능과 튜닝 | 2012.10.29 22:47 | Posted by 조대협

항상 성능 테스트를 준비할때 마다, 어떤 부하 테스트툴을 쓰느냐가 고민이다.

어느정도 틀이 갖춰진 조직이나, 예산이 충분한 경우에는 뒤도 안돌아보고 파트너 불러다가 Load Runner돌린다.


1. Load Runner

국내 인력도 많고, 다양한 시나리오에 대한 부하 테스트를 안정적으로 할 수 있어서 언제나 #1 옵션이지만, 비용이 살인적이다.


2. 기타 오픈소스

그래서 오픈소스를 검토해봤는데,

Apache JMeter의 경우 예전에 하도 실패를 많이 했고, 테스트 중 GC 발생도 그렇고, 툴 자체가 매우 세밀하게 사용을 해야 한다. 부하 테스트 툴이라기 보다는 부하테스트 프레임웍 정도로 보는게 맞지 않을까?

그 다음으로 예전에 Multimechnizer라는 툴을 고민했었는데, Erlang기반이고 Python으로 스크립트를 짠다. Erlang기반이라서 분산 환경을 기반으로한 대규모 부하테스트가 가능하고 신뢰성도 있었으나, Learning Curve가 커서 접어야 했던 제품

근래에 툴들을 리서치 해보니,

Grinder를 기반으로 해서, NHN이 Curbrid를 DB로 바꾸고, WEB UI를 사용한 nGrinder라는 제품을 국내에서는 꽤 많이 쓰는듯 하다. 오픈소스이긴 한데, 글로벌 개발팀(해외에 개발팀이 있는 경우)에는 기술 지원이나 자료 부족등의 이유 때문에 망설여지는 툴

다음으로 LoadUI라는 도구다. 라이센스가 대략 10,000USD정도 하는데, SOAPUI를 만들었던 회사의 제품이다. UI나 저작 도구가 매우매우 특이하다. Mashup 제품을 보는 느낌이라고 할까?

일단 기술 지원이 가능하니 심각하게 고려하고 있는 제품


이외에, 국내 블로그를 찾아보니 Apache AP나 HP HTTPerf(근래에 구글에 인수됨)이 있는데, 이 제품들은 웹서버에 성능 테스트에 최적화 되어 있다. 즉, 대규모의 HTTP 부하를 날리 수 는 있지만, HTTP message를 programatic하게 생성한다던가 (ID를 로직에 따라서 생성). HTTP Response를 비교하는 등의 기능이 없다. 단순하게 부하만 생성해서 날려 주는 거라서, 말 그대로 웹서버의 성능 측정은 가능할지 몰라도, 애플리케이션 로직에 대한 부하 테스트는 불가능 한것으로 보인다.


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

G1GC Collector

성능과 튜닝/JVM | 2009.04.22 12:15 | Posted by 조대협
Garbage First Collector 이하 G1 Collector는 예전에 잠깐 소개한적이 있는데,
오늘 지금 하는 프로젝트에서 메모리 관련 이슈가 있어서 잠깐 살펴보았는데, 대략 적인 원리는 다음과 같다.

CMS (Concurrent Mark & Sweep)의 원리는 
기존에, New 영역을 Old영역으로 보낸후에, Old 영역의 Object Tree를 Search해서 Dead Object를 Mark하고, Mark 된 Object들을 deallocate하는 방식이었다. 이 작업을 Concurrent Thread가 연속적으로 한다는 것인데, (Full GC 시간에 몰아서 하는 것이 아니라. ) 

G1 Collector는 메모리 구조 부터가 틀리다.
New/Old 영역을 Physical하게 따로 나누지 않고, 메모리를 Region이라는 일정 크기의 블럭으로 나눈다.
Region안에 있는 객체들이 다른 Region에 있는 객체들에 의해서 참조되는 지를 Remembered List를 이용하여 관리한다. 그리고, Region 단위로 Live Object가 있는지 없는지를 판단하여 해당 Regiion이 다 차면, 살아 있는 Object들을 다른 Region으로 copy한후 해당 Region을 모두 날려 버린다. 이러니 Fragmentation에 대한 문제가 많이 해결 된다.
 Fragmentation이 없기(?) 때문에, 처음에 메모리를 Allocation할때, 기존 메모리 관리모델에서는 그 크기의 공간이 어디 있는지를 Search해야 했지만, G1 Collector의 경우, Region 단위로 빈공간을 찾기 때문에, Allocation의 효율성이 매우 높다.

아직 상세하게 살펴보지는 못했는데, 대강의 원리는 이렇게 되는것 같고.. 엔터프라이즈 자바 애플리케이션에서 그간 메모리 관리로 인한 Concern이 많았는데, (2G이상의 Heap을 사용할때 GC성능) G1 Collector가 이 문제를 시원하게 풀어줬으면 좋겠다


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

'성능과 튜닝 > JVM' 카테고리의 다른 글

VisualVM을 이용한 JVM 모니터링  (0) 2013.09.05
G1GC Collector  (0) 2009.04.22
새로운 GC Collector G1.  (1) 2009.04.20
JVM 튜닝  (5) 2008.03.12
Sun JVM HeapDump 얻기  (1) 2007.11.28
-XX:PretenureSizeThreshold  (0) 2007.11.10

새로운 GC Collector G1.

성능과 튜닝/JVM | 2009.04.20 13:12 | Posted by 조대협
재미있는 소식이 있어서 포스팅합니다.
JDK 1.6 update 14에 G1이라는 형식의 Garbage collector가 추가됩니다.
기존에 CMS (Concurrent mark and sweep)과 는 다르게, Compaction을 사용하며, 특히 주목할만한 것은 Large Memory에서 latency time을 극소화 했다는 것입니다.
 쉽게 풀어서 설명하자면, 이제 GC 시간 때문에 대용량 Heap을 사용하지 못하는 일이 없어진다는 것입니다. 애플리케이션이 메모리에서 많이 자유로워 지는 것이지요...

 물론 뚜껑을 열어봐야 알겠지만, CMS옵션도 1.4에 release되어 1.5에는 꽤나 쓸만한 모습을 갖춘만큼. G1 Collector도 앞으로 많은 기대가 됩니다.

참고

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

'성능과 튜닝 > JVM' 카테고리의 다른 글

VisualVM을 이용한 JVM 모니터링  (0) 2013.09.05
G1GC Collector  (0) 2009.04.22
새로운 GC Collector G1.  (1) 2009.04.20
JVM 튜닝  (5) 2008.03.12
Sun JVM HeapDump 얻기  (1) 2007.11.28
-XX:PretenureSizeThreshold  (0) 2007.11.10
TAG g1, gc, JVM, Performance
저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Java File Writing 성능 비교

성능과 튜닝/자바 성능팁 | 2009.03.31 17:39 | Posted by 조대협
JAPM을 업그레이드 할까 싶어서 Log Writing 부분을 개선하고자 해서
File Writing을 어떻게 하는 것이 제일 빠를까 테스트를 해봤다.
크게 아래 케이스인데.

1. FileWriter fw = new FileWriter(LOG_HOME+"writer.log");
2. BufferedWriter bw = new BufferedWriter(new FileWriter(LOG_HOME+"writer.log"));
3. FileOutputStream fos = new FileOutputStream(LOG_HOME+"outputstream.log");
4. BufferedOutputStream fos =  new BufferedOutputStream(new FileOutputStream(LOG_HOME+"bufferedoutputstream.log"));
5. FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannel.log"))).getChannel(); + Byte Buffer 매번 생성
6. FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannel.log"))).getChannel(); + ByteBuffer 재사용

테스트의 정확성을 위해서 측정중에 GC가 발생하지 않도록 NewSize와 HeapSize를 크게 해놓고 테스트를 하였다. Vm 옵션은 -XX:NewSize=480m -ms768m -mx768m -verbosegc 이다.
환경 : Windows XP, Sun JVM 1.5, Centrio VPro Dual core, IBM Thinkpad X61s

결과는 다음과 같다.
   1K  2K  5K  10K  50K  
 FileWriter  31  32  94 203  1281  
 FileWriter + BufferedWriter  15  31  94  188  1000  
 FileOutputStream  32 47  109  188  1063  
 FileOutputStream + BufferedOutputStream  31  47  109  203  1578  
FileChannel  47  63  109  219  2906  
FileChannel + Byte Buffer 재사용 31 47 188 250 2766

(해당 레코드를 1000번씩 write)

예상하기로는 NIO의 FileChannel이 가장 빠를것이라 생각했는데, 의외로 FileWrite와 FileOutputStream의 성능이 높게 나왔다. NIO의 경우 JVM 벤더에 종속성이 있겠지만 이런 결과가 나오는것은 약간 의외였다.
 오히려 프로그래밍 기법을 내서 FileChannel을 사용할 경우 최대 3배까지 성능이 나쁜 경우가 올 수 있으니. 이런건 차라리 모르는게 나을 수 도 있겠다~~ 싶다.. ^^

BufferedWriter등의 경우 GC를 유발하는 문제가 있을 수 있기 때문에 또 다른 성능 저하 요인이 될 수 는 있겠지만, 현재까지 테스트 결과로는 Windows Platform에서는 FileWriter + BufferedWriter의 경우가 성능이 제일 좋은것 같다.
아래는 테스트에 사용한 소스 코드
==
package bcho.filewriter.test;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import junit.framework.TestCase;

public class FileWriterTest extends TestCase {
final static int loop = 1000;
final static String LOG_HOME="c:/temp/";
static String LOG_STRING="";
static {
}
public void testSuite() throws Exception{
int stringSize[]={100,200,500,1000,5000};
for(int i=0;i<stringSize.length;i++){
LOG_STRING="";
for(int j=0;j<stringSize[i];j++) LOG_STRING+="1234567890";
log(stringSize[i]+"0 bytes");
testFileWriter();
System.gc();
testBufferedWriter();
System.gc();
testFileOutputStream();
System.gc();
testFileBufferedOutputStream();
System.gc();
testFileChannel();
System.gc();
testFileChannelOneBuffer();
System.gc();
}
}
public  static void log(String str){
System.out.println(str);
}
// java.io.FileWriter
private void testFileWriter() throws Exception {
FileWriter fw = new FileWriter(LOG_HOME+"writer.log");
long st = Timer.getCurrentTime();
for(int i =0;i<loop;i++){
fw.write(LOG_STRING);
}
log("FileWriter :"+Timer.getElapsedTime(st) +"ms");
fw.close();
}
// java.io.BufferedWriter
private void testBufferedWriter() throws Exception {
BufferedWriter bw = new BufferedWriter(new FileWriter(LOG_HOME+"writer.log"));
long st = Timer.getCurrentTime();
for(int i =0;i<loop;i++){
bw.write(LOG_STRING);
}
log("BufferedWriter :"+Timer.getElapsedTime(st) +"ms");
bw.close();
}
// java.io.FileOutputStream
private void testFileOutputStream() throws Exception{
FileOutputStream fos = new FileOutputStream(LOG_HOME+"outputstream.log");
long st = Timer.getCurrentTime();
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
fos.write(buf);
}
log("FileOutputStream :"+Timer.getElapsedTime(st) +"ms");
fos.close();
}
// java.io.FileOutputStream
// + java.io.BufferedOutputStream
private void testFileBufferedOutputStream() throws Exception{
BufferedOutputStream fos = 
new BufferedOutputStream(
new FileOutputStream(LOG_HOME+"bufferedoutputstream.log"));
long st = Timer.getCurrentTime();
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
fos.write(buf);
}
log("FileBufferedOutputStream :"+Timer.getElapsedTime(st) +"ms");
fos.close();
}
private void testFileChannel() throws Exception {
FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannel.log"))).getChannel();
long st = Timer.getCurrentTime();
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
ByteBuffer bytebuffer = ByteBuffer.allocate(buf.length);
bytebuffer.put(buf);
bytebuffer.flip();
fc.write(bytebuffer);
}
log("FileChannel  :"+Timer.getElapsedTime(st) +"ms");
fc.close();
}
private void testFileChannelOneBuffer() throws Exception {
FileChannel fc =(new FileOutputStream(new File(LOG_HOME+"filechannelonebuf.log"))).getChannel();
int BUF_SIZE=1000;
long st = Timer.getCurrentTime();
ByteBuffer bytebuffer = ByteBuffer.allocate(BUF_SIZE);
for(int i=0;i<loop;i++){
byte[] buf = LOG_STRING.getBytes();
int offset=0;
int length= buf.length;
while(offset < length){
int chunkSize = BUF_SIZE > length-offset ? length-offset-1 : BUF_SIZE;
bytebuffer.put(buf, offset, chunkSize);
bytebuffer.flip();
offset+=BUF_SIZE;
fc.write(bytebuffer);
bytebuffer.clear();

}
}
log("FileChannel with reusing buffer:"+Timer.getElapsedTime(st) +"ms");
fc.close();
}


}


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

'성능과 튜닝 > 자바 성능팁' 카테고리의 다른 글

Java File Writing 성능 비교  (8) 2009.03.31
이번에는 앞에서 설명한 Aspect J와 JMX를 이용하여 초간단 APM을 만들었습니다.
JAPM (Java Application Performance Monitor)
상용 APM 툴 설치하기 어렵거나, 간단하게 Tracing하고 싶을때 사용하시면 됩니다.
소스코드와 설치 메뉴얼등을 첨부합니다.

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
JMX를 이용하여 Thread 별 CPU 시간을 측정하는 방법

==
 ThreadMXBean mbThread = (ThreadMXBean) ManagementFactory.getThreadMXBean();
 
 long[] ids = mbThread.getAllThreadIds();
 for (long id: ids)
 {
  System.out.println(mbThread.getThreadInfo(id).getThreadName());
  System.out.println(" CPU Time(" + id+ ")" + mbThread.getThreadCpuTime(id));
  System.out.println(" User Time(" + id+ ")" + mbThread.getThreadCpuTime(id));
 }

==
AOP에서 before에서 자기 Thread의 CPU 시간을 저장한후 after에서 다시 측정하여 결과를 빼면 해당 메서드의 CPU  사용 시간을 측정할 수 있다.

참고 : 단위는 nano second ( /1000,000 = milisecond)

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
AOP를 이용하면 특정 클래스의 메서드 실행 전후에 Code를 삽입할 수 있다.
사용자 애플리케이션의 비지니스 메서드에 Code를 삽입하여
1. 시작 시간, 종료시간을 측정하면 비지니스 메서드의 응답 시간을 측정할 수 있고
2. JMX를 이용하여 CPU 사용시간을 측정하면 비지니스 메서드의 CPU 사용률을 측정할 수 있다.
3. Thread Local 변수를 이용하면, Transaction이 시작될때부터 끝날때 까지를 측정할 수 있다. (예를 들어 JSP는 JSPServlet : service 메서드에서부터 시작되기 때문에, AOP에서 before advice에 transaction id를 set하게 하고, after advice에서 Trace내용을 flush하게 하면 된다. 그 중간에는 Thread Local에 Trace 정보를 저장하게 한다.)

초간단하게 작성한 AspectJ를 이용한 메서드별 응답시간 측정 프로그램이다.
Aspect J 1.5이상 JVM 1.5이상에서만 동작한다.

AbstractTracerAspect.aj 소스
==
package bcho.aspectj.aspects;

import bcho.aspectj.logger.xml.*;

public abstract aspect AbstractTracerAspect {
abstract pointcut scope();
pointcut targetMethod(): scope() && execution(* *(..));
Object around(): targetMethod(){
long startTime = System.currentTimeMillis();
try{
return proceed();
}finally{
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
Logger.log(thisJoinPoint,startTime,elapsedTime);
}// try
}
}
===

Logger.java 소스

==
package bcho.aspectj.logger.xml;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
public class Logger {
static public void log(JoinPoint point,long startTime,long elapsedTime){
Signature signature;
Object args[];
signature = point.getSignature();
args = point.getArgs();
System.out.println("["+startTime+"|signature] "+signature);
System.out.println("\tElapsed time :"+elapsedTime);
for(int i=0;i<args.length;i++){
System.out.println("\targ["+i+"] :"+args[i]);
}
}
}
==

코드 작성후 ajc로 컴파일 한후에 jar로 묶고 측정할 Application이 있는 디렉토리에 배포

%TRACER_HOME%/aspectjTracer.jar
%TRACER_HOME%/META-INF/aop.xml 을 다음과 같이 작성

==
 <aspectj>
    <aspects>
       <concrete-aspect name="myTracerAspect" extends="bcho.aspectj.aspects.AbstractTracerAspect">
          <pointcut name="scope" expression="within(bcho..*) &amp;&amp; !within(bcho.aspectj..*) " />
       </concrete-aspect>
    </aspects>
</aspectj>
==

expression 부분이 Weaving을 할 class에 대한 정의 
bcho.* 시작되는 모든 클래스를 profiling하되 bcho.aspectj.* 클래스는 제거하도록 설정함. (XML이기 때문에, &를 &amp;로 정의해야 함)

다음 CLASSPATH에 다음 내용을 추가함
set CLASSPATH=%TRACER_HOME%;%CLASSPATH%
set CLASSPATH=%TRACER_HOME%/aspectjTracer.jar;%CLASSPATH%

java 실행시 다음과 같이 -Xagent 옵션 추가

java -javaagent:%ASPECTJ_HOME%/lib/aspectjweaver.jar {실행할 JAVACLASS}

를 해주면 Runtime에 Weaving이 되면서 메서드 수행 시간을 trace해준다.

자세한 내용은 다 만들면 다시 정리하겠다... ^^






저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

JVM 튜닝

성능과 튜닝/JVM | 2008.03.12 11:48 | Posted by 조대협
진짜 오래전 문서인데..
JVM 튜닝 문서 링크
==
JVM GC와 메모리 튜닝




자바스터디 네트워크 [www.javastudy.co.kr]

조대협 [bcho_N_O_SPAM@j2eestudy.co.kr]




모든 Java Application은 JVM(Java Virtual Machine)위에서 동작한다.
이 JVM이 동작하는데 있어서, 메모리의 구조와 특히 GC는 Application의 응답시간과 성능에 밀접한 관계를 미친다. 이번 강좌에서는 JVM 의 메모리 구조와 GC 알고리즘 (JDK 1.4.X에 포함된 새로운 알고리즘 포함) 그리고, JVM의 메모리 튜닝을 통한 Application의 성능향상방법에 대해서 알아보도록 하자.


1.GC란 무엇인가?


GC는 Garbage Collection의 약자로 Java 언어의 중요한 특징중의 하나이다.
GC는 Java Application에서 사용하지 않는 메모리를 자동으로 수거하는 기능을 말한다.
예전의 전통적인 언어 C등의 경우 malloc, free등을 이용해서 메모리를 할당하고, 일일이 그 메모리를 수거해줘야했다. 그러나 Java 언어에서는 GC 기술을 사용함에 따라서 개발자로 하여금 메모리 관리에서 부터 좀더 자유롭게 해주었다.


2.GC의 동작 방법은 어떻게 되는가?


1) JVM 메모리 영역

GC의 동작 방법을 이해하기 위해서는 Java의 메모리 구조를 먼저 이해할 필요가 있다.
일반적으로 Application에서 사용되는 객체는 오래 유지 되는 객체보다, 생성되고 얼마안있어서 사용되지 않는 경우가 많다. <그림 1 참조>


<그림 1. 메모리 foot print>


그래서 Java에서는 크게 두가지 영역으로 메모리를 나누는데 Young 영역과 Old 영역이 그것이다.
Young 영역은 생긴지 얼마 안된 객체들을 저장하는 장소이고, Old영역은 생성된지 오래된 객체를 저장하는 장소이다. 각 영역의 성격이 다른 만큼 GC의 방법도 다르다.
먼저 Java의 메모리 구조를 살펴보자.


<그림 2. Java 메모리 구조>


Java의 메모리 영역은 앞에서 이야기한 두 영역 (Young 영역,Old 영역)과 Perm 영역 이렇게 3가지로 영역으로 구성된다.


<표 1. Java 메모리 영역>



2) GC 알고리즘

그러면 이 메모리 영역을 JVM이 어떻게 관리하는지에 대해서 알아보자.
JVM은 New/Young 영역과, Old영역 이 두영역에 대해서만 GC를 수행한다. Perm영역은 앞에서 설명했듯이 Code가 올라가는 부분이기 때문에, GC가 일어날 필요가 없다. Perm영역은 Code가 모두 Load되고 나면 거의 일정한 수치를 유지한다.


○ Minor GC
먼저 New/Young영역의 GC방법을 살펴보자 New/Young 영역의 GC를 Minor GC라고 부르는데, New/Young영역은 Eden과 Survivor라는 두가지 영역으로 또 나뉘어 진다. Eden영역은 Java 객체가 생성되자 마자 저장이 되는곳이다. 이렇게 생성된 객체는 Minor GC가 발생할때 Survivor 영역으로 이동된다.

Survivor 영역은 Survivor 1과 Suvivor2 영역 두 영역으로 나뉘어 지는데, Minor GC가 발생하면 Eden과 Survivor1에 Alive되어 있는 객체를 Suvivor2로 복사한다. 그리고 Alive되어 있지 않는 객체는 자연히 Suvivor1에 남아있게 되고, Survivor1과 Eden영역을 Clear한다. (결과적으로 Alive된 객체만 Survivor2로 이동한것이다.)
다음번 Minor GC가 발생하면 같은 원리로 Eden과 Survivor2영역에서 Alive되어 있는 객체를 Survivor1에 복사한다. 계속 이런 방법을 반복적으로 수행하면서 Minor GC를 수행한다.

이렇게 Minor GC를 수행하다가, Survivor영역에서 오래된 객체는 Old영역으로 옮기게 된다.

이런 방식의 GC 알고리즘을 Copy & Scavenge라고 한다. 이 방법은 매우 속도가 빠르며 작은 크기의 메모리를 Collecting하는데 매우 효과적이다. Minor GC의 경우에는 자주 일어나기 때문에, GC에 소요되는 시간이 짧은 알고리즘이 적합하다.

이 내용을 그림을 보면서 살펴보도록 하자.


<그림 3-1. 1st Minor GC>


Eden에서 Alive된 객체를 Suvivor1으로 이동한다. Eden 영역을 Clear한다.


<그림 3-2. 2nd Minor GC>


Eden영역에 Alive된 객체와 Suvivor1영역에 Alive된 객체를 Survivor 2에 copy한다.
Eden영역과 Suvivor2영역을 clear한다.


<그림 3-3. 3rd Minor GC>


객체가 생성된 시간이 오래지나면 Eden과 Suvivor영역에 있는 오래된 객체들을 Old 영역으로 이동한다.


○ Full GC

Old 영역의 Garbage Collection을 Full GC라고 부르며, Full GC에 사용되는 알고리즘은 Mark & Compact라는 알고리즘을 이용한다. Mark & Compact 알고리즘은 전체 객체들의 reference를 쭉 따라가다면서 reference가 연결되지 않는 객체를 Mark한다. 이 작업이 끝나면 사용되지 않는 객체를 모두 Mark가 되고, 이 mark된 객체를 삭제한다.<그림 4 참고> (실제로는 compact라고 해서, mark된 객체로 생기는 부분을 unmark된 즉 사용하는 객체로 메꾸어 버리는 방법이다.)

Full GC는 매우 속도가 느리며, Full GC가 일어나는 도중에는 순간적으로 Java Application이 멈춰 버리기 때문에, Full GC가 일어나는 정도와 Full GC에 소요되는 시간은 Application의 성능과 안정성에 아주 큰 영향을 준다.


<그림 4. Full GC>




3. GC가 왜 중요한가?


Garbage Collection중에서 Minor GC의 경우 보통 0.5초 이내에 끝나기 때문에 큰문제가 되지 않는다. 그러나 Full GC의 경우 보통 수초가 소요가 되고, Full GC동안에는 Java Application이 멈춰버리기 때문에 문제가 될 수 있다.
예를 들어 게임 서버와 같은 Real Time Server를 구현을 했을때, Full GC가 일어나서 5초동안 시스템이 멈춘다고 생각해보자.
또 일반 WAS에서도 5~10초동안 멈추면, 멈추는동안의 사용자의 Request가 Queue에 저장되었다가 Full GC가 끝난후에 그 요청이 한꺼번에 들어오게 되면 과부하에 의한 여러 장애를 만들 수 있다..
그래서 원할한 서비스를 위해서는 GC를 어떻게 일어나게 하느냐가 시스템의 안정성과 성능에 큰 변수로 작용할 수 있다.


4. 다양한 GC 알고리즘


앞에서 설명한 기본적인 GC방법 (Scavenge 와 Mark and compact)이외에 JVM에서는 좀더 다양한 GC 방법을 제공하고 그 동작방법이나 사용방법도 틀리다. 이번에는 다양한 GC 알고리즘에 대해서 알아보자. 현재 (JDK 1.4)까지 나와 있는 JVM의 GC방법은 크게 아래 4가지를 지원하고 있다.

- Default Collector
- Parallel GC for young generation (from JDK 1.4 )
- Concurrent GC for old generation (from JDK 1.4)
- Incremental GC (Train GC)

1) Default Collector
이 GC 방법은 앞에서 설명한 전통적인 GC방법으로 Minor GC에 Scavenge를, Full GC에 Mark & compact 알고리즘을 사용하는 방법이다. 이 알고리즘에는 이미 앞에서 설명했기 때문에 별도의 설명을 하지는 않는다.

JDK 1.4에서부터 새로 적용되는 GC방법은 Parallel GC와 Concurrent GC 두가지 방법이 있다. Parallel GC는 Minor GC를 좀더 빨리하게 하는 방법이고 (Throughput 위주) Concurrent GC는 Full GC시에 시스템의 멈춤(Pause)현상을 최소화하는 GC방법이다.

2) Parallel GC
JDK1.3까지 GC는 하나의 Thread에서 이루어진다. Java가 Multi Thread환경을 지원함에도 불구하고, 1 CPU에서는 동시에 하나의 Thread만을 수행할 수 밖에 없기때문에, 예전에는 하나의 CPU에서만 GC를 수행했지만, 근래에 들어서 하나의 CPU에서 동시에 여러개의 Thread를 실행할 수 있는 Hyper Threading기술이나, 여러개의 CPU를 동시에 장착한 HW의 보급으로 하나의 HW Box에서 동시에 여러개의 Thread를 수행할 수 있게 되었다.

JDK 1.4부터 지원되는 Parallel GC는 Minor GC를 동시에 여러개의 Thread를 이용해서 GC를 수행하는 방법으로 하나의 Thread를 이용하는것보다 훨씬 빨리 GC를 수행할 수 있다.


<그림 7. Parallel GC 개념도>


<그림 7> 을 보자 왼쪽의 Default GC방법은 GC가 일어날때 Thread들이 작업을 멈추고, GC를 수행하는 thread만 gc를 수행한다. (그림에서 파란영역), Parallel GC에서는 여러 thread들이 gc를 수행이 가능하기 때문에, gc에 소요되는 시간이 낮아진다.

Parallel GC가 언제나 유익한것은 아니다. 앞에서도 말했듯이 1CPU에서는 동시에 여러개의 thread를 실행할 수 없기 때문에 오히혀 Parallel GC가 Default GC에 비해서 느리다. 2 CPU에서도 Multi thread에 대한 지원이나 계산등을 위해서 CPU Power가 사용되기 때문에, 최소한 4CPU의 256M 정도의 메모리를 가지고 있는 HW에서 Parallel GC가 유용하게 사용된다.

Parallel GC는 크게 두가지 종류의 옵션을 가지고 있는데,Low-pause 방식과 Throughput 방식의 GC방식이 있다.

Solaris 기준에서 Low-pause Parallel GC는 ?XX:+UseParNewGC 옵션을 사용한다. 이 모델은 Old 영역을 GC할때 다음에 설명할 Concurrent GC방법과 함께 사용할 수 있다. 이 방법은 GC가 일어날때 빨리 GC하는것이 아니라 GC가 발생할때 Application이 멈춰지는 현상(pause)를 최소화하는데 역점을 뒀다.

Throughput 방식의 Parallel GC는 ?XX:+UseParallelGC (Solaris 기준) 옵션을 이용하며 Old 영역을 GC할때는 Default GC (Mark and compact)방법만을 사용하도록 되어 있다.Minor GC가 발생했을때, 되도록이면 빨리 수행하도록 throughput에 역점을 두었다.

그외에도 ParallelGC를 수행할때 동시에 몇개의 Thread를 이용하여 Minor영역을 Parallel GC할지를 결정할 수 있는데, -XX:ParallelGCThreads= 옵션을 이용하여 Parallel GC에 사용되는 Thread의 수를 지정할 수 있다.

3) Concurrent GC

앞에서도 설명했듯이, Full GC즉 Old 영역을 GC하는 경우에는 그 시간이 길고 Application이 순간적으로 멈춰버리기 때문에, 시스템 운용에 문제가 된다.

그래서 JDK1.4부터 제공하는 Concurrent GC는 기존의 이런 Full GC의 단점을 보완하기 위해서 Full GC에 의해서 Application이 멈추어 지는 현상을 최소화 하기 위한 GC방법이다.
Full GC에 소요되는 작업을 Application을 멈추고 진행하는것이 아니라, 일부는 Application이 돌아가는 단계에서 수행하고, 최소한의 작업만을 Application이 멈췄을때 수행하는 방법으로 Application이 멈추는 시간을 최소화한다.


<그림 8. Concurrent GC 개념도>


그림 8에서와 같이 Application이 수행중일때(붉은 라인) Full GC를 위한 작업을 수행한다. (Sweep,mark) Application을 멈추고 수행하는 작업은 일부분 (initial-mark, remark 작업)만을 수행하기 때문에, 기존 Default GC의 Mark & Sweep Collector에 비해서 Application이 멈추는 시간이 현저하게 줄어든다.

Solaris JVM에서는 -XX:+UseConcMarkSweepGC Parameter를 이용해 세팅한다.

4) Incremental GC (Train GC)

Incremental GC또는 Train GC라고도 불리는 GC방법은 JDK 1.3에서부터 지원된 GC방법이다. 앞에서 설명한 Concurrent GC와 비슷하게, 의도 자체는 Full GC에 의해서 Application이 멈추는 시간을 줄이고자 하는데 있다.

Incremental GC의 작동방법은 간단하다. Minor GC가 일어날때 마다 Old영역을 조금씩 GC를 해서 Full GC가 발생하는 횟수나 시간을 줄이는 방법이다.


<그림 9. Incremental GC 개념도>


그림 9에서 보듯이. 왼쪽의 Default GC는 FullGC가 일어난후에나 Old 영역이 Clear된다. 그러나, 오른쪽의 Incremental GC를 보면 Minor GC가 일어난후에, Old 영역이 일부 Collect된것을 볼 수 있다.

Incremental GC를 사용하는 방법은 JVM 옵션에 ?Xinc 옵션을 사용하면 된다.
Incremental GC는 많은 자원을 소모하고, Minor GC를 자주일으키고, 그리고 Incremental GC를 사용한다고 Full GC가 없어지거나 그 횟수가 획기적으로 줄어드는 것은 아니다. 오히려 느려지는 경우가 많다. 필히 테스트 후에 사용하도록 하자.

※ Default GC이외의 알고리즘은 Application의 형태나 HW Spec(CPU수, Hyper threading 지원 여부), 그리고 JVM 버전(JDK 1.4.1이냐 1.4.2냐)에 따라서 차이가 매우 크다. 이론상으로는 실제로 성능이 좋아보일 수 있으나, 운영환경에서는 여러 요인으로 인해서 기대했던것만큼의 성능이 안나올 수 있기 때문에, 실환경에서 미리 충분한 테스트를 거쳐서 검증한후에 사용해야 한다.


5. GC 로그는 어떻게 수집과 분석


JVM에서는 GC 상황에 대한 로그를 남기기 위해서 옵션을 제공하고 있다.
Java 옵션에 ?verbosegc 라는 옵션을 주면되고 HP Unix의 경우 ?verbosegc ?Xverbosegc 옵션을 주면 좀더 자세한 GC정보를 얻을 수 있다. GC 정보는 stdout으로 출력이 되기 때문에 “>” redirection등을 이용해서 file에 저장해놓고 분석할 수 있다.

Example ) java ?verbosegc MyApplication

그럼 실제로 나온 GC로그를 어떻게 보는지를 알아보자.


<그림 5. 일반적인 GC 로그, Windows, Solaris>


<그림 5>는 GC로그 결과를 모아논 내용이다. (실제로는 Application의 stdout으로 출력되는 내용과 섞여서 출력된다.)
Minor GC는 ”[GC “로 표기되고, Full GC는 “[Full GC”로 표기된다.
그 다음값은 Heap size before GC인데,GC 전에 Heap 사용량 ( New/Young 영역 + Old 영역 + Perm 영역)의 크기를 나타낸다.

Heap size after GC는 GC가 발생한후에 Heap의 사용량이다. Minor GC가 발생했을때는 Eden과 Survivor 영역으 GC가 됨으로 Heap size after GC는 Old영역의 용량과 유사하다.(Minor GC에서 GC되지 않은 하나의 Survivor영역내의 Object들의 크기도 포함해야한다.)

Total Heap Size는 현재 JVM이 사용하는 Heap Memory양이다. 이 크기는 Java에서 ?ms와 ?mx 옵션으로 조정이 가능한데. 예를 들어 ?ms512m ?mx1024m로 해놓으면 Java Heap은 메모리 사용량에 따라서 512~1024m사이의 크기에서 적절하게 늘었다 줄었다한다. (이 늘어나는 기준과 줄어드는 기준은 (-XX:MaxHeapFreeRatio와 ?XX:MinHeapFreeRation를 이용해서 조정할 수 있으나 JVM vendor에 따라서 차이가 나기때문에 각 vendor별 JVM 메뉴얼을 참고하기 바란다.) Parameter에 대한 이야기는 추후에 좀더 자세히하도록 하자.

그 다음값은 GC에 소요된 시간이다.

<그림 5>의 GC로그를 보면 Minor GC가 일어날때마다 약 20,000K 정도의 Collection이 일어난다. Minor GC는 Eden과 Suvivor영역 하나를 GC하는 것이기 때문에 New/Young 영역을 20,000Kbyte 정도로 생각할 수 있다.

Full GC때를 보면 약44,000Kbyte에서 1,749Kbyte로 GC가 되었음을 볼 수 있다. Old영역에 큰 데이타가 많지 않은 경우이다. Data를 많이 사용하는 Application의 경우 전체 Heap이 512이라고 가정할때, Full GC후에도 480M정도로 유지되는 경우가 있다. 이런 경우에는 실제로 Application에서 Memory를 많이 사용하고 있다고 판단할 수 있기 때문에 전체 Heap Size를 늘려줄 필요가 있다.

이렇게 수집된 GC로그는 다소 보기가 어렵기 때문에, 좀더 쉽게 분석할 수 있게 하기 위해서 GC로그를 awk 스크립트를 이용해서 정제하면 분석이 용이하다.


<표 2. gc.awk 스크립트>


이 스크립트를 작성한후에 Unix의 awk 명령을 이용해서

% awk ?f gc.awk GC로그파일명

을 쳐주면 아래<표 3>와 같이 정리된 형태로 GC 로그만 추출하여 보여준다.


<표 3. gc.awk 스크립트에 의해서 정재된 로그>


Minor와 Major는 각각 Minor GC와 Full GC가 일어날때 소요된 시간을 나타내며, Alive는 GC후에 남아있는 메모리양, 그리고 Freed는 GC에 의해서 collect된 메모리 양이다.

이 로그파일은 excel등을 이용하여 그래프등으로 변환해서 보면 좀더 다각적인 분석이 가능해진다.

※ JDK 1.4에서부터는 ?XX:+PrintGCDetails 옵션이 추가되어서 좀더 자세한 GC정보를 수집할 수 있다.


※ HP JVM의 GC Log 수집

HP JVM은 전체 heap 뿐 아니라 ?Xverbosegc 옵션을 통해서 Perm,Eden,Old등의 모든 영역에 대한 GC정보를 좀더 정확하게 수집할 수 있다.

Example ) java ?verbosegc ?Xverbosegc MyApplication ß (HP JVM Only)

HP JVM의 GC정보는 18개의 필드를 제공하는데 그 내용을 정리해보면 <표 4.>와 같다.

<GC : %1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12 %13 %14 %15 %16 %17 %18>


<표 4. HP JVM GC 로그 필드별 의미>


이 로그를 직접 보면서 분석하기는 쉽지가 않다. 그래서, HP에서는 좀더 Visual한 환경에서 분석이 가능하도록 HPJtune이라는 툴을 제공한다. 다음 URL에서 다운로드 받을 수 있다.

http://www.hp.com/products1/unix/java/java2/hpjtune/index.html


<그림 6. HP Jtune을 이용해서 GC후 Old영역의 변화 추이를 모니터링하는 화면>




6. GC 관련 Parameter


GC관련 설정값을 보기전에 앞서서 ?X와 ?XX 옵션에 대해서 먼저 언급하자. 이 옵션들은 표준 옵션이 아니라, 벤더별 JVM에서 따로 제공하는 옵션이기 때문에, 예고 없이 변경되거나 없어질 수 있기 때문에, 사용전에 미리 JVM 벤더 홈페이지를 통해서 검증한다음에 사용해야한다.

1) 전체 Heap Size 조정 옵션

전체 Heap size는 ?ms와 ?mx로 Heap 사이즈의 영역을 조정할 수 있다. 예를 들어 ?ms512m ?mx 1024m로 설정하면 JVM은 전체 Heap size를 application의 상황에 따라서 512m~1024m byte 사이에서 사용하게 된다. 그림2의 Total heap size

메모리가 모자를때는 heap을 늘리고, 남을때는 heap을 줄이는 heap growing과 shirinking 작업을 수행하는데, 메모리 변화량이 큰 애플리케이션이 아니라면 이 min heap size와 max heap size는 동일하게 설정하는 것이 좋다. 일반적으로 1GB까지의 Heap을 설정하는데에는 문제가 없으나, 1GB가 넘는 대용량 메모리를 설정하고자 할 경우에는 별도의 JVM 옵션이 필요한 경우가 있기때문에 미리 자료를 참고할 필요가 있다.

※ IBM AIX JVM의 경우
%export LDR_CNTRL=MAXDATA=0x10000000
%java -Xms1500m -Xmx1500m MyApplication

2) Perm size 조정 옵션

Perm Size는 앞에서도 설명했듯이, Java Application 자체(Java class etc..)가 로딩되는 영역이다. J2EE application의 경우에는 application 자체의 크기가 큰 편에 속하기 때문에, Default로 설정된 Perm Size로는 application class가 loading되기에 모자른 경우가 대부분이기 때문에, WAS start초기나, 가동 초기에 Out Of Memory 에러를 유발하는 경우가 많다.

PermSize는 -XX:MaxPermSize=128m 식으로 지정할 수 있다.
일반적으로 WAS에서 PermSize는 64~256m 사이가 적절하다.

3) New 영역과 Old 영역의 조정New 영역은 ?XX:NewRatio=2 에 의해서 조정이 된다.
NewRatio Old/New Size의 값이다. 전체 Heap Size가 768일때, NewRatio=2이면 New영역이 256m, Old 영역이 512m 로 설정이 된다.
JVM 1.4.X에서는 ?XX:NewSize=128m 옵션을 이용해서 직접 New 영역의 크기를 지정하는 것이 가능하다.

4) Survivor 영역 조정 옵션
-XX:SurvivorRatio=64 (eden/survivor 의 비율) :64이면 eden 이 128m일때, survivor영역은 2m가 된다.

5) -server와 ?client 옵션
JVM에는 일반적으로 server와 client 두가지 옵션을 제공한다.
결론만 말하면 server 옵션은 WAS와 같은 Server환경에 최적화된 옵션이고, client옵션은 워드프로세서와 같은 client application에 최적화된 옵션이다. 그냥 언뜻 보기에는 단순한 옵션 하나로보일 수 있지만, 내부에서 돌아가는 hotspot compiler에 대한 최적화 방법과 메모리 구조자체가 아예 틀리다.

○ -server 옵션

server용 application에 최적화된 옵션이다. Server application은 boot up 시간 보다는 user에 대한 response time이 중요하고, 많은 사용자가 동시에 사용하기 때문에 session등의 user data를 다루는게 일반적이다. 그래서 server 옵션으로 제공되는 hotspot compiler는 java application을 최적화 해서 빠른 response time을 내는데 집중되어 있다.

또한 메모리 모델 역시, 서버의 경우에는 특정 사용자가 서버 운영시간동안 계속 서버를 사용하는게 아니기 때문에 (Login하고, 사용한 후에는 Logout되기 때문에..) 사용자에 관련된 객체들이 오래 지속되는 경우가 드물다. 그래서 상대적으로 Old영역이 작고 New 영역이 크게 배정된다. <그림 7. 참조 >

○ -client 옵션

client application은 워드프로세서 처럼 혼자 사용하는 application이다. 그래서 client application은 response time보다는 빨리 기동되는데에 최적화가 되어 있다. 또한대부분의 client application을 구성하는 object는GUI Component와 같이 application이 종료될때까지 남아있는 object의 비중이 높기 때문에 상대적으로 Old 영역의 비율이 높다.


<그림 7. ?server와 ?client 옵션에 따른 JVM Old와 New영역>


이 두옵션은 가장 간단한 옵션이지만, JVM의 최적화에 아주 큰부분을 차지하고 있는 옵션이기 때문에, 반드시 Application의 성격에 맞춰서 적용하기 바란다.
(※ 참고로, SUN JVM은 default가 client, HPJVM는 default가 server로 세팅되어 있다.)

○ GC 방식에 대한 옵션

GC 방식에 대한 옵션은 앞에서도 설명했지만, 일반적인 GC방식이외에, Concurrent GC,Parallel GC,Inceremental GC와 같이 추가적인 GC Algorithm이 존재한다. 옵션과 내용은 앞장에서 설명한 “다양한 GC알고리즘” 을 참고하기 바란다.


7.JVM GC 튜닝


그러면 이제부터 지금까지 설명한 내용을 기반으로 실제로 JVM 튜닝을 어떻게 하는지 알아보도록 하자.

STEP 1. Application의 종류와 튜닝목표값을 결정한다.

JVM 튜닝을 하기위해서 가장 중요한것은 JVM 튜닝의 목표를 설정하는것이다. 메모리를 적게 쓰는것이 목표인지, GC 횟수를 줄이는것이 목표인지, GC에 소요되는시간이 목표인지, Application의 성능(Throughput or response time) 향상인지를 먼저 정의한후에. 그 목표치에 근접하도록 JVM Parameter를 조정하는것이 필요하다.

STEP 2. Heap size와 Perm size를 설정한다.

-ms와 ?mx 옵션을 이용해서 Heap Size를 정한다. 일반적으로 server application인 경우에는 ms와 mx 사이즈를 같게 하는것이 Memory의 growing과 shrinking에 의한 불필요한 로드를 막을 수 있어서 권장할만하다.

ms와mx사이즈를 다르게 하는 경우는 Application의 시간대별 memory 사용량이 급격하게 변화가 있는 Application에 효과적이다.
PermSize는 JVM vendor에 따라 다소 차이가 있으나 일반적으로 16m정도이다. Client application의 경우에는 문제가 없을 수 있지만, J2EE Server Application의 경우 64~128m 사이로 사용이 된다.

Heap Size와 Perm Size는 아래 과정을 통해서 적정 수치를 얻어가야한다.

STEP 3. 테스트 & 로그 분석.

JVM Option에 GC 로그를 수집하기 위한 ?verbosegc 옵션을 적용한다. (HP의 경우 ?Xverbosegc 옵션을 적용한다.)

LoadRunner나 MS Strest(무료로 MS社의 홈페이지에서 다운로드 받을 수 있다.)와 같은 Strest Test툴을 통해서 Application에 Strest를 줘서. 그 log를 수집한다. 튜닝에서 있어서 가장 중요한것은 목표산정이지만, 그만큼이나 중요한것은 실제 Tuning한 Parameter가 Application에 어떤 영향을 주는지를 테스트하는 방법이 매우 중요하다. 그런 의미에서 적절한 Strest Tool의 선정과, Strest Test 시나리오는 정확한 Tuning을 위해서 매우 중요한 요인이다.

○ Perm size 조정
아래 그림8.은 HP JVM에서 ?Xverbosegc 옵션으로 수집한 GC log를 HP Jtune을 통해서 graph로 나타낸 그래프이다. 그림을 보면 Application이 startup되었을때 Perm 영역이 40m에서. 시간이 지난후에도 50m 이하로 유지되는것을 볼 수 있다. 특별하게 동적 classloading등이 수십m byte가 일어나지 않는등의 큰 변화요인이 없을때, 이 application의 적정 Perm 영역은 64m로 판단할 수 있다.


<그림 8. GC 결과중 Perm 영역 그래프>


○ GC Time 수행 시간 분석

다음은 GC에 걸린 시간을 분석해보자. 앞에 강좌 내용에서도 설명햇듯이. GC Tuning에서 중요한 부분중 하나가 GC에 소요되는 시간 특히 Full GC 시간이다.

지금부터 볼 Log는 모社의 물류 시스템의 WAS 시스템 GC Log이다. HP JVM을 사용하며, -server ?ms512m ?mx512m 옵션으로 기동되는 시스템이다.

그림 9를 보면 Peak 시간 (첫번째 동그라미) 14시간동안에 Full GC(동그란점)가 7번일어난것을 볼 수 있다. 각각에 걸린 시간은2.5~6sec 사이이다.
여기서 STEP 1.에서 설정한 AP Tuning의 목표치를 참고해야하는데.

Full GC가 길게 일어나서 Full GC에 수행되는 시간을 줄이고자 한다면 Old 영역을 줄이면 Full GC가 일어나는 횟수는 늘어나고, 반대로 Full GC가 일어나는 시간을 줄어들것이다.

반대로 Full GC가 일어나는 횟수가 많다면, Old 영역을 늘려주면 Full GC가 일어나는 횟수는 상대적으로 줄어들것이고 반대로 Full GC 수행시간이 늘어날 것이다.

특히 Server Application의 경우Full GC가 일어날때는 JVM자체가 멈춰버리기 때문에, 그림 9의 instance는 14시간동안 총 7번 시스템이 멈추고, 그때마다 2.5~6sec가량 시스템이 response를 못하는 상태가 된것이다. 그래서 멈춘 시간이 고객이 납득할만한 시간인지를 판단해야 하고, 거기에 적절한 Tuning을 해야한다.

Server Application에서 Full GC를 적게일어나게하고, Full GC 시간을 양쪽다 줄이기 위해서는 Old영역을 적게한후에, 여러개의 Instance를 동시에 뛰어서 Load Balancing을 해주면, Load가 분산되기 때문에 Full GC가 일어나는 횟수가 줄어들테고, Old 영역을 줄였기 때문에, Full GC에 드는 시간도 줄어들것이다. 또한 각각의 FullGC가 일어나는동안 하나의 서버 instance가 멈춰져 있어도, Load Balancing이 되는 다른 서버가 response를 하고 있기때문에, Full GC로 인한 Application이 멈추는것에 의한 영향을 최소화할 수 있다.


<그림 9. GC 소요시간>


데이타에 따라서 GC Tuning을 진행한후에는 다시 Strest Test를 진행해서 응답시간과 TPS(Throughput Per Second)를 체크해서 어떤 변화를 주었는지를 반드시 체크해봐야한다.


<그림 10. GC후의 Old 영역>


그림 10은 GC후에 Old 영역의 메모리 변화량을 나타낸다.

금요일 업무시간에 메모리 사용량이 올라가다가. 주말에가서 완만한 곡선을 그리는것을 볼 수 있다. 월요일 근무시간에 메모리 사용량이 매우 많고, 화요일에도 어느정도 메모리 사용량이 있는것을 볼 수 있다. 월요일에 메모리 사용량이 많은것을 볼때, 이 시스템의 사용자들이 월요일에 시스템 사용량이 많을 수 있다고 생각할 수 있고, 또는 다른 주의 로그를 분석해봤을때 이 주만 월요일 사용량이 많았다면, 특별한 요인이나 Application 변경등이 있었는지를 고려해봐야할것이다.

이 그래프만을 봤을때 Full GC가 일어난후에도 월요일 근무시간을 보면 Old 영역이 180M를 유지하고 있는것을 볼 수 있다. 이 시스템의 Full GC후의 Old영역은 80M~180M를 유지하는것을 볼 수 있다. 그래서 이 시스템은 최소 180M이상의 Old 영역을 필요로하는것으로 판단할 수 있다.

STEP 4. Parameter 변경.
STEP 3에서 구한 각 영역의 허용 범위를 기준으로 Old영역과 New 영역을 적절하게 조절한다.
PermSize와 New영역의 배분 (Eden,Survivor)영역등을 조정한다.
PermSize는 대부분 Log에서 명확하게 나타나기 때문에, 크게 조정이 필요가 없고 New영역내의 Eden과 Survivor는 거의 조정하지 않는다. 가장 중요한것은 Old영역과 New 영역의 비율을 어떻게 조정하는가가 관건이다.

이 비율을 결정하면서, STEP1에서 세운 튜닝 목표에 따라서 JVM의 GC Algorithm을 적용한다. GC Algorithm을 결정하는 기본적인 판단 내용은 아래와 같다.



이렇게 Parameter를 변경하면서 테스트를 진행하고, 다시 변경하고 테스트를 진행하는 과정을 거쳐서 최적의 Parameter와 GC Algorithm을 찾아내는것이 JVM의 메모리 튜닝의 이상적인 절차이다.


지금까지 JVM의 메모리 구조와 GC 모델 그리고 GC 튜닝에 대해서 알아보았다.

정리하자면 GC 튜닝은 Application의 구조나 성격 그리고, 사용자의 이용 Pattern에 따라서 크게 좌우 되기때문에, 얼마만큼의 Parameter를 많이 아느냐 보다는 얼마만큼의 테스트와 로그를 통해서 목표 값에 접근하느냐가 가장 중요하다.
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

'성능과 튜닝 > JVM' 카테고리의 다른 글

G1GC Collector  (0) 2009.04.22
새로운 GC Collector G1.  (1) 2009.04.20
JVM 튜닝  (5) 2008.03.12
Sun JVM HeapDump 얻기  (1) 2007.11.28
-XX:PretenureSizeThreshold  (0) 2007.11.10
Out Of Memory에 대한 대처 방안  (0) 2007.10.08
TAG gc, JVM, 튜닝

Sun JVM HeapDump 얻기

성능과 튜닝/JVM | 2007.11.28 15:24 | Posted by 조대협

SUN JVM 에서

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid.hprof

 이런 옵션으로 떨어진 바이너리 heapdump 를 아래와 같이 분석툴로 분석이 가능합니다.

 1. HAT ( free 버전 ) : 정보가 좀 부족하게 나옵니다.

https://hat.dev.java.net/

 2. YourKit Java profiler ( 15 trial ) : 상용프로그램이라서 분석정보가 아주 좋습니다

http://www.yourkit.com/

===

신고
크리에이티브 커먼즈 라이선스
Creative Commons License

'성능과 튜닝 > JVM' 카테고리의 다른 글

새로운 GC Collector G1.  (1) 2009.04.20
JVM 튜닝  (5) 2008.03.12
Sun JVM HeapDump 얻기  (1) 2007.11.28
-XX:PretenureSizeThreshold  (0) 2007.11.10
Out Of Memory에 대한 대처 방안  (0) 2007.10.08
JVM 튜닝 옵션 정리  (0) 2007.10.08
TAG HeapDump, JVM
 

티스토리 툴바