설정에 대한 튜닝은 반드시 해야 한다. 대개 기본값으로 최대한의 성능을 낼 수 있는 것은 없다. 웹 기반의 시스템도 정상적으로 작동하게 하려면 세팅이 대단히 중요하다. 프로그램에 문제가 없는데 세팅 값 하나 때문에 애플리케이션의 성능이 안 좋아지는 경우가 굉장히 많기 때문이다. 거꾸로 이야기하면, 만약 성능상 이슈가 있을 때 그 원인이 설정 때문이라면 아주 쉽게 해결할 수 있다.
개발하는 것만큼 중요한 것이 서버의 세팅이다. 개발된 프로그램이 0.1초 걸린다고 해도 서버 세팅을 잘못하면 1초가 걸릴 수도 있고, 10초가 걸릴 수도 있다. 이러한 문제를 진단하는 가장 좋은 방법은 성능 테스트를 통해서 병목 지점을 미리 파악하는 것이다. 무조건 애플리케이션 위주로 병목을 찾는 것보다, 일단 문제가 될만한 세팅 값을 먼저 진단하는 것이 가장 효율적이다. 웹 기반의 시스템에서 성능에 영향을 줄만한 세팅을 나열해 보면 다음과 같다.
그러면 각 서버별로 어떤 값의 세팅을 유념해야 하는지 자세히 알아보자.
웹 서버의 세팅을 먼저 알아보자. WAS를 웹 서버로 사용하면 안 된다. 웹 서버를 WAS 뒤에 둘 사람은 없겠지만, 웹 서버는 반드시 WAS 앞에 두어야 한다. 왜냐하면 WAS는 Web Application Server이기 때문이다. 웹에서 사용하는 애플리케이션 서버이지 웹 서버가 아니다. 정적인 부분은 웹 서버에서 처리해야 한다. 그렇지 않으면 WAS 서버에서 웹 서버의 역할까지 수행해야 한다. 웹 서버를 WAS 서버 앞에 두지 않으면 이미지, CSS, 자바 스크립트, HTML 등을 처리하느라 아까운 WAS 서버의 스레드를 점유하게 된다. 반드시 상용 웹 서버나 아파치 웹 서버를 WAS 단 앞에 놓고 운영해야 한다. 간혹 그럴 필요가 없다고 하는 사람들이 있지만, 웹 서버 하나에 WAS가 두 개 이상 연동되는 경우 등을 고려하면 웹 서버를 앞 단에 두는 것이 좋다.
여기서는 아파치 웹 서버에서 성능에 영향을 줄 수 있는 세팅을 알아보자.
아파치 웹 서버는 MPM이라는 것을 사용한다. MPM은 Multi Processing Module의 약자로 여러 개의 프로세싱 모듈 기반의 서비스를 제공한다는 의미다. 가장 쉽게 아파치 웹 서버의 설정을 바꾸는 방법은, 설치 폴더 하단의 conf 디렉터리에 있는 httpd.conf 파일을 수정하는 것이다. 이 파일의 중간 부분을 보면 다음과 같은 설정이 있다.
ThreadsPerChild 250
MaxRequestsPerChild 0
ThreadsPerChild는 웹 서버가 사용하는 스레드의 개수를 지정한다. 이와 같이 지정하면, 아파치 프로세스 하나당 250개의 스레드가 만들어진다. 만약 이 수치가 적게 지정되어 있다면, 이 수치를 늘려줘야 한다. 그래야 서버가 더 많은 사용자의 요청을 처리할 수 있게 된다.
MaxRequestPerChild는 최대 요청 개수를 지정하는 부분이다. 0이면 그 수에 제한을 두지 않겠다는 의미가 된다. 만약 이 값을 10으로 둔다면, 그 이상의 처리는 하지 않게 된다. 가급적 기본값인 0으로 두고 사용해야 한다.
스레드와 관련된 내용을 보다 세밀하게 지정하려면 httpd.conf 파일에서 #으로 주석처리 되어 있는 "include conf/extra/httpd-mpm.conf"를 찾아서 주석을 해제한다. 이렇게 하면 세밀한 스레드 설정 정보를 http-mpm.conf를 통해서 지정할 수 있게 된다. 스레드 방식을 사용하기 위해서는 worker 부분을 수정해 주어야 한다. 이 파일의 내용을 들여다 보자.
<IfModule mpm_worker_module>
StartServers 2
MaxClient 150
MinSpareThreads 25
MaxSpareThreads 75
ThreadsPerChild 25
MaxRequestsPerChild 0
</IfModule>
...
파일에 이렇제 지정되어 있는 부분이 있을 것이다. 각각의 내용에 대해서 간단히 알아보자.
여기서는 프로세스 수(StartServers)가 2개이고, 프로세스 당 스레드 수(ThreadsPerChild)가 25이므로 기본적으로 50개의 요청을 처리할 수 있다. 또한 최대 여유 스레드(MaxSpareThreads)가 75개이므로, 최대 사용 가능한 클라이언트 수(MaxClient)는 150이 된다. 중요한 점은 이 서버는 150명이 최대라는 것이다. 150명 이상의 요청은 서버 리소스에 여유가 있어도 처리를 안한다.
이렇게 설정되어 있을 때 사용자가 늘어나거나 WAS가 멈추면 어떤 상황이 되는지 예를 들어 알아보자. 어떤 서비스가 초당 150명의 요청을 받고 있다고 가정해 보자. 이 상황에서 웹 서버에서 150명의 요청을 받고, 그 요청을 전달받은 WAS도 마찬가지로 150명의 요청을 받는다. 그런데, 자바는 GC를 할 때 JVM 자체가 멈춘다. 만약 이 GC가 2초 걸리면 어떻게 될까? 아파치 웹 서버에 총 300명의 요청이 기달리게 될 것 이다. 그런데 GC를 하는 동안 WAS가 멈추기 때문에 새로운 연결을 할 수 없다. 이 경우 Tomcat에서는 AJP Conntector라는 웹 서버와 WAS 사이의 커넥터에 설정한 backlog라는 값의 영향을 받는다. 만약 이 값을 설정하지 않으면 기본값은 100이다. 즉 WAS가 응답하지 않을 때 100개의 요청까지 큐에 담아둔다는 말이다. 따라서, 이 100개를 넘는 요청들은 503(Service Unavailable)이라는 HTTP 헤더 코드 값을 리턴 받게 된다. 그러면 사용자에게 503이라는 에러 메시지가 화면에 뿌려질 것이다. 이러한 값을 받지 않으려면 다음과 같은 조치를 취하는 것이 좋다.
1) 서버를 늘린다. 어떻게 보면 가장 편한 방법이다.
2) 서비스를 튜닝한다. 서비스가 응답이 안 되는 원인을 찾고 튜닝한다. 하지만 실제로 그 원인 찾는 데는 몇 시간이 소요도리 수도 있으며, 몇 달이 걸릴 수도 있다.
3) GC 튜닝을 한다. 만약 GC가 오래 소요되어 응답이 안될 경우 GC 튜닝을 한다.
4) 각종 옵션 값을 튜닝한다. 하지만, 잘못 설정할 경우 오히려 더 큰 문제가 야기될 수도 있기 때문에 해당 웹 서버 및 WAS 전문가와 엔지니어와 이야기해서 옵션 값을 설정해야 한다.
만약 서버가 매우 좋아서 더 많은 요청을 처리해야 할 필요가 있다면 httpd.conf 파일이나 httpd-mpm.con 파일을 수정하여 서버가 최대한의 자원을 사용하도록 변경해야 한다.
모든 웹 서버를 설정할 때 또 한 가지 중요한 값이 있다. 바로 KeepAlive 설정이다. 아파치 웹 서버의 경우, httpd.conf 파일에 다음의 설정이 없으면 간단하게 한 줄 추가하면 된다.
KeepAlive on
웹 서버와 웹 브라우저가 연결이 되었을 때 keepAlive 기능이 켜져 있지 않으면, 매번 HTTP 연결을 맺었다 끊었다 하는 작업을 반복한다. 초기 화면이 매우 간단한 구글과 같은 사이트는 해당 사이트에 KeepAlive와 같은 설정을 하지 않더라도 그리 느리지 않을 것이다. 하지만 네이버와 다음과 같이 초기 화면에서 엄청나게 많은 이미지와 CSS, 자바 스크립트 등의 파일을 담아야 하는 사이트에서 KeepAlive 옵션이 적용되어 있지 않다면, 초기 화면을 띄우는 데 몇 분씩 소요될 지도 모른다. 즉, 이미지와 같은 모든 개체들도 서버에 매번 접속을 해야 하는 상황이 발생한다. 하지만 KeepAlive 기능이 켜져 있으면 두 개 정도의 연결을 열어서 끊지 않고, 연결을 계속 재사용한다. 이렇게 되면 연결을 하기 위한 대기 시간이 짧아지기 때문에 사용자가 느끼는 응답 속도도 엄청나게 빨라진다.
사용자의 접근이 많은 사이트에서는 이미지와 CSS와 같이 정적인 파일들을 일반적인 웹 사이트에서 처리하지 않고, CDN(Content Delivery Network)이라고 하는 서비스를 사용한다. 즉, 별도의 URL에서 해당 컨텐츠들을 내려받도록 설정하고, 동적인 컨텐츠들은 WAS에서 처리하도록 해 놓으면 Web-WAS 서버의 부담도 줄어들게 된다.(하지만 비용이 좀 비싸다는 단점이 있다.)
KeepAlive 설정을 할 때 반드시 같이 해야 하는 설정이 있다. 바로 KeepAlive-Timeout 설정이다. 이 설정은 초 단위로 KeepAlive가 끊기는 시간을 설정하기 위한 부분이다. 마지막 연결이 끝난 이후에 다음 연결이 될 때까지 얼마나 기다릴 지를 지정한다. 설정은 다음과 같다.
KeepAliveTimeout 15
만약 사용자가 너무 많아 접속이 잘 안될 경우, 이 설정을 5초 정도로 짧에 주는 것도 서버의 리소스를 보다 효율적으로 사용할 수 있는 방법이다. 추가로 서비스의 상황에 따라서 KeepAlive 옵션을 껐을 때 좋은 성능이 나오게 되는 경우가 존재한다. 무조건 KeepAlive를 켜야 한다는 것이 아니니 상황에 맞게 사용해야 한다.
WAS에서 설정해야 하는 값은 너무나 많다. 그 중 가장 성능에 영향을 주는 DB Connection Pool고 스레드 개수에 대해서 알아보자. 이 두 항목의 개수는 메모리와 관련이 있다. 많이 사용할수록 메모리를 많이 점유하게 된다. 그렇다고 메모리를 위해서 DB Connection Pool과 스레드 개수를 적게 지정하면, 서버에서는 많은 요청을 처리하지 못하고 대기할 수 밖에 없다.
대부분의 WAS에서는 DB Connection Pool의 개수를 최소치, 증가치, 최대치 등으로 자세하게 지정할 수 있다. 최소치는 말 그대로 서버가 기동될 때 연결을 수행하는 개수이다. 개발자용 PC에서는 이 값이 높을 필요가 없으므로 이 값은 최소한으로 짖어되도록 하자. 최소 개수가 많으면 많을수록 서버 기동하는 시간이 오래 소요되므로, 개발자가 디버그를 하기 위해서 여러 번 재기동을 할 때는 좋지 않다.
하지만 운영 중에는 최소 및 최대 값을 동일하게 하는 것이 좋다. 사용자 수가 갑자기 증가하면 DB Connection Pool의 개수도 증가 되어야 하고, 증가할 때 대기 시간이 발생할 확률이 크기 때문이다. 만약 DB 서버의 리소스가 부족하다면 최소 값을 적게 해 놓는 것도 한 방법이 될 수 있다.
대부분 WAS에서 두 설정의 기본 개수가 10~20개 정도이다. 따라서 기본 값으로 서비스를 오픈하면 서버가 원하는 요청량을 처리하지 못하게 된다. DB Connection Pool은 보통 40~50개로 지정하며, 스레드 개수는 이보다 10개 정도 더 지정한다. 이렇게 지정하는 이유는, 스레드 개수가 DB Connection Pool의 개수보다 적으면 적은 수 만큼의 연결은 필요 없기 때문이다. 예를 들어 스레드를 40개로 지정하고, DB Connection의 개수를 50개로 지정하면, 적어도 10개의 연결은 전혀 사용을 하지 않게 된다. 쉽게 스레드는 입구이고 DB Connection Pool은 출구라고 생각하면 된다.
그럼 스레드의 개수가 DB 연결 개수보다 많아야 하는 이유는 뭘까? 모든 애플리케이션이나 화면이 DB에 접속하는 것은 아니다. 또한 관리자 콘솔을 사용하여 서버에 접속할 수도 있기 때문에, 그만큼 여유분을 갖도록 지정하는 것이 보통이다.
그러면 가장 적합한 DB Connection Pool과 스레드 개수는 몇 개일까? 웹 서버의 세팅 값도 그렇지만, 이 수치도 서버와 애플리케이션 상황에 따라 다르며, 완벽한 값은 없다. 서비스 및 서버의 상황에 맞게 값을 지정해야 하는 것이다. 가장 좋은 방법은 성능 테스트를 통해서 가장 적절한 값을 구하는 것이다.
DB Connection Pool의 개수를 기준으로 적절한 수치를 찾는 방법을 생각해보자. DB Connection Pool의 개수를 40개로 잡아 놓았다고 가정하자. 40개 전부를 사용하면서 DB의 CPU 사용량이 100%에 도달햇다면 어덯게 해야 할까? 이 경우에는 DB의 CPU를 점유하는 쿼리를 찾아서 튜닝을 해야 한다. 다시 말하면, 인덱스가 없거나 테이블을 풀 스캔하는 쿼리가 있는 것은 아닌지 쿼리의 플랜을 떠서 확인해 봐야 한다. 40개를 전부 사용한다고 해서 DB Connection Pool 개수를 80~200개로 늘리면, 모든 DB와의 연결을 전부 사용하고 응답 시간은 엄청나게 느려질 뿐이다.
이번에는 DB의 CPU 사용량은 50%도 되지 않는 상황에서 WAS의 CPU 사용량이 100%에 도달하고 있는 상황을 생각해보자. 그 때 사용하는 DB Connection Pool의 개수는 20개 정도밖에 되지 않는다. 이럴 때는 어떻게 할까? 이 경우에는 WAS의 애플리케이션을 튜닝해야 한다. 하지만 이미 튜닝이 된 상태라면 이 서버의 DB Connection Pool의 개수는 약간의 여유를 두기 위해서 25~30개 정도로 지정하는 것이 좋다.
Connection Pool의 개수만큼 중요한 값이 있다. 바로 대기 시간(wait time)과 관련된 값이다. MyBatis와 같이 DB와 자바 프로그램을 매핑(mapping)해 주는 프레임워크에는 각종 설정 값들이 잇는데, 이 값 중에서 대기 시간을 나타내는 wait time과 관련된 값들이 존재한다.
이 값이 왜 중요할까? 앞서 이야기한대로 DB Connection Pool의 개수를 넘어섰을 때 애플리케이션에서는 어디 남는 Connection 없나?하고 두리번거리면서 기다린다. 그런데, 이 기다리는 시간이 바로 대기 시간이다. MyBatis에서는 poolTimeToWait라는 값으로 이 대기 시간을 결정하며, 기본은 20초다. 다시 말해서 이 값을 그냥 놔 둘 경우 DB 연결을 못해 기다리는 사용자들이 적어도 20초는 대기해야 한다는 말이다.
그렇다고 대기 시간을 아주 짧게 주면 어떻게 될까? 예를 들어 100ms 정도로 줄 경우에는 문제가 없을까? 메모리를 1GB로 할당한 WAS에서 300ms 이하의 Full GC 시간을 만들기는 매우 어렵다. 만약 DB 연결을 하려고 대기하는 순간 Full GC가 발생하면 그 순간에 대기하고 있는 모든 스레드는 DB와 연결을 못했다고 Timeout을 내뿜을 수도 있다.
결론적으로 DB와 연동하는 프레임워크를 설정할 때는 Pool의 개수, Wait과 관련된 값이 들어간 값, 그리고 Timeout과 관련된 설정들을 시스템의 상황에 맞게 설정해야 사용자들이 예기치 못한 오류페이지를 만나지 않을 것이다.
하나의 장비에 WAS의 인스턴스를 몇 개로 해야 된다는 어느 문서에도 존재하지 않는다. 이 또한 절대값은 없다는 것이다. 하지만 절대값이 없다고 해서 무한정 인스턴스를 늘리는 것이 답이 될 수는 없다.
당연한 이야기지만, 서버의 WAS 인스턴스 개수를 늘리면 늘릴수록 CPU가 처리해야 하는 양이 많아진다. 예를 들어 하나의 장비에 20개 이상의 인스턴스를 운영하고 있지만 CPU 코어의 개수가 4~8개 정도 밖에 안되는 사이트가 있다고 해보자. 그런 사이트는 성능이 잘 안나오는 경우가 대부분이다. 여러 개의 인스턴스에서 경합을 하면서 CPU를 차지하려고 하기 때문이다. 아무리 메모리가 싸서 많이 꽂아 둔다 하들 서버의 처리량이 증가하지 않는다. 어떤 애플리케이션이 어떤 CPU를 점유하는지 지정할 수 있으면 좀 달라지겟지만, 그렇지 않은 경우가 대부분이다. 보통 1~2개의 CPU당 하나의 인스턴스를 지정하는 것이 좋다고 이야기한다. 하지만, 앞서 이야기한 대로 장비 하나당 인스턴스 개수는 성능 테스트를 통해서 구하는 것이 가장 바람직하다.
예를 들어 CPU core 개수가 모두 36개인 장비가 있다. 이 장비는 일반적으로 이야기하는 대로 18개에서 26개의 인스턴스를 띄워야 할까? 인스턴스 1개일 때 500 TPS가 나오고, 인스턴스 2개일 때 700 TPS, 인스턴스 3개일 때 720TPS, 인스턴스 4개일 때 730 TPS가 나온다고 가정하자.
이 상황에서는 인스턴스를 2~3개 정도 띄우는 것이 낫다. 인스턴스를 더 늘린다고 해서 TPS가 증가하지 않는 상황에서는 오히려 유지보수성만 떨어지기 때문이다. 한 번 개발해놓고 수정하지 않는 시스템은 없기 때문에 인스턴스 개수가 많을 수록 배포하기도 어렵고, 모니터링 및 장애 상황 시 문제를 찾고 해결하기가 어려워진다.
추가로 만약 WAS 장비에 4GB의 여유 메모리가 있다고 하더라도 하나의 인스턴스에 4GB의 메모리를 지정하여 사용하는 것은 굉장히 좋지 않은 방법이다. 왜냐햐면 Full GC가 발생할 때마다 많은 시간이 소요될 확률이 커지기 때문이다. 가급적이면 512MB ~ 2GB 사이에서 메모리를 지정하는 것이 좋다. 위의 예의 상황에서는 1GB로 메모리를 지정하여 2개의 인스턴스를 사요하는 것이 좋은 방법일 것이다. 다른 애플리케이션이나 OS에서도 메모리를 사용하게 되므로 어느 정도 여유를 주는 것이 좋다. 단독 인스턴스를 구성하여 사용하는 것은 서버에 예기치 못한 상황이 발생했을 때 서비스가 불가능해지므로 되도록 피해야 한다. 장비가 한 대여도, 두 대 이상의 인스턴스가 서로 클러스터링하도록 지정하여 사용자의 세션 정보(보통 로그인 정보가 포함)를 공유하도록 하는 것이 좋다. 그리고 스레드나 DB Connection Pool의 개수를 100개 이상으로 설정하려고 할 때는 보통 인스턴스를 두 개로 분리한다.
가장 간과하게 쉬운 것이 세션의 종료 시간 설정 부분이다. 이 설정은 WAS에 종속적인 설정이 아닌 WEB-INF 폴더 하단의 web.xml 파일에서 설정하며 서블릿 스펙에 정의된 표준 설정 값이다.
...
<session-timeout>30</session-timeout>
...
세션 종료 시간 설정 값은 분 단위이다. 설정되어 있는 분만큼 요청이 없으면 세션을 메모리에서 제거한다. 이 설정을 하지 않은 상태에서 WAS에서 따로 설정한 바가 없거나, 세션 객체의 invalidate() 메서드가 수행되지 않으면 세션은 삭제되지 않으므로 유의해야 한다.
참고