본문 바로가기

공부/컴퓨터

서버 만들때 읽어 볼 글 - PPC 기법 ( 폭주중 잠시만 기다려 주십시오 )

>
> {쿼리문 실행이 오래걸릴 경우}
> {손님(guest), lumb2000@hanmail.net}
>
> jdbc프로그램에서 쿼리문의 실행시간이 오래걸리면 어떻게 처리를 하는지 궁금합니다.
> 제가 쓰는 쿼리문의 경우 여러 테이블을 조인해야만 원하는 데이타를 가져올 수
> 있습니다. 여러개의 테이블을 조인하기도 하고, 각 테이블에 데이타가 많아서 쿼리문의
> 결과를 얻는데 시간이 오래걸리더군요.
>
> 그런데 문제는 이렇게 오래 걸리는 동안 계속 pool을 잡고 있어서, 반환하기 전에 요청
> 이 들어오면 다시 pool이 잡혀서 Current in use의 개수가 계속 늘어만 갑니다.
> 물론 어느정도 시간이 지나면 다시 반환되기는 하지만 많은 유저가 붙었을 때는
> 시스템이 제대로 동작하지 않습니다.
> 이렇게 처리시간이 오래 걸리는 경우 보통 어떻게 처리하는지 궁금합니다.
>
> 그리고 쿼리문이 실행되는 동안 다른 링크를 클릭하거나 중지를 눌렀을 때, 링크를
> 클릭한 경우는 이동한 화면으로, 중지를 누른 경우는 멈추는 것으로 브라우저의 상태가
> 바뀌어 버리지만 이미 실행된 쿼리문은 계속 자기 일을 수행하더군요.
> 이미 다른 액션이 취해진 상태에서 쿼리문이 수행되고 있는것은 의미가 없는데
> 이것이 pool을 계속 잡고 있습니다.
> 이런 경우 즉, 현재 페이지에서 쿼리문을 날리다가 다른곳을 클릭하거나,
> 중지시켰을 때 바로 쿼리문수행도 중지하고 connection을 닫고 나오게 하는 방법이
> 있는지요?
>
> 더운신데 수고많으십니다.
> 감사합니다.


질문하신 분이 기대하는 바와 같이, 일단 실행된 Thread 를 중지할 방법은 없습니다.
사용자 브라우져의 forward/backword/stop 혹은 브라우져를 close 하는 것과 무관하게
일단 그 요청을 server side에서 받아 처리하고 있는 Thread 는 끝까지 자기 일을
수행하게 됩니다.

결국, 해당 어플리케이션의 단위시간당 수행할 수 있는 능력(개수) 대비, 단위시간당
요청빈도가 넘어설 경우, 요청을 하여도 응답이 급격하게 느려지는 "hang현상" 이
유독 웹기반의 사이트에서 빈번하게 발생하는 주 원인이 여기에 있습니다.
사용자는 응답이 안올 경우, 묵묵하게 기다려주는 인내심은 커녕, 또다시 버튼을 마구
누르는 경향이 있잖습니까.

1. 1차적으로는 해당  쿼리를 튜닝하셔서, 단위시간당 요청빈도 보다 단위시간당
  처리능력을 높이셔야 합니다.

2. 쿼리가 느린 서비스일 경우, 대부분 어플리케이션서버 Tier의 CPU는 25-50% 를
  넘어서지 못하고 결과를 기다리는 I/O waiting 상황에 빠지는 것이 일반적입니다.
  이 경우, DB서버의 CPU/Memory가 허용하는 한도까지 Connection Pool의 개수를 적당히
  늘려 줌으로써, 가용한 DB연결에 대한 Bufferring 효과를 보실 수도 있습니다.

3. 쿼리수행속도 저하로 인해 단위시간당 요청빈도가 단위시간당 처리능력보다 크고,
  bottle-neck의 원인이 DB서버에 분명히 있다면, 어플리케이션서버 Tier를 여러대로
  클러스트링하는 것은 전혀 효과가 없으며, (더이상의 쿼리튜닝이 불가능할 경우)
  DB서버의 H/W적 Capacity 를 늘리셔야 합니다.

  Performance Tuning 에 대한 이론적인 부분은 아래 세 글을 참조하세요.

  [강좌]웹기반시스템하에서의 성능에 대한 이론적 고찰
  http://www.javaservice.net/~java/bbs/read.cgi?m=resource&b=consult&c=r_p&n=1008701211

  Performance Tuning QuickGuide for BenchmarkTest
  http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=985764595

  WebSphere BMT 최적 파라메터 셋팅 방법론
  http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=989760940


4. 만약, DB서버의 CPU나 Memory가 bottle-neck의 원인이 아니라 DB Lock 혹은 한순간엔
  반드시 하나씩만 수행되어야 하는 것과 같은 어플리케이션 레벨의 문제로 인한
  bottle-neck 이라면 해당 어플리케이션을 튜닝하는 것만이 유일한 해결방법이 될
  것입니다.

5.이도저도 불가능할 경우, PPC(Peak Point Control) 기법을 적용하셔야 합니다.
성능저하로 인한 장애의 원인으로 발견된 어플리케이션이 있다면, 그것을 원천적으로
개선하여야 겠지만, 그것이 도저히 불가능할 경우, 특정 블록에 동시에 접근하는
Thread의 수를 제어 함으로써, 제한적으로 서비싱 하여야 할 것입니다.
특정 개수 이상이 동시에 진입할 경우, 더이상 진입케 하지 말고, "서비스 폭주중입니다.
잠시뒤에 다시 이용해 주십시요" 와 같은 메세지를 사용자에게 내보내고, 해당 서비스의
진입 자체를 막아 줌으로서, 단위시간당 처리능력(처리개수)보다 단위시간당 요청빈도
를 근본적으로 줄여 주어야 하는 것이지요...

이러한 PPC 기법은 최근 웹기반의 Performance Tuning 을 하다보면 항상 직면하게
되는 문제중의 하나입니다만, 아직 이것의 필요성을 인식하고 있는 분이 드문듯 합니다.

자바에서 PPC를 적용하는 것은 아주 간단합니다. 만약, 특정 instance method 에 PPC
기법을 적용하는 방법은 다음과 같습니다.

-------------------------------------------------------------------------------
public class BadPerfClass
{
     public void performTask(…) throws … {
         <original business logic>
     }
}
-------------------------------------------------------------------------------

위 소스를 아래와 같이 변형합니다.

-------------------------------------------------------------------------------
public class BadPerfClass
{
     private static int MAX_THREADS = 30;
     private static int count = 0;
     private static Object lock = new Object();

     public void performTask(…) throws … {
         boolean executable = true;
         try {
             synchronized ( lock ) {
                 if ( count >= MAX_THREADS ) executable = false;
                 else count++;
             }
             if ( executable == false ) {
                 < "maximum threads reached !!!">
                 throw or return;
             }
            
             performTask2(….);
         }
         finally {
             synchronized( lock ) {
                 if ( executable ) count--;
             }
         }
     }
     public void performTask2(…) throws … {
         <original business logic>
     }
}
-------------------------------------------------------------------------------


만약, 특정 Servlet의 성능이 너무나 저하되어, 시스템 장애의 핵심원인이라면, 해당
특정 서블렛에 한해서, 동시에 실행될 개수제한을 두고 싶다면, 다음과 같이 하시면
될 것입니다.

-------------------------------------------------------------------------------
public class BadPerfServlet extends HttpServlet
{
     public void doGet(...) .... {
        performTask(...);
     }
     public void doPost(...) .... {
        performTask(...);
     }
     public void performTask(…) throws … {
         <original business logic>
     }
}
-------------------------------------------------------------------------------

위 소스를 아래와 같이 변형합니다.

-------------------------------------------------------------------------------
public class BadPerfServlet extends HttpServlet
{
     public void doGet(...) .... {
        performTask(...);
     }
     public void doPost(...) .... {
        performTask(...);
     }

     private int MAX_THREADS = 30;
     private int count = 0;

     public void performTask(...) throws ... {
         boolean executable = true;
         try {
             synchronized ( this ) {
                 if ( count >= MAX_THREADS ) executable = false;
                 else count++;
             }
             if ( executable == false ) {
                 < "maximum threads reached !!!">
                 throw or return;
             }
            
             performTask2(...);
         }
         finally {
             synchronized( this ) {
                 if ( executable ) count--;
             }
         }
     }
     public void performTask2(...) throws ... {
         <original business logic>
     }
}
-------------------------------------------------------------------------------



만약, 특정 Base Servlet 을 상속받아 모든 Servlet 들이 한 곳을 경유하여 실행되도록
JDF(Java Development Framework)와 같은 기법으로 적용되어 있는 Servlet 의 경우라면
다음과 같이 BaseServlet에만 변경 적용함으로써, BaseServlet을 상속받는 모든 Servlet
들이 동시에 실행될 수 있는 개수제어을 할 수도 있을 것입니다.

-------------------------------------------------------------------------------
public abstract class BaseServlet extends HttpServlet
{
     public void doGet(...) .... {
        preformPreTask(...);
     }
     public void doPost(...) .... {
        preformPreTask(...);
     }
     public void performPreTask(...) throws ... {
         ...
         preformTask(...);
     }
     protected abstract void performTask(...);
}

public class BadPerfServlet extends BaseServlet
{
     public void performTask(...) throws ... {
         <original business logic>
     }
}
-------------------------------------------------------------------------------

위 소스를 아래와 같이 변형합니다.

-------------------------------------------------------------------------------
public abstract class BaseServlet
{
     public void doGet(...) .... {
        preformPreTask(...);
     }

     public void doPost(...) .... {
        preformPreTask(...);
     }

     private static int __MAX_THREADS = 50;
     private static int __count = 0;
     private static Object __lock = new Object();

     public void performPreTask(...) throws ... {
         boolean executable = true;
         try {
             synchronized ( __lock ) {
                 if ( __count >= MAX_THREADS ) executable = false;
                 else __count++;
             }
             if ( executable == false ) {
                 < "maximum threads reached !!!">
                 throw or return;
             }
            
             performTask(...);
         }
         finally {
             synchronized( __lock ) {
                 if ( executable ) __count--;
             }
         }
     }
     protected abstract void performTask(...);
}
-------------------------------------------------------------------------------

위의 예는 단지 예제일 뿐이며, 여하한의 곳에서 동시에 진입할 Thread 의 개수를
제한 시켜야 하는 곳에서 다양하게 응용해서 사용할 수 있을 것입니다.
synchronized ( lock ) 에서 Lock 객체를 뭘로하느냐에 따라, JVM 시스템 전체를 제어
하거나, 혹은 특정 영역의 lock 객체를 사용함으로써, 각 응용어플리케이션 개별적으로
각각 적용할 수도 있을 것입니다.


웹서버 -- 어플리케이션서버 -- DB서버 와 같은 전형적인 구조 외에도, Backend 서비스가
CICS CORBOL이나, Socket 통신으로 C Socket Gateway를 통해 IBM HOST CICS 혹은 IMS DB
CORBOL 프로그램과 연동하는 경우도 생각할 수 있으며, 여타의 Backend 서비스와 연동되어
운영되는 n-tier 시스템의 경우, 과거 HOST단말기 혹은 C/S 시스템의 제한된 사용자에게
서비스하던 처리 능력을 과신하다보면, 인터넷으로 오픈된 웹시스템으로의 전환 시점에,
대부분 bottle-neck point 는  backend 서비스라는 것에 주목하셔야 합니다.
이 경우 Backend 시스템의 성능을 근본적으로 높일 수 있는 방법은 대부분 제한적일 수
밖에 없을 것이고, 이 때 이슈로 떠 오르는 기법이 PPC가 될 것입니다.

PPC기법은 사실 돌이켜 생각해 보면, 새로운 그 무엇도 아니며, 이미 광범위하게 적용되어
있는 일반적인 기법입니다. Apache 웹서버의 MaxClients 파라메터가 그것이며, DB
Connection Pooling 의 Maximum 개수 제한을 두는 것이 그 단적인 예가 될 것입니다.
이 기법을 제품이나, 특정 영역에 국한시키지 말고, 응용어플리케이션에서도 적용시켜야
한다는 것을 고급개발자라면 각인하고 있어야 할 것입니다.

특정환경에서 특정 어플리케이션은 단위시간당 최대로 처리할 수 있는 개수는 반드시
제한되어 있습니다. 1초당 200개 혹은 1분당 2만개 등과 같이 TPS(Transaction Per
Second) , TPM(Transaction Per Minute), 혹은 PPS(Page Per Second), RPS(Request Per
Second)  등과 같이 스트레스 툴을 통해 다양한 이름으로 그 성능이 측정되곤 합니다.
PPC기법은 단위시간당 처리할 수 있는 성능의 한계를 넘어서서 특정시점에 몰려드는
Request 들 중, 처리할 수 있는 개수만 처리하고, 나머지를 한가한 시간대에 다시
요청하도록 유도함으로써, 결과적으로 부하를 분산시키고 있는 형태가 됩니다.

MAXIMUM 수치에 도달했을 때, 곧바로 service fail을 발생시켜 return 시키지 말고,
일정 시간동안 가용한 자원이 생길 때까지 약간의 시간을 기다려 주는 기법을 연상하실
수 있을 것입니다. 그리고, 그 "약간의 시간"을 제어함으로써, PPC 적용의 깊이(?)를
또 다시 제어하고 싶으실 겁니다.
Hans Vergstain 이 만든 DBConnectionManager.java 라는 DB Connection Pool 소스에
getConnection( long timeout ) 메소드가 있음에도 불구하고, 이것을 왜 사용해야
하는지에 대해 정확히 인식하고 있는 개발자가 또한 드문 듯 합니다. 변동이 심하게
요청빈도가 달라지는 경우, 순간적으로 동시에 수행할 최대치에 도달했다고 하여
곧바로 request fail 을 발생시키는 것은 때론 적절치 않을 수도 있습니다. 약 1-3초
정도 기다려 주었다가 가용한 자원이 그 시간내에 생길 경우, 그대로 정상적인
서비스를 하게 하는 것이 더욱 바람직 할 수도 있다는 것이지요.

위의 PPC기법에 Timeout 기법을 적용하시려면, 다음 글에 포함된 소스를 응용하면
될 겁니다.

Object Pool Contorl 기법
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=javatip&c=r_p&n=974750350

그렇다면 MAXIMUM 수치 혹은 Timeout 수치를 얼마로 하는 것이 가장 적절하냐라는 부분이
바로 Performance Tuning 의 핵심적인 사항이며, 해당 시스템의 특성에 맞게 적절히
적용하여야 하는 부분입니다. 이 수치는 결코 결정된 수치가 존재할 수는 없으며,
해당 시스템의 H/W용량과 아키텍쳐, 그리고, 응용어플리케이션의 특성에 따라 항상
달라지기 때문입니다. 어떻게 달라지느냐는 스트레스 테스팅 툴을 통해 측정함으로써
그 변화의 정도의 판단할 수 있을 것입니다.

주지하셔야 할 것은, 이같은 PPC 기법은 가장 마지막 수단인 궁여지책의 방법이라는
것입니다. 요청을 날렸는데, "폭주중..."이라는 메세지가 나오는 것은 해당 비즈니스
요건에는 존재치 않았을 것이며, 해당 서비스는 이유야 어떠하든 "실패(!)"할 수 있다는
것을 허용하겠다는 의미가 됩니다. "hang현상"과 같은 장애를 극복하기위한 몸부림(?)인
것이지요.

PS: PPC(Peak Point Control)이라는 이름을 최초로 명명한 분은 한국IBM의 이형기님
  입니다.


-------------------------------------------------------  
  본 문서는 자유롭게 배포/복사 할 수 있으나 반드시
  이 문서의 저자에 대한 언급을 삭제하시면 안됩니다
================================================
  자바서비스넷 이원영
  E-mail: javaservice@hanmail.net
  PCS:011-898-7904
================================================