블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

톰캣 튜닝

조대협


이번에는 톰캣 서버에 대한 튜닝 옵션에 대해서 한번 알아보자.

애플리케이션 관점에서의 튜닝도 중요하지만, 각 솔루션에 대한 특성을 업무 시나리오에 맞춰서 튜닝하는 것도 못지 않게 중요하다. 여기서 톰캣 튜닝을 설명하는 것은 톰캣 자체에 대한 튜닝 옵션을 소개하는 것도 목적이 있지만, 그보다 업무형태에 따라서 어떠한 접근을 해서 톰캣을 튜닝하는지를 소개하기 위함이다.

 

가정

여기서 튜닝 하는 톰캣은 HTTP/JSON형태의 REST 형태로 서비스를 제공하는 API 서버의 형태이다. 여러대의 톰캣을 이용하여 REST 서비스를 제공하며, 앞단에는 L4 스위치를 둬서 부하를 분산하며, 서비스는 stateless 서비스로 공유되는 상태 정보가 없다. 

server.xml 튜닝

톰캣의 대부분 튜닝 패러미터는 ${Tomcat_HOME}/conf/server.xml 파일에 정의된다.

몇몇 parameter를 살펴보도록 하자.

 

Listener 설정

 <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" /> 

이 옵션은 tomcat이 기동할 때, root 사용자이면 기동을 하지 못하게 하는 옵션이다. 서버를 운영해본 사람이라면 종종 겪었을 실수중의 하나가 application server root 권한으로 띄웠다가 다음번에 다시 실행하려고 하면 permission 에러가 나는 시나리오 이다. root 권한으로 서버가 실행되었기 때문에, 각종 config 파일이나 log 파일들의 permission이 모두 root로 바뀌어 버리기 때문에, 일반 계정으로 다시 재 기동하려고 시도하면, config 파일이나 log file들의 permission 이 바뀌어서 파일을 읽어나 쓰는데 실패하게 되고 결국 서버 기동이 불가능한 경우가 있다. 이 옵션은 이러한 실수를 막아 줄 수 있다.

 

Connector 설정

 

protocol="org.apache.coyote.http11.Http11Protocol"

먼저 protocol setting인데, Tomcat은 네트워크 통신하는 부분에 대해서 3가지 정도의 옵션을 제공한다. BIO,NIO,APR 3 가지이다. NIO Java NIO 라이브러리를 사용하는 모듈이고, APR Apache Web Server io module을 사용한다. 그래서 C라이브러리를 JNI 인터페이스를 통해서 로딩해서 사용하는데, 속도는 APR이 가장 빠른것으로 알려져 있지만, JNI를 사용하는 특성상, JNI 코드 쪽에서 문제가 생기면, 자바 프로세스 자체가 core dump를 내면서 죽어 버리기 때문에 안정성 측면에서는 BIO NIO보다 낮다. BIO는 일반적인 Java Socket IO 모듈을 사용하는데, 이론적으로 보면 APR > NIO > BIO 순으로 성능이 빠르지만, 실제 테스트 해보면 OS 설정이나 자바 버전에 따라서 차이가 있다. Default BIO이다.

 

acceptCount="10"

이 옵션은 request Queue의 길이를 정의한다. HTTP request가 들어왔을때, idle thread가 없으면 queue에서 idle thread가 생길때 까지 요청을 대기하는 queue의 길이이다. 보통 queue에 메세지가 쌓였다는 것은 해당 톰캣 인스턴스에 처리할 수 있는 쓰레드가 없다는 이야기이고, 모든 쓰레드를 사용해도 요청을 처리를 못한다는 것은 이미 장애 상태일 가능성이 높다.

그래서 큐의 길이를 길게 주는 것 보다는, 짧게 줘서, 요청을 처리할 수 없는 상황이면 빨리 에러 코드를 클라이언트에게 보내서 에러처리를 하도록 하는 것이 좋다. Queue의 길이가 길면, 대기 하는 시간이 길어지기 때문에 장애 상황에서도 계속 응답을 대기를 하다가 다른 장애로 전파 되는 경우가 있다.

순간적인 과부하 상황에 대비 하기 위해서 큐의 길이를 0 보다는 10내외 정도로 짧게 주는 것이 좋다.

 

enableLookups="false"

톰캣에서 실행되는 Servlet/JSP 코드 중에서 들어오는 http request에 대한 ip를 조회 하는 명령등이 있을 경우, 톰캣은 yahoo.com과 같은 DNS 이름을 IP주소로 바뀌기 위해서 DNS 서버에 look up 요청을 보낸다. 이것이 http request 처리 중에 일어나는데, 다른 서버로 DNS 쿼리를 보낸다는 소리이다. 그만큼의 서버간의 round trip 시간이 발생하는데, 이 옵션을 false로 해놓으면 dns lookup 없이 그냥 dns 명을 리턴하기 때문에, round trip 발생을 막을 수 있다.

 

compression="off"

HTTP message body gzip 형태로 압축해서 리턴한다. 업무 시나리오가 이미지나 파일을 response 하는 경우에는  compression을 적용함으로써 네트워크 대역폭을 절약하는 효과가 있겠지만, 이 업무 시스템의 가정은, JSON 기반의 REST 통신이기 때문에, 굳이 compression을 사용할 필요가 없으며, compression에 사용되는 CPU를 차라리 비지니스 로직 처리에 사용하는 것이 더 효율적이다.

 

maxConnection="8192"

하나의 톰캣인스턴스가 유지할 수 있는 Connection의 수를 정의 한다.

이 때 주의해야 할 점은 이 수는 현재 연결되어 있는 실제 Connection의 수가 아니라 현재 사용중인 socket fd (file descriptor)의 수 이다. 무슨 말인가 하면 TCP Connection은 특성상 Connection 이 끊난 후에도 바로 socket close 되는 것이 아니라 FIN 신호를 보내고, TIME_WAIT 시간을 거쳐서 connection을 정리한다. 실제 톰캣 인스턴스가 100개의 Connection 으로 부터 들어오는 요청을 동시 처리할 수 있다하더라도, 요청을 처리하고 socket close 되면 TIME_WAIT에 머물러 있는 Connection 수가 많기 때문에, 단시간내에 많은 요청을 처리하게 되면 이 TIME_WAIT가 사용하는 fd 수 때문에, maxConnection이 모자를 수 있다. 그래서 maxConnection은 넉넉하게 주는 것이 좋다.

이외에도 HTTP 1.1 Keep Alive를 사용하게 되면 요청을 처리 하지 않는 Connection도 계속 유지 되기 때문에, 요청 처리 수 보다, 실제 연결되어 있는 Connection 수가 높게 된다.

그리고, process당 열 수 있는 fd수는 ulimit -f 를 통해서 설정이 된다. maxConnection 8192로 주더라도, ulimit -f에서 fd 수를 적게 해놓으면 소용이 없기 때문에 반드시 ulimit -f 로 최대 물리 Connection 수를 설정해놔야 한다.

 

maxKeepAliveRequest="1"

HTTP 1.1 Keep Alive Connection을 사용할 때, 최대 유지할 Connection 수를 결정하는 옵션이다. 본 시나리오에서는 REST 방식으로 Connectionless 형태로 서비스를 진행할 예정이기 때문에, Kepp Alive를 사용하지 않기 위해서 값을 1로 준다.

만약에 KeepAlive를 사용할 예정이면, maxConnection과 같이 ulimit에서 fd수를 충분히 지정해줘야 하낟.

 

maxThread="100"

사실상 이 옵션이 가장 중요한 옵션이 아닌가 싶다. 톰캣내의 쓰레드 수를 결정 하는 옵션이다. 쓰레드수는 실제 Active User 수를 뜻한다. 즉 순간 처리 가능한 Transaction 수를 의미한다.

일반적으로 100 내외가 가장 적절하고, 트렌젝션의 무게에 따라 50~500 개 정도로 설정하는 게 일반적이다. 이 값은 성능 테스트를 통해서 튜닝을 하면서 조정해 나가는 것이 좋다.

 

tcpNoDelay="true"

TCP 프로토콜은 기본적으로 패킷을 보낼때 바로 보내지 않는다. 작은 패킷들을 모아서 버퍼 사이즈가 다 차면 모아서 보내는 로직을 사용한다. 그래서 버퍼가 4K라고 가정할때, 보내고자 하는 패킷이 1K이면 3K가 찰 때 까지 기다리기 때문에, 바로바로 전송이 되지 않고 대기가 발생한다.

tcpNoDelay 옵션을 사용하면, 버퍼가 차기전에라도 바로 전송이 되기 때문에, 전송 속도가 빨라진다. 반대로, 작은 패킷을 여러번 보내기 때문에 전체적인 네트워크 트래픽은 증가한다. (예전에야 대역폭이 낮아서 한꺼번에 보내는 방식이 선호되었지만 요즘은 망 속도가 워낙 좋아서 tcpNoDelay를 사용해도 대역폭에 대한 문제가 그리 크지 않다.)

 

Tomcat Lib 세팅

다음으로 자바 애플리케이션에서 사용하는 라이브러리에 대한 메모리 사용률을 줄이는 방법인데, 일반적으로 배포를 할때 사용되는 라이브러리(jar) *.war 패키지 내의 WEB-INF/jar 디렉토리에 넣어서 배포 하는 것이 일반적이다. 보통 하나의 war를 하나의 톰캣에 배포할 때는 큰 문제가 없는데, 하나의 톰캣에 여러개의 war 파일을 동시 배포 하게 되면, 같은 라이브러리가 각각 다른 클래스 로더로 배포가 되기 때문에, 메모리 효율성이 떨어진다.

그래서 이런 경우는 ${TOMCAT_HOME}/lib 디렉토리에 배포를 하고 war 파일에서 빼면 모든 war가 공통 적으로 같은 라이브러리를 사용하기 때문에 메모리 사용이 효율적이고, 또한 시스템 운영 관점에서도 개발팀이 잘못된 jar 버전을 패키징해서 배포하였다 하더라도, lib 디렉토리의 라이브러리가 우선 적용되기 때문에, 관리가 편하다.

반대로 war의 경우, war만 운영중에 재배포를 하면 반영이 가능하지만, lib 디렉토리의 jar 파일들은 반드시 톰캣 인스턴스를 재기동해야 반영되기 때문에, 이 부분은 주의해야 한다.

 

JVM Tuning

Java Virtual Machine 튜닝은 java 기반 애플리케이션에서는 거의 필수 요소이다.

-server

제일 먼저 해야할일은 JVM 모드를 server 모드로 전환하는 것이다. JVM 내의 hotspot 컴파일러도 클라이언트 애플리케이션이나 서버 애플리케이션이냐 에 따라서 최적화 되는 방법이 다르다.

그리고 메모리 배치 역시 클라이언트 애플리케이션(MS 워드와같은)의 경우 버튼이나 메뉴는 한번 메모리에 로드 되면, 애플리케이션이 끝날 때 까지 메모리에 잔존하기 때문에 Old 영역이 커야 하지만, 서버의 경우 request를 받아서 처리하고 응답을 주고 빠져서 소멸되는 객체들이 대부분이기 때문에, New 영역이 커야 한다.

이런 서버나 클라이언트냐에 대한 최적화 옵션이 이 옵션 하나로 상당 부분 자동으로 적용되기 때문에, 반드시 적용하기를 바란다.

 

메모리 옵션

앞에서도 설명하였듯이 JVM 튜닝의 대부분의 메모리 튜닝이고 그중에서도 JVM 메모리 튜닝은 매우 중요하다. 결국 Full GC 시간을 줄이는 것이 관건인데, 큰 요구 사항만 없다면, 전체 Heap Size 1G 정도가 적당하다. 그리고 New Old의 비율은 서버 애플리케이션의 경우 1:2 비율이 가장 적절하다. 그리고 PermSize class가 로딩되는 공간인데, 배포하고자 하는 애플리케이션이 아주 크지 않다면 128m 정도면 적당하다. (보통 256m를 넘지 않는다. 256m가 넘는다면 몬가 애플린케이션 배포나 패키징에 문제가 있다고 봐야 한다.)

그리고 heap size JVM에서 자동으로 늘리거나 줄일 수 가 있다. 그래서 -Xms -Xmx로 최소,최대 heap size를 정할 수 있는데, Server 시스템의 경우 항상 최대 사용 메모리로 잡아 놓는 것이 좋다. 메모리가 늘어난다는 것은 부하가 늘어난다는 것이고, 부하가 늘어날때 메모리를 늘리는 작업 자체가 새로운 부하가 될 수 있기 때문에, 같은 값을 사용하는 것이 좋다.

이렇게 JVM 메모리를 튜닝하면 다음과 같은 옵션이 된다.

-Xmx1024m Xms1024m -XX:MaxNewSize=384m -XX:MaxPermSize=128m

이렇게 하면 전체 메모리 사용량은 heap 1024m (이중에서 new 384m) 그리고 perm 128m 가 되고, JVM 자체가 사용하는 메모리가 보통 300~500m 내외가 되서 java process가 사용하는 메모리 량은 대략 1024+128+300~500 = 대략 1.5G 정도가 된다.

 

32 bit JVM의 경우 process가 사용할 수 있는 공간은 4G가 되는데, 이중 2G는 시스템(OS)이 사용하고 2G가 사용자가 사용할 수 있다. 그래서 위의 설정을 사용하면 32bit JVM에서도 잘 동작한다.

64 bit JVM의 경우 더 큰 메모리 영역을 사용할 수 있는데, 일반적으로 2G를 안 넘는 것이 좋다.(최대 3G), 2G가 넘어서면 Full GC 시간이 많이 걸리기 시작하기 때문에, 그다지 권장하지 않는다. 시스템의 가용 메모리가 많다면 Heap을 넉넉히 잡는 것보다는 톰캣 인스턴스를 여러개 띄워서 클러스터링이나 로드밸런서로 묶는 방법을 권장한다.

 

OutOfMemory

자바 애플리케이션에서 주로 문제가 되는 것중 하나가 Out Of Memory 에러이다. JVM이 메모리를 자동으로 관리해줌에도 불구하고, 이런 문제가 발생하는 원인은 사용이 끝낸 객체를 release 하지 않는 경우이다. 예를 들어 static 변수를 통해서 대규모 array hashmap reference 하고 있으면, GC가 되지 않고 계속 메모리를 점유해서 결과적으로 Out Of Memory 에러를 만들어낸다.

Out Of Memory 에러를 추적하기 위해서는 그 순간의 메모리 레이아웃인 Heap Dump가 필요한데, 이 옵션을 적용해놓으면, Out Of Memory가 나올때, 순간적으로 Heap Dump를 떠서 파일로 저장해놓기 때문에, 장애 발생시 추적이 용이하다.

-XX:-HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid<pid>.hprof

 

GC 옵션

다음은 GC 옵션이다. Memory 옵션 만큼이나 중요한 옵션인데, Parallel GC + Concurrent GC는 요즘은 거의 공식처럼 사용된다고 보면 된다. 이때 Parallel GC에 대한 Thread 수를 정해야 하는데, Thread수는 전체 CPU Core수 보다 적어야 하고, 2~4개 정도가 적당하다.

-XX:ParallelGCThreads=2 -XX:-UseConcMarkSweepGC

GC 로그 옵션

그리고 마지막으로 GC Log 옵션이다. 서버와 JVM이 건강한지 메모리상 문제는 없는지 GC 상황은 어떻게 디는지를 추적하려면 GC 로그는 되도록 자세하게 추출할 필요가 있다. GC로그를 상세하게 걸어도 성능 저하는 거의 없다.

-XX:-PrintGC -XX:-PrintGCDetails -XX:-PrintGCTimeStamps -XX:-TraceClassUnloading -XX:-TraceClassLoading

 

마지막에 적용된 TraceClassLoading은 클래스가 로딩되는 순간에 로그를 남겨준다. 일반적으로는 사용하지 않아도 되나, OutOfMemory 에러 발생시 Object가 아니라 class에서 발생하는 경우는 Heap dump로는 분석이 불가능 하기 때문에, Out Of Memory 에러시 같이 사용하면 좋다.

 

지금까지 간략하게 나마 톰켓 솔루션에 대한 튜닝 parameter 에 대해서 알아보았다. 사실 이러한 튜닝은 일반적인 개발자에게는 힘든 일이다. 해당 솔루션에 대한 많은 경험이 있어야 하기 때문에, 이런 parameter vendor의 기술 지원 엔지니어를 통해서 가이드를 받고, 성능 테스트 과정에서 최적화를 하고 표준화된 parameter를 정해서 사용하는 것이 좋다. Apache Tomcat의 경우에도 오픈소스이기는 하지만, Redhat등에서 기술 지원을 제공한다.

 

Continuous Deployment 
(Auto Deployment)


빌드와 테스트까지 자동화 했으면 그 다음 문제는 배포이다.

수동으로 배포하는 경우 한 두개의 서버라면 별 걱정이 없겠지만, 개발,테스트,운영 환경과 같이 여러 환경에 또한 각 환경에 수십대의 서버에 배포를 해야 한다면, 문제는 달라진다. 그래서 요즘에서 CI에 배포의 개념을 더한 CD (Continuous Delivery 또는 Continuous Deployment)라는 개념이 유행하는데, 이는 빌드가 완료된 후, 배포까지 자동화 하는 방법이다.



이런 배포를 지원하는 도구는 여러가지 타입이 있다.

   특정 솔루션에 종속적인 도구

Tomcat이나 WebLogic 같은 WAS의 경우 각 제품에 특화된 배포 도구를 가지고 있다. Tomcat의 경우 Tomcat Client Deployer (http://tomcat.apache.org/tomcat-6.0-doc/deployer-howto.html#Deploying using the Client Deployer Package ) 와 같은 도구가 있는데, Remote에서 war 파일을 배포해줄 수 있는 도구 이다.

이러한 도구들의 특징은 해당 솔루션에 최적화가 되어 있기 때문에, RunTime Deploy나 기타 해당 솔루션에 특화된 기능을 활용할 수 있기 때문에, 안정적인 배포가 가능하다는 장점을 가지고 있다.

그러나 반대로 솔루션에 관련된 애플리케이션 파일만 배포할 수 있다는 단점을 가지고 있다. 무슨 이야기인가 하면, war 파일 이외에, 다른 디렉토리에 configuration 파일을 배포하고 싶을 경우, 이런 파일들은 배포가 불가능하다는 것이다. 또한 여러개의 인스턴스에 동시 배포를 하고 싶을 때, 인스턴스들이 제품에서 제공하는 클러스터링 기능등을 사용하고 있지 않을 경우, 각 인스턴스들을 일일이 각각 배포해야 하는 단점이 있다. 쉽게 말해서 매우 제품에 종속적이라는 것이다.

   Configuration Management

다음으로는 Puppet이나 Chef와 같은 Configuration Management 도구 기반의 배포 방식이 있다. 이러한 도구들은 원래 태생이 Deployment보다는 초기 솔루션을 설치하거나, 다수의 서버나 솔루션에 대한 Configuration 정보를 중앙 관리하기 위해서이다. 이에 비해서 Deploy 과정은 대부분 파일을 복사하고 서버를 restart 시키는 과정 정도의 단순 작업이기 때문에, 만약에 이러한 Configuration Management 인프라를 갖추고 있거나, 또는 배포 과정이 솔루션의 설정을 포함하여 매우 복잡한 경우 일때는 매우 효율적으로 사용될 수 있으나, 반대로 이러한 인프라가 없는 상태에서 단순한 배포만하고자 할 경우에는 오히려 배보다 배꼽이 커질 수 도 있다.

   Remote Shell 기반의 도구

마지막으로, Remote Shell 기반의 배포 도구가 있다. SSH RSH과 같은 명령을 툴로 실행시켜 주는 도구 인데, 파일 복사에서 부터 커맨드 라인에서 입력하는 명령들을 원격으로 실행시켜 준다.

솔루션에 종속적이지 않으며 또한 자유도가 높으며 사용이 매우 편리하다.

Python 으로 된 도구로는 Python Fabric이라는 도구가 있고, Ruby쪽에서는 Capistrano라는 도구가 있다.

Case Study

필자의 경우 클라우드 프로젝트 이전에만 해도, 환경 자체가 그리 크지 않았기 때문에, 웹로직이나 Tomcat의배포 툴을 사용해서 배포를 진행했었다. 클라우드 환경 기반에서 프로젝트를 진행하고 또한 진행하는 프로젝트의 규모가 커짐에 따라서 이러한 배포 자동화가 꼭 필요하게 되었는데, 이 배포는 Configuration Deployment 두 가지로 나눠서 생각해볼 수 있다.

애플리케이션을 배포하기 전에 먼저 OS Tomcat과 같은 솔루션을 설치 하는 Configuration, 그 다음이 이 환경 위에 애플리케이션을 배포하는 Deployment이다.

Configuration의 경우에는 Puppet이나 Chef를 도입하고 싶었으나, 팀의 규모와 시간 관계상 도입이 어려웠고, 환경의 복잡도가 낮아서 별도의 Configuration management 도구는 도입하지 않기로 결정하였다.

대신 OS Tomcat Pre-install된 표준 VM 이미지를 만들어 놓고, 배포가 필요할때 마다 이 이미지를 Loading한 후에, 애플리케이션을 Deployment 하는 형태를 사용하였다.

이 방식은 표준 이미지를 만들어 놓고 계속 재 사용하기 때문에, 관리가 쉽지만 반대로 Configuration을 변경하고자 할 때, 이미 배포된 이미지들에 대한 Configuration을 일일이 다시 변경해야 하는 단점이 있다. (어느 정도 규모가 되면 Configuration Management 도구로 넘어가는 것이 좋은듯 하다.)

Fabric을 이용한 배포

그러면 Fabric을 이용한 Tomcat 애플리케이션에 대한 간단한 배포 시나리오를 살펴보도록 하자.

구성은 아래와 같다. Load Balancer 아래에 N개의 Tomcat 인스턴스들이 연결되어 있는 구조 이고, 배포는 다음과 같은 순서를 따르도록 한다.

   먼저 배포하고자 하는 Tomcat 인스턴스를 Load Balancer에서 제외한다.

   다음으로 배포하고자 하는 인스턴스를 Stop한다.

   배포하고자 하는 war 파일을 해당 Tomcat 인스턴스에 복사한다.

   그리고 해당 Tomcat 인스턴스를 리스타트한다.

   위의 1)~4) 과정을 다른 인스턴스에도 반복한다.



이 배포 방식은 간단하기는 하지만, 하나의 서비스내에서 배포 과정중에, 배포가 완료된 인스턴스와 배포 예정인 인스턴스에 애플리케이션이 다르기 때문에, 애플리케이션 변경이 많은 경우에는 적용하기가 어렵다 . 대규모 변경이 있는 경우에는 전체 클러스터를 내렸다가 전체 배포 후 서비스를 다시 시작하는 방식을 사용해야 하는데 이 경우에는 배포 중에 서비스에 대한 순간적인 정지가 발생할 수 있다.

무정지 배포 아키텍쳐

이런 문제를 해결 하기 위해서 일부 자바 기반의 application server의 경우 runtime redeployment (시스템을 운영중에, 정지없이 프로그램을 변경하는 행위로, 일부 자바 기반의 application server에서는 WAR와 같은 웹모듈이나, EJB 같은 모듈을 시스템을 무정지 상태로 재 배포 할 수 있는 기능을 제공한다.) 를 제공하는 제품들이 많다. redeployment 의 원리는 새로운 application load하고, classloader reload하는 형식인데, classloader reload이 위험도가 높은 작업이기도 하고, application을 작성할때, redeploy를 고려하지 않은 경우 정상적으로 runtime redeploy가 되지 않는 경우가 많다. 가장 확실한 redeploy 기법은 application server를 정지시킨후, 재배포한후에 restart하는 것이 가장 안정적이다. 이렇게 restart기반으로 redeploy를 할때, 시스템을 정지 상태를 최소화는 구조를 무정지 배포 아키텍쳐라고 하는데, 구조는 다음과 같다.




application server를 두개의 클러스터 그룹으로 나눈후, 각 클러스터 앞에 reverse proxy를 각각 배치 시킨다. 그리고 reverse proxy 앞에는 L4 스위치를 둬서 각 클러스터로 load balancing을 할 수 있도록 한다.

배포를 할 때는 Cluster A 앞에 있는 reverse proxy를 정지 시킨다. 이렇게 하면, 앞단의 L4 로드 밸런서에서 Cluster A request를 보내지는 않으나, Cluster A 자체는 살아 있다. 그 후에 Cluster A의 각 인스턴스에 애플리케이션을 재 배포 한후,Cluster A reverse proxy를 재기동 시킨다. 마찬 가지 방법으로 Cluster B에도 같은 방법으로 redeploy를 수행한다.

이 구조를 택하면 전체 서비스 중지 없이 그리고 애플리케이션 변화에 대해서 일관성 있게 한꺼번에 재 배포가 가능하다.

Fabric을 이용하여 AWS Tomcat war 파일 배포 하기

그러면 실제로 Fabric을 이용해서 어떻게 배포를 할 수 있을까?

다음은 아주 간단한 Fabric을 이용한 배포 스크립트 이다.

순서는 tomcat stop > copy war > start 와 같다. EC2상에서 pem (SSH)를 이용하여, 두대의 Host deploy하는 스크립트이다.

#fabfile.py

from fabric.api import run,env,execute,task

from fabric.operations import local,put

 

def tomcat_cluster():

        env.user ='root'

        env.hosts=['host1.server.com','host2.server.com'] # list of server setting

        env.key_filename='~/pem/pemfile.pem' # pem file

 

def hostname():

        run('uname -a')

 

def start():

        run('/etc/init.d/tomcat6 start')  # tomcat instance stop

 

def stop():

        run('/etc/init.d/tomcat6 stop') # tomcat instance stop

 

def copy():

        put('./dummy.war','/root/war') # file copy

 

def deploy():

        execute(stop)

        execute(copy)

        execute(start)

 

해당 파일을 fabfile.py에 저장후에

%fab tomcat_cluster deploy

명령어로 실행을 하면 아래와 같은 순서로 배포가 진행된다.

host1.stop()

host1.copy()

host1.start()

host2.stop()

host2.copy()

host2.start()

 

위의 예제는 Fabric의 설명을 하기 위한 아주 간단한 예제로, 필요에 따라서 수정해서 사용하기 바란다.

앞의 예제에서는 간단하게 war 파일만을 복사하는 형태로 배포 스크립트를 작성하였지만, maven 설명할때 언급했던 것 처럼, 기타 Configuration 파일을 함께 배포하고, roll back이나 버전 관리가 용이하게하기 위해서 rpm 형태로 배포하는 것을 권장한다. 또한 rpm 파일을 관리하기 위해서 내부적으로 자체 yum repository를 만들어서 관리하는 것이 좋다.

배포에서 고민해야 할것 들

배포주기

근 몇 년전만 해도, 배포는 몇 달간의 개발이 끝나면 테스트를 거쳐서 특정한 날짜를 잡아서 대규모 배포를 형태가 일반적이었다.

그러나 근래에는 전체적인 IT 트렌드가 SNS와 같은 B2C 서비스가 중심이 되면서, 경쟁 서비스에 비해서 좋은 서비스를 빠르게 제공하기 위해서 업데이트 주기가 매우 짧아지고 있다. 이런 이유에서 배포도 자동화가 필요하게 되었고 Continuous Delivery와 같은 개념이 근래에 유행하게 된것일 수도 있는데, SNS 서비스의 경우 빠르면 하루 단위로 배포하는 경우 까지 있다.

배포 주기가 짧은 경우에는 몇 가지 더 고려할 사항이 있는데, 배포는 코드 개발보다는 인프라 관리나 빌드에 관련된 부분이 많다. 그래서 배포의 주체는 이런 운영 주체가 되는 경우가 일반적인데, 배포시 문제가 생기는 경우가 있을 시에는 개발팀의 도움이 필요하고, 배포가 정상적으로 되었을 경우에는 개발팀으로 부터의 확인등이 필요하기 때문에 조직 구조상 운영팀과 개발팀이 분리된 조직에서는 잦은 배포가 여러가지로 어려운점이 많다. 그래서 근래에는 이런 개발과 운영 조직을 합쳐서 개발팀을 운영하는 DevOps (Deveopment + Operation)형태의 조직구조로 전환하여 개발,배포,운영을 통합하여 관리 하는 쪽으로 이동하고 있다.

이 경우 단순히 조직을 합치는 것뿐만 아니라 개발자에게는 인프라나 운영에 대한 이해 능력을 그리고 운영팀에는 개발에 대한 어느정도 선까지의 능력을 요구하게 되고 업무 프로세스등의 변화가 필요하기 때문에 조직을 융합시키는 차원이 아니라 조금 더 높은 수준의 접근이 필요하다.  

수동 배포

운영 환경 배포는 반드시 수동으로 하는 것을 권고한다.

지금까지 자동으로 배포하는 방법을 설명해놓고, 왠 갑자기 수동 배포를 언급하는가 싶을 수도 있을텐데, 이유는 다음과 같다.

배포는 앞서 본 것과 같이 쉘 명령등을 수행해서 여러가지 명령 (Shutdown, Copy, Restart)을 동시에 여러 서버에 수행한다. 즉 중간에 에러가 날 가능성이 매우 높다.

그래서 배포 작업을 수행할때는 반드시 사람이 지켜보면서 배포 스크립트를 수행하는 것이 좋다. 개발환경이나 QA 환경 같은 경우에는 거의 업무 시간에 빌드를 하면서 배포가 수행이 되기 때문에, 배포 오류가 났을 경우에는 사람이 인지하기가 쉽고, 에러가 났을때 서비스에 직접적인 영향을 주지는 않는다. 그래서 개발이나 QA 환경 같은 경우에는 사람이 지켜보지 않고, CI 프로세스의 일부로 빌드 테스트가 끝나면 자동으로 배포를 하도록 해도 된다.

그렇지만 운영 환경의 경우에는 서비스에 직접적인 영향을 주기 때문에, 반드시 사람이 지켜보면서 수동으로 배포를 시작 및 모니터링 하도록 하는 것이 좋다.

배포 roll back

서비스에 대한 배포시, 테스트를 아무리 잘했다하더라도, 에러가 발생할 수 있다.

에러가 발생하였을 때는 이전의 버전으로 신속하게 roll back할 수 가 있어야 하는데, 이렇게 하기 위해서는 애플리케이션의 이전 버전을 반드시 저장하고 있어야 하고, 배포 스크립트 역시 예전 버전을 다시 배포할 수 있는 기능을 구현해야 한다.

릴리즈 노트

마지막으로 배포가 끝나면 반드시 릴리즈 노트를 작성 및 함께 배포하는 것이 좋다.

배포란 개발이나 운영이 주로 주도하는 작업이기 때문에, 비지니스 쪽에서 필요한 기능 변화에 대한 릴리즈 노트의 중요성을 잃어 버리는 경우가 많은데,  서비스가 변경이 되었을때, 어떠한 기능이 추가 되었는지, 그리고 어떠한 버그가 FIX가 되었는지를 사용자에게 알려줄 필요가 있으며, 특히 서비스를 판매나 영업하는 입장에서는 어떤 변화가 있었는지를 확인하는 것이 중요하기 때문에, 이 부분을 반드시 챙기도록 한다.

릴리즈 노트는 배포 되는 대상 (독자)에 따라서 다르게 작성되야 하는데,

내부 릴리즈 노트는 다음과 같은 내용이 포함되는 것을 권고한다.

Ÿ   릴리즈 버전, 날짜 및 빌드 넘버

Ÿ   새로운 기능 및 설명

Ÿ   버그 number 및 버그 수정 내용

또는 JIRA와 같은 Tak Management 도구를 사용하는 경우, JIRA에 등록된 기능이나 Bug 수정 내용을 릴리즈 시기에 자동으로 JIRA로 부터 생성해 낼 수 있다. (JIRA에서 릴리즈 노트 생성하기 https://confluence.atlassian.com/display/JIRA/Creating+Release+Notes)

만약 서비스나 제품 사용자를 대상으로 하는 경우, 위의 릴리즈 노트의 내용을 사용할 수 도 있지만, 조금 더 직관적이고 읽기 쉬운 형태로 릴리즈 노트를 작성하는 것이 좋다.

다음은 몇몇 잘 정의된 릴리즈 노트의 샘플이다.

Ÿ   안드로이드 릴리즈 노트 : http://developer.android.com/sdk/RELEASENOTES.html

Ÿ   Fire Fox 릴리즈 노트 http://www.mozilla.org/en-US/firefox/23.0.1/releasenotes/

Ÿ   Maven 릴리즈 노트 http://maven.apache.org/release-notes-all.html

릴리즈 노트는 청중들에게 이번 릴리즈 기능의 변경 사항을 제대로 알리기 위함이다. JIRA와 같은 시스템의 기능을 이용하건, 아니면 일일이 손으로 새로 쓰건간에 반드시 보는 사람이 어떤 변화가 있었는지 쉽게 이해할 수 있는 형태라야 한다.

Apache Tomcat Tuning

성능과 튜닝/WAS 튜닝 | 2013.03.13 23:47 | Posted by 조대협

Tomcat Tuning Guide

 

Tomcat configuration $Tomcat/conf/server.xml

Assumption

This configuration is optimized for REST/HTTP API call. And it doesn’t use any reverse proxy like Apache, NginX etc. We will reside simple L4 switch infront of tomcat groups.

In addition we will not use Tomcat Clustering, Session etc. So the clustering configuration is omitted.

Listener Setting

 <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" /> 

checkedOsUser setting means Unix system user “root” cannot start Tomcat. If user starts tomcat as a root user it makes log file as a root user permission. In that case tomcat user cannot delete the log file.

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> 

This makes detect memory leak.

Connector Setting

protocol="org.apache.coyote.http11.Http11Protocol" 

It makes tomcat use BIO. Tomcat has options for IO (BIO,NIO,APR). APR is fastest IO setting. It uses Apache web server IO module, so it is fastest. But it uses C code (JNI call), it can have a risk to kill tomcat instance. (with core dump). APR is more faster about 10% than BIO. But BIO is more stable. Use BIO. (Default is BIO)

acceptCount="10"

It specifies server request queue length. If message is queued in the request queue, it means server cannot handle incoming message (it is overloaded). It will wait for idle thead and the request message will be pending. This setting reduce total size of request queue to 10. If the queue has been overflowed, client will get a error. It can protect server from high overload and let system manager to know the server has been overloaded.

enableLookups="false"

In Java Servlet Code, user can look up request message origin (IP or URL). For example user in yahoo.com send request to server, and Tomcat try to resolve incoming request IP address. “enableLooksups” option enables return DNS name not a IP address. During this processing Tomcat look up DNS. It brings performance degradation. This option removes DNS look up stage and increase performance.

compression="off" 

We are using REST protocol not a normal web contents like HTML,Image etc. This options allows to compress HTTP message. It consumes computing power but it can reduce network payload. In our environment compression is not required. It is better to save computing power. And in some particular Telco network, compression is not supported.

 connectionTimeout="10000"

It is HTTP Connection time out (client to server). It is milliseconds. (10,000 = 10 sec).

If server cannot make a connection from client til 10 sec. It will throw HTTP time out error. In normal situation, our API response time is under 5 sec. So 10 sec means, server has been overloaded. The reason why I increased the time up to 10 sec is, depends on network condition, connection time will be deferred.

maxConnections="8192"

The maximum number of connection, tomcat can handle. It means tomcat can handle maximum 8192 socket connection in a time. This value is restricted by Unix system parameter “ulimit –f” (You can check up in unix console)

maxKeepAliveRequests="1"

As I mentioned above, this configuration is optimized to REST API request not a common web system. It means client will send REST API call only. It sends the request and get a response. Client will not send request in a short time. It means we cannot reuse the connection from the client. So this setting turn of HTTP Keep Alive. (After response the request from client, tomcat disconnect the connection immediately)

maxThreads="100"

This defines total number of thread in Tomcat. It represents max number of active user at that time. Usually 50~500 is good for performance. And 100~200 is best (it is different depends on use case scenario).

Please test with 100 and 200 values and find value for performance. This parameter also get a impact from DB connection pool setting, even if we have a lot of thread , and the total number of db connection is not enough, the thread will wait to acquire the connection. 

tcpNoDelay="true"

This allows us to use TCP_NO_DELAY in tcp/ip layer. It makes send small packet without delay. In TCP, to reduce small package congestion, it gathers small packet to tcp buffer until it has been filled and send the packet. TCP_NO_DELAY option makes send small packet immediately even though TCP buffer is not full.

 

JVM Tuning

Java Virtual Machine tuning is also very important factor to run Tomcat

The focus of JVM tuning is reducing Full GC time.

-server

This option makes JVM to optimize server application. It tunes HotSpot compiler etc internally. This option is very important and mandatory in server side application

-Xmx1024m –Xms1024m -XX:MaxNewSize=384m -XX:MaxPermSize=128m

This memory tuning options, our infrastructure is using c1.mediuem amazon instance, so the available memory is about 1.7 gb total. Heap size is 1G and let them to have fixed size. It defines max 1Gb, min 1Gb heap size. The NewSize is 384mb (1/3 size of total heap size). 1/3 New Size is best performance usually. Perm size is defines area of memory to load class. 64mb is enough. But we will use 128m first time and tune based on gc log analysis later.

Total physical memory consumption is 1G heap + 128mb perm = 1.128 GB and JVM internally uses memory to run JVM itself. It consumes about 350~500mb. So total estimated required memory is about 1.128GB+500m = 1.5 GB.

As I mentioned, c1.mediuem size has only 1.7GB physical memory. If consumed memory exceeds actual physical memory, it makes disk swapping. If JVM memory is swapped out to disk, the performance is significantly degraded. Please take care swapping is not occurred.

-XX:-HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid<pid>.hprof

These options are for trouble shooting “OOM (Java Out Of Memory Error”. If out of memory error has been occurred. The memory layout will be dumped to disk. The location of dumpfile is specified by “-XX:HeapDumpPath” option

 -XX:ParallelGCThreads=2 -XX:-UseConcMarkSweepGC

These options specify GC strategy. It uses ParallelGC for Minor collection and 2 threads will be used for the Minor GC. And for Old area, concurrent gc will be used. It will reduce Full gc time

-XX:-PrintGC -XX:-PrintGCDetails -XX:-PrintGCTimeStamps -XX:-TraceClassUnloading -XX:-TraceClassLoading

These option specifies GC logging. It logs the GC log detail to stderr (console output). It shows usage trend os Java Heap memory, time stamp etc. (it contains old,new & perm area usage).

Especially, ClassLoading & UnLoading option show what class is loaded and unloaded to memory. It helps us to trace Perm Out of memory error.

 

Reference : http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

Logging

1.     모든 log catalina.out 하나의 파일에 모두 쌓이게 할것 à LogAppender Console Appender로 변경하면 된다.

     Tomcat 자체가 쓰는 로그 (별도로 지정 안하면 원래 Console로 나옴)

     Application에서 LogBack을 이용해서 로깅 되는 로그 (별도로 ConsoleAppender를 개발단에서 정의해줘야 함)

     GC 로그 (별도로 지정 안하면 원래 Console로 나옴)

DB Connection Pool Setting

Please use tomcat dbpcp connection pool. Apache-common connection pool is not updated frequently. Tomcat dbcp connection pool is updated well.

The basic setting guide is “Let number of connection in the pool to keep exact number of connection”. It can be done by set min conn and max conn to same number.

Total number of connections (including read + write) should be around. 70~80. We are using 100 threads in one tomcat instance. The 70~80% will use db connection at the same time.

Library Setting

Developer packaged java lib inside war file (WEB-INF/lib) it can increase usage of perm memory. And sometime can bring confusion about “which lib is actually used”. It means if same lib(jar file) resides in $TOMCAT_HOME/lib and WEB-INF/lib. The lib in $TOMCAT_HOME/lib wil be used and lib is WEB-INF/lib will be ignored.

To solve this problem. Remove common library like my-sql-jdbc driver and dbcp lib jar file from WEB-INF/lib and move it into $TOMCAT_HOME/lib

Tomcat 7.0 Parameter Tuning

성능과 튜닝/WAS 튜닝 | 2013.03.01 00:51 | Posted by 조대협

server.xml

--

<?xml version='1.0' encoding='utf-8'?>

<Server port="8005" shutdown="SHUTDOWN">

  <!-- Security listener. Documentation at /docs/config/listeners.html -->

  <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" /> root 사용자로 start 하지 못하게 함

  <!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->

  <Listener className="org.apache.catalina.core.JasperListener" />

  <!-- Prevent memory leaks due to use of particular java/javax APIs-->

  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> 메모리 leak detector

  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />

  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />


  <!-- Global JNDI resources

       Documentation at /docs/jndi-resources-howto.html

  -->

  <GlobalNamingResources>

    <!-- Editable user database that can also be used by

         UserDatabaseRealm to authenticate users

    -->

    <Resource name="UserDatabase" auth="Container"

              type="org.apache.catalina.UserDatabase"

              description="User database that can be updated and saved"

              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"

              pathname="conf/tomcat-users.xml" />

  </GlobalNamingResources>


  <Service name="Catalina">


    <Connector port="8080" 

protocol="org.apache.coyote.http11.Http11Protocol" BIO 사용

acceptCount="30" back log를 30으로 제안

enableLookups="false" DNS 사용 금지. 

compression="off" 압축 금지 (REST 사용 및 Mobile 고려)

                connectionTimeout="10000" Connection time out 10초

maxConnections="8192" 

maxKeepAliveRequests="1"

maxThreads="100"

tcpNoDelay="true"

               redirectPort="8443" />

    <Engine name="Catalina" defaultHost="localhost">

      <Realm className="org.apache.catalina.realm.LockOutRealm">

        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"

               resourceName="UserDatabase"/>

      </Realm>


      <Host name="localhost"  

   appBase="webapps" <!-- /bin/apps/ -->

            unpackWARs="true" 

   autoDeploy="false">


        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"

               prefix="localhost_access_log." suffix=".txt"

               pattern="%h %l %u %t &quot;%r&quot; %s %b" />


      </Host>

    </Engine>

  </Service>

</Server>


--

Context에서 auto reload 부분 제외 해야 하고..
dbcp connection pool 튜닝 필요 --> dbcp 말고 tomcat pool 사용
context.xml에 설정
<Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
               maxActive="100" maxIdle="30" maxWait="10000"
               username="javauser" password="javadude" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/javatest"/>

애플리케이션 배포를 쉽게 하기 위해서, 위의 Context.xml은 war/META-INF/context.xml을 사용하는게 좋음


shared lib 처리 필요
JVM은 -server -Xms1024m -Xmx1024m -XX:NewSize=384m -XX:PermSize=128m + ParallelGC, GCThread=3, +PrintGCDetial
디렉토리 처리 (1 server , 1was를 기준으로 함)
tomcat 설치는 /usr/local/tomcat
JVM은 /usr/local/jvm
application은 /applications/war/
log는 /applications/log/access.log catalina.out 기타 application logs
기타 스크립트는 /applications/scripts
설정 파일 /appllications/properties

아니면
/product/j2ee/jdk
/product/j2ee/tomcat

/applications/j2ee/war
/applications/j2ee/logs
/applications/j2ee/scripts
/applications/j2ee/properties

아니면
/product/tomcat
/product/jdk
/product/tomcat7 (link)
/product/jdk1.6 (link)
/applications/tomcat/war
/applications/tomcat/logs/access.log, catalina.out
/applications/tomcat/scripts/deploy/fabfile
/applications/tomcat/scripts/logbackup/fabfile
/applications/tomcat/properties

boot up시 최신 war 파일을 repository에서 읽어오는 부분 추가

튜닝 하려면 한 2일 걸리겠네...




Fabric을 이용한 간단한 Tomcat deploy

프로그래밍/Python | 2013.01.29 18:46 | Posted by 조대협

Tomcat war deploy시, 가장 이상적인 방법은 

tomcat stop > copy war > start 순서이다.

아래는 간단하게 Python 기반의 Fabric을 이용하여, EC2상에서 pem (SSH)를 이용하여, Host들에 deploy하는 과정을 정의함


#fabfile.py

from fabric.api import run,env,execute,task

from fabric.operations import local,put


def tomcat_cluster():

        env.user ='root'

        env.hosts=['host1.server.com','host2.server.com'] # list of server setting

        env.key_filename='~/pem/pemfile.pem' # pem file


def hostname():

        run('uname -a')


def start():

        run('/etc/init.d/tomcat6 start')  # tomcat instance stop


def stop():

        run('/etc/init.d/tomcat6 stop') # tomcat instance stop


def copy():

        put('./dummy.war','/root/war') # file copy


def deploy(): 

        execute(stop)

        execute(copy)

        execute(start)

해당 파일을 fabfile.py에 저장후에

%fab tomcat_cluster deploy

해주면 아래와 같은 순서로 수행됨

host1.stop()

host1.copy()

host1.start()

host2.stop()

host2.copy()

host2.start()


기타 참고할만한 명령어
- sudo
- get : 파일 copy 해옴 
- local : local machine에서 명령어 수행
- with cd('{dir}') : 특정 디렉토리를 베이스로 명령어등을 수행할때 사용


※ 참고 : 외부에서 Host 명을 dynamic하게 받는 방법

--------------------------------

외부에서 host list 받는 방법

def deploy(lookup_param):

    # This is the magic you don't get with @hosts or @roles.

    # Even lazy-loading roles require you to declare available roles

    # beforehand. Here, the sky is the limit.

    host_list = external_datastore.query(lookup_param)

    # Put this dynamically generated host list together with the work to be

    # done.

    execute(do_work, hosts=host_list)

    

실행할때는

$ fab deploy:app


------ 또는 -----------

# Marked as a publicly visible task, but otherwise unchanged: still just

# "do the work, let somebody else worry about what hosts to run on".

@task

def do_work():

    run("something interesting on a host")


@task

def set_hosts(lookup_param):

    # Update env.hosts instead of calling execute()

    env.hosts = external_datastore.query(lookup_param)

    

$ fab set_hosts:app do_work

One benefit of this approach over the previous one is that you can replace do_work with any other “workhorse” task:


$ fab set_hosts:db snapshot

$ fab set_hosts:cassandra,cluster2 repair_ring

$ fab set_hosts:redis,environ=prod status


Tomcat 6.0에서 Oracle ADF Faces 를 이용하여 JSF 실행 환경을 구축하는 방법입니다.



혹시 난 이거저거 다 귀찮다고 하시는 분들은 아래 공유된 파일을 받아다가 $TOMCAT_HOME/lib 디렉토리에 압축만 푸셔도 됩니다.

이 환경에서 JSF 프로그래밍을 하는 방법은 http://bcho.tistory.com/366 문서를 참고하시면 되겠습니다.