[출처] How to find bottleneck in J2EE application|작성자 히트

J2ee application을 운영하다보면, 시스템이 극도로 느려지거나, 멈춰버리는 현상이 생기고는 한데, 분명히 개발하면서 테스트할때는 문제가 없었는데, 왜 이런일이 생기고, 어떻게 대처해야하는지에 대해서 알아보도록 하자.


일반적으로 J2ee application을 서비스 하기 위해서는 아래와 같은 구조를 가지게 된다.


<그림 1. 일반적인 J2ee application의 구조>


J2ee application의 동작에 필요한 구성 요소를 나눠보면 위와 같이 Network, User Application (이하 User AP), WAS 또는 Servlet Engine(이하 통칭해서 WAS),JVM과 RDBMS 이렇게 크게 다섯가지 조각으로 나뉘어 진다. 물론 JCA를 이용해서 Legacy와 연결할 수 도 있고, RMI/CORBA를 이용하여 다른 Architecture를 구현할 수 는 있으나, 이 강좌는 어디까지나 일반론을 설명하고자 하는것임으로 범위에서는 제외하겠다.


1. Hang up과 slow down현상의 정의


먼저 용어를 정의하도록 하자.. 시스템이 느려지거나 멈추는 현상에 대해서 아래와 같이 용어를 정의하자

    - Hang up : Server Instance는 실행되고 있느나, 아무런 응답이 없는 상황 (멈춤 상태)
    - Slowdown : Server Instance의 response time이 아주 급격히 떨어지는 상태 (느려짐)

이 Hangup과 slowdown현상은, 대부분이 그림 1에서 설명한 다섯가지 요소중 하나 이상의 병목으로 인해서 발생한다. 즉, 이 병목 구간을 발견하고, 그 구간을 제거하면 정상적으로 시스템을 운영할 수 있게 되는것이다.


2. Slow down analysis in WAS & User AP


2-1. WAS의 기본 구조

이 병목 구간을 발견하는 방법에 앞서서, 먼저 WAS 시스템의 기본적인 내부 구조를 이해할 필요가 있다.


<그림 2. WAS 시스템의 구조>


<그림 2>는 일반적인 WAS의 구조이다.
WAS는 Client로 부터 request를 받아서, 그 Request의 내용을 분석한다 JMS인지, HTTP , RMI request인지를 분석한후 (Dispatcher) 그 내용을 Queue에 저장한다.

Queue에 저장된 내용은 WAS에서 Request를 처리할 수 있는 Working Thread들이 있을때 각각의 Thread들이 Queue에서 Request를 하나씩 꺼내서 각각의 Thread들이 그 기능을 수행한다.

여기서 우리가 주의깊게 봐야하는것은 Thread pooling이라는 것인데.
Thread를 미리 만들어놓고, Pool에 저장한체로 필요할때 그 Thread 를 꺼내서 사용하는 방식이다. (Connection Pooling과 같은 원리이다.)

WAS 에서는 일반적으로 업무의 성격마다 이 Thread Pool을 나누어서 만들어놓고 사용을 한다. 즉 예를 들어서 금융 시스템의 예금 시스템과 보험 시스템이 하나의 WAS에서 동작할때, 각각의 업무를 Thread Pool을 나누어서 분리하는 방식이다. 이 방식을 사용하면 업무의 부하에 따라서 각 Thread Pool의 Thread수를 조정할 수 있으며, 만약에 Thread가 모자르거나 deadlock등의 장애사항이 생기더라도 그것은 하나의 Thread Pool에만 국한되는 이야기이기 때문에, 다른 업무에 영향을 거의 주지 않는다.

2-2. Thread Dump를 통한 WAS의 병목 구간 분석

위 에서 살펴봤듯이, WAS는 기본적으로 Thread 기반으로 작동하는 Application이다. 우리가 hang up이나 slow down은 대부분의 경우가 WAS에서 현상이 보이는 경우가 많다. (WAS에 원인이 있다는 소리가 아니라.. 다른 요인에 의해서라도 WAS의 처리 속도가 느려질 수 있다는 이야기다.)

먼저 이 WAS가 내부적으로 어떤 Application을 진행하고 있는지를 알아내면 병목 구간에 한층 쉽게 접근할 수가 있다.

1) What is thread dump?

이를 위해서 JVM에서는 Java Thread Dump라는 것을 제공한다.
Thread Dump란 Java Application이 작동하는 순간의 X-Ray 사진, snapshot을 의미한다.
즉 이 Thread dump를 연속해서 수번을 추출한다면, 그 당시에 Application이 어떻게 동작하고 진행되고 있는가를 살펴볼 수 있다.

Thread dump를 추출하기 위해서는
    - Unix 에서는 kill -3 pid
    - Windows 계열에서는 Ctrl + break
를 누르면 stdout으로 thread dump가 추출 된다.

Thread dump는 Application을 구성하고 있는 현재 구동중인 모든 Thread들의 각각의 상태를 출력해준다.


<그림 3-2. Thread dump>


그림 3-2는 전체 Thread dump중에서 하나의 Thread를 나타내는 그림이다.
Thread Dump에서 각각의 Thread는 Thread의 이름과, Thread의 ID, 그리고 Thread 의 상태와, 현재 이 Thread가 실행하고 있는 Prorgam의 스택을 보여준다.

- Thread name
각 쓰레드의 이름을 나타낸다.
※ WAS나 Servlet Engine에 따라서는 이 이름에 Thread Queue이름등을 배정하는 경우가 있다.

- Thread ID
쓰레드의 System ID를 나타낸다. 이 강좌 나중에 이 ID를 이용해서 각 Thread별 CPU 사용률을 추적할 수 있다.

- Thread Status
매우 중요한 값중의 하나로 각 Thread의 현재 상태를 나타낸다. 일반적으로 Thread가 사용되고 있지 않을때는 wait를 , 그리고 사용중일때는 runnable 을 나타내는게 일반적이다.
그외에, IO Wait나 synchronized등에 걸려 있을때 Wait for monitor entry, MW (OS별JVM별로 틀림) 등의 상태로 나타난다.

- Program stack of thread
현재 해당 Thread가 어느 Class의 어느 Method를 수행하고 있는지를 나타낸다. 이 정보를 통해서 현재 WAS가 어떤 작업을 하고 있는지를 유추할 수 있다.

2) How to analysis thread dump

앞에서 Thread dump가 무엇이고, 어떤 정보를 가지고 있는지에 대해서 알아봤다. 그러면 이 Thread dump를 어떻게 분석을 하고, System의 Bottle neck을 찾아내는데 이용할지를 살펴보기로 하자.

System 이 Hangup이나 slowdown에 걸렸을때, 먼저 Thread dump를 추출해야 한다. 이때 한개가 아니라 3~5초 간격으로 5개 정도의 dump를 추출한다. 추출한 여러개의 dump를 연결하면 각 Thread가 시간별로 어떻게 변하고 있는지를 판별할 수 있다.

먼저 각각의 Thread 덤프를 비교해서 보면, 각각의 Thread는 적어도 1~2개의 덤프내에서 연속된 모습을 보여서는 안되는게 정상이다. 일반적으로Application은 내부적으로 매우 고속으로 처리되기 때문에, 하나의 Method에 1초이상 머물러 있는다는것은 거의 있을 수 없는 일이다.
Thread Dump의 분석은 각각의Thread가 시간이 지남에 따라서 진행되지 않고 멈춰 있는 Thread를 찾는데서 부터 시작된다.

예를 들어서 설명해보자 . 아래 <그림 3-3>의 프로그램을 보면 MY_THREAD_RUN()이라는 메소드에서 부터 MethodA()aMethodB()aMethodC() 를 차례로 호출하는 형태이다.


<그림 3-3. sample code>


이 프로그램을 수행하는 중에 처음부터 Thread Dump를 추출 하면 대강 다음과 같은 형태일것임을 예상할 수 있다. 함수 호출에 따라서 Stack이 출력되다가 ◇의 단계 즉 MethodC에서 무한루프에 빠지게 되면 더이상 프로그램이 진행이 없기 때문에 똑같은 덤프를 유지하게 된다.



우리는 이 덤프만을 보고 methodC에서 무엇인가 잘못되었음을 유추할 수 있고, methodC의 소스코드를 분석함으로써 문제를 해결할 수 있다.

그렇다면 이제부터 Slow down과 Hang up현상을 유발하는 일반적인 유형과 그 Thread Dump의 모양에 대해서 알아보도록 하자.


[CASE 1] lock contention

잘 알다싶이 Java 는 Multi Threading을 지원한다. 이 경우 공유영역을 보호하기 위해서 Synchronized method를 이용하는데, 우리가 흔히들 말하는 Locking이 이것이다.

예 를 들어 Thread1이 Synchronized된 Method A의 Lock을 잡고 있는 경우, 다른 쓰레드들은 그 Method를 수행하기 위해서, 앞에서 Lock을 잡은 쓰레드가 그 Lock을 반환하기를 기다려야한다. <그림 3-4>


<그림 3-4. Thread 간에 Lock을 기다리는 형태>


만 약에 이 Thread 1의 MethodA의 수행시간이 아주 길다면 Thread 2,3,4는 마냥 이 수행을 아주 오랜 시간 기다려야하고, 다음 2번이 Lock을 잡는다고 해도 3,4번 Thread들은 1번과 2번 쓰레드가 끝난 시간 만큼의 시간을 누적해서 기다려야 하기때문에, 수행시간이 매우 느려지는 현상을 겪게 된다.

이처럼 여러개의 Thread가 하나의 Lock을 동시에 획득하려고 하는 상황을 Lock Contention 이라고 한다.


<그림 3-5. Lock Contention 상황에서 Thread의 시간에 따른 진행 상태>


이런 상황에서 Thread Dump를 추출해보면 <그림 3-6> 과 같은 형태를 띠게 된다.


<그림 3-6. Lock Contention에서 Lock을 기다리고 있는 상황의 Thread Dump>


그 림 3-6의 덤프를 보면 12번 Thread가 org.apache.axis.utils.XMLUtils.getSAXParser에서 Lock을 잡고 있는것을 볼 수 있고, 36,15,14번 쓰레드들이 이 Lock을 기다리고 있는것을 볼 수 있다.

[CASE 1] 해결방안

이 런 Lock Contention 상황은 Multi Threading환경에서는 어쩔 수 없이 발생하는 상황이기는 하지만, 이것이 문제가 되는 경우는 Synchronized Method의 실행 시간이 불필요하게 길거나, 불필요한 Synchronized문을 사용했을 때 발생한다.

그래서 이 문제를 해결하기 위해 불필요한 Sychronized Method의 사용을 자제하고, Synchronized block 안에서의 최적화된 Algorithm을 사용하는것이 필요하다.


[CASE 2] dead lock

이 Locking으로 인해서 발생할 수 있는 또 다른 문제는 dead Lock 현상이다. 두개 이상의 쓰레드가 서로 Lock을 잡고 기다리는 “환형대기조건” 이 성립되었을때, 서로 Lock이 풀리지 않고 무한정 대기하는 현상을 이야기 한다.

< 그림 3-7>을 보면 Thread1은 MethodA를 수행하고 있는데, sychronized methodB를 호출하기전에 Thread2가 methodB가 끝나기를 기다리고 있다. 마찬가지로 Therad2는 Thread3가 수행하고 있는 methodC가 끝나기를 기다리고 있고, methodC에서는 Thread1에서 수행하고 있는 methodA가 끝나기를 기다리고 있다.
즉 각각의 메소드들이 서로 끝나기를 기다리고 있는 “환형 대기조건” 이기 때문에 이 Application의 3개의 쓰레드들은 프로그램이 진행이 되지 않게 된다.


<그림 3-7. 환형 대기조건에 의한 deadlock>


이러한 “환형대기조건”에 의한 deadlock은 Thread Dump를 추출해보면 <그림 3-8> 과 같은 패턴을 띠우고 있으며 시간이 지나도 풀리지 않는다.


<그림 3-8. Deadlock이 걸렸을때 시간 진행에 따른 Thread의 상태>



<그림 3-9. Deadlock이 걸린 IBM AIX Thread Dump>


DeadLock 의 검출은 Locking Condition을 비교함으로써 검출할 수 있으며, 최신 JVM (Sun 1.4이상등)에서는 Thread Dump 추출시 만약 Deadlock이 있다면 해당 Deadlock을 찾아주는 기능을 가지고 있다.

< 그림 3-9>를 보자. IBM AIX의 Thread 덤프이다. 1)항목을 보면 현재 8번 쓰레드가 잡고 있는 Lock은 10번과 6번 쓰레드가 기다리고 있음을 알 수 있으며. Lock은 OracleConnection에 의해서 잡혀있음을 확인할 수 있다. (아래)



<그림 3-9>의 2)번 항목을 보면 이 6번쓰레드는 OracleStatement에서 Lock을 잡고 있는데, 이 Lock을 8번 쓰레드가 기다리고 있다. (아래)



결과적으로 6번과 8번은 서로 Lock을 기다리고 있는 상황이 되어 Lock이 풀리지 않는 Dead Lock상황이 된다.

[CASE 2] 해결 방안

Dead Lock의 해결 방안은 서로 Lock을 보고 있는것을 다른 Locking Object를 사용하거나 Lock의 방향 (Synchronized call의 방향)을 바꿔줌으로써 해결할 수 있다.

User Application에서 발생한 경우에는 sychronized method의 호출 순서를 “환형대기조건”이 생기지 않도록 바꾸도록 하고.
위와 같이 Vendor들에서 제공되는 코드에서 문제가 생긴경우에는 Vendor에 패치를 요청하도록 하여 해결한다.


[CASE 3] wait for IO response

다음으로 많이 볼 수 있는 패턴은 각각의 Thread들이 IO Response를 기다리는데… IO작업에서 response가 느리게 와서 시스템 처리속도가 느려지는 경우가 있다.


<그림 3-10. Thread들이 IO Wait를 할때 시간에 따른 Thread Dump 상황>



<그림 3-11. Thread가 DB Query Wait에 걸려 있는 stack>


< 그림 3-10> 상황은 DB에 Query를 보내고 response를 Thread들이 기다리고 있는 상황이다. AP에서 JDBC를 통해서 Query를 보냈는데, Response가 오지 않으면 계속 기다리게 있게 되고, 다른 Thread가 같은 DB로 보냈는데. Response가 오지 않고, 이런것들이 중복되서 결국은 사용할 수 없는 Thread가 없게 되서 새로운 request를 처리하지 못하게 되고, 기존에 Query를 보낸 내용도 응답을 받지 못하는 상황이다.

<그림 3-11>은 각각의 Thread의 stack dump인데, 그 내용을 보면 ExecuteQuery를 수행한후에, Oracle로 부터 데이타를 read하기 위해 대기 하는것을 확인할 수 있다.

이 런 현상은 주로 DB 사용시 대용량 Query(시간이 많이 걸리는)를 보냈거나, DB에 lock들에 의해서 발생하는 경우가 많으며, 그외에도 Socket이나 File을 사용하는 AP의 경우 IO 문제로 인해서 발생하는 경우가 많다.

[CASE 3] 해결 방안

이 문제의 해결 방안은 Thread Dump에서 문제가 되는 부분을 발견한후에, 해당 소스코드나 시스템등에 접근하여 문제를 해결해야한다.


[CASE 4] high CPU usage

시스템이 느려지거나 거의 멈추었을때, 우리가 초기에 해볼 수 있는 조치가 무엇인가 하면, 현재 시스템의 CPU 사용률을 체크해볼 필요가 있다.

해당 시스템의 CPU 사용률이 높은 경우, 문제가 되는경우가 종종 있는데. 이런 문제에서는 CPU를 많이 사용하는 모듈을 찾아내는것이 관건이다.

이 를 위해서 먼저 top 이나 glance(HP UX)를 통해서 CPU를 많이 점유하고 있는 Process를 판독한다. 만약 WAS 이외의 다른 process가 CPU를 많이 사용하고 있다면, 그 Process의 CPU 과사용 원인을 해결해야 한다. CPU 사용률이외에도, Disk IO양이 많지는 않은지.. WAS의 JVM Process가 Swap out (DISK로 SWAP되는 현상) 이 없는지 살펴보도록 한다. JVM Process가 Swapping이 되면 실행속도가 엄청나게 느려지기 때문에, JVM Process는 Swap out 되어버리면 안된다.

일단 CPU를 과 사용하는 원인이 WAS Process임을 발견했으면 프로그램상에 어떤 Logic이 CPU를 과점유하는지를 찾아내야한다.

방식을 정리해보면 다음과 같다.
WAS Process의 Thread별 CPU 사용률을 체크한다. ← 1)
Thread Dump를 추출한다. ← 2)

1)과, 2)에서 추출한 정보를 Mapping하여, 2)의 Thread Dump상에서 CPU를 과점유하는 Thread를 찾아내어 해당 Thread의 Stack을 분석하여 CPU 과점유하는 원인을 찾아낸다.

대부분 이런 요인을 분석해보면 다음과 같은 원인이 많다.

과도한 String 연산으로 인해서 CPU 사용률이 높아지는 경우

잘 알고 있다시피 String 연산은 CPU 를 아주 많이 사용한다. String과 Loop문(for,while등)의 사용은 CPU부하를 유발하는 경우가 많기 때문에 가급적이면 String Buffer를 사용하도록 하자.

과도한 RMI Cal
RMI 호출은 Java Object를 Serialize하고 Deserialize하는 과정을 수반하는데, 이는 CPU를 많이 사용하는 작업이기 때문에 사용에 주의를 요한다. 특별히 RMI를 따로 코딩하지 않더라도 EJB를 호출하는것이 Remote Call일때는 기본적으로 RMI호출을 사용하게 되고, 부하량이 많을때 이 부분이 주로 병목의 원인이 되곤한다. 특히 JSP/ServletaEJB 호출되는것이 같은 System의 같은 JVM Process안이라도 WAS별로 별도의 설정을 해주지 않으면 RMI Call을 이용하는 형태로 구성이 되기 때문에, 이에 대한 배려가 필요하다.
※ WebLogic에서 Call By Reference를 위한 호출 방법 정의

참 고로 WebLogic의 경우에는 Servlet/JSPaEJB를 호출하는 방식을 Local Call을 이용하기 위해서는 같은 ear 파일내에 패키징해야하고, EJB의 weblogic-ejb-jar.xml에 enable-call-by-reference를 true로 설정해줘야한다. (8.1이상)

자세한 내용은
http://www.j2eestudy.co.kr/lecture/lecture_read.jsp?table=j2ee&db=lecture0201_1&id=1&searchBy=subject&searchKey=deploy&block=0&page=0를 참고하시 바란다.

JNDI lookup

JNDI lookup은 Server의 JNDI에 Binding된 Object를 읽어오는 과정이다. 이 과정은 위에서 설명한 RMI call로 수행이되는데. 특히 EJB를 호출하기 위해서 Home과 Remote Interface를 lookup하는 과장에서 종종 CPU를 과점유하는 형태를 관찰 할 수 있다.

그래서 JNDI lookup의 경우에는 EJB Home Interface를 Caller Side(JSP/Servlet 또는 poor Java client)등에서 Caching해놓고 사용하는 것을 권장한다. 단. 이경우에는 EJB의 redeploy기능을 제약받을 수 있다.

다음은 각 OS별로 CPU 사용률이 높은 Thread를 검출해내는 방법이다.


■ Solaris에서 CPU 사용률이 높은 Thread를 검출하는 방법


1.prstat 명령을 이용해서 java process의 LWP(Light Weight process) CPU 사용률을 구한다.

% prstat -L -p [WeblogicPID] 1 1


<그림 3-6. Lock Contention에서 Lock을 기다리고 있는 상황의 Thread Dump>


2. pstack 명령어를 이용해서 native thread와 LWP 간의 id mapping을 알아낸다.
(※ 전에 먼저 java process가 lwp로 돌아야되는데, startWebLogic.sh에 LD_LIBRARY_PATH에 /usr/lib/lwp 가 포함되어야 한다.)

% pstack [WebLogicPID]


<그림 3-6. Lock Contention에서 Lock을 기다리고 있는 상황의 Thread Dump>


3. 1에서 얻은 LWP ID를 pstack log를 통해서 분석해보면 어느 Thread에 mapping되는지를 확인할 수 있다.
여기서는 LWP 8이 Thread 24과 mapping이 되고 있음을 볼 수 있다.

kill -3 [WebLogicPID]를 해서 ThreadDump를 얻어낸다.


<그림 3-6. Lock Contention에서 Lock을 기다리고 있는 상황의 Thread Dump>


Thread dump에서 nid라는 것이 있는데, 2에서 얻어낸 thread id를 16진수로 바꾸면 이값이 nid값과 같다. 즉 2에서 얻어낸 thread 24는 16진수로 0x18이기 때문에, thread dump에서 nid가 0x18인 쓰레드를 찾아서 어떤 작업을하고 있는지를 찾아내면 된다.



■ AIX Unix에서 CPU 사용률이 높은 Thread 검출해 내기


1. ps 명령을 이용하여, WebLogic Process의 각 시스템thread의사용률을 구한다.

% ps -mp [WeblogicPID] -0 THREAD



여기서 CP가 가장 높은 부분을 찾는다. 이 시스템 쓰레드가 CPU를 가장 많이 점유하고 있는 시스템 쓰레드이다. (여기서는 66723 이다.)

2. dbx 명령을 이용해서 1.에서 찾은 시스템 쓰레드의 Java Thread ID를 얻어온다.

1) % dbx -a [WebLogicPID]
2) dbx에서 “thread” 명령을 치면 Thread ID 를 Listing할 수 있다.



k-tid 항목에서 1에서 찾은 Thread ID 를 찾고, 그 k-tid에 해당하는 thread id를 찾는다. (여기서는 $t17이 된다.)

3) dbx에서 $t17 번 쓰레드의 Java Thread ID를 얻는다.
dbx에서 “th info 17” 이라고 치면 $t17번 쓰레드의 정보를 얻어온다.



pthread_t 항목에서 Java Thread ID를 얻는다. 여기서는 1011이된다.

3. Java Thread Dump에서 2에서 얻어온 Java Thread ID를 이용해서 해당 Java Thread를 찾아서 Java Stack을 보고 CPU를 많이 사용하는 원인을 찾아낸다.

1) kill -3 [WebLogicPID]
2) Thread dump를 보면 native ID 라는 항목이 있는데, 2.에서 찾은 Java Thread ID와 이 항목이 일치하는 Execute Thread를 찾으면 된다.





■ HP Unix에서 CPU 사용률이 높은 Thread 검출해내기


1. 먼저 JVM이 Hotspot mode로 작동하고 있어야 한다. (classic 모드가 아니어야 한다.) 옵션을 주지 않았으면 Hotspot 모드가 default이다.

2. glance를 실행해서 ‘G’를 누르고 WAS의 PID를 입력한다.
각 Thread의 CPU 사용률이 실시간으로 모니터링이 되는데.



여기서 CPU 사용률이 높은 Thread의 TID를 구한다.

3. kill -3 을 이용해서 Thread dump를 추출해서. 2에서 구한 TID와 Thread Dump상의 lwp_id가 일치하는 Thread를 찾으면 된다.



지 금까지 Thread Dump를 이용하는 방법을 간단하게 살펴보았다. 이 방법을 이용하면 WAS와 그 위에서 작동하는 Appllication의 Slow down 이나 hangup의 원인을 대부분 분석해낼 수 있으나, Thread Dump는 어디까지나 분석을 위한 단순한 정보이다. Thread Dump의 내용이 Slow down이나 hang up의 원인이 될수도 있으나, 반대로 다른 원인이 존재하여 그 결과로 Thread Dump와 같은 Stack이 나올 수 있기 때문에, 여러 원인을 동시에 살펴보면서 분석할 수 있는 능력이 필요하다.


3. Slow down in JVM


WAS의 성능에 큰 영향을 주는것중의 하나가 JVM이다.
JVM 의 튜닝 여부에 따라서 WAS상에서 작동하는 Ap의 성능을 크게는 20~30% 까지 향상시킬 수 있는데, 우리가 지금 살펴보고 있는 slow down과 hangup 을 일으키는 직접적인 요인이 되는것은 JVM의 Full GC이다.

간단하게 JVM의 메모리 구조를 검토하고 넘어가보도록 하자.

<그림 4-1. JVM의 메모리 구조>


JVM은 크게 New영역과 Old영역, 그리고 Perm영역 3가지로 분류가 된다.
Perm 영역은 Class나 Method들이 로딩되는 영역이고 성능상의 영향을 거의 미치지 않는다.

우리가 주목해야할 부분은 객체의 생성과 저장에 관련되는 New와 Old 영역인데, 모든 객체는 생성이 되자 마자 New 영역에 저장되고, 시간이 지남에 따라 이 객체들은 Old 영역으로 이동이 된다.

New 영역을 Clear하는 과정을 Minor GC라하고, Old 영역을 Clear하는 과정은 Major GC또는 Full GC라 하는데, 성능상의 문제는 이 Full 영역에서 발생한다.

Minor GC의 경우는 1초 이내에 아주 고속으로 이뤄지는 작업이기 때문에, 신경을 쓸 필요가 없지만, Full GC의 경우에는 시간이 매우 오래걸린다.
또 한 Full GC가 발생할 동안은 Application이 순간적으로 멈춰 버리기 때문에 시스템이 순간적으로 Hangup 으로 보이거나 또는 Full GC가 끝나면서 갑자기 request가 몰려버리는 현상 때문에 종종 System의 장애를 발생시키는 경우가 있다.

Full GC는 통상 1회에 3~5초 정도가 적절하고, 보통 하루에 JVM Instance당 5회 이내가 적절하다고 여겨진다. (절대 값은 없다.)

Full GC가 자주 일어나는것이 문제가 될경우에는 JVM의 Heap영역을 늘려주면 천천히 일어나지만 반대로 Full GC에 소요되는 시간이 증가한다.

개당 Full GC 시간이 오래걸릴 경우에는 JVM의 Heap 영역을 줄여주면 빨리 Full GC가 끝나지만 반대로 Full GC가 자주 일어난다는 단점이 있다.

그래서 이 부분에 대한 적절한 Tuning이 필요하다.

대부분의 Full GC로 인한 문제는 JVM자체나 WAS의 문제이기 보다는 그 위에서 구성된 Application이 잘못 구성되어 메모리를 과도하게 사용하거나 오래 점유하는 경우가 있다.

예를 들어 대용량 DBMS Query의 결과를 WAS상의 메모리에 보관하거나 , 또는 Session에 대량의 데이타를 넣는것들이 대표적인 예가 될 수 가 있다.

좀더 자세한 튜닝 방법에 대해서는http://www.j2eestudy.co.kr/lecture/lecture_read.jsp?db=lecture0401_1&table=j2ee&id=1를 참고하기 바란다.


4. Slow down analysis in DBMS


Application이 느려지는 원인중의 많은 부분을 차지 하고 있는 것은 DBMS의 성능 문제가 있는 경우가 많다.

흔히들 DBMS Tuning을 받았더니 성능이 많이 향상되었다고 하는 경우가 많은데, 그건 그만큼 DB 설계를 제대로 하지 못했다는 이야기가 된다.

DBMS 자체 Tuning에 대한 것은 이 문서와는 논외기 때문에 제외하기로 하고, DBMS에 전송되는 각각의 SQL문장의 실행 시간을 Trace할 수 있는 것만으로도 많은 성능 향상을 기대할 수 있는데, 간단하게 SQL 문장을 실행시간은 아래 방법들을 이용해서 Trace할 수 있다.

http://eclipse.new21.org/phpBB2/viewtopic.php?printertopic=1&t=380&start=0&postdays=0&postorder=asc&vote=viewresult

http://www.j2eestudy.co.kr/qna/bbs_read.jsp?table=j2ee&db=qna0104&id=5&searchBy=subject&searchKey=sql&block=0&page=0


5. Slow down analysis in Webserver & network


WAS 와 DBMS 앞단에는 WebServer와 Network 이 있기 때문에 이 Layer에서 문제가 되면 속도저하를 가지고 올 수 있다. 필자의 경험상 대부분의 slow down이나 hangup은 이 부분에서는 거의 일어나지 않지만 성능상에 종종 영향을 주는 Factor가 있는데,

WebServer 와Client간의 KeepAlive

특히 WebServer의 Keep Alive설정이 그것이다.
WebBrowser 와 WebServer간에는 KeepAlive 설정을 하는것이 좋은데. 그 이유는 WebBrowser에서 하나의 HTML 페이지를 로딩하기 위해서는 Image와 CSS등의 여러 파일등을 로딩하는데, KeepAlive 설정이 없으면 각각의 파일을 로딩하는것에 각각의 Connection을 open,request,download,close를 한다. 잘들알고 있겠지만 Socket을 open하고 close하는데에는 많은 자원이 소요된다. 그래서 한번 연결해놓은 Connection을 계속 이용해서 HTTP data를 주고 받는 설정이 KeepAlive이다.
이 KeepAlive 설정은 웹을 이용한 서비스 제공에서 많은 성능 변화를 주기 때문에 특별한 이유가 없는한 KeepAlive 설정을 유지하기 바란다. 설정 방법은 각 WebServer의 메뉴얼을 참고하기 바란다.

※ Apache2.0의 Keep Alive 설정은
http://httpd.apache.org/docs-2.0/mod/core.html#keepalive를 참고하기 바란다. Default가 KeepAlive 가 On으로 되어 있다.

WebServer와 WAS간의 KeepAlive

WebServer 와 WAS간에는 WebServer에서 받은 request를 forward하기 위해서 WebServer Side에 WAS와 통신을 하기 위한 plug-in 이라는 모듈을 설치하게 된다. 이 역시 WebServer와 Client간의 통신과의 같은 원리로 KeepAlive를 설정하게 되는데, 이 역시 성능에 영향을 줄 수 있는 부분이기 때문에 가급적이면 설정하기를 권장한다.

※ WebLogic에서 Webserver와의 KeepAlive설정은
http://e-docs.bea.com/wls/docs81/plugins/plugin_params.html#1143055을 참고하기 바란다.
Default는 KeepAlive가 True로 설정되어 있다.

OS에서 Kernel Parameter 설정

OS 의 TCP/IP Parameter와, Thread와 Process등의 Kernel Parameter 설정이 운영에 있어서 영향을 미치는 경우가 있다. 이 Parameter들은 Tuning하기가 쉽지 않기 때문에, WAS또는 OS Vendor에서 제공하는 문서를 통해서 Tuning하기 바란다.

※ WebLogic의 OS별 설정 정보은
http://e-docs.bea.com/platform/suppconfigs/configs81/81_over/overview.html를 참고하기 바란다.


6. Common mistake in developing J2EE Application


지 금까지 간단하게나마 J2EE Application의 병목구간을 분석하는 부분에 대해서 알아보았다. 대부분의 병목은 Application에서 발생하는 경우가 많은데, 이런 병목을 유발하는 Application에 자주 발생하는 개발상의 실수를 정리해보도록 하자.

1) Java Programming

sycnronized block
위에서도 설명 했듯이 sychronized 메소드는 lock contention과 deadlock등의 문제를 유발할 수 있다. 꼭 필요한 경우에는 사용을 해야하지만, 이점을 고려해서 Coding해야한다.

String 연산

이미 많은 개발자들이 알고 있는 내용이겠지만 String 연산 특히 String연산중 “+” 연산은 CPU를 매우 많이 소모하게 되고 종종 slow down의 직접적인 원인이 되는 경우가 매우 많다.
String 보다는 가급적 StringBuffer를 사용하기 바란다.

Socket & file handling

Socket 이나File Handling은 FD (File Descriptor)를 사용하게 되는데, 이는 유한적인 자원이기 때문에 사용후에 반드시 close명령을 이용해서 반환해야한다. 종종 close를 하지 않아서, FD가 모자르게 되는 경우가 많다.

2) Servlet/JSP Programming

JSP Buffer Size

Jsp 에서는 JSP의 출력 내용을 저장하는 buffer 사이즈를 지정할 수 있다.

<% page buffer=”12kb” %>

이 buffer size는 출력 내용을 buffering했다가 출력하는데, 만약에 쓰고자하는 내용이 Buffer size 보다 클 경우에는 여러번에 걸쳐서 socket write가 일어나기 때문에 performance에 영향을 줄 수 있으므로 가능하다면 buffersize를 화면에 뿌리는 내용의 크기를 예측해서 지정해주는것이 바람직하다. 반대로 너무 큰 버퍼를 지정해버리면 메모리가 불필요하게 낭비 될 수 있기 때문에 이점을 주의하기 바란다.

참고로 jsp page buffer size는 지정해주지 않는경우 default로 8K로 지정된다.

member variable

Servlet/JSP는 기본적으로 Multi Thread로 동작하기 때문에, Servlet과 JSP 내에서 선언된 멤버 변수들은 각 Thread간에 공유가 된다.
그 래서 이 변수들을 read/write할경우에는 sychronized method로 구성해야 하는데, 이 synchronized는 속도 저하를 유발할 수 있기 때문에, member 변수로는 read 만 하는 객체를 사용하는게 좋다.

특히 Servlet이나 JSP에서 Data Base Connection을 멤버 변수로 선언하여 Thread간 공유하는 예가 있는데, 이는 별로 좋지 않은 프로그래밍 방법이고, 이런 형태의 패턴은 Servlet이 단 하나만 실행되거나 하는것과 같은 제약된 조건 아래에서만 사용해야 한다.

Out of Memory in file upload

JSP에서 File upload control을 사용하는 경우가 많다. 이 control을 구현하는 과정에서 upload되는 파일 내용을 몽땅 메모리에 저장했다가 업로드가 끝나면 한꺼번에 file에 writing하는 경우가 있는데, 이는 큰 사이즈의 파일을 업로드할때, 파일 사이즈만큼의 메모리 용량을 요구하기 때문에, 자칫하면 Out Of Memory 에러를 발생 시킬 수 있다.
File upload는 buffer를 만들어서 읽고, 파일에 쓰는 작업을 병행하도록 해야한다.

3) JDBC Programming

Connection Leak

JDBC Programming에서 가장 대표적으로 발생되는 문제가 Connection Leak이다. Database Connection을 사용한후에 close않아서 생기는 문제인데,Exception이 발생하였을때도 반드시 Connection을 close하도록 해줘야한다.


<그림. Connection close의 올바른 예>


Out of memory in Big size query result

SQL 문장을 Query하고 나오는 resultset을 사용할때, 모든 resultset의 결과를 Vector나 hashtable등을 이용해서 메모리에 저장해놓는 경우가 있다. 이런 경우에는 평소에는 문제가 없지만, SQL Query의 결과가 10만건이 넘는것과 같은 대용량일때 이 모든 데이타를 메모리 상에 저장할려면 Out Of Memory가 나온다.
Query의 결과값을 처리할때는 ResultSet을 직접 리턴받아서 사용하는것이 메모리 활용면에서 좀더 바람직하다.

Close stmt & resultset

JDBC 에서 Resultset이나 Statement 객체는 기본적으로 Connection을 close하게 되면 자동으로 닫히게 된다. 그러나 WAS나 Servlet Container의 경우에는 성능향상을 위해서 Connection Pooling 기법을 이용해서 Connection을 관리하기 때문에 Connection Pooling에서 Connection을 close하는것은 실제로 close하는것이 아니라 Pool에 반환하는 과정이기 때문에 해당 Connection에 연계되어 사용되고 있는 Statement나 ResultSet이 닫히지 않는다.

Connection Pooling에서 Statement와 ResultSet을 사용후에 닫아주지 않으면 Oracle에서 too many open cursor와 같은 에러가 나오게된다. (Statement는 DB의 Cursor와 mapping이 된다.)

4) EJB Programming

When we use EJB?

EJB는 분명 강력하고 유용한 개발 기술임에는 틀림이 없다. 그러나 EJB의 장점과 용도를 모르고 사용하면 오히려 안쓰는것만 못한 경우가 많다.
각 EJB 모델 (Session Bean,Entity Bean)이 어떤때 유용한지를 알고 사용하고, 정확한 Transaction Model등을 결정해서 사용해야 한다.

Reduce JNDI look up

위 에서도 설명했듯이 EJB의 Home Interface를 lookup 해오는 과정은 객체의 Serialization/DeSerialization을 동반하기 때문에, 시스템 성능에 영향을 줄 수 있다. EJB Home을 한번 look up한후에는 Hashtable등에 저장해서 반복해서 Remote Call(Serialization / DeSerialization)하는 것을 줄이는게 좋다.

Do not use hot deploy in production mode

WAS Vendor 마다 WAS 운영중에 EJB를 Deploy할 수 있는 HotDeploy 기능을 제공한다. 그러나 이는 J2ee spec에 있는 구현이 아니라 각 vendor마다 개발의 편의성을 위해서 제공하는 기능이다. 운영중에 EJB를 내렸다 올리는것은 위험하다. (Transaction이 수행중이기 때문에) Hot Deploy 기능은 개발중에만 사용하도록 하자.

5) JVM Memory tuning

Basic Tuning

Application 을 개발해놓고, 운영환경으로 staging할때 별도의 JVM 튜닝을 하지 않는 경우가 많다. 튜닝이 아니더라도 최소한의 메모리 사이즈와 HotSpot VM 모델 (server/client)는 설정해줘야지 어느정도의 Application의 성능을 보장 받을 수 있다. 최소한 메모리 사이즈와 VM모델정도는 설정을 해주고 운영을 하도록 하자.


7. 결론



J2EE Application의 병목구간을 확인하기 위해서는 그 문제를 발견하고 툴과 경험을 이용해서 문제의 원인을 발견하고 제거해야한다.

대부분의 WAS또는 User Application의 slow down이나 hang up은 Thread dump를 통한 분석을 통해서 대부분 발견 및 해결을 할 수 있다.

그외에 부분 JVM이나 WebServer,Network 등에 대해서는 별도의 경험과 Log 분석등을 알아내야하고 DB에 의한 slow down이나 hang up현상은 DB 자체의 분석이 필요하다.

 


'개발 이야기 > Java Story' 카테고리의 다른 글

Hostname 과 IP 가져오기  (0) 2010.11.18
Java GC의 원리  (0) 2010.02.18
JVM GC와 메모리 튜닝 (조대협)  (0) 2010.02.11
Java jstat로 메모리 모니터링  (0) 2010.02.11
iBatis 튜토리얼 문서  (0) 2008.05.15
Posted by 서오석
,