docker-compose를 이용한 Nginx + Tomcat 클러스터링 샘플

|

Nginx와 Tomcat을 이용하여 클러스터 환경을 구축/테스트 진행하였는데 서버를 각각 3대나 띄우려니 여간 귀찮을수가 없더군요.
docker-compose를 이용하여 가장 심플하고 최소한의 설정만으로 한방에 띄우는 방법 및 샘플을 포스팅합니다.
샘플 소스는 jistol/docker-compose-nginx-tomcat-clustering-sample에서 다운로드 가능합니다.

기본구조

2대의 Tomcat 컨테이너를 올리고 앞단에 Nginx로 reverse proxy 합니다.
2대의 Tomcat은 가장 기본적인 클러스터링 설정을 사용하며 multicast 방식에 의해 세션 공유를 합니다.

서버 구성도

server structure

샘플 폴더 구조

sample file list

Nginx와 Tomcat의 설정은 각 폴더별로 구분하고 Docker Build시 copy하도록 설정해두었습니다.

docker-compose.yml 설정

docker명령어로 일일히 다 올리기 귀찮기 때문에 docker-compose를 이용하여 한방에 올립니다.
docker-compose에 관한 자세한 사항은 docker compose doc을 참고하세요.

# 일단 버전은 3을 사용합니다. 덕분에 extends기능이 없어졌더군요 ;(
version: '3'

# 각 서비스 컨테이너를 정의합니다. ( nginx * 1 + tomcat * 2 )
services:
    # tomcat 1번 서버입니다. 같은 설정을 2번에서도 사용하기 때문에 &was로 명명하고 tomcat2에서 참조합니다.
    # tomcat1 서비스의 모든 관련파일은 ./tomcat1 폴더에서 가져옵니다. 
    tomcat1: &was
        # tomcat 기동시 java option값을 추가하기 위해 아래 설정을 추가했습니다. 
        environment:
            - JAVA_OPTS=-Dspring.profiles.active=docker -Dfile.encoding=euc-kr
        build: 
            context: .
            # Dockerfile을 실행시 conf/server.xml과 webapps에 파일 배포를 위해 argument를 추가합니다.
            # 아래 값은 DockerfileTomcat에서 사용됩니다.
            args:
                conf: tomcat1/conf
                warpath: tomcat1/webapps/ROOT.war
            # tomcat 서버 이미지 빌드를 위한 Dockerfile을 별도로 지정해줍니다.
            dockerfile: ./DockerfileTomcat
        # 
        # Docker 컨테이너에 붙지 않고 각 tomcat서버의 로그를 따로 확인하기 위해 외부 저장소와 연결합니다.
        volumes:
            - ./tomcat1/logs/:/usr/local/tomcat/logs/
    
    # tomcat2번 서버입니다. tomcat1 서비스에서 설정한 내용을 그대로 사용하고 달라지는 설정에 대해서는 아래와 같이 직접 입력해줍니다.
    tomcat2:
        <<: *was
        build: 
            context: .
            args:
                conf: tomcat2/conf
                warpath: tomcat2/webapps/ROOT.war
            dockerfile: ./DockerfileTomcat
        volumes:
            - ./tomcat2/logs/:/usr/local/tomcat/logs/
    # nginx 설정입니다.
    nginx:
        build:
            context: .
            # nginx 서버 이미지 빌드를 위한 Dockerfile을 별도로 지정해줍니다.
            dockerfile: ./DockerfileNginx
            # Dockerfile실행시 conf/nginx.conf파일이 복사 될 수 있도록 argument를 추가합니다.
            # 아래 값은 DockerfileNginx에서 사용됩니다.
            args:
                conf: nginx/conf
        # 외부에서 직접 8080포트로 붙어야 하기 때문에 컨테이너 포트를 외부로 열어줍니다.
        ports: 
            - "8080:8080"

위에서 설정한 설정에서 참조값들을 모두 적용한 문서를 보고 싶을 때는 아래와 같은 명령어로 실행할 수 있습니다.

$ docker-compose config

Dockerfile 설정

위에서 docker-compose 실행시 각 컨테이너가 Dockerfile을 실행하도록 설정하였습니다.
다음과 같이 nginx / tomcat 용 Dockerfile을 생성합니다.

DockerfileNginx

FROM nginx:latest
MAINTAINER jistol <pptwenty@gmail.com>

ARG conf

COPY $conf/nginx.conf /etc/nginx/nginx.conf

WORKDIR /usr/local/tomcat/bin
CMD ["nginx", "-g", "daemon off;"]

ARG conf값은 docker-compose.yml에 설정되어 있습니다.

DockerfileTomcat

FROM tomcat:latest
MAINTAINER jistol <pptwenty@gmail.com>

ARG conf
ARG warpath

RUN rm -rf /usr/local/tomcat/webapps/*
COPY $conf/* /usr/local/tomcat/conf/
COPY $warpath /usr/local/tomcat/webapps/ROOT.war

WORKDIR /usr/local/tomcat/bin
CMD ["catalina.sh", "run"]

ARG conf, ARG warpath값은 docker-compose.yml에 설정되어 있습니다.

Tomcat 설정

server.xml파일에 아래와 같이 설정합니다.

<!-- server.xml -->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>

자세한 설정은 Tomcat 8 세션 클러스터링 하기에 포스팅한 내용을 참고하세요.

Nginx 설정

nginx.conf파일에 reverse proxy를 위한 설정을 합니다.

http {
    
    ...
    
    # proxy 설정할 서버목록을 만듭니다.
    # host명은 docker 컨테이너의 service 이름과 동일하게 맞추어 줍니다. 
    upstream was-list {
        server tomcat1:8080;
        server tomcat2:8080;
    }
    
    ...
    
    server {
    
        # nginx 서버가 8080을 listen하도록 설정합니다.
        listen       8080;
        server_name  localhost;

        # 8080포트로 들어오는 모든 요청을 위에 설정한 'was-list'그룹으로 보냅니다.
        location / {
            proxy_pass http://was-list;
        }
    }
    
    ...
    
}

실행

다음과 같이 실행합니다.

$ docker-compose up -d

-d 옵션을 사용해야 실행후 각 컨테이너 콘솔 화면에서 detach됩니다.

기본적으로 docker-compose 실행시 이미지를 기존 빌드된 것으로 캐쉬하기 때문에 설정파일이나 배포파일이 바뀔 경우 아래와 같이 실행하여 새로 이미지를 만들도록 합니다.

$ docker-compose up -d --build

실행시 아래와 같이 이미지 빌드 로그와 함께 각 컨테이너가 실행되는 것을 볼 수 있습니다.

$ docker-compose up -d --build
Building nginx
Step 1/6 : FROM nginx:latest
 ---> da5939581ac8
Step 2/6 : MAINTAINER jistol <pptwenty@gmail.com>
 ---> Using cache
 ---> 2d9af4961368
Step 3/6 : ARG conf
 ---> Using cache
 ---> ead97bc0569d
Step 4/6 : COPY $conf/nginx.conf /etc/nginx/nginx.conf
 ---> Using cache
 ---> 5ab748ec2a17
Step 5/6 : WORKDIR /usr/local/tomcat/bin
 ---> Using cache
 ---> 3eabdd2a3dd5
Step 6/6 : CMD nginx -g daemon off;
 ---> Using cache
 ---> 7f4f2405e032
Successfully built 7f4f2405e032
Successfully tagged tomcatdocker1_nginx:latest
Building tomcat2
Step 1/9 : FROM tomcat:latest
 ---> 0fbedce2f08c
Step 2/9 : MAINTAINER jistol <pptwenty@gmail.com>
 ---> Using cache
 ---> 13adee263b5d
Step 3/9 : ARG conf
 ---> Using cache
 ---> 57a78bc9e8ce
Step 4/9 : ARG warpath
 ---> Using cache
 ---> 638fca357d24
Step 5/9 : RUN rm -rf /usr/local/tomcat/webapps/*
 ---> Using cache
 ---> 928ef1b94bb2
Step 6/9 : COPY $conf/* /usr/local/tomcat/conf/
 ---> Using cache
 ---> 30f8faae0012
Step 7/9 : COPY $warpath /usr/local/tomcat/webapps/ROOT.war
 ---> 3bd3ddeff2d6
Removing intermediate container 9c0b330ce6f6
Step 8/9 : WORKDIR /usr/local/tomcat/bin
 ---> dc773d206d87
Removing intermediate container 693e8b125384
Step 9/9 : CMD catalina.sh run
 ---> Running in c430115e5460
 ---> 3879504509c0
Removing intermediate container c430115e5460
Successfully built 3879504509c0
Successfully tagged tomcatdocker1_tomcat2:latest
Building tomcat1
Step 1/9 : FROM tomcat:latest
 ---> 0fbedce2f08c
Step 2/9 : MAINTAINER jistol <pptwenty@gmail.com>
 ---> Using cache
 ---> 13adee263b5d
Step 3/9 : ARG conf
 ---> Using cache
 ---> 57a78bc9e8ce
Step 4/9 : ARG warpath
 ---> Using cache
 ---> 638fca357d24
Step 5/9 : RUN rm -rf /usr/local/tomcat/webapps/*
 ---> Using cache
 ---> 26dea2133cee
Step 6/9 : COPY $conf/* /usr/local/tomcat/conf/
 ---> Using cache
 ---> c546b169bf25
Step 7/9 : COPY $warpath /usr/local/tomcat/webapps/ROOT.war
 ---> 4f1edfc42b8d
Removing intermediate container f02a70122c3a
Step 8/9 : WORKDIR /usr/local/tomcat/bin
 ---> 50683e072451
Removing intermediate container e2ca57d27775
Step 9/9 : CMD catalina.sh run
 ---> Running in 8da01a9227c2
 ---> 1e050e465174
Removing intermediate container 8da01a9227c2
Successfully built 1e050e465174
Successfully tagged tomcatdocker1_tomcat1:latest
Creating tomcatdocker1_nginx_1 ... 
Creating tomcatdocker1_tomcat1_1 ... 
Creating tomcatdocker1_tomcat2_1 ... 
Creating tomcatdocker1_nginx_1
Creating tomcatdocker1_tomcat1_1
Creating tomcatdocker1_tomcat1_1 ... done

docker 프로세스를 확인해보면 다음과 같이 3개의 컨테이너가 올라간 것을 확인 할 수 있습니다.

$ docker ps -a
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS                     PORTS                                                NAMES
1e2240de4a77        tomcatdocker1_tomcat1    "catalina.sh run"        4 minutes ago       Up 4 minutes               8080/tcp                                             tomcatdocker1_tomcat1_1
9706f7d0f85a        tomcatdocker1_tomcat2    "catalina.sh run"        4 minutes ago       Up 4 minutes               8080/tcp                                             tomcatdocker1_tomcat2_1
20aa7abec733        tomcatdocker1_nginx      "nginx -g 'daemon ..."   4 minutes ago       Up 4 minutes               80/tcp, 0.0.0.0:8080->8080/tcp                       tomcatdocker1_nginx_1

중지

아래 명령어를 통해 중지할 수 있습니다.

$ docker-compose down

Tomcat 8 Session Manager

|

세션을 어떻게 복제/관리 할 지를 결정합니다. Clustering에 사용되는 Manager는 DeltaManager, BackupManager, PersistentManager이며
그 중 in-memory 방식은 DeltaManager, BackupManager를 사용해야합니다.

StandardManager

tomcat에서 기본적으로 사용하는 Manager로 메모리에 세션을 가지고 있다가 tomcat이 중지될때 SESSIONS.ser라는 파일에 세션을 저장하고 재기동시 해당 파일의 내용을 메모리에 올리고 파일을 지웁니다.
conf/context.xml의 pathname에 지정된 이름을 사용하며 파일을 생성하지 않으려면 pathname=""로 설정하면 됩니다.

tomcat에 별도의 설정을 하지 않았을 경우 해당 Manager를 사용하게 됩니다.

DeltaManager

모든 노드에 동일한 세션을 복제합니다. 정보가 변경될때마다 복제하기 때문에 노드 개수가 많을 수록 네트워크 트래픽이 높아지고 메모리 소모가 심해집니다.

BackupManager

Primary Node와 Backup Node로 분리되어 모든 노드에 복제하지 않고 단 Backup Node에만 복제합니다. 하나의 노드에만 복제하기 때문에 DeltaManager의 단점을 커버할 수 있고 failover도 지원한다고 합니다.
동작 방식은 아래 예를 참고하세요.

  • tomcat을 3대, 앞단 loadbalancer를 둔 상태로 session1이 접근합니다.
  • session1이 tomcat1로 접속
  • tomcat1은 정보저장(primary node)후 tomcat2에 정보전달(backup node)
  • session1이 tomcat3으로 접속
  • tomcat3은 session1의 정보가 없으므로 tomcat2에 정보 요청
  • tomcat2는 tomcat3에게 정보 전달

PersistentManager

DB나 파일시스템을 이용하여 세션을 저장합니다. IO문제가 생기기 떄문에 실시간성이 떨어집니다.

각 Manager의 상세 옵션은 Apache Tomcat 8 Configuration Reference - The Cluster object에서 확인하세요.

참고

Apache Tomcat 8 Configuration Reference - The Cluster object
Tomcat Clustering Series Part 4 : Session Replication using Backup Manager
Tomcat Session StandardManager

Tomcat 8 세션 클러스터링 하기

|

WAS간 세션 공유해야하는 일이 생겨서 Tomcat Clustering을 한 내용을 정리해봅니다.

설정하기

Apache Tomcat 8 - Clustering/Session Replication HOW-TO 문서를 보면 정말 간단합니다.

<!-- server.xml -->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>

설치 후 기본으로 포함되어 있는 server.xml 파일에서 위 라인의 주석만 제거해주면 설정 끝. 그 다음에 WEB-INF/web.xml 파일에 아래와 같이 한 줄 넣어주면 됩니다.

<!-- web.xml -->
<distributable/>

위와 같이 작성하면 기본적으로 아래와 같이 동작합니다.

  1. multicast 방식으로 동작하며 address는 ‘228.0.0.4’, port는 ‘45564’를 사용하고 서버 IP는 java.net.InetAddress.getLocalHost().getHostAddress()로 얻어진 IP 값으로 송출됩니다.
  2. 먼저 구동되는 서버부터 4000 ~ 4100 사이의 TCP port를 통해 reqplication message를 listening합니다.
  3. Listener는 ClusterSessionListener, interceptor는 TcpFailureDetectorMessageDispatchInterceptor가 설정됩니다.

아래와 같이 설정되었다고 보면됩니다.

<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">

    <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/>

    <Channel className="org.apache.catalina.tribes.group.GroupChannel">
        <Membership className="org.apache.catalina.tribes.membership.McastService"
                    address="228.0.0.4"
                    port="45564"
                    frequency="500"
                    dropTime="3000"/>
        <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                  address="auto"
                  port="4000"
                  autoBind="100"
                  selectorTimeout="5000"
                  maxThreads="6"/>

        <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
            <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
        </Sender>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/>
    </Channel>

    <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
    <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>

    <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
            tempDir="/tmp/war-temp/"
            deployDir="/tmp/war-deploy/"
            watchDir="/tmp/war-listen/"
            watchEnabled="false"/>

    <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>

Manager

세션을 어떻게 복제할지를 책임지는 객체로 Clustering시 사용되는 매니저는 아래와 같이 3가지 입니다.

  1. DeltaManager
    모든 노드에 동일한 세션을 복제합니다. 정보가 변경될때마다 복제하기 때문에 노드 개수가 많을 수록 네트워크 트래픽이 높아지고 메모리 소모가 심해집니다.
  • notifyListenersOnReplication : 다른 tomcat에서 세션이 생성/소멸시 알림을 받을지 여부입니다.
  • expireSessionsOnShutdown : tomcat서버가 shutdown될 때 모든 노드의 모든 세션들을 expire할지 여부로 default는 false입니다.
  1. BackupManager
    Primary Node와 Backup Node로 분리되어 모든 노드에 복제하지 않고 단 Backup Node에만 복제합니다. 하나의 노드에만 복제하기 때문에 DeltaManager의 단점을 커버할 수 있고 failover도 지원한다고 합니다.

  2. PersistentManager DB나 파일시스템을 이용하여 세션을 저장합니다. IO문제가 생기기 떄문에 실시간성이 떨어집니다.

참고 : The ClusterManager object

Channel

서로 다른 tomcat간의 메시지 송수신에 관련된 하위 Component를 그룹핑합니다.
하위 Component로는 Membership, Sender, Sender/Transport, Receiver, Interceptor가 있고 현재 Channel구현체는 org.apache.catalina.tribes.group.GroupChannel가 유일합니다.

참고 : The Cluster Channel object

Channel/Membership

Cluster안의 노드들을 동적으로 분별하는데 multicast IP/PORT를 통해 frequency에 설정된 간격으로 각 노드들이 UDP packet을 날려 heartbeat 확인합니다.
dropTime에 설정된 시간동안 heartbeat가 없을 경우 장애로 판단하고 각 노드에 알리게 됩니다.

참고 : The Cluster Membership object

Channel/Sender, Channel/Sender/Transport

Sender는 노드에서 Cluster로 메시지를 보내는 역활을 합니다. 사실상 빈 껍데기로 상세 역확을 Transport에서 정의됩니다.
Transport는 기본적으로 org.apache.catalina.tribes.transport.nio.PooledParallelSender를 사용하는데 non-blocking 방식으로 동시에 여러 노드로 메시지를 보낼수도, 하나의 노드에 여러 메시지를 동시에 보낼수도 있습니다. org.apache.catalina.tribes.transport.bio.PooledMultiSender는 blocking 방식을 사용합니다.

참고 : The Cluster Sender object

Channel/Receiver

Cluster로부터 메시지를 수신하는 역활을 하며 blocking방식 org.apache.catalina.tribes.transport.bio.BioReceiver와 non-blocking방식인 org.apache.catalina.tribes.transport.nio.NioReceiver을 지원합니다.
tomcat에서는 non-blocking방식을 추천하며 노드수가 많아져서 제한된 thread를 통해 많은 메시지를 받아들일 수 있다고 합니다. 기본적으로 노드당 1개의 thread를 할당합니다.

참고 : The Cluster Receiver object

Channel/Interceptor

Membership 알림 또는 메시지를 가로챌수 있고, documentation에도 각 interceptor에 대한 자세한 설명은 안나왔지만 각 클래스 명으로 역활 구분이 가능한 수준인것 같습니다.

참고 : The Channel Interceptor object

Valve

org.apache.catalina.ha.ClusterValve를 구현한 객체로 일반적인 Tomcat Valve처럼 HTTP Request processing에 관여하는 역활을 하는데 clustering시 중간 interceptor역활을 합니다.
예를 들어 org.apache.catalina.ha.tcp.ReplicationValve의 경우 HTTP Request가 끝나는 시점에 다른 복제를 해야할지 말아야 할지 cluster에 알리는 역활을 합니다.
org.apache.catalina.ha.session.JvmRouteBinderValve의 경우 mod_jk를 사용중 failover시 session에 저장한 jvmWorker속성을 변경하여 다음 request부터는 해당 노드에 고정시킵니다.

참고 : The Cluster Valve object

Deployer

WAR배포시 cluster안의 다른 노드에도 같이 배포해줍니다.

참고:The Cluster Deployer object

ClusterListener

Cluster내 다른 노드의 메시지를 받습니다. DeltaManager를 사용할 경우 Manager는 ClusterSessionListener를 통해 메시지를 받게 됩니다.

참고:The ClusterListener object

기타

AWS를 포함한 모든 클라우드 서비스는 multicast를 지원하지 않고 있어 tomcat clustering 방식을 사용할 수 없습니다.

참고

Apache Tomcat 8 - Clustering/Session Replication HOW-TO
Apache Tomcat 8 Configuration Reference - The Cluster object
Tomcat Clustering Series Part 4 : Session Replication using Backup Manager
Tomcat Session StandardManager

(Thymeleaf) 파라메터 모두 출력하는 샘플 코드 (th:each, ${param})

|

${param}변수를 th:each를 태워 Request의 모든 파라메터를 출력하는 예제 소스 입니다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Title</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous"/>
</head>
<body>
<div class="container-fluid">
    <div th:each="res : ${param}">
        <div class="row"><div class="col-2" th:text="${res.key + ' : '}"></div><div class="col-6" th:text="${res.value[0]}"></div></div>
    </div>
</div>
</body>
</html>

(Thymeleaf) th:attr 사용시 변수와 문자열 섞어쓰는 방법

|

th:attr사용시 변수와 문자열 섞어 쓰는 방법을 정리해봅니다.

예를 들어 http://localhost:8080/demo/test 와 같은 URL값을 input value에 넣을 때 ‘http://localhost:8080’를 변수처리 하는 방법입니다.

# application.yml
base.url: http://localhost:8080

설정 파일에 위와 같이 설정 되 있을 경우 html파일에서 다음과 같이 사용 할 수 있습니다.

<!-- html source -->
<input type="text" th:attr="value=${@environment.getProperty('base.url') + '/demo/test'}"/>

환경 설정 값에서 가져오기 위해 @environment.getProperty를 사용했고 문자열을 ${...} 안에서 + 기호로 합치면 됩니다.