본문으로 건너뛰기

20.5 웹 애플리케이션 서버 튜닝

웹 애플리케이션 서버 튜닝에서는 실행 스레드, 기본 I/O, JDBC 커넥션 풀링, EJB, 클러스터링 5가지 항목을 설명한다.

데이터베이스 커넥션 풀링

새롭게 구축되는 시스템에서 성능을 최대로 발휘하기 위해서 데이터베이스 커넥션 풀과 스레드 풀을 튜닝하는 것은 중요한 부분이다.

시스템 리소스 관점에서 보면 데이터베이스 커넥션을 열거나 닫는 것은 매우 비용이 많이 드는 작업이다. 최근에는 많지는 않지만, 애플리케이션 개발 시 데이터베이스 조회나 트랜잭션에 대해 매번 새로운 커넥션을 생성하고 바로 닫는 경우도 있다. 이러한 방식은 트랜잭션 처리에 큰 오버헤드가 발생하여 성능 감소의 원인이 된다. JBoss EAP6에서 데이터베이스 커넥션 풀 기능을 제공하고 있으니, 이를 이용하는 방법이 성능이나 안정적인 서비스를 위해 권장한다.

스레드 튜닝

스레드 풀은 애플리케이션의 성능을 튜닝 하는 데 있어서 두 번째로 중요한 부분이다. JBoss EAP 6는 견고한 스레드 풀 기능을 갖추고 있지만 이를 운영 환경에 적합하게 설정하기 위해서는 각각의 스레드 풀의 용도와 애플리케이션 성능에 미치는 영향을 파악해야 한다.

애플리케이션 종류에 따라 사용하는 스레드 풀이 다르며 어떤 부분이 병목이 있는지도 다르다.

애플리케이션 종류나 사용 방식에 따라 매우 달라서 주의가 필요하다.

스레드 수가 많다고 무조건 성능이 향상되지는 않는다. 스레드는 CPU 성능과 관련이 깊어 스레드 수가 성능에 미치는 영향은 다음과 같다.

  • 스레드 수가 너무 많은 경우 스레드 관리를 위한 오버헤드가 증가해서 반대로 성능이 감소한다. CPU 사용량이 피크에 도달한 상황에서는 스레드 수를 줄이면 성능이 향상될 수 있다.
  • 스레드 수가 너무 적은 경우 CPU 사용률이 감소하고 지연 시간이 발생할 수 있다. 이 경우 스레드 수를 늘려 성능을 향상할 수 있다.

최적의 스레드 수는 시스템 구성(특히 CPU 수, CPU 성능)에 따라 각각 다르다. 부하 테스트 등으로 스레드 수의 최적 값을 찾아야 한다.

스레드 튜닝 시 유의점

  • CPU의 사용량이 100%가 아닐 때 클라이언트의 요청이 자주 블록되거나 거절되는 경우에만 실행 스레드 수를 튜닝하라.
  • 스레드 수를 튜닝할 때 처리량이 떨어지거나 CPU 사용량이 떨어지거나 일정하게 유지되는 경우에는 튜닝을 중지하라.
  • 애플리케이션 컴포넌트를 파티셔닝하거나 지정된 수만큼의 자원을 컴포넌트에 할당하기 위해서는 사용자 정의(user-defined)된 실행 큐를 설정한다.
  • 커스텀 실행 큐를 사용하면 잠재적인 크로스서버 데드락(cross-server deadlock)을 방지할 수 있다.
  • 메세지 드리븐 빈(Message-driven Bean)에 지정된 리소스를 할당하기 위해서는 배치된 각각의 메세지 드리븐 EJB마다 개별적인 실행 큐를 사용하라.
  • JBoss EAP에서 오랫동안 실행되는 요청이나 데드락을 해결하려면, 원인을 찾기 위해서 적당한 간격의 일련의 스레드 덤프(thread dump)를 받아 분석하라. 예를 들어 3~5초 간격 5회 스레드 덤프를 받아 분석한다.

웹 서브시스템

JBoss EAP 6만 사용하여 웹 시스템을 구축할 수 있지만, 일반적으로는 JBoss EAP 6의 앞단에 Apache 웹 서버를 배치해, 정적 콘텐츠(HTML, CSS, 이미지 등의 정적 파일)의 전달이나 로드 밸선싱을 수행한다. 아파치 웹서버를 이용하여 로드 밸런싱을 구축하는 경우, mod_jk 또는 mod_cluster 등의 모듈을 사용하여 소프트웨어 로드 밸런스형태로 사용한다.

그림 7. 웹 애플리케이션 서버 시스템의 일반 구성

위 그림은 웹 애플리케이션 서버 시스템의 일반적인 구성이다. 서버 시스템은 외부에서 HTTP 요청을 받고 HTTP 응답을 반환하는 웹 서버, 웹 서버에서 전달된 요청에 대한 업무를 처리하는 애플리케이션 서버, 업무 처리 결과 데이터를 업데이트하는 데이터베이스 서버로 구성된다.

동시 접속수

웹 서브시스템은 HTTP/1. 1 커넥터(기본 포트 번호 8080) 및 AJP/1. 3 커넥터(기본 포트 번호 8009)를 사용해 앞 단의 웹 서버와 통신한다. 여기에서는 주로 HTTP 혹은 AJP 요청을 처리하는 JBoss EAP 6의 워커 스레드 수를 ‘동시 접속 수’로 튜닝하는 방법을 소개한다.

웹 서브시스템은 클라이언트 요청에 대해서 스레드를 할당해 처리한다. 웹 시스템은 이러한 요청을 처리하기 위하여 워커 스레드를 사용하게 되며, 워커 스레드의 생성과 소멸을 효율적으로 처리하기 위해 내부적으로 쓰레드 풀링을 사용한다.

웹 서브시스템은 클라이언트의 요청을 수신하면 풀에서 대기 상태의 스레드를 할당하고 요청이 완료(클라이언트에의 응답 완료 시)되면 스레드 풀에 반환한다. JBoss EAP 6의 앞단에 Apache 웹 서버를 설치한 경우 요청 처리 완료 후에도 Apache와 JBoss EAP 6간의 연결은 일정 기간 연결된 상태로 남아있어, 스레드 풀로 반환은 연결이 완전히 끊어진 이후가 된다.

그림 8. 웹 서버와 애플리케이션 서버에서 사용하는 풀

요청이 들어왔을 때 스레드 풀의 최대값에 도달하지 않는 경우 새로운 스레드를 생성하여 할당한다. 스레드 풀이 최대값에 도달한 경우에는 클라이언트와 연결되지 않는다.

클라이언트의 동시 접속 수는 이 스레드 풀의 최대값에 의해 결정된다. 스레드 풀은 커넥터마다 생성하여 관리한다. 여러 개의 웹 애플리케이션이 배포된 서버에서는 같은 커넥터를 사용해 애플리케이션간에 스레드 풀을 공유한다.

스레드 풀의 최대값은 커넥터의 max-connections 속성에 설정한다. 동시접속 수는 애플리케이션 서버보다 앞에 설치하는 웹 서버의 최대 동시 접속 수나 웹 서버의 대수를 고려해 값을 계산한다. max-connections로 설정한 값이 웹 서버의 동시접속 수보다 적은 경우, 웹 서브시스템의 워커 스레드가 부족하여, 접속 에러가 발생할 수 있어 주의해야 한다.

  • AJP/1. 3 커넥터
위치속성설명
/subsystem=web/connector=ajpmax-connections워커 스레드 풀의 최대값. 기본값은 JVM이 사용할 수 있는 CPU 코어수 × 512 이다.
  • HTTP/1. 1 커넥터
위치속성설명
/subsystem=web/connector=httpmax-connections워커 스레드 풀의 최대값. 기본값은 JVM이 사용할 수 있는 CPU 코어수 × 512 이다.

동시 접속 수 튜닝

다음은 CLI를 이용하여 AJP/1. 3 커넥터의 max-connections의 값을 2000으로 설정하는 예이다.

  1. 웹 서브시스템의 AJP 커넥터의 max-connections속성을 2000으로 설정한다.

    [standalone@localhost:9999 /] cd /subsystem=web/connector=ajp

    [standalone@localhost:9999 connector=ajp] :write-attribute(name=max-connections, value=2000)

    {
    "outcome" => "success",
    "response-headers" => {
    "operation-requires-reload" => true,
    "process-state" => "reload-required"
    }
    }
  2. reload 한다.

    [standalone@localhost:9999 /] /:reload

    {"outcome" => "success"}
  3. 웹 서브시스템의 AJP 커넥터의 max-connections속성을 확인한다.

    [standalone@localhost:9999 /] cd /subsystem=web/connector=ajp

    [standalone@localhost:9999 connector=ajp] :read-attribute(name=max-connections)

    {
    "outcome" => "success",
    "result" => 2000,
    "response-headers" => {"process-state" => "reload-required"}
    }
    • max-connections 산정 방법

      max-connections 의 값을 결정하는 계산 식은 다음과 같다.

💡 TIP

max-connections = (Apache 의 MaxClients) X (웹 서버 수) X α

※ MaxClients 는 Apache 웹서버의 MPM 에서 설정한 최대동시접속자수 이다.

※ α (가중치):일반적으로 1.0을 사용하지만, 스레드 수가 부족하여 에러가 발생한다면 1.5~2.0 정도로 설정한다. 다른 JBoss EAP 6 인스턴스가 다운됐을 경우에서도, 모든 웹 서버로부터의 요청을 처리할 수 있도록, 웹 서버의 대수를 고려해야 한다. 또한, 스레드의 생성과 소멸은 비동기이기 때문에 가중치의 조정이 필요하다.

HTTP/AJP 커넥션에 대한 스레드 풀 설정

HTTP/AJP 커넥터에 대한 스레드 풀의 최대값을 충분히 설정해야 한다.

<connector name="ajp" protocol="AJP/1.3" scheme="http" socket-binding="ajp" executor="http-thread-pool" max-connections="3260"/>

<connector name="http" protocol="HTTP/1.1" scheme="http" socketbinding="http" enable-lookups="false" executor="http-thread-pool" max-connections="3260"/>

<subsystem xmlns="urn:jboss:domain:threads:1.1">

… 생략 …

<unbounded-queue-thread-pool name="http-thread-pool">

<max-threads count="250"/>

<keepalive-time time="60" unit="minutes"/>

</unbounded-queue-thread-pool>

… 생략 …

</subsystem>

커넥션 타임아웃

로드 밸런스등의 용도로 mod_jk, mod_proxy, mod_cluster 등의 모듈을 사용하기 위해 Apache를 JBoss EAP 6 앞에 배치했을 경우, Apache 웹 서버와 JBoss EAP 6간의 연결은 일정 시간 연결된 상태를 유지할 수 있도록 한다.

지정한 시간 동안 요청이 없는 경우 타임아웃으로 끊어져 할당되어 있던 워커 스레드는 스레드 풀에 반환한다.

연결 타임아웃은 시스템 프로퍼티로 지정한다. AJP 및 HTTP 각각의 커넥터의 connection 타임아웃 지정은 다음과 같은 시스템 프로퍼티를 사용하여 설정한다.

  • AJP/1. 3 커넥터
커넥터설명
org.apache.coyote.ajp.DEFAULT_CONNECTION_TIMEOUT연결요청에 대해 응답이 없는 경우 대기 시간을 밀리 세컨드로 지정한다. 기본값은 -1(무제한)이어서 요청이 없어도 connection은 끊어지지 않는다.
  • HTTP/1. 1 커넥터
커넥터설명
org.apache.coyote.http11.DEFAULT_CONNECTION_TIMEOUT연결 요청에 대해 응답이 없는 경우 대기 시간을 밀리 세컨드로 지정한다. 기본값은 60000 밀리 세컨드(60 초)이다.

커넥션 타임아웃 튜닝

Apache 웹 서버와 JBoss EAP 6간에 유지되는 연결은 아파치 웹 서버에 대한 타임아웃 설정이 있으며 지정한 시간 동안 응답이 없으면 웹 서버에서 연결이 끊어지게 된다.

다음은 시스템 프로퍼티 org.apache.coyote.ajp.DEFAULT_CONNECTION_TIMEOUT값을 60000 으로 설정하는 예이다.

또, mod_jk, mod_cluster(mod_proxy)의 connection 타임아웃 설정의 항목명은 다음 표와 같다.

커넥터타임아웃 속성설명
mod_jkconnection_pool_timeoutJBoss 대기 상태 connection의 타임아웃 시간(초) 지정한다.
mod_cluster (mod_proxy)ttlJBoss와 연결 대기 상태 타임아웃 시간(초).

표 5. 웹 커넥터의 타임아웃 설정

네이티브 커넥터

JBoss EAP 6는 Pure Java로 만들어진 Java EE애플리케이션 서버이다. JBoss Web에서는 성능 향상을 위해 JBoss Web Native라는 C언어로 작성된 네이티브 커넥터를 제공한다. 네이티브 커넥터는 APR(Apache Portable Runtime)라는 API를 이용하고 있어 APR 커넥터라고도 말한다. Native 커넥터를 사용하면 HTTP 및 AJP의 요청과 응답 처리가10% 정도 향상된다.

Native 커넥터를 이용하기 위해서는, JBoss EAP 6의 Native 컴포넌트를 Red Hat 고객 포털에서 다운로드하여 설치해야 한다.

그림 9. 레드햇 고객포탈의 네이티브 모듈 구성

사용하는 OS에 해당하는 Native 컴포넌트 ZIP파일을 다운로드하여 JBoss EAP 6가 설치된 디렉터리에 압축을 풀기만 하면 된다.

$ cd $JBOSS_HOME

$ cd ..

$ unzip ~/Downloads/jboss-eap-native-6.2.0-RHEL6-x86_64.zip

Archive: /home/admin/Downloads/jboss-eap-native-6.2.0-RHEL6-x86_64.zip

creating: jboss-eap-6.2/

creating: jboss-eap-6.2/modules/

creating: jboss-eap-6.2/modules/system/

creating: jboss-eap-6.2/modules/system/layers/

creating: jboss-eap-6.2/modules/system/layers/base/

creating: jboss-eap-6.2/modules/system/layers/base/org/

creating: jboss-eap-6.2/modules/system/layers/base/org/jboss/

creating: jboss-eap-6.2/modules/system/layers/base/org/jboss/as/

..생략

creating: jboss-eap-6.2/modules/system/layers/base/org/hornetq/main/lib/

creating: jboss-eap-6.2/modules/system/layers/base/org/hornetq/main/lib/linux-x86_64/

inflating: jboss-eap-6.2/modules/system/layers/base/org/hornetq/main/lib/linux-x86_64/libHornetQAIO.so

inflating: jboss-eap-6.2/SHA256SUM

finishing deferred symbolic links:

jboss-eap-6.2/modules/system/layers/base/org/jboss/as/web/main/lib/linux-x86_64/libssl.so -> /usr/lib64/libssl.so.10

jboss-eap-6.2/modules/system/layers/base/org/jboss/as/web/main/lib/linux-x86_64/libcrypto.so -> /usr/lib64/libcrypto.so.10

jboss-eap-6.2/modules/system/layers/base/org/jboss/as/web/main/lib/linux-x86_64/libapr-1.so -> /usr/lib64/libapr-1.so.0

설치 후에 ‘/modules/system/layers/base/org/jboss/as/web/main/lib/linux-x86_64’ 디렉터리가 $JBOSS_HOME 디렉터리에 있는지 확인한다.

$ ls $JBOSS_HOME/modules/system/layers/base/org/jboss/as/web/main/lib/linux-x86_64/

libapr-1.so libcrypto.so libssl.so libtcnative-1.so

웹 서브시스템의 네이티브 커넥터를 사용하기 위해서 CLI 모드에서 다음과 같이 실행한다.

[standalone@localhost:9999 /] /subsystem=web:write-attribute(name=native,value=true)

{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}

네이티브 커넥터가 동작하는지 확인하려면 다음과 같이 AprLifecyleListener Logger를 DEBUG레벨로 추가하여 확인할 수 있다.

[standalone@localhost:9999 /] /subsystem=logging/logger=org.apache.catalina.core.AprLifecycleListener:add(category=org.apache.catalina.core.AprLifecycleListener,level=DEBUG)

{"outcome" => "success"}

서버 시작시 server.log 파일에 다음과 같이 네이티브 커넥터를 로딩하는 것이 표시된다.

01:47:22,910 INFO [org.jboss.as.security] (MSC service thread 1-1) JBAS013170: Current PicketBox version=4.0.19.SP2-redhat-1
01:47:22,938 INFO [org.jboss.as.naming] (MSC service thread 1-2) JBAS011802: Starting Naming Service
01:47:23,191 DEBUG [org.apache.catalina.core.AprLifecycleListener] (MSC service thread 1-1) Loaded: apr-1
01:47:23,191 DEBUG [org.apache.catalina.core.AprLifecycleListener] (MSC service thread 1-1) Loaded: z
01:47:23,220 INFO [org.jboss.as.mail.extension] (MSC service thread 1-2) JBAS015400: Bound mail session [java:jboss/mail/Default]
01:47:23,290 DEBUG [org.apache.catalina.core.AprLifecycleListener] (MSC service thread 1-1) Loaded: crypto
01:47:23,291 DEBUG [org.apache.catalina.core.AprLifecycleListener] (MSC service thread 1-1) Loaded: ssl
01:47:23,292 DEBUG [org.apache.catalina.core.AprLifecycleListener] (MSC service thread 1-1) Loaded: tcnative-1
01:47:24,292 INFO [org.infinispan.configuration.cache.EvictionConfigurationBuilder] (ServerService Thread Pool -- 31) ISPN000152: Passivation configured without an eviction policy being selected. Only manually evicted entities will be passivated.

Context Root의 변경

JBoss EAP 6 는 기본으로 Context 루트에 ROOT.war 애플리케이션이 배포되어 있다. 이 때문에 JBoss EAP 6 를 시작하고 http://localhost:8080/ 에 접근하면 다음과 화면이 표시된다.

그림 10. 기본 ROOT 애플리케이션

운영 환경에서 새로운 웹 애플리케이션을 배포할 때 기존 컨텍스트 루트를 변경하는 것이 일반적이다. 컨텍스트 루트를 변경하지 않는 경우에도 이 ROOT.war 를 활성화한 채 운영하는 것은 보안 측면에서도 바람직하지 않다.

  • 기본 컨텍스트 루트 변경

ROOT.war 는 웹 서브시스템의 가상 서버 설정인 virtual-server 자원으로 설정되어 있다. 아래와 같이 enable-welcome-root 속성을 false로 하여 ROOT.war 를 비활성화하는 것이 가능하다.

[standalone@localhost:9999 /] cd /subsystem=web/virtual-server=default-host

[standalone@localhost:9999 virtual-server=default-host] :write-attribute(name=enable-welcome-root, value=false)

{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}

reload 오퍼레이션을 실행한다.

[standalone@localhost:9999 virtual-server=default-host] /:reload

{"outcome" => "success"}
warning

🔥 CAUTION: 컨텍스트 루트 비활성화와 배포 오류

디폴트 컨텍스트 루트를 비활성화하지 않고 컨텍스트 루트가 설정된 애플리케이션을 배포하는 경우 배포 오류가 발생한다.

데이터소스 서브시스템

성능을 최대화하려고 할 때 데이터베이스 커넥션 풀과 스레드 풀 튜닝은 가장 중요한 영역들이다. 특히 시스템 리소스 관점에서 보면 데이터베이스 커넥션 대한 연결은 매우 비용이 비용이 많이 드는 작업이다.

데이터베이스 커넥션 풀링 기능은 웹 애플리케이션 서버 시작시, 미리 데이터베이스와의 연결을 맺어 풀에 두었다가 다시 사용하는 기술이다.

자바 애플리케이션에서 데이터베이스를 사용하는 방법은 데이터베이스에 연결한 다음 SQL 문을 실행하여 데이터 가져와 처리하고 마지막으로 데이터베이스를 닫는 것이다. 이 때 데이터베이스 연결을 맺고 끊는 작업은 매우 작업시간이 오래 걸리는 작업이다. 요청이 있을 때마다 이러한 단계를 거치는 것은 매우 비효율적이고 데이터베이스와 연결하는 부분에서 병목이 될 가능성이 높다. 이것을 방지하기 위해 커넥션 풀링을 사용한다.

애플리케이션 개발 시 데이터베이스 조회 시 매번 새로운 연결을 생성하고 바로 닫는 애플리케이션을 개발하는 경우도 있다. 이러한 방식은 트랜잭션 처리에 큰 오버헤드가 발생하여 성능이 느려지는 원인이 된다.

데이터소스는 DB와의 연결을 미리 생성해 커넥션 풀에 보관한다. 이 커넥션 풀에 의해 매번 요청 마다 DB 연결을 맺고 끊는 시간이 줄어든다.

연결 풀은 애플리케이션 스레드에서 connection 요청시(javax.sql.DataSource#getConnection)에 풀 안의 사용하지 않는 커넥션을 주고, 사용한 후에 커넥션 풀에 반환한다.

데이터소스 튜닝 시에는 주로 연결 풀에 대한 다음 사항들을 검토해야 한다.

  • 최대 커넥션 수 설정
  • DB 커넥션에 대한 유효성 점검
  • 타임아웃 설정
  • 스테이트먼트 설정

다음에서는 위의 항목에 대해 튜닝 시 검토해야 할 주요 파라미터에 대해 설명한다.

JDBC 연결 풀 설정 시 주의점

  • 커넥션수가 증가하지 않도록 데이터베이스 커넥션 수를 지정한다. ‘min-pool-size’와 ‘max-pool-size’를 같게 설정한다.

    즉 운영 시 [최초 커넥션 풀 수] = [최대 커넥션 풀 수]로 한다.

    성능 테스트를 하여 부하가 많을 때 최대 커넥션 수를 확인한다. 커넥션 풀 수는 동적으로 증가, 감소할 수는 있지만, 부하가 많이 걸리는 작업이므로 서버 오버헤드를 일으킬 수 있다. JBoss 시작 시 데이터베이스 커넥션 수를 최대 커넥션 수로([최초 커넥션 풀 수] = [최초 커넥션 풀 수]) 하는 것을 권장한다. 연결을 맺어 놓는 다소 시간은 걸리지만 운영중 연결 생성, 소멸의 오버헤드를 줄일 수 있다

  • 스레드 풀의 크기는 DB 커넥션 풀의 ‘max-pool-size’보다 크거나 최소한 같게 설정한다

    만약 ‘스레드 수 = 최대 커넥션 수’일 때 스레드가 각각 하나의 커넥션을 사용한다고 가정하면 부하가 많은 상황에서는 스레드가 최대 연결 즉, 모든 커넥션을 사용한다고 할 수 있다. 만약 스레드 수 < 최대 커넥션 수라면 스레드가 부족하여 커넥션이 풀에 남게 되어, 커넥션을 낭비한다. 그런데 애플리케이션의 종류에 따라 DB 연결을 하지 않는 작업이 많다면 ‘스레드 수 > 최대 커넥션 수’로 하는 것이 낭비를 줄일 수 있는 설정이다.

  • 커넥션을 비활성화(inactive)하려면, ‘idle-timeout-minutes’을 설정한다.

  • 커넥션 누수에 대한 트랙킹(track-statements) 옵션은 커넥션 풀에서 close 안한 커넥션을 로그에 남기고 연결을 닫아준다. 운영 서버에서는 이 옵션을 사용하지 않는 것을 권장한다. 이 옵션은 일반적으로 커넥션 풀의 동작을 약간 느리게 한다.

  • 데이터베이스 커넥션 테스트는 이로 인한 부하를 허용하는 경우에만 ‘check-valid-connection-sql’을 사용한다.

  • ‘check-valid-connection-sql’에 실제 사용하는 테이블을 지정하지 않는다. 더미 테이블(예, 오라클의 경우 dual)을 사용한다.

  • 가능한 경우 ‘check-valid-connection-sql’보다는 ‘valid-connection-checker-class-name’을 사용한다. 쿼리가 아닌 JDBC 내부 API를 사용하기 때문에 속도가 더 빠르다.

  • prepared나 callable statements의 성능을 개선하고자 할 때는 ‘prepared-statements-cache-size’를 사용한다.

  • 오라클 JDBC드라이버 버전이 10g 이하일 경우에는 오라클 XA 데이터소스를 사용할 때는 반드시 ‘no-tx-separate-pools’ 옵션을 설정한다. 오라클 XA드라이버에서 XA 연결시 글로벌, 로컬, No-트랜잭션을 혼용해서 사용할 경우 별도 커넥션이 맺어진다.

  • JBoss의 경우 커넥션 풀이 생성될 때 min-pool-size에 지정된 연결이 맺어지지 않는다. 애플리케이션이 맨 처음 사용할 때 연결을 맺는다. 애플리케이션 실행 성능을 높이려면 prefill 속성을 true로 설정하여 커넥션 풀이 생성될 때 min-pool-size의 연결이 맺어지도록 설정하는 것이 좋다.

💡 TIP: 스레드 수와 최대 커넥션 풀 수의 관계

운영 시 일반적인 설정 기준은 [최초 커넥션 풀 수] = [최대 커넥션 풀 수]= 스레드 ± α로 하는 것이 좋다. 부하 테스트를 통해 시스템에 가장 적합한 설정을 찾아야 한다.

커넥션 풀 수 설정

연결 풀은, 기본적으로 ‘데이터베이스와 커넥션을 미리 맺어 두고 필요할때 재사용한다.’라는 전략으로 데이터베이스 연결의 속도를 고속화해 성능을 향상하는 방법으로, datasources 서브 시스템의 자원으로 관리한다.

또 데이터소스에 대해 Non-XA 및 XA 데이터소스 두 가지 방식이 있다.

  • Non-XA데이터소스의 경우
/subsystem=datasources/data-source=<데이터소스명>
  • XA데이터소스의 경우
/subsystem=datasources/xa-data-source=<데이터소스명>

커넥션 풀 설정을 위한 데이터소스에 대한 주요한 속성은 데이터소스 서브 시스템에서 참조한다.

커넥션 풀의 타임아웃 설정

커넥션 풀의 동작 방법을 살펴보면 작업이 필요한 경우 애플리케이션 스레드는 커넥션 풀로부터 커넥션을 얻는다. 이때, 커넥션 풀이 가능한 연결 개수의 최대값에 도달하여 사용 가능한 커넥션이 없는 상태라면, 다른 애플리케이션 스레드가 사용하고 있는 커넥션이 반환되고 사용 가능한 커넥션이 풀에 확보될 때까지 기다리게 된다. 이때 얼마나 기다리고 있을지 타임아웃을 설정할 수 있다. 이외의 다양한 타임아웃 설정에 대해 살펴보자.

속성명기본값설명
blocking-timeout-wait-millis30000연결 대기로 블록 되는 최대 시간을 밀리 세컨드로 지정한다. 연결의 대기 시간이 설정 시간을 초과했을 경우, javax.resource.ResourceException이 발생한다. ※ 설정 파일(XML)의 파라미터 명은 blocking-timeout-millis 이다.
idle-timeout-minutes30풀에서 커넥션이 Idle 상태로 남아있는 시간을 분단위로 지정한다. 연결이 마지막 사용되고 지정된 시간 동안 사용되지 않으면 연결은 폐기된다. 0을 설정하면 Idle 상태의 커넥션은 폐기되지 않는다. Idle상태인 커넥션 체크는 설정 값의 1/2의 간격으로 실행된다.
set-tx-query-timeoutfalseJDBC의 쿼리 타임아웃에 트랜잭션 타임아웃 시간을 설정할 것인지를 true, false로 지정한다. 트랜잭션 타임아웃이 발생했을 경우, 트랜잭션 타임아웃 발생 시점에 실행 중인 쿼리는 중단된다.
query-timeout-JDBC 쿼리 타임아웃을 초 단위로 지정한다. 쿼리 실행 전에 java.sql.Statement#setQueryTimeout를 사용해 타임아웃을 적용한다. 기본값은 타임아웃을 적용하지 않는다.
allocation-retry0연결을 얻지 못할 때 재시도 횟수를 지정한다. 기본값은 처음 연결을 얻지 못하면 에러가 발생한다(Throw한다).
allocation-retry-wait-millis5000연결을 얻지 못하여 재시도할 때, 실행 간격을 밀리 세컨드로 지정한다.

표 6. DB 커넥션 풀의 타임아웃 속성

PreparedStatement 튜닝

  • PreparedStatement 란?

    PreparedStatement는 그 이름대로 JDBC 문(SQL 문장)을 캐시하여 성능을 향상하는 기능이다. 다음은 일반 Statement를 이용한 소스와 PreparedStatement를 이용한 소스의 예제이다.

  • Statement를 이용한 JDBC 연결

Statement stmt = conn.createStatement();

for ( int ID = 0 ; id < 10000 ; id++ ) {
String SQL = "SELECT NAME FROM ITEM WHERE I_ID =" + id;
ResultSet rs = stmt.executeQuery( sql );

while ( rs.next() ) {
// 테이블의 내용 처리
}
}

// PreparedStatement를 이용한 JDBC 연결
String SQL = "SELECT NAME FROM ITEM WHERE I_ID =?";

PreparedStatement PS = conn.prepareStatement( sql );

for ( int ID = 0; id < 10000 ; id++ ) {
ps.setInt( 1 , id );
ResultSet rs = ps.executeQuery();

while ( rs.next() ) {
// 테이블의 내용 처리
}
}

Statement 는 실행했을 때 마다 서버에서 SQ문을 분석해야 하는 반면, PreparedStatement는 미리 컴파일되기 때문에 쿼리의 수행속도가 Statement 에 비해 빠르고 또한 한번 만 분석되면 재사용이 된다.

Statement 와 비교할 때 preparedStatement 가 유리한 경우는 동일한 쿼리에 대해서 특정 값만 바꾸어서 여러 번 실행할 때, 많은 데이터를 조작하여 쿼리문이 복잡할 경우, 파라미터가 많을 경우이다.

위의 예제를 실제 실행해 보면, PreparedStatement를 사용하는 것이 성능이 월등히 빠르다.

실제 웹 애플리케이션에서는 preparedStatement 자체로 성능향상이 있기는 하겠지만, 이 부분도 전체 성능 중에 일부분이기 때문에 현실과는 차이가 있다.

위의 예제 프로그램의 경우 한 번 SQL문을 준비한 후, 같은 PreparedStatement를 10,000번 반복 실행하도록 작성되었다. 현실에서는 개발자가 웹 애플리케이션을 개발할 때 위의 예제처럼 한 번 SQL문을 준비한 후, PreparedStatement를 10,000 번 실행하고 Close 하는 경우는 없다. 즉, JSP와 서블릿이 호출될 때마다 PreparedStatement를 만들고 실행한 후 닫아버리기 때문이다.

애플리케이션 서버의 PreparedStatement 캐싱 기능

대부분 애플리케이션 서버는 ‘PreparedStatement 캐시’라는 기능을 제공하고 있다. PreparedStatement 캐시는 한 번 생성된 PreparedStatement를 버리지 않고 애플리케이션 서버의 메모리에 캐시한다. 같은 SQL 실행 요청일 경우 캐시에서 PreparedStatement를 불러와 다시 사용한다. 즉 SQL 문장 해석 작업을 다시 수행하지 않게 된다. 일반 웹 애플리케이션의 경우, 같은 SQL이 여러 번 사용되는 경우가 많아서 이를 통해 성능 향상을 기대할 수 있다.

PreparedStatement 캐시의 효과

PreparedStatement 캐시를 이용할 경우 다음의 효과를 예측할 수 있다.

  1. Java 객체 생성 비용 절감

    객체 생성의 비용은 의외로 높다. 캐시를 이용하여 객체 생성 횟수를 줄일 수 있다. 또한, 마찬가지로 삭제된 객체 수가 줄어들게 되어 JVM 가비지 컬렉션이 줄어든다는 장점도 있다. 애플리케이션 서버의 CPU 사용률을 줄일 수 있다.

  2. 데이터베이스와 통신 횟수 감소

    PreparedStatement사용 시 SQL문 해석을 위해 데이터베이스에 요청을 보내게 되지만, 캐시된 PreparedStatement를 사용하게 되면 그만큼 통신 횟수가 줄어들게 된다. 그러면 애플리케이션 서버, 데이터베이스 서버의 CPU 사용률 및 네트워크 사용률을 줄일 수 있다.

  3. 데이터베이스에서 Parse 횟수 감소

    PreparedStatement사용시 데이터베이스에서 쿼리 파싱과 쿼리 플랜을 만든다. 캐시를 사용하게 되면 파싱된 SQL 분석문을 버리지 않기 때문에 다시 사용할 때 파싱은 다시 실행되지 않는다. 이렇게 데이터베이스 서버의 CPU 사용률을 줄일 수 있다.

그림 . PreparedStatement 캐시

Statement 설정

Statement에 관한 데이터소스 자원 주요 속성은 아래 표와 같다. Non-XA 및 XA에서 모두 사용할 수 있다.

속성명기본값설명
track-statementsnowarn연결이 풀에 반환되었을 때 닫히지 않은 java.sql.PreparedStatement나 java.sql.ResultSet를 어떻게 트랙킹 할지를 다음 값으로 지정한다. * true - PreparedStatement 또는 ResultSet 가 닫히지 않은 경우에, 경고 메시지가 출력된다. * false - 트랙킹을 하지 않는다. * nowarn - 트랙킹은 하지만, 경고 메시지는 출력되지 않는다(기본값).
prepared-statements-cache-size-연결마다 캐시되는 PreparedStatement 개수를 지정한다. ※ 설정 파일(XML)의 파라미터명은 prepared-statement-cache-size이다.

표 . Statement 관련 설정

  • Statement설정 튜닝

    PreparedStatement를 사용하면 JDBC 연결의 성능을 향상하는 것은 잘 알려졌다. 몇몇 웹 애플리케이션 서버는 ‘PreparedStatement 캐시’라는 기능을 가지고 있으며, 이를 통해 성능을 향상할 수 있다.

    prepared-statements-cache-size에 1 이상의 정수를 지정하여, 데이터베이스 접속마다 Statement가 캐시 된다. 일반적으로 애플리케이션에서 사용하는 SQL (java.sql.PreparedStatement를 사용하는 것)의 수를 지정한다. SQL 문의 종류가 많을 경우, DBMS의 로그 등을 분석하여 캐시 사이즈를 설정한다.

  • CLI를 사용한 커넥션 풀 속성 변경

    CLI 에서 postgresDS 라는 풀에 대해서 min-pool-size, max-pool-size, pool-prefill를 설정하는 방법은 아래와 같다.

    1. postgresDS의 풀의 min-pool-size와 max-pool-size를 각각 ‘40’, pool-prefill를 ‘true’로 설정한다.

      [standalone@localhost:9999/] cd subsystem=datasources/xa-data-source=postgresDS

      [standalone@localhost:9999 xa-data-source=postgresDS] :write-attribute(name=min-pool-size, value=40)

      {"outcome" => "success"}

      [standalone@localhost:9999 xa-data-source=postgresDS] :write-attribute(name=max-pool-size, value=40)

      {"outcome" => "success"}

      [standalone@localhost:9999xa-data-source=postgresDS] :write-attribute(name= pool-prefill, value=true)

      {
      "outcome" => "success",
      "response-headers" => {
      "operation-requires-reload" => true,
      "process-state" => "reload-required"
      }
      }

      [standalone@localhost:9999xa-data-source=postgresDS] /:reload
    2. min-pool-size 및 max-pool-size의 설정 내용을 확인한다.

      [standalone@localhost:9999/] cd subsystem=datasources/xa-data-source=postgresDS

      [standalone@localhost:9999 xa-data-source=postgresDS] :read-attribute(name=min-pool-size)

      {
      "outcome" => "success",
      "result" => 40
      }

      [standalone@localhost:9999 xa-data-source=postgresDS] :read-attribute(name=max-pool-size)

      {
      "outcome" => "success",
      "result" => 40
      }

      [standalone@localhost:9999 xa-data-source=postgresDS] :read-attribute(name=pool-prefill)

      {
      "outcome" => "success",
      "result" => true
      }

서브시스템의 추가/삭제

서브시스템에서 설명한 것처럼, JBoss EAP 6에서는 이용하지 않는 서브시스템을 삭제할 수 있다.

어느 서브 시스템을 남기고 어느 서브시스템을 삭제할지는 JBoss EAP 6상에서 동작하는 애플리케이션이나 시스템에 따라 달라서 한 마디로는 말할 수 없지만, 운영 환경에서 반드시 삭제해야 하는 서브시스템이 있다.

H2 Database 의 삭제

H2 Database는 운영 환경에서 사용하는 것이 지원되지 않는다. 기본값인 H2 Database를 사용한 데이터소스 ExampleDS로 정의되어 있기 때문에 이를 삭제한다.

H2 Database 삭제 방법이다..

[standalone@localhost:9999] /subsystem=datasources/data-source=ExampleDS:remove

Deployment Scanner 삭제

배포 스캐너 삭제방법이다.

[standalone@localhost:9999] /subsystem=deployment-scanner/scanner=default:remove

로깅

애플리케이션 생명 주기를 보면 개발 단계와 테스트 단계에서 개발자는 로깅을 최대한으로 활용한다. 그러나 운영환경에서는 로깅이 병목이 될 수도 있다. 로깅으로 인해 성능에 영향을 주지 않고 유익한 정보를 제공해 주기를 원할 것이다.

애플리케이션을 운영하기 위해서 아래의 내용을 확인하라.

  • 운영환경에서는 콘솔 로깅은 사용하지 않는다.

    모든 로그를 볼 수가 있도록 JBoss EAP의 기본적인 구성에서는 콘솔 로깅이 활성화되어 있다. 운영환경에서 이것은 I/O를 소비하는 많이 하는 매우 비용이 많이 드는 프로세스이다. 대용량 처리 애플리케이션에서 콘솔 로깅을 끄는 것만으로도 성능 향상 이득을 얻을 수 있다.

  • Verbose 모드를 사용하지 않는다.

    로그가 적으면 적을수록 I/O는 발생하지 않기 때문에 애플리케이션 성능도 향상된다. 로깅은 항상 성능과 트레이드 오프이다. 운영환경에서는 실제로 얼마나 로깅이 필요한지 주의해서 생각해야 한다.

  • 비동기 로깅을 활용한다.

    대용량 처리를 하는 애플리케이션에서는 비동기 로깅을 사용하는 것만으로도 큰 차이가 생긴다. 비동기 로깅은 로그 메시지를 큐에 보내고 애플리케이션은 마치 로깅이 완료된 것으로 처리하고 제어권을 반환한다. 해당 로그 메시지는 다른 스레드가 큐에서 가져와 로그 처리를 한다.

  • 디버그용 로그 문장을 If(debugEnabled())로 작성한다.

    애플리케이션이 많은 양의 디버그 코드를 포함하고 있으면, if 문으로 디버그 로그를 남겨야 성능이 빠르다. 이 조건문이 없으면 애플리케이션은 각각의 로그 문장을 모두 String 오브젝트로 생성하고 Log4j는 각각의 로그메시지를 LoggingEvent 오브젝트로 변환한다. 로그 레벨은 오브젝트가 생성된 다음에 체크되기 때문에, 로그 레벨 설정과 관계 없이 객체가 생성되고 로그 이벤트가 발생한다. 때에 따라 이것은 무수히 많은 일시적인 String 오브젝트와 LoggingEvent 오브젝트들이 생성되기 때문에 그 결과 메모리와 가비지 컬렉션 문제가 발생하게 되어 성능이 대폭 감소하게 된다. 디버그 로그 코드를 조건절에서 코딩하면 불필요한 로그 처리가 발생하지 않아 성능에 영향을 미치지 않는다.