20장. JBoss EAP 7 튜닝

Name

Date

Reason For Changes

Version

오픈나루

2013/11

Initial Version

1.0

전준식, jjeon@opennaru.com

2018/02

Second Version

2.0

이번 장에서는 JBoss EAP 6 튜닝과 그 배경 지식에 대해 설명한다. JBoss EAP 6 는 기본 설정으로도 충분한 성능을 발휘할 수 있지만, 튜닝을 하여 더 높은 성능을 얻을 수 있다.

성능 튜닝은 애플리케이션 개발, 운영, 유지 보수에서 중요한 작업 중 하나이다. 커스텀 애플리케이션을 개발하거나 패키지 애플리케이션을 도입하는 경우에도 시스템을 구성하는 각각의 계층들인 애플리케이션, 데이터베이스, 미들웨어 부분에 튜닝이 필요하다.

이 문서는 JBoss EAP 6를 처음 사용하거나 성능 튜닝의 경험이 없는 운영자들이 시스템을 운영하기 전에 알아야 하는 웹 시스템 성능에 관한 배경 지식과 베스트 프랙티스를 설명한다. 이미 시스템 성능에 관한 문제들이나 튜닝에 대해서 익숙한 운영자도 미들웨어 기술들은 항상 변화하고 있기 때문에 지속적인 관심이 필요한 부분이다.

덧붙여 이번 장에서 소개하는 설정 값은 어디까지나 참조용이며, 최적의 값은 시스템 환경에 따라 달라서 테스트를 통한 튜닝 값을 얻어야 한다.

20-1.왜 성능 튜닝을 해야 하는가?

응답속도가 느린 웹 사이트는 고객들의 불만이 쌓이고 결국에는 다른 웹 사이트로 이동해 버리게 된다. 수익을 목적으로 하는 웹 사이트를 운영하는 기업이라면 고객의 관심뿐만 아니라, 비즈니스 기회도 잃어버릴 수 있다. 고객이 어쩔 수 없이 성능이 낮은 애플리케이션을 계속 사용해야 한다면 결국에는 비즈니스 트랜잭션이 줄어 버리거나, 서비스 사용을 포기하고 떠나가 버릴지도 모른다.

기업에서 임직원이 사용하는 애플리케이션이라도 예기치 못한 대기 시간이 발생하거나, 낮은 성능은 기업의 생산성에 영향을 미치게 된다. 이러한 상황에서 사용자는 시스템에 대한 불만이 생기고, 업무시간을 낭비하게 되어 IT부서에 대해서 부정적인 인식이 생기게 된다.

성능이 높은 애플리케이션의 특징은 하드웨어나 소프트웨어 자원을 보다 효율적으로 사용하는 것이다. 튜닝을 통해 최적화된 애플리케이션은 노후화된 시스템을 더욱 오랫동안 사용할 수 있게 되며, 신규 시스템 경우에는 도입 규모를 축소하더라도 기대한 성능을 제공하여 더 적은 투자비용으로 운영할 수 있다.

기업은 튜닝을 통하여 하드웨어 투자를 최적화할 수 있다. 소프트웨어 측면에서 보더라도 애플리케이션이 잘 튜닝되어 성능이 좋으면 더 작은 수의CPU를 사용하게 되고, 이에 따라 소프트웨어 라이선스 비용도 절감할 수 있다. 튜닝의 효과로 인해 기업은 시스템을 구축/운영/유지보수 하는 전 단계를 거쳐 비용을 절감하게 된다.

성능 튜닝 목표

성능에 대한 튜닝을 어느 수준까지 할지를 결정하는 것은 어려운 문제이다. 시스템 운영 초기의 성능은 주로 애플리케이션의 성능 최적화에 크게 의존하며, 이후에는 하드웨어, 데이터베이스, 애플리케이션 서버의 성능에 의해 결정된다. 많은 경험을 가진 엔지니어라 하더라도 하드웨어 사양이나 애플리케이션 서버의 종류에 따라 예상되는 성능을 추측하는 것은 불가능하다. 시스템 성능을 예측하기 위한 가장 현실적인 방법은 부하테스트를 진행하여 주어진 애플리케이션, 하드웨어, 소프트웨어에서 최적화된 성능 기준을 확인하고 이를 바탕으로 추론하는 것이다.

튜닝된 시스템을 대상으로 부하를 발생시키면 애플리케이션 서버의 부하가 가장 높아진다. 만약 애플리케이션 서버의 CPU 사용률이 80%가 넘을 경우, 미들웨어 측면에서 리소스에 의한 병목이 없어서 해당 하드웨어와 애플리케이션 서버에서 성능의 한계 값에 가깝다고 할 수 있다.

성능 튜닝은 병목 구간을 발견하고 이에 대해 대처해야 하는 수준 높은 기술을 요구한다. 튜닝 작업은 애플리케이션 서버 공급 업체 컨설팅 또는 경험이 풍부한 엔지니어를 찾는 등 외부와 내부의 지식을 효율적으로 사용하여 수행하는 것을 권장한다.

성능 튜닝의 기본 원칙

많은 기업이 IT 업계에서 일반적으로 사용하는 성능 벤치마크를 기준으로 미들웨어를 선택한다. 제품 공급 벤더에서 벤치마크는 판매를 위한 좋은 기준이 되지만 벤치마크 애플리케이션은 일반적인 기업의 운영환경 애플리케이션과는 달라서 고객에게 잘못된 정보를 전달하는 경우가 있다.

공식적인 성능 테스트 결과와 그 구성환경에 대해서 이해하더라도 기업마다 다르게 구축되는 시스템 환경으로 인하여 성능에 대한 지표를 참조할 수 있는 경우는 많지 않다. 현명한 판단을 위해서는 벤치마크 결과 전체를 받아들이기보다는 거기서 사용된 소프트웨어와 하드웨어의 구성, 서버와 네트워크의 구성, 각종 설정 상태, 애플리케이션 아키텍처를 조사하고 그것이 기업의 애플리케이션 환경과 비교하는 것이 중요하다.

원칙1 : 현재 운영 시스템의 성능을 파악한다.

성능 튜닝의 첫 번째 단계는 현재 운영 중인 시스템의 상황과 성능을 정확하게 객관적인 수치로 파악하는 것이다. 만약 운영 중인 시스템을 대체하는 경우라면 운영자들은 그동안 여러 가지 운영 경험이 있을 것이다. 접속 사용자 수, 1일 트랜잭션 수 , 1주/1달/1년 주기의 트랜잭션 수, 부하의 종류나 변화 등의 지표를 이미 알고 있을 것이다.

이전 시스템이 어떻게 운영되는지를 더 깊게 이해하면 더 뛰어난 성능 튜닝 결과를 이끌어 낼 수 있다.

원칙2 - 성능 기준은 평균이 아닌 피크 시간으로 한다.

성능 요건을 분석할 때는 피크 시간의 부하에 주의를 기울이면서 애플리케이션을 프로파일링하는 것이 중요하다. 일반적인 비즈니스 애플리케이션 경우 피크시간은 출근 직후 나 점심식사 시간 이후 오전과 오후에 발생한다. 애플리케이션에 따라 월말이나 마지막 분기 말마다 피크가 발생하는 경우도 있다. 운영자나 개발자 모두 피크 시간의 부하 상황에 대해서 항상 주의를 기울여야 한다.

개발자들이 많이 하는 성능과 관련된 오해 중 하나는 하루의 평균 워크로드를 기준으로 삼는 것이다. 평균값을 기초로 개발된 애플리케이션은 피크시의 부하에 충분히 대응할 수 있는 성능을 제공할 수 없다.

원칙 3 : 성능은 항상 모니터링 해야 한다.

모든 애플리케이션은 성능을 분석하기 위한 정보를 제공하고 있다. 고객의 사용패턴이나 부하는 시간에 따라 변화하여 아무리 성능이 뛰어난 애플리케이션이라도 상황에 따라서 좋은 성능을 보장하기 어렵다. 애플리케이션 성능이 측정되지 않는 상황에서 장애가 발생하면 그 원인을 밝히는 것은 더욱 어렵다.

애플리케이션을 모니터링 하면 사전에 비즈니스 상태의 변화를 감지하여 문제가 발생하기 전에 시스템 운영 환경을 그에 맞게 구성하는 것이 가능하다.

성능에 관련된 지표들을 어느 정도 수준으로 모니터링 할지를 결정하는 것은 튜닝에 대한 전문 지식과 시간 그리고 목적에 따라 결정해야 한다.

원칙 4 : 병목구간을 파악한다

애플리케이션을 모니터링하는 큰 이유 중에 하나는 처리 시간이 많이 소모되는 병목 구간을 찾아내는 것이다. 시스템을 구성하는 전체 스택 중 각각의 계층별로 걸리는 시간을 파악하고 싶다고 해도 애플리케이션 서버에 포함된 도구들은 애플리케이션 서버의 일부 정보만을 제공한다.

만약 애플리케이션이 사용자 응답시간 대부분을 데이터베이스에서 소비한다면, 문제의 원인을 밝혀내기 위해서는 데이터베이스가 제공하는 통계정보를 살펴봐야 한다.

애플리케이션에서 어느 구간에 시간이 많이 소요되는지를 객관적으로 파악하는 것이 성능문제를 해결하기 위한 지름길이다.

원칙 5 : 운영환경과 같은 환경을 구성한다.

운영환경에서 시스템이 어느 정도 성능이 제공되는지 정확하게 파악하기 위해서는 테스트 환경에서 성능을 확인해 봐야 한다. 기업에 따라서는 운영환경과 동일한 테스트 환경을 갖추는 것을 표준으로 하는 곳도 있다. 대규모 시스템을 운영하는 환경에서 애플리케이션이 어떻게 확장해 나가는지를 여러 가지 관점에서 살펴볼 수 있는 환경을 구성해 놓는 것이 이상적인 방법이다.

그러나 기업에서 운영 환경과 동일하게 갖추기 위해서 비용이 많이 들기 때문에 테스트 환경은 소규모로 구성한다. 테스트 환경에서 성능과 운영환경에서 성능의 관련성을 찾기 위한 모델이 필요하다. 이것이 시스템을 확장하는데 필요한 모델이다.

성능 병목 구간 찾기

성능 튜닝은 우선 병목 구간을 찾는 것부터 시작한다. 병목 구간을 찾은 후 어떤 상황에서 성능이 감소 되는지를 파악하는 것이 중요하다. 시스템의 성능 분석은 부하 테스트와 애플리케이션 프로파일링을 동시에 수행한다.

프로토타입 단계에서 성능이 나오지 않는 경우

아키텍처 설계 단계에서 프로토타입을 만들고 성능 테스트를 하였을 때 성능이 나오지 않는 경우가 있다. 성능상의 문제점을 사전에 파악하는 것이 프로토타입의 목적이기도 하다. 이 단계에서는 성능분석 작업에 아키텍처 설계자와 프로토타입 개발자(한 사람이 두 가지 역할을 하는 경우도 있다.)가 참여하고 있기 때문에 문제점이 발견되면 그것을 수정하는데 많은 시간이 걸리지 않는다. 또한, 프로그램 규모가 작아서 프로파일도 효과적으로 진행할 수 있다.

종합 테스트 단계에서 성능이 나오지 않는 경우

종합 테스트 단계에서 기대한 성능에 도달하지 못한 경우에는 테스트를 진행하여 초기 단계에서 성능에 문제를 발견한 것에 비해 범위는 넓어지지만, 기능별로 성능상의 문제가 되는 것을 비교적 쉽게 찾을 수 있다. 이 시점에서는 아직 서버 설정 변경을 자유롭게 할 수 있으므로 불필요한 요소를 제거하면서 테스트를 수행할 수 있다.

운영 중인 시스템에서 성능이 문제가 되는 경우

운영 중인 시스템에서 사용자가 "응답속도가 느리다."등의 불만이 있을 경우가 있다. 먼저 시스템의 어떤 계층에 문제가 있는지를 분명히 해 두지 않으면 쓸데없는 시간을 낭비하게 될 수 있다. 예를 들어 과부하시 전체적으로 응답이 나빠지는 것과 같은 막연한 상황에서는 운영 환경의 여러 계층을 의심해야 한다.

평상시에 주의 깊게 살펴봐야 할 모니터링 매트릭스를 지정하여 문제 발생할 때 즉시 대응할 수 있도록 운영 환경에서 모니터링을 한다.

성능 튜닝 개요

  • 하드웨어 / 네트워크

  • 운영체제

  • Java VM

  • 애플리케이션 서버

  • J2EE 서비스

  • 애플리케이션

전체 시스템의 성능을 높이기 위해서는 각 계층의 튜닝 포인트를 잘 이해하고 개발 단계의 초기 단계에서부터 성능 데이터를 수집하는 것이 중요하다.

image

그림 1. 성능 튜닝 요소

부하 테스트 및 프로파일링

부하가 낮은 상태에서는 성능이 느려지는 경우가 없기 때문에 부하 테스트 도구를 사용하여 부하가 높은 상태에서 병목 구간을 찾을 수 있다.

일반적으로 시스템의 성능은 응답 시간과 처리량의 두 가지 측면으로 나타낼 수 있다. 클라이언트 수(병렬 처리 요청 수)가 많아지면 처리량은 증가하지만, 응답 시간은 반대로 감소한다. 클라이언트 수의 증가에 따라 특정 시점을 경계로 처리량 증가가 멈추고(혹은 반대로 감소), 응답 시간이 급격히 감소하게 된다. 이렇게 처리량이 멈추는 시점을 Saturation 포인트라고 한다. 먼저 부하 테스트를 통해서 Saturation 포인트를 찾고, 이 한계점보다 앞의 중간 부하 수준(동시 접속 수)에서 한다.

http://photos1.blogger.com/blogger/2809/2566/1600/saturation.jpg

그림 2. 부하 테스트 시 Saturation 포인트

여러 공급 업체에서 부하 테스트 도구를 제공하고 있지만, 오픈 소스 부하테스트 도구인 Apache JMeter(http://jmeter.apache.org/)을 이용하면 상용 도구에 비하면 기능은 적지만 튜닝의 효과를 확인하는 수준이라면 이것으로 충분하다.

병목이 걸리는 특정 코드는 Java 프로파일링 도구(프로파일러)를 사용하면 된다. 프로파일링 도구를 사용하지 못하는 환경이라면, Java의 스레드 덤프를 이용하여 분석할 수 있다. 스레드 덤프는 ‘kill -3 <pid>’를 실행하면 현재 실행 중인 스레드들의 스택 트레이스를 stdout에 출력한다. 마치 JVM에 대해 X-Ray 사진을 찍는 것과 같다. 스레드의 변화하는 상태를 확인하려면 스레드 덤프를 여러 번 연속해서 얻는 것이 좋다. 예를 들어 3초 간격으로 5~10회 얻어 분석한다.

20-2.OS 튜닝

OS에 따라 설정 방법은 다르지만, 다음과 같은 유형의 구성에 주목하고 부족한 것이 있으면 설정 값을 늘린다.

  • 라지 페이지 메모리

  • 오픈 가능한 파일 수

  • 프로세스 수 / 스레드 수

  • 데이터 세그먼트 크기 / 스택 크기

  • TCP / IP 파라미터

Linux의 Large Page Memory 조정

옛날의 메인 프레임에서부터 지금까지 OS의 메모리 페이지 사이즈는 4 KB이다. 이 4 KB 단위는 메인 프레임 아키텍처에서 Magic number이며, 메인 프레임 시스템 전체가 바로 4 KB 로 튜닝 되었다. 예를 들면 I/O 명령의 처리 단위는 4 KB 이고, 디스크 장치의 블록 사이즈도 4 KB 가 최적 값이다. 키보드를 입력하면 전송되는 데이터와 응답 화면에 표시되는 데이터양도 4 KB 이하였다. 즉, 모든 I/O 처리나 메모리 처리를 4 KB 단위이기 때문에 4 KB 로 처리하면 최고의 성능을 얻을 수 있는 구조였다.

하지만 세월이 흘러 IT 환경이 변화하면서 데이터 크기가 점점 커지게 되고, 메모리 가격도 저렴해짐에 따라 현재 4 KB의 페이지 크기는 적합하지 않게 되었다. 수십 GB에서 몇 TB의 메모리를 탑재하는 서버도 4 KB 단위로 가상 메모리를 관리하면, 페이지 테이블 크기가 커져서 메모리에 부담을 줄 뿐만 아니라 페이징 등의 처리에서 오버헤드가 발생한다.

최근에 64 비트 시스템에서는 라지 메모리 페이지만 사용해도 높은 성능향상을 기대할 수 있다. 기본적인 메모리 페이지 사이즈는 4kb 이다. 만약 이 상태로 대용량의 메모리를 사용하게 될 경우 메모리 어드레싱 횟수가 많이 증가한다. 예를 들면 1기가(1,048,576 Kbyte)라고 하더라도 262,144개의 메모리 페이지가 필요하며 이것은 시스템에 큰 오버헤드가 발생한다.

리눅스에서는 빈번하게 발생하는 메모리 페이지 맵핑의 오버헤드를 줄이기 위하여 라지 메모리 페이지는 디스크에 스왑핑하지 않는다.

힙 영역에 대하여 디스크에 스왑을 하게 되면 애플리케이션 성능에 큰 영향을 미치기 때문에 라지 메모리 페이지는 성능에 매우 중요한 요소가 된다.

라지 페이지 메모리 크기는 하드웨어에 따라 다르겠지만 2MB~ 256 MB이다. 이 값은 서버 환경에 따라 달라서 사용하고 있는 서버 환경에 적합한 값을 찾아서 적용해야 한다.

대부분 자바 가상 머신들은 리눅스에서 라지 메모리 페이지를 지원하고 있다. 이 값의 설정은 쉽지 않기 때문에 전문가와 논의하여 진행해야 한다.

라지 페이지 메모리 즉 Huge Page를 설정하게 되면 리눅스 상에 다른 일반 애플리케이션에서는 사용할 수 없게 되고 지정된 애플리케이션에서만 사용하게 된다.

이 메모리는 특정 애플리케이션에 할당되어 있기 때문에 다른 애플리케이션들은 메모리가 제거된 것처럼 보이게 된다.

라지 페이지 메모리 설정 방법

  1. JVM에 Large Page 메모리 적용

    Sun JVM과 Open JDK에서 Large-Page를 사용하려면 다음과 같은 옵션이 필요하다.

    *-XX:+UseLargePages*
  1. 커널 파라미터 설정

    /etc/sysctl.conf 파일에 아래의 3개의 커널 파라미터를 추가한다.

    • kernel.shmmax = n

      n값은 시스템에서 허용되는 공유 메모리 세그먼트의 최대 바이트 값이 된다. 사용하고 싶은 최대 JVM heap 사이즈 값이 필요하다. 또는 시스템의 총 메모리 용량을 설정할 수도 있다.

    • vm.nr_hugepages = n

      n값은 Large-Page의 값이다. Large-Page의 사이즈는 /proc/meminfo를 참조하라.

    • vm.huge_tlb_shm_group = gid

      gid는 Large-Page 접근할 수 있는 그룹 ID이다. 이 설정으로 라지 메모리세그먼트 접근을 제한할 수 있다.

  1. limits.conf 파라미터 설정

    ‘/etc/security/limits.conf’의 memlock의 제한값 설정:

    <username> soft memlock n
    
    <username> hard memlock n

    <username>에는 JVM 사용자 계정이며, nvm.nr_hugepages의 페이지 값과 /proc/meminfo에 KB로 기재되어 있는 페이지 사이즈를 곱한 값을 설정한다.

  1. 설정을 적용

    # sysctl –p
  1. 리부팅

    OS에 메모리페이지를 할당한 후 안전한 시스템 운영을 위하여 시스템을 리부팅한다.

  1. 적용 값 확인

    Large-Page가 할당되었다면, /proc/meminfo로 HugePages_Total에 ‘0’ 이 아닌 숫자가 표시되게 된다. ‘0’으로 표시되어 있다면 Large-Page는 사용되지 않은 것으로 구성에 문제가 있는 것이다.

라지 메모리 페이지 설정 예

서버에 8 GB 메모리를 탑재하고 있다고 가정하자. JBoss EAP 6의 JVM과 MySQL 데이터베이스가 공유할 수 있도록 6 GB를 할당한다.

  1. Hugepagesize확인

    ‘/proc/meminfo’에 있는 페이지 사이즈는 2 MB이다(Hugepagesize: 2048 KB).

    # *cat /proc/meminfo*
    
    … 생략 …
    
    HugePages_Total: 0
    
    HugePages_Free: 0
    
    HugePages_Rsvd: 0
    
    HugePages_Surp: 0
    
    *Hugepagesize: 2048 kB*
    
    DirectMap4k: 685952 kB
    
    DirectMap2M: 16082944 kB
  1. /etc/sysctl.conf 설정

    • 최대 공유 메모리 세그먼트 사이즈를 8 GB로 변경

      kernel.shmmax = 8589934592
    • 사용자 계정에서 접근할 수 있도록, hugetlb_shm_group에 gid를 설정

      vm.hugetlb_shm_group = 501
    • JVM와 MySQL로 공유할 수 있도록, 2 MB페이지를 3072개 사용하여 6GB 할당

      vm.nr_hugepages = 3072
      
      kernel.shmmax = 8589934592
      
      vm.hugetlb_shm_group = 501
      
      vm.nr_hugepages = 3072
    • 계산식:1024 * 1024 * 1024 * 8 = 8589934592

      image103

  1. /etc/security/limits.conf 설정

    • JVM와 MySQL가 Large-Page메모리에 액세스 할 수 있도록 memlock에 제한 값 추가

      *jboss soft memlock 6291456*
      
      *jboss hard memlock 6291456*
      
      *mysql soft memlock 6291456*
      
      *mysql hard memlock 6291456*
      
      *root soft memlock 6291456*
      
      *root hard memlock 6291456*
    • 계산식 : 2048KB는 페이지 사이즈

      image104

  1. /etc/group 설정:

    사용자 계정에 공유 메모리 세그먼트 접근 권한을 주려면 ’/etc/group’의 501(hugetlb) 그룹에 JBoss와 MySQL를 추가한다.

    … 생략 …
    
    *hugetlb:x:501:jboss,mysql*

리눅스에서 가상 메모리 매니저 튜닝

Linux에서 가상 메모리 매니저 튜닝도 가능하다. 간단히 설정을 변경하여 성능을 향상할 수 있다.

Linux 2.6 커널의 swappiness라는 새로운 커널 파라미터가 추가되어 관리자가 Linux 스왑 처리 조정이 가능하다. 이 파라미터는 0 ~ 100 사이의 값을 설정하지만, 값이 클수록 페이지 스와핑을 많이 하고 값이 작으면 많은 메모리가 애플리케이션에 확보된다는 것이다. 메모리 스와핑이 너무 많이 발생하면 시스템 성능이 낮아지게 된다.

/etc/sysctl.conf 에 vm.swappiness의 값을 1로 설정하면 애플리케이션이 디스크에 스와핑하는 것을 막을 수가 있다.

애플리케이션 서버나 데이터베이스 서버와 같이 높은 성능을 요구하는 시스템에서 자주 스와핑이 발생하지 않도록 하는 것이 좋다. 스와핑이 발생하는 것을 최소화하기 위해 값을 0 ~ 10 정도의 값을 사용할 것을 권장한다.

vm.swappiness의 값을 확인하는 방법은 다음과 같다. 0으로 하면 실제 메모리를 다 사용할 때까지 스와핑 하지 않게 된다.

vm.swappiness 설정

  1. swappiness 값을 확인

    현재 swappiness 값을 확인(기본 60)

    # cat /proc/sys/vm/swappiness
    
    60
  1. vm.swappiness 설정 변경

    파일에 추가(10으로 설정하여 커널 파라미터를 반영)

    # vi /etc/sysctl.conf
    
    … 생략 …
    
    vm.swappiness = 10
    
    … 생략 …

    또는

    # sysctl -w vm.swappiness=10
  1. 설정을 반영

    # sysctl -p

    vm.swappiness의 기본값은 60인데 기본값의 절반인 10~15 정도로 줄이는 것이 스왑 사용량을 더 줄일 수 있다. 0으로 설정할 경우 스와핑하지 않기 때문에 어떤 상황에서는 메모리 부족이 빨리 발생될 수 있고 심하면 시스템이 멈추는 현상이 발생할 수도 있다.

    이처럼 튜닝할 경우 트레이드 오프가 있기 때문에 이런 튜닝 값들은 테스트를 통해 최적 값을 찾아야 한다. swappiness값을 줄일 때 주의할 점은 메모리가 부족한 상황이 발생하였을 때 시스템이 가용메모리 확보를 위해 스와핑하지 않기 때문에 시스템이 느려질 수도 있다.

20-3.Apache HTTPD 튜닝

Apache Httpd MPM모듈

아파치 웹 서버는 다양한 환경과 플랫폼에서 동작할 수 있도록 강력하고 유연하게 설계되었다. 아파치는 모듈화된 설계로 다양한 환경에 적응해 왔고, 관리자는 환경에 적합하게 컴파일이나 실행 시 어떤 모듈을 로딩할지 선택하여 서버가 제공하는 기능을 결정할 수 있다.

Apache Httpd 2.0은 이러한 모듈화된 설계를 웹 서버의 가장 기본적인 부분까지 확장하였다. HTTPD서버는 자식 프로세스에 분배하는 다중처리 모듈(Multi-Processing Modules, MPM)을 통해 시스템의 네트워크 포트에 연결하고, 요청을 받아들이며, 받아들인 요청을 처리하는 방법을 선택할 수 있다.

MPM은 Apache Httpd 2.0 서버에서 요청을 처리하는 부분이다. Apache 웹 서버의 MPM은 4가지 종류가 있다.

  • prefork

  • worker

  • perchild

  • winnt

다음은 리눅스 환경에서 Apache HTTPD의 MPM 을 확인하는 방법이다.

# httpd -l

Compiled in modules:

core.c

prefork.c

http_core.c

mod_so.c

Prefork MPM 과 Worker MPM 의 비교

Apache HTTPD 2.0 버전에 추가된 MPM 중 가장 많이 사용하는 Prefork MPM과 worker MPM의 차이점을 간단하게 설명한다. 먼저 prefork MPM은 Apache 1.3 버전에서 사용하던 방식으로 자식 프로세스를 먼저 시작해 놓고, 클라이언트 요청에 대해서 각각의 자식 프로세스가 통신을 담당하는 방식이다. 따라서 자식 프로세스가 어떤 원인으로 정지하더라도 다른 자식 프로세스에 영향을 주지 않는 특징이 있다. Worker MPM은 자식 프로세스에서 멀티 스레드로 실행되며, 클라이언트 요청을 스레드가 처리하는 방식이다. 하나의 프로세스가 멀티 스레드를 이용해 여러 요청을 담당하게 되어 prefork방식과 비교할때, 시작 시 프로세스 수를 줄일 수 있고, 메모리 사용량이 낮으며, 부팅 시간이 빠르다.

(r : 요청수, n : 자식 프로세스 수, m: 각 자식 프로세스당 스레드 수)

Prefork MPM Worker MPM

프로세스(멀티 프로세스)를 사용해 요청을 처리

멀티 스레드와 멀티 프로세스를 사용해 요청을 처리

r = n

r = n × m

PHP를 사용하는 웹사이트에서 높은 성능이 필요하다면 prefork MPM 을 사용

스레드는 메모리를 작게 사용하고 프로세스 방식보다 시작 시간이 빠르며 성능이 좋다.

image

image

<IfModule prefork.c>

  StartServers 8

  MaxClients 256

  MinSpareServers 5

  MaxSpareServers 20

  ServerLimit 256

  MaxRequestsPerChild 4000

</IfModule>
<IfModule worker.c>

  StartServers 2

  MaxClients 150

  MinSpareThreads 25

  MaxSpareThreads 75

  ThreadsPerChild 25

  MaxRequestsPerChild 0

</IfModule>

표 1. Apache HTTPD의 prefork방식과 worker 방식의 비교

위의 예에서 보면 StartServers는 prefork와 worker 모두 있는 항목이지만 prefork는 8개이고 worker는 2개이다. 이 차이는 프로세스와 스레드의 차이에 의한 것이다. 위의 설정에서 prefork 는 5에서 20개의 프로세스를 항상 유지한다. worker는 25에서 75개의 여유 스레드를 유지한다. 반면 worker 는 프로세스의 제한은 명확하게 설정되어 있지 않다. 위의 예에서 worker는 시작 시 2개 프로세스에서 사용 가능한 스레드가 범위에 없는 경우 프로세스를 추가하거나 불필요한 프로세스를 제거한다. 또한, MaxRequestsPerChild 0으로 무제한으로 설정되어 있다. 서버는 요청을 기다리고 있는 서버 스레드 개수를 체크하여 MaxSpareThreads, 즉 75개 이상이 있으면 서버 프로세스를 제거하고, MinSpareThreads 25개 미만인 경우 새로운 프로세스를 생성한다.

Apache HTTPD MPM 주요 설정

다음 표에서 Prefork 와 Worker 에서 사용하는 속성들을 정리한다.

속성 설명 비고

StartServers

Apache HTTPD 시작 시에 생성하는 프로세스 수를 지정한다. 여기서 지정한 프로세스 수는 시작 시에만 의미가 있으며, prefork의 경우 시작 이후 프로세스 수는 MinSpareServers와 MaxSpareServers 지정 값에 따라 조정된다.

MinSpareServers

클라이언트 요청을 기다리는 httpd서버 프로세스(idle 상태)의 최소값을 지정한다. 여기에서 지정한 값보다 프로세스 수가 작아지면, 새로운 프로세스를 생성한다. 트래픽이 많지 않은 사이트의 경우 초기값으로 설정할 것을 권장한다.

prefork만 해당

MaxSpareServers

클라이언트 요청을 기다리는 httpd서버 프로세스(idle 상태)의 최대값을 지정한다. 여기에서 지정한 값보다 프로세스 수가 많아지면 프로세스를 제거한다. 트래픽이 많지 않은 사이트의 경우 초기 값으로 설정할 것을 권장한다.

prefork만 해당

ServerLimit

prefork의 경우 서버 프로세스와 클라이언트 수가 같아지기 때문에 MaxClients에 설정한 최대값이다. worker의 경우 ThreadLimit와 함께 계산하여 MaxClients에 설정한 최대값이다.

prefork만 해당

MaxClients

동시에 처리할 수 있는 httpd서버 프로세스의 수를 지정한다. 즉, 클라이언트의 최대 접속 수이다. prefork의 경우는 프로세스 수를 지정하고 worker의 경우는 스레드 수를 지정한다.

MaxRequestsPerChild

하나의 httpd 프로세스로 처리할 수 있는 요청의 최대값을 지정한다. 0은 무제한이다.

MinSpareThreads

클라이언트 요청을 기다리는 스레드(idle 상태)의 최솟값을 지정한다. prefork의 MinSpareServers에 해당한다.

Worker만 해당

MaxSpareThreads

클라이언트 요청을 대기하는 스레드(idle 상태)의 최대값을 지정한다. prefork의 MaxSpareServers에 해당한다.

Worker만 해당

ThreadsPerChild

worker 프로세스 내에서 생성하는 스레드 수를 지정한다.

Worker만 해당

표 2. Apache HTTPD MPM 주요 속성

Worker MPM 으로 전환

Apache HTTPD 를 사용할 때 prefork MPM 방식을 적용해서 성능의 문제가 된다면 worker 방식으로 변경한다.

Prefork 경우 ServerLimit는 Apache프로세스의 MaxClients에 설정 가능한 최대값을 설정한다. 이는 ‘동시 클라이언트 수(MaxClients) = 최대 서버 프로세스 수(ServerLimit)’가 되며, ServerLimit는 MaxClients 값 이상으로 설정할 필요가 없다.

Worker 경우에는 ‘MaxClients(총 스레드 수) / ThreadsPerChild(하나의 프로세스가 생성하는 스레드 수) = 최대 서버 프로세스 수(ServerLimit)’가 된다.

image105

앞서 설명한 내용들에 대한 이해를 돕기 위해 다음 예를 들어 설명한다.

<IfModule worker.c>

  ServerLimit 60

  StartServers 2

  MaxClients 1500

  MinSpareThreads 25

  MaxSpareThreads 75

  ThreadsPerChild 25

  MaxRequestsPerChild 1000

</IfModule>

예제에서는 MaxClients(접속 가능한 요청의 최대값)를 1500으로 할 때, ServerLimit 설정 값은 ‘1500(MaxClients)/25(ThreadsPerChild)=60프로세스(ServerLimit)’가 되므로 60이 된다.

image106

20-4.JVM 튜닝

개요

Java 언어에서 오브젝트에 대한 메모리 할당과 해제는 자바 가상 머신이 자동으로 관리한다. 메모리에서 사용된 오브젝트를 자동으로 제거하는 메커니즘을 ‘가비지 컬렉션’이라 한다. 가비지 컬렉션은 Java 프로그램 성능에 결정적인 영향을 주기 때문에 그 동작방법을 이해하고 튜닝하는 것은 매우 중요하다.

가비지 컬렉션 역할

자바언어에서는 생성된 오브젝트를 메모리에 할당하기 위해 코드에서 명시적으로 메모리 공간을 확보하거나 제거할 필요가 없다. 이것은 C나 C 등의 언어와 자바 언어를 비교하였을 때 가장 큰 차이점 중의 하나이다. C나 C에서는 명시적으로 프로그램이 사용하는 메모리 영역을 할당하고 사용한 영역을 해제하도록 프로그램 코드로 작성해야 한다.

자바 가상 머신은 실행되고 있는 자바 프로그램 내에서 어디에도 참조되지 않는 불필요한 자바 오브젝트를 찾아 해당 영역의 메모리를 자동으로 해제한다. 이렇게 자바 오브젝트를 자동으로 제거하는 방법을 ‘가비지 컬렉션’이라고 한다. 자바 가상 머신 내부에서 가비지 컬렉션은 별도의 스레드로 주기적으로 동작한다. 이 가비지 컬렉션이 동작하는 주기는 자바 가상머신 힙의 크기와 애플리케이션에서 힙 메모리를 어떻게 사용하는지에 따라 달라진다.

Java 애플리케이션이 실행되면 오브젝트는 메모리에 로드된다. 사이즈가 큰 객체를 사용하거나 사용하는 오브젝트 개수가 많으면 그만큼 메모리 사용량이 증가한다. 자바에서 사용 가능한 메모리 영역을 힙 영역이라고 한다. 힙 영역 이외에도 Permanent 영역이 있다. 새롭게 생성된 오브젝트들을 계속 로드하면 자바가 사용할 수 있는 메모리 공간이 가득 찬다.

메모리가 가득 차면 새 객체를 로드 할 수 없어서 프로그램을 실행할 수 없다. 그래서 이를 방지하기 위한 구조가 가비지 컬렉션, GC(Garbage Collection)이다. GC는 힙에 여유가 없을 때 사용되는 객체와 사용되지 않는 객체를 판별하여 사용되지 않은 객체를 청소 대상으로 하여 메모리에서 삭제하는 방법으로 메모리 공간을 확보한다.

http://farm6.staticflickr.com/5144/5693012875_1aaf45b709.jpg

그림 3. 자바에서 GC란 메모리에 대한 분리 수거

자바 프로그래머들이 GC와 같은 JVM의 내부 작동 방법에 관해서 관심을 기울이지 않을 수도 있다. 하지만 신뢰성이 높고 성능이 좋은 자바시스템을 구축하기 위해서는 JVM 내부 메모리 관리 기능인 GC에 대해 그 구조를 파악하는 것이 매우 중요하다. 메모리 관련(OutOfMemoryError 등) 장애가 발생하였을 때 신속하게 파악하고 조치를 취하기 위해서는 GC 에 대한 지식은 도움이 된다.

가비지 컬렉션 장점

C 나 C++ 언어에서 프로그래머가 객체에 할당된 메모리 영역이 불필요하게 되면 프로그래머가 책임을 지고 해당 오브젝트가 사용한 메모리를 명시적으로 해제해야 한다. 만약 개발자가 이를 잊고 해제하지 않은 경우 메모리 누수와 잘못된 메모리 해제로 인하여 애플리케이션이 크래쉬 되어 장애의 원인이 된다.

이런 메모리 문제로 인한 버그는 잘못된 프로그램 코드와 실제 문제가 발생한 부분이 달라서 재현과 디버깅이 매우 어렵다. 장시간 동안 안정되게 운영해야 하는 웹 애플리케이션 서버에서는 큰 문제가 될 수 있다.

자바 런타임 환경에서는 메모리 공간을 확보하기 위해 프로그래머가 명시적으로 메모리 해제에 대해 신경 쓸 필요가 없다. 메모리 해제에 대한 개발자 실수가 없어서 신뢰성, 안전성이 높은 애플리케이션 개발을 할 수 있다. 물론 자바에서도 잘못된 코딩으로 메모리 누수가 발생하는 경우가 있다.

가비지 컬렉션으로 메모리 관리의 장점이 있지만 반면 이 때문에 애플리케이션의 성능이 감소하고, 응답 시간이 지연되는 등 성능 병목 현상의 원인이 될 수 있다.

특히 메모리가 크고, 많은 CPU를 사용하는 대규모 시스템에서는 가비지 컬렉션으로 인한 성능 저하도 비례하여 증가하게 된다. 가비지 컬렉션이 큰 메모리 영역을 여러 CPU로 효율적으로 처리하는 것이 과제이다. 이것을 해결하기 위해서 가비지 컬렉션 작업을 여러 스레드로 동시에 진행하는 병렬 가비지 컬렉션과 애플리케이션 스레드와 가비지 컬렉션 작업을 동시에는 하는 Concurrent 가비지 컬렉션 방법 등이 있다.

자바 힙 메모리 이해

자바 가상 머신에서 힙 영역은 신세대(Young Generation : New 영역)와 구세대 세대(Tenured Generation 혹은 Old Generation : Old 영역) 그리고 영구 세대(Permanent Generation 영역)로 세대(Generation)라는 개념으로 나누고 각각 다른 알고리즘으로 가비지 컬렉션을 수행한다.

자바에서 새로운 오브젝트를 생성하면 New 영역에 저장되고, 자주 참조되는 오래된 오브젝트들은 Old 영역에 저장된다. Permanent 영역에는 클래스나 메소드 등의 정보가 저장된다.

가비지 컬렉션은 신세대와 구세대 중 어떤 힙 영역을 대상으로 하느냐에 따라 다음 두 가지 종류가 있다. 첫 번째로 ‘Scavenge GC’는 신세대(Young / New Generation) 공간이 부족하면 실행되며, 비교적 자주 발생하고 짧은 시간에 처리가 끝난다. 두 번째로 ‘Full GC’는 Old 영역과 Permanent 영역이 부족하면 실행되며, 비교적 부하가 많은 작업이기 때문에 처리 시간이 오래 걸린다.

image

그림 4. 자바 메모리 구조

각 메모리영역에 대하여 간략하게 설명한다.

(1) New 영역(Young Generation)

New 영역은 Eden과 Survivor 로 나누어지고, Survivor는 다시 From, To로 나누어져 총 세 개의 영역으로 나뉜다. 프로그램이 실행된 후 새로운 오브젝트는 먼저 Eden 영역에 생성된다. Eden 영역이 새로운 오브젝트들로 가득 차게 되면, Scavenge GC(New / Young 영역 GC)에 의해 사용되지 않은 객체는 삭제되고, 사용 중인 객체는 From 영역과 To 영역으로 이동한다. Scavenge GC는 번갈아 한번은 Eden영역과 From영역에 오브젝트들을 조사하여 사용하지 않은 오브젝트는 삭제하고 사용된 오브젝트는 To 영역으로 복사하고, 다음번에 Eden 영역과 To 영역을 조사하여 사용하지 않은 오브젝트는 삭제하고 사용된 오브젝트는 From 영역으로 복사한다. 이런 방법으로 오브젝트들의 From 영역과 To 영역을 이동하는 횟수를 세어 사용 빈도가 높다고 판단된(MaxTenuringThreshold :기본값은 32회) 오브젝트들을 OLD 영역으로 이동하여 Full GC 전까지 살아남을 수 있다. New 영역이 부족하면 Scavenge GC가 자주 발생한다.

(2) Old 영역(Tenured Generation 혹은 Old Generation)

New 영역에서 살아남은 오브젝트가 저장되는 영역이다. 즉 수명이 긴 오브젝트들이 이 영역에 배치된다. 자바 가상머신은 Old 영역이 가득 차면 Full GC 를 수행한다.

(3) Permanent 영역(Permanent Generation)

클래스나 메서드 정보가 저장되는 영역이다. 많은 클래스를 로드하거나 JSP를 많이 사용하는 경우 Permanent 영역이 부족할 수 있어 보통 크기를 늘려 사용한다. 자바 가상머신은 Permanent 공간이 부족해도 Full GC를 수행한다.

힙 영역과 Permanent 영역을 설정하려면 다음과 같이 Java 옵션을 지정한다. m은 MB이다.

*-Xms1024m -Xmx1024m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:PermSize=64m -XX:MaxPermSize=256m*

다음은 힙 영역의 가비지 컬렉션 종류별 대상 영역에 대한 그림을 참조한다.

image

그림 5. 메모리 세대별 가비지 컬렉션

또한 Full GC가 수행되는 동안 다른 작업을 할 수 없어서 시스템은 정지 상태가 되며 이를 ‘Stop The World’라고 한다. Full GC는 시스템 성능에 큰 영향을 주기 때문에 가능한 발생하지 않도록 조정해야 한다.

다음은 자바 실행 옵션 중 메모리와 관련한 주요 옵션들을 정리한 것이다.

항목 설명

-Xms : 힙 메모리 초기값

-Xms : 힙 메모리 최대값

-Xmx와 -Xms는 각각 Young 영역과 Old(Tenured) 영역을 더 한 Heap 전체의 초기 메모리와 최대 메모리 크기이다. 초기값과 최대값이 다를 경우 힙 메모리의 변화에 따라 조정하는 작업으로 성능이 느려질 수 있다. 하드웨어 자원이 충분한 경우 성능 감소를 막기 위해서 초기값과 최대값을 같게 설정한다.

-XX:NewSize : New 영역의 초기값

-XX:MaxNewSize : New 영역의 최대값

NewSize와 MaxNewSize는 각각 Young 영역의 초기값과 최대값이다. 초기값과 최대값이 다를 경우 힙 메모리의 변화에 따라 조정하는 작업으로 성능이 느려질 수 있다. Young 영역이 작으면 Scavenge GC가 빈번하게 발생되며 동시에 비교적 수명이 짧은 오브젝트가 Tenured 영역으로 이동하여 Full GC의 빈도도 높아진다. Young 영역을 크게 잡으면 Scavenge GC 비용이 증가한다.

-XX:PermSize Permanent영역 초기값

-XX:MaxPermSize Permanent 영역 최대값

Permanent 영역은 자바 오브젝트의 클래스 정보가 저장되는 영역이다. 클래스 수는 애플리케이션 로직과 구조에 따라 결정되며, 클래스 수가 늘어나면 Permanent 영역의 크기가 증가한다. 클래스는 동적으로 JVM에서 로드되기 때문에, 애플리케이션의 크기에 따라 Permanent 사이즈도 증가한다. Permanent 영역이 부족하면 Full GC도 자주 일어난다. JBoss EAP 6에서 배포 스캐너를 적용한 상태에서 JBoss 실행 중에 재배포를 하는 경우, 클래스를 교체해야 해서 Permanent 영역을 충분히 크게 확보해야 한다.

-Xss

개별 스레드의 스택 사이즈를 지정한다. 스택을 어느 정도 사용하는지는 애플리케이션 로직에 따라 다르다. 예를 들어 스레드 스택 사이즈가 1M 이고, 스레드가 최대 100개 사용된다면 최대 100m의 메모리를 사용하게 된다. 많은 수의 스레드를 사용하는 애플리케이션은 스레드 스택 메모리도 많이 사용한다. 스택 저장영역이 부족하면 OutOfMemory 또는 프로세스 장애가 발생한다.

표 3. 주요 자바 메모리 옵션

자바 애플리케이션에서 ‘Stop-The-World’

Java 플랫폼은 가비지 컬렉션으로 인한 치명적인 문제를 내장하고 있다. 자바 애플리케이션은 Full GC를 수행하는 동안 모든 애플리케이션 스레드가 중지해야 한다. 아래의 그림은 Parallel GC 를 적용하였을 때 Stop The World가 발생하는 모습이다.

image

그림 6. Parallel GC의 Stop the world

Stop-The-World는 가비지 컬렉터가 애플리케이션에서 동작중인 모든 스레드를 멈춘 후 가비지 컬렉션을 수행하는 방식이다. 이 때문에 정지 시간이 실제로 애플리케이션에 얼마나 영향을 주었는지를 분석하기 위해서는 GC 로그를 확인해야 한다.

자바에서 가비지 컬렉터로 사용할 수 있는 방식은 다음 표와 같다. 해당 옵션에 대한 자세한 정보는 Oracle의 Java 가상 머신에 대한 페이지를 확인하라.

구분 옵션

Serial GC

-XX:+UseSerialGC

Parallel GC

-XX:+UseParallelGC

-XX:ParallelGCThreads=value

Parallel Compacting GC

-XX:+UseParallelOldGC

CMS GC

-XX:+UseConcMarkSweepGC

-XX:+UseParNewGC

-XX:+CMSParallelRemarkEnabled

-XX:CMSInitiatingOccupancyFraction=value

-XX:+UseCMSInitiatingOccupancyOnly

G1

-XX:+UnlockExperimentalVMOptions

-XX:+UseG1GC

표 . 자바 GC 옵션

자바 가상 머신 성능 튜닝

시스템 내부에서는 수많은 구성 요소들이 함께 동작하고 서로 복잡하게 의존하고 있다. 따라서 어떤 부분이 병목인지 찾는 것은 매우 어려운 일이다.

자바 가상 머신의 성능 튜닝은 웹 시스템 전체의 성능 튜닝 중 일부이며, 제일 마지막 단계의 병목을 찾아내는 부분이라 할 수 있다. 자바 가상 머신의 튜닝은OS, 웹 서버, 데이터베이스 서버, 웹 애플리케이션 서버 등의 튜닝에 비하여 노력 대비 얻는 효과가 높지 않지만, 안정된 서비스를 제공해야 하는 측면에서는 매우 중요하며 난이도와 노력이 많이 필요한 부분이다.

특히 자바 가상 머신의 성능 튜닝은 필요한 모든 데이터를 모니터링하고 수집하고 분석해서 추측하는 과정을 반복해서 수행해야 하는 과정이다. 성능 문제를 신속하게 해결하지 않으면 안 되는 긴급한 상황이 많기 때문에 모니터링과 분석 단계에 시간을 주지 않고, 추측이나 가설만 가지고 대응해 버리는 경우가 많다. 그러나 이러한 임기응변적인 대응은 상황을 더욱 악화시키게 된다. 시급하고 중요할수록 일정한 절차에 따라 성능 튜닝을 실시하는 것이 바람직하다.

앞부분에서 자바 성능의 가장 중요한 가비지 컬렉션에 대한 동작 방법과 주요 옵션들을 다루었다. 가비지 컬렉션 이외의 자바성능 튜닝에 관련된 상세한 내용은 자바 책들에서 다루고 있어 생략하도록 한다.

GC 로그, 자바 프로파일링, 자바 스레드 모니터링, 힙 메모리 등의 도구들은 각각의 용도에 맞게 오픈소스부터 상용 제품까지 다양한 제품이 있으니, 시스템 운영 상황에 맞는 적절한 분석도구들을 사용하여 측정 결과를 확인하고 성능 개선과 안정성 확보를 위한 작업을 지속해서 수행한다.

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 등의 모듈을 사용하여 소프트웨어 로드 밸런스형태로 사용한다.

image

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

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

동시 접속수

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

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

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

image

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

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

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

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

  • AJP/1. 3 커넥터

위치 속성 설명

/subsystem=web/connector=ajp

max-connections

워커 스레드 풀의 최대값. 기본값은 JVM이 사용할 수 있는 CPU 코어수 × 512 이다.

  • HTTP/1. 1 커넥터

위치 속성 설명

/subsystem=web/connector=http

max-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"
      }
    }
  1. reload 한다.

    [standalone@localhost:9999 /] /:reload
    
    {"outcome" => "success"}
  1. 웹 서브시스템의 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 의 값을 결정하는 계산 식은 다음과 같다.

      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_jk

connection_pool_timeout

JBoss 대기 상태 connection의 타임아웃 시간(초) 지정한다.

mod_cluster (mod_proxy)

ttl

JBoss와 연결 대기 상태 타임아웃 시간(초).

표 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 고객 포털에서 다운로드하여 설치해야 한다.

image

그림 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/ 에 접근하면 다음과 화면이 표시된다.

image

그림 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"}
컨텍스트 루트 비활성화와 배포 오류

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

데이터소스 서브시스템

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

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

자바 애플리케이션에서 데이터베이스를 사용하는 방법은 데이터베이스에 연결한 다음 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의 연결이 맺어지도록 설정하는 것이 좋다.

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

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

image107

커넥션 풀 수 설정

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

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

  • Non-XA데이터소스의 경우

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

    /subsystem=datasources/xa-data-source=<데이터소스명>

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

커넥션 풀의 타임아웃 설정

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

속성명 기본값 설명

blocking-timeout-wait-millis

30000

연결 대기로 블록 되는 최대 시간을 밀리 세컨드로 지정한다. 연결의 대기 시간이 설정 시간을 초과했을 경우, javax.resource.ResourceException이 발생한다.

※ 설정 파일(XML)의 파라미터 명은 blocking-timeout-millis 이다.

idle-timeout-minutes

30

풀에서 커넥션이 Idle 상태로 남아있는 시간을 분단위로 지정한다. 연결이 마지막 사용되고 지정된 시간 동안 사용되지 않으면 연결은 폐기된다. 0을 설정하면 Idle 상태의 커넥션은 폐기되지 않는다.

Idle상태인 커넥션 체크는 설정 값의 1/2의 간격으로 실행된다.

set-tx-query-timeout

false

JDBC의 쿼리 타임아웃에 트랜잭션 타임아웃 시간을 설정할 것인지를 true, false로 지정한다. 트랜잭션 타임아웃이 발생했을 경우, 트랜잭션 타임아웃 발생 시점에 실행 중인 쿼리는 중단된다.

query-timeout

-

JDBC 쿼리 타임아웃을 초 단위로 지정한다. 쿼리 실행 전에 java.sql.Statement#setQueryTimeout를 사용해 타임아웃을 적용한다.

기본값은 타임아웃을 적용하지 않는다.

allocation-retry

0

연결을 얻지 못할 때 재시도 횟수를 지정한다. 기본값은 처음 연결을 얻지 못하면 에러가 발생한다(Throw한다).

allocation-retry-wait-millis

5000

연결을 얻지 못하여 재시도할 때, 실행 간격을 밀리 세컨드로 지정한다.

표 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 사용률을 줄일 수 있다.

    The caching of PreparedStatement’s in the database.

    그림 . PreparedStatement 캐시

Statement 설정

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

속성명 기본값 설명

track-statements

nowarn

연결이 풀에 반환되었을 때 닫히지 않은 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 오브젝트들이 생성되기 때문에 그 결과 메모리와 가비지 컬렉션 문제가 발생하게 되어 성능이 대폭 감소하게 된다. 디버그 로그 코드를 조건절에서 코딩하면 불필요한 로그 처리가 발생하지 않아 성능에 영향을 미치지 않는다.

20-6.관련 시스템 튜닝

성능의 80%는 애플리케이션 설계 시 결정되지만, 단순한 실수로 성능의 문제가 발생하는 경우도 있다. 문제가 발생하였을 때 논리적으로 어느 부분이 병목인지 조사해 볼 수 있는 몇 가지 예를 살펴보자.

웹 서버 튜닝

예를 들어 Apache 경우 대량의 접근이 급격하게 집중했을 경우, 많은 부하를 안정적으로 처리하기 위해 다음 파라미터를 조정한다.

  • StartServers

  • MaxClients

  • MaxSpareServers / MinSpareServers

사이즈가 큰 파일로 인한 웹 서버 성능 감소

  • 현상

    HTTP 서버의 큰 역할은 웹 브라우저에서 요청한 정적 파일을 브라우저로 다시 전송하는 것이다. 즉, 크기가 큰 이미지 파일이 많이 포함된 페이지가 많은 경우 자주 다운로드하기 때문에 웹 서버의 부하가 높다. HTTP 서버의 부하가 높아지면 당연히 Java 애플리케이션 서버로 중계하는 통신 프로세스가 느려져 전체적으로 성능이 감소할 수 있다.

  • 조치

    웹 서버의 하드웨어 성능 향상을 하는 것이 해결 방법이다. 즉 하드웨어 CPU 수, 메모리 등을 업그레이드한다. 또는 로드밸랜서(L4) 등을 통하여 웹 서버 시스템을 Scale-Out 한다.

SSL 사용으로 인한 성능 감소

  • 현상

    SSL을 사용하는 페이지가 많은 경우 성능이 감소 될 수 있다. SSL을 사용하면 약 CPU를 30% 정도 추가 요구한다.

  • 조치

    하드웨어 업그레이드나 웹 서버에 대한 Scale-Out을 통한 해결방법이 있다. SSL로 인해 성능 문제가 되는 경우 SSL을 사용하는 페이지 개수를 줄이는 것도 튜닝 방법의 하나다.

데이터베이스 튜닝

데이터베이스 성능 튜닝은 애플리케이션 서버와 관련성이 없다. 일반적인 데이터베이스 튜닝 작업을 진행한다.

애플리케이션 서버 튜닝

애플리케이션 서버의 성능 문제 원인을 찾는 것은 어렵다. 사실, 문제의 원인을 찾는 것은 그렇게 단순하지 않고 개발한 코드가 성능을 감소시키는 원인일 경우가 많다. 이것을 어떻게 찾아가면 좋을까?

  1. 웹 시스템의 계층별 부하 상황 파악

    HTTP 서버, 애플리케이션 서버, 데이터베이스 서버의 부하 데이터를 얻는다. 부하 테스트 도구를 이용하면 좋을 것이다. 만약 하나의 서버만 과부하 상태라면 위에서 설명한 것과 같은 항목을 의심하고 원인을 조사하자.

  2. 구간별 네트워크 부하 데이터를 파악

    인터넷, HTTP 서버, 애플리케이션 서버, 데이터베이스 서버 사이의 네트워크 부하 상태를 확인한다. 네트워크 로드가 높은 경우는 부하를 주는 패킷이 무엇인지를 확인한다. 대부분은 애플리케이션을 실행하는 웹 애플리케이션 서버가 병목이 될 가능성이 가장 높다고 할 수 있다. 먼저 HTTP 서버를 시스템에서 분리하여 테스트한다. 일반적인 방법으로 웹 서버에 걸리는 부하를 애플리케이션 서버에 직접 걸어 테스트하여도 여전히 성능이 나오지 않는 경우엔 애플리케이션 서버 내에서 더 자세히 확인해야 한다.

부하 상황에서 애플리케이션 서버의 CPU 사용률이 늘지 않는 경우

부하 테스트 도구를 사용해도 애플리케이션 서버의 CPU 사용률이 오르지 않으면 어떤 리소스에 락이 걸려 그 락이 풀리기를 기다리고 있을 수 있다. 일반적인 원인으로 생각되는 것들은 다음과 같다.

  1. 커넥션 풀의 대기

    우선 생각할 수 있는 것은 SQL 처리에 시간이 걸리는 경우이다. 커넥션 풀 수가 부족해서 여러 스레드가 데이터베이스 연결을 얻기 위해 대기한다.

  2. 동기화 객체 사용

    동기화(synchronized)를 정의하는 클래스를 구현하는 경우에 발생한다. 특히 syncronized가 선언된 클래스나 메서드의 처리가 느린 경우 리소스가 해제될 때까지 다른 스레드가 기다리게 된다.

  3. 스레드 데드락

    애플리케이션 서버 내에서 사용자 프로그램이 스레드를 생성하여 동기화 같은 경우는 많지 않다. 스레드를 사용할 때는 많은 주의가 필요하다. 스레드 데드락은 스레드 덤프를 출력하면 표시된다.

    위 같은 경우는 부하가 높은 상태에서 교착 상태(여러 스레드가 서로 대기 있고, 락이 걸리는 현상, Dead Lock)가 되기 쉽다. 따라서 성능 감소와 함께 교착 상태에 빠져 있지 않은지도 확인하는 것이 좋다.