IBM에 등록된 자룐데... 쓸일이 있어서 기록해둠..

보통 톰캣으로 서비스 구축할 때 안정적으로 서비스하려면 한 서버에 톰캣을 4개 정도 구동시켜야 한다고 하던데 ㅎㅎ

http://www.ibm.com/developerworks/kr/library/opendw/20061017/

웹 개발자에게 있어 톰캣은 JSP를 배우거나 간단한 테스트를 하는 정도의 웹 컨테이너로 생각하는 경우가 많다. 하지만 근래 들어 기업 및 대형 포탈에서 상용 서비스를 위한 웹 컨테이너로서 톰캣을 선택해, 성공적으로 적용한 사례들이 늘고 있다. 톰캣에서 안정적인 웹 서비스를 제공하기 위해서 지원하는 기능은 5가지가 있다. 아파치 웹서버와 연동, 로드밸런싱, 세션 클러스터링, 데이터베이스 처리, 모니터링 및 관리 등이 그것이다.
이 문서에서는 로드밸런싱과 세션 클러스터링 위주로 설명을 할 것이며, 다음에 기회가 된다면 다른 부분에 대해서도 자세히 알아보도록 하겠다.

아파치 웹 서버와 톰캣의 연동

일반적으로 정적인 페이지를 서비스할 때는 웹서버가 훨씬 더 좋은 성능을 발휘한다. 또한 이렇게 역할 분담을 함으로 톰캣에 가중되는 부하를 줄여주는 효과도 얻을 수 있다. 아파치웹서버와 톰캣을 연동하는 것을 일반적으로 ‘커넥터(Connector)'라고 부르며, 여기에는 WARP 커넥터, JK 커넥터 그리고 JK2 커넥터가 있다. 이중에서 WARP와 JK2는 공식 커넥터에서 제외되었고 현재 남아 있는 것은 JK 커넥터뿐이다. 그럼 먼저 JK 커넥터를 이용해서 아파치 웹서버와 톰캣을 연동해 보도록 하겠다.
아파치 웹사이트에서 바이너리 혹은 소스 코드를 다운로드 받도록 하자. 유닉스 혹은 리눅스는 mod_jk.so이며 윈도우용은 mod_jk.dll이다. 이 파일을 아파치 웹서버의 modules 디렉토리에 저장한다(주의, 아파치 웹서버를 컴파일해서 사용하는 경우는 컴파일시에 DSO 기능이 가능하도록 설정해줘야 한다). 저장을 한 후에 아파치 웹서버에 해당 모듈을 인식시켜야 하며 아파치 웹서버의 httpd.conf 파일에 다음 내용을 추가하도록 하자.


리스트 1. httpd.conf

        ...
LoadModule jk_module modules/mod_jk.so    # 모듈 추가
JkWorkersFile "conf/workers.properties"   # JK 설정 파일 위치 및 이름
 
JkLogFile "logs/mod_jk.log"               # JK에 대한 로그 파일 위치
JkLogLevel info                           # 로그 레벨 지정
JkLogStampFormat "[%a %b %d %H:%M:%S %Y]"   # 로그 시간 포맷 지정
JkRequestLogFormat "%w %V %T"             # 로그 내용 포맷
JkMount /* loadbalancer                   # URL 링크 -> 모든 요청을 톰캣으로 지정
JkMount /servlet/* loadbalancer           # URL 링크 -> servlet 요청을 톰캣으로 지정
...

위와 같은 설정을 하게 되면 아파치 웹서버로 들어온 모든 요청을 톰캣으로 재전송 하게 된다. 만일 JSP와 서블릿만 톰캣에서 서비스를 하고 나머지는 아파치 웹서버에서 서비스 하고자 한다면 다음과 같이 수정하면 된다.

  
JkMount /*.jsp loadbalancer                # URL 링크 -> *.jsp 요청을 톰캣으로 지정 
JkMount /servlet/* loadbalancer           # URL 링크 -> servlet 요청을 톰캣으로 지정 

httpd.conf에는 위의 내용이 전부이다. 그럼 이제 workers.properties 파일을 작성해 보도록 하겠다. 이 파일이 실제 로드밸런싱을 위한 설정이 되겠다.




위로


라운드 로빈 방식의 로드밸런싱 설정

톰캣에서 제공하는 로드밸런싱은 정확히 톰캣 자체에서 제공하는 것이 아니라 아파치 웹서버와 연동되는 커넥터에 의해서 제공된다(로드밸런싱은 JK, JK2 커넥터에서만 제공된다). 현재는 라운드 로빈(Round Robin) 방식만이 제공되며 로드밸런싱에 대한 설정은 workers.properties 파일에서 정의하게 된다.

리스트 2. workers.properties

  
worker.list=tomcat1, tomcat2, loadbalancer
 
worker.tomcat1.type=ajp13
worker.tomcat1.host=localhost
worker.tomcat1.port=11009
worker.tomcat1.lbfactor=100
 
worker.tomcat2.type=ajp13
worker.tomcat2.host=localhost
worker.tomcat2.port=12009
worker.tomcat2.lbfactor=200
 
worker.loadbalancer.type=lb
worker.loadbalancer.balanced_workers=tomcat1,tomcat2

worker라는 개념은 톰캣의 프로세스로 보면 된다. 즉 worker를 설정하는 구성 요소는 JK 커넥터를 연결하는 방식(JK는 ajp13을 이용한다), 톰캣이 실행되어 있는 IP 혹은 도메인, ajp13 서비스 포트, 그리고 작업 할당량이다. 여기서 주의 깊게 볼 것이 작업 할당량인데 로드밸런싱 시에 lbfactor라는 작업량의 비율을 보고 라운드 로빈 방식의 서비스를 제공하게 된다. 여기서는 tomcat1과 tomcat2를 1대 2의 비율로 작업량을 할당한 것이다.
그럼 이제 남은 작업은 2개의 톰캣 프로세스를 실행시키는 것이다. 톰캣 프로세스를 여러 개 띄우는 방법은 2가지가 있다.

  • 톰캣을 2개 설치해서 기동시킨다. 이때 포트 충돌을 피하기 위해 서버 포트, AJP13과 HTTP 1.1 커넥터 포트 2개를 충돌되지 않게 재정의 한다.
  • 하나의 톰캣에 2개의 서비스를 정의하고 톰캣을 기동시킨다. 이때 AJP13과 HTTP1.1 커텍터 포트 2개를 충돌되지 않게 재정의 한다.

먼저 2개의 바이너리를 설치했다고 가정하면 각각의 톰캣은 다음과 같은 형태의 server.xml 파일로 적용해 준다.

리스트 3. server.xml

<Server port="11005" shutdown="SHUTDOWN"> <!-- 톰캣 프로세스를 관리하는 포트 --> <Service name="Catalina"> <Connector port="11080"/> <!-- 아파치를 통하지 않고 직접 접속하고자 할때의 포트 --> <Connector port="11009" protocol="AJP/1.3"/> <!-- 아파치와 연동하기 위한 포트 --> <!-- jvmRoute 명 JK 커넥터에서 톰캣 프로세스를 구분하는데 사용. 프로세스 별로 다르게 적용해야 함 --> <Engine jvmRoute="tomcat1" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> </Server>


리스트 4. server.xml
<Server port="12005" shutdown="SHUTDOWN"> <!-- 톰캣 프로세스를 관리하는 포트 --> <Service name="Catalina"> <Connector port="12080"/> <!-- 아파치를 통하지 않고 직접 접속하고자 할때의 포트 --> <Connector port="12009" protocol="AJP/1.3"/> <!-- 아파치와 연동하기 위한 포트 --> <!-- jvmRoute 명 JK 커넥터에서 톰캣 프로세스를 구분하는데 사용. 프로세스 별로 다르게 적용해야 함 --> <Engine jvmRoute="tomcat2" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> </Server>

리스트 3은 tomcat1의 환경 설정이고 리스트 4는 tomcat2의 환경 설정이다. 두 환경 설정의 차이는 3개의 포트번호와 <Engine> 태그의 jvmRoute 속성이다. <Server> 태그의 포트는 톰캣을 종료 시키는 작업을 할 때 사용하는 것이며 <Connector> 태그의 포트들은 아파치를 통하지 않고 직접 톰캣에 접속할 경우와 아파치와 연계하여 JK 커넥터와 연동할 때 사용하는 포트이다. 마지막으로 <Engine> 태그는 JK 커넥 터에서 로드밸런싱을 수행할 때 해당 값을 구분자로 활용하게 되는데 이 값을 반드시 다른 톰캣 프로세스와 다른 이름으로 지정해야 한다. 지금까지의 환경 설정은 하나의 아파치 웹서버와 두 개의 톰캣 간의 연계를 위한 것이며 톰캣은 동일한 하드웨어 장비에 설치되어 있다는 가정하에 적용한 것이다. 만일 각각의 톰캣이 서로 다른 하드웨어에 존재한다면 jvmRoute명만 다르게 하고 포트명은 동일해도 상관이 없다. 하지만 만일 하나의 장비에 4개의 톰캣 프로세스를 실행시키고 로드밸런싱을 하려고 한다면 어떻게 될까? 톰캣을 4번 설치하고 각각의 환경 설정 파일을 수정해 주어야 할까? 만일 필요한 환경 설정 내용이 변경된다면(예를 들어 JNDI Resource 정보) 모두 운영자가 환경 설정 파일을 수정해 주어야 할까? 다행히도 톰캣에서는 하나의 바이너리에 여러 개의 프로세스가 뜨도록 할 수 있다. 톰캣의 server.xml 태그는 다음과 같은 구조를 가지고 있다.

<Server> --> <Service> --> <Engine> --> <Host> --> <Context>

여기서 Server 태그는 유일해야 하며 Server 태그 밑에는 여러 개의 <Service> 태그가 올 수 있다. 여기서 말하는 <Service> 태그가 바로 하나의 톰캣 프로세스가 된다. 만일 2개의 <Service> 태그를 정의했다면 2개의 프로세스가 구동되는 것이다. 리스트 5는 이렇게 구현한 환경 설정 파일이다.

리스트 5. server.xml

<Server port="8005" shutdown="SHUTDOWN"> <!-- Service 1 --> <Service name="Catalina1"> <Connector port="11080"/> <Connector port="11009" protocol="AJP/1.3"/> <Engine jvmRoute="tomcat1" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> <!-- Service 1 --> <Service name="Catalina2"> <Connector port="12080"/> <Connector port="12009" protocol="AJP/1.3"/> <Engine jvmRoute="tomcat2" name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps"/> </Engine> </Service> </Server>

리스트 5는 하나의 톰캣 바이너리를 통해 2개의 프로세스를 실행시키는 것이다. 이렇게 하면 환경 설정의 편리성을 가져올 수 있지만 특정 서비스만 실행하거나 종료 시키는 것은 아직 지원되지 않는다. 즉 모든 서비스가 동시에 실행되거나 혹은 동시에 종료되는 것을 의미한다. 이런 점을 잘 판단해서 두 가지 형태의 환경 설정 중 하나를 선택하면 되겠다.
지금까지는 로드밸런싱에 대해 알아보았다. 위의 환경설정을 가지고 테스트를 하다 보면 한가지 문제가 발생한다. 예를 들어 어떤 사용자가 tomcat1을 이용해서 쇼핑몰 서비스를 받고 있다가 tomcat1이 비정상 종료를 하게 되었다. 이때 사용자가 웹 페이지를 요청하게 되면 아파치 웹서버는 tomcat1이 종료된 것을 인지하고 그 이후부터 서비스를 tomcat2로 요청하게 된다. 하지만 tomcat1에 저장되어 있던 쇼핑바구니 정보 즉 세션 정보는 사라진 상태다. 즉, 서비스는 유지되지만 사용자는 다시 이유도 모르게 처음부터 쇼핑 항목들을 등록해야 하는 문제를 가지게 된다. 이제부터는 이런 문제를 해결할 수 있는 톰캣 프로세스 간의 세션 정보 공유에 대해서 알아보겠다.




위로


세션 클러스터링 설정

클러스터링은 톰캣 5.x 버전부터 지원이 되고 있지만 아직은 초기 단계이고 세션 클러스터링만이 제공되고 있는 수준이다. 기능이 많이 부족하긴 하지만 로드밸런싱과 더불어 사용할 경우에는 좀 더 안정적인 서비스를 제공할 수 있다. 작업을 해주어야 할 것은 다음과 같다.

  • server.xml에 <Cluster> 태그 정의
  • 웹 어플리케이션의 web.xml에 <distributable/> 태그 추가
그럼 server.xml에 설정해 보자. <Cluster> 태그는 <Host> 태그의 하위에 정의해 주면 된다. 즉 여러 개의 호스트(예를 들어 가상 호스트) 를 설정했다면 각각의 경우에 맞게 설정해 주어야 한다. 여기서는 tomcat1과 tomcat2가 동일한 하드웨어에 별도의 바이너리 형태로 설치되어 있다고 가정하고 진행하겠다.

리스트 6. server.xml

... <Cluster className="org.apache.catalina.cluster.tcp.SimpleTcpCluster" managerClassName="org.apache.catalina.cluster.session.DeltaManager" expireSessionsOnShutdown="false" useDirtyFlag="true"> <Membership className="org.apache.catalina.cluster.mcast.McastService" mcastAddr="228.0.0.105" mcastPort="45564" mcastFrequency="500" mcastDropTime="3000"/> <Receiver className="org.apache.catalina.cluster.tcp.ReplicationListener" tcpListenAddress="auto" tcpListenPort="4001" tcpSelectorTimeout="100" tcpThreadCount="6"/> <Sender className="org.apache.catalina.cluster.tcp.ReplicationTransmitter" replicationMode="pooled"/> <Valve className="org.apache.catalina.cluster.tcp.ReplicationValve" filter=".*\.gif;.*\.js;.*\.jpg;.*\.htm;.*\.html;.*\.txt;"/> </Cluster> ...

리스트 6은 tomcat1의 server.xml에 적용한 <Cluster> 태그이다. 이 내용은 <Host> 태그 사이에 위치하게 된다. <Cluster> 태그 사이에는 <Membership>, <Receiver>, <Sender>라는 3개의 태그가 위치하는데 <Membership>은 멤버 그룹을 정의하는 것으로 해당 값이 동일한 모든 톰캣 프로세스는 클러스터로 묶이게 된다. <Receiver>는 클러스터 그룹에서 보내오는 메시지와 세션 정보 등을 받아오는 것이며 <Sender>는 자신의 세션 정보 및 메시지를 전송하는 것이다. 위의 내용을 tomcat2의 server.xml에 Receiver 태그의 tcpListenPort 값을 4002로 변경해서 적용하도록 하자.




위로


웹 어플리케이션 작성을 통한 테스트

먼저 테스트를 위해서 간단한 웹 어플리케이션을 작성하도록 하겠다. 여기서 웹 어플리케이션 이름은 lbtest라고 하겠다.

리스트 7. index.jsp

<%@ page contentType="text/html; charset=euc-kr" %> <HTML> <HEAD> <TITLE>세션 JSP 테스트</TITLE> </HEAD> <BODY> <h1>세션 JSP 테스트</h1> <% Integer ival = (Integer)session.getAttribute("sessiontest.counter"); if(ival==null) { ival = new Integer(1); } else { ival = new Integer(ival.intValue() + 1); } session.setAttribute("sessiontest.counter", ival); %> 당신은 이곳을 <b> <%= ival %> </b>번 방문 했습니다.<p> 여기를 클릭하세요. <a href="index.jsp">여기</a> <p> <h3>request 객체와 관련된 세션 데이터</h3> 요청된 세션 ID : <%= request.getRequestedSessionId() %><br /> 쿠키로 부터 요청된 세션 ID 인가? : <%= request.isRequestedSessionIdFromCookie() %><br /> URL로부터 요청된 세션 ID 인가? : <%= request.isRequestedSessionIdFromURL() %><br /> 유효한 세션 ID 인가? : <%= request.isRequestedSessionIdValid() %><br /> <script type="text/javascript"> var _tiq = 'undefined' !== typeof _tiq ? _tiq : []; _tiq.push(['__setConfig', {enableScroll:true, enableClick:true, enableButton:true}]); _tiq.push(["__setParam", "svcdomain", "user.tistory.com"]); (function(d) { var se = d.createElement('script'); se.type = 'text/javascript'; se.async = true; se.src = location.protocol + '//m2.daumcdn.net/tiara/js/td.min.js'; var s = d.getElementsByTagName('head')[0]; s.appendChild(se); })(document); _tiq.push(['__setParam', 'param_ex', JSON.stringify({"userId":"421112","blogId":"465329","entryId":"121"})]); _tiq.push(['__trackPageview']); _tiq.push(['__content', 't_content', { 'c_id':'465329_121', 'c_title':'%5BApache%20-%20Tomcat%5D%20%EB%A1%9C%EB%93%9C%20%EB%B0%B8%EB%9F%B0%EC%8B%B1%2C%20%EC%84%B8%EC%85%98%20%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81', 'type':'article', 'author':'', 'author_id':'421112', 'cp':'%EB%B4%89%EB%B4%89%EC%A3%BC%EC%84%B8%EC%9A%94', 'cp_id':'465329', 'regdata':'2007-10-09 11:59:30', 'plink':'http://bongbong.net/121', 'media':'pcweb', }]); var addEvent = function (evt, fn) { window.addEventListener ? window.addEventListener(evt, fn, false) : window.attachEvent('on' + evt, fn); }; var removeEvent = function(evt, fn) { window.removeEventListener ? window.removeEventListener(evt, fn, false) : window.detachEvent('on' + evt, fn);}; var ua = navigator.userAgent.toLowerCase(); var isIOS = /iP[ao]d|iPhone/i.test(ua); var contentExStat = function() { _tiq.push(['__content', 't_content_ex', { 'data_type':'usage' , 'meta': { 'c_id':'465329_121', 'c_title':'%5BApache%20-%20Tomcat%5D%20%EB%A1%9C%EB%93%9C%20%EB%B0%B8%EB%9F%B0%EC%8B%B1%2C%20%EC%84%B8%EC%85%98%20%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81', 'type':'article', 'author':'', 'author_id':'421112', 'cp':'%EB%B4%89%EB%B4%89%EC%A3%BC%EC%84%B8%EC%9A%94', 'cp_id':'465329', 'regdata':'2007-10-09 11:59:30', 'plink':'http://bongbong.net/121', 'media':'pcweb', } }]); removeEvent(isIOS ? 'pagehide' : 'beforeunload', contentExStat); }; addEvent(isIOS ? 'pagehide' : 'beforeunload', contentExStat); </script> <script type="text/javascript">window.roosevelt_params_queue = window.roosevelt_params_queue || []; window.roosevelt_params_queue.push({channel_id: "dk", channel_label: 'tistory'});</script> <script type="text/javascript" src="//t1.daumcdn.net/midas/rt/dk_bt/roosevelt_dk_bt.js" async></script><script type="text/javascript">if(window.console!=undefined){setTimeout(console.log.bind(console,"%cTISTORY","font:8em Arial;color:#EC6521;font-weight:bold"),0);setTimeout(console.log.bind(console,"%c 나를 표현하는 블로그","font:2em sans-serif;color:#333;"),0);}</script><iframe style="position:absolute;width:1px;height:1px;left:-100px;top:-100px" src="http://hajinsu.tistory.com/api" id="editEntry"></iframe> <div id="tistorytoolbarid" style="position:absolute;height:20px;top:4px;right:0px;background-color:transparent;background-image:none;z-index:11;"> <div class="tistorytoolbar tt_menubar_login"> <div class="tt_menubar_black"> <div id="ttMenubarInnerWrap" class="tt_menubar_inner"> <div class="tt_menubar_bg_toolbar"></div> <h2 style="display:none;">티스토리 툴바</h2> <div class="tt_menubar_item tt_menubar_mainmenu"><a class="tt_menubar_link_tit tt_menubar_link_tit_eng" href="http://www.tistory.com">Tistory</a> </div> <div class="tt_menubar_item tt_menubar_bg_bar"></div> <div class="tt_menubar_item tt_menubar_logout"><a class="tt_menubar_link_tit" href="https://www.tistory.com/auth/login">로그인</a> </div> </div> </div> </div> </div> </BODY> </HTML>

작성된 웹 애플리케이션을 tomcat1과 tomcat2에 배포한다. 이때 가장 중요한 것이 web.xml에 반드시 <distributable/>이라는 항목을 넣어 야 한다. 그럼 이제 테스트를 해보도록 하자. 먼저 아파치 웹서버, tomcat1, tomcat2를 차례로 실행시켜 보자. 그리고 http://ipaddress/lbtest/index.jsp 접속하여 세션 객체를 생성해보자. 결과 화면은 그림 1과 같다. 여기서 요청된 세션ID를 보면 뒤에 어떤 톰캣에 접속한 상태인지를 알 수 있다. 이 화면상에서는 tomcat2에 접속한 세션이다. 그럼 tomcat2를 강제로 종료시켜 보도록 하자. 종료 후 화면에 보이는 “여기”를 계속 눌러 보자. 분명히 tomcat2가 종료되었음에도 불구하고 세션 값이 유지되고 있음을 알 수 있다. 이젠 반대로 tomcat2를 다시 실행시킨 후에 tomcat1을 종료시켜 보도록 하자. 역시 마찬가지로 세션 정보가 유지되는 것을 확인할 수 있을 것 이다.

test 결과화면
그림 1. 테스트 결과 화면

이상으로 톰캣을 이용한 로드밸런싱과 세션 클러스터링에 대해서 알아보았다. 일반적으로 로드밸런싱과 클러스터링은 성능 향상이라는 측면과 안정성 확보에 그 목적을 가지고 있다. 물론 고가의 상용 웹 어플리케이션 서버에 비하면 많이 부족하고 하드웨어를 이용한 로드밸런싱과 클러스터링에 비하면 안정성이 떨어질 수도 있지만 저렴한 비용으로 최대의 안정성과 성능을 얻고자 한다면 한번쯤 시도해 볼만한 좋은 기능이라고 할 수 있다. 아무쪼록 리눅스, 아파치, 톰캣 그리고 오픈소스를 통해 즐거운 웹 어플리케이션 개발 환경을 느껴보기 바란다.




위로


참고 자료
The Apache Tomcat Connector: http://tomcat.apache.org/connectors-doc/
Clustering/Session Replication HOW-TO: http://tomcat.apache.org/tomcat-5.5-doc/cluster-howto.html

신고
Posted by 봉보리봉봉

블로그 이미지
봉보리봉봉

공지사항

Yesterday33
Today6
Total131,869