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를 사용했고 문자열을 ${...} 안에서 + 기호로 합치면 됩니다.

(SpringBoot) application.yml 에서 값이 8진수로 변경되는 경우

|

문제

프로그램 버그를 잡는중 아래와 같은 문제가 생겼습니다.

 # apllication.yml
 tran-cd:
    req: 00100000

@Value("${tran-cd.req}") private String code; // expect "00100000" but "32768"

위와 같이 해당 설정값이 이상하게 변경되어 있는 것입니다. “00100000”로 나와야하는데 자꾸 “32768”로 나옵니다. 문제는 yaml 1.1 버전에서 맨 앞자리가 “0”으로 시작하면 해당 값을 8진수로 인식하게 되고 Spring에서 @Value로 가져올 때 해당 값을 10진수로 변경하여 String 으로 반환하는 것이였습니다.

해결방법

yaml에서는 쌍따옴표(double quotes), 외따옴표(single quote)로 문자열을 쌓을수 있습니다.

#application.yml
tran-cd:
    req: "00100000"

그 외

yaml문법중 %YAML 태그를 이용하여 버전을 명시 할 수 있습니다.

% YAML 1.2

YAML 1.2에서는 8진수 표현법이 바뀌어서 위와 같은 오류를 막을수 있으나 application.yml에 적용해도 위 문제가 해결되지 않는 것으로 보아 SpringBoot에서 쓰는 Yaml Parser가 1.1로만 인식하나 봅니다. 해결 방법은 찾지 못했네요.

참고

%YAML 1.1 # Reference card

express-generator - Node.js + Express 프로젝트 생성하기

|

처음 Node.js 개발환경을 구성 할 때 이것저것 설정할게 많은데 간단하게 “Node.js + Express”구조의 뼈대를 만들어주는 express-generator라는 도구가 있습니다.

“Java + Spring Boot” 개발환경의 뼈대를 만들어주는 Spring Initializr와 비슷한 녀석입니다.

설치

npm을 통해서 아래와 같이 설치합니다.

$ npm install -g express-generator

프로젝트 만들기

express라는 명령어로 실행이 가능한데 도움말을 보면 아래와 같이 설정을 볼 수 있습니다.

$ express -h

  Usage: express [options] [dir]

  Options:

    -h, --help           output usage information
        --version        output the version number
    -e, --ejs            add ejs engine support
        --pug            add pug engine support
        --hbs            add handlebars engine support
    -H, --hogan          add hogan.js engine support
    -v, --view <engine>  add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)
    -c, --css <engine>   add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)
        --git            add .gitignore
    -f, --force          force on non-empty directory

먼저 express를 이용하여 뼈대가 되는 소스를 만들고 해당 프로젝트 폴더 내에서 npm install을 실행하여 dependency를 다운 받고 사용하면 됩니다.
만약 template engine은 handlebars를 사용하고 css engine은 sass를 사용한다면 아래와 같이 실행하시면 됩니다.

$ express --view=hbs --css=sass <project dir>
$ cd <project dir>
$ npm install
$ npm start 

기본적으로 package.json에 start커맨드를 통해 서버를 올릴 수 있도록 script를 만들어주며 실행시 http://localhost:3000으로 접속하여 동작 화면을 확인 할 수 있습니다.
run-server

app.js 파일을 열면 다음과 같이 template/css engine이 설정 되어 있는 것을 확인 할 수 있습니다.

// app.js
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');

...

app.use(sassMiddleware({
  src: path.join(__dirname, 'public'),
  dest: path.join(__dirname, 'public'),
  indentedSyntax: true, // true = .sass and false = .scss
  sourceMap: true
}));

SASS 변경하기

설정을 보면 기본적으로 sass를 사용하도록 되어 있습니다. 저는 scss확장자를 쓰고 싶으니 변경해 보도록 하겠습니다.

// app.js
app.use(sassMiddleware({
  ... 
  indentedSyntax: false, // true = .sass and false = .scss
  ...
}));

기본적으로 CSS경로는 /public/stylesheets 하위로 설정되어 있습니다. sass파일을 scss로 바꿉니다.

style.sass -> style.scss

그리고 설정을 scss문법에 맞게 수정합니다.

// style.scss
body {
  padding: 51px;
  font: 25px "Lucida Grande", Helvetica, Arial, sans-serif;
  color: #445544;
}

a {
  color: #AAB7FF;
}

node-sass-middleware의 옵션 중에 css파일을 압축해주는 ‘compressed’ 옵션이 있습니다. 아래와 같이 적용해봅니다.

// app.js
app.use(sassMiddleware({
  ... 
  outputStyle: 'compressed',
  ...
}));

적용이 완료되고 npm start명령을 통해 서버를 실행하고 http://localhost:3000을 호출하여 다운받은 style.css파일을 확인하면 다음과 같이 변경된 것을 확인 할 수 있습니다.

css/template파일을 변경시 바로 적용되나 app.js파일 수정시에는 반드시 express server를 재시작해야 합니다.

compressed-scss-file

참고

npm express-generator
npm node-sass-middleware