본문 바로가기

서버운영 (TA, ADMIN)/미들웨어

[톰캣] 세션 클러스터

휘발성 영역인 JVM 메모리 내에 세션 객체가 생성되어 있을때 Tomcat이 중지한다면, 메모리 내에 생성되어 있던 모든 세션은 제거된다. Tomcat 인스턴스를 다중화하더라도 특정 Tomcat 인스턴스가 중지한다면 그 Tomcat 내의 세션은 모두 사라지게 된다. 세션을 통해 구현한 범위에 따라 영향도가 달라지나, 흔히 로그인을 다시 해야한다는 등의 상황이 벌어진다.

 

이러한 문제를 해결하기 위해 세션 클러스터를 사용한다. 동일 업무를 처리하는 여러 인스턴스를 하나의 클러스터 그룹으로 묶으면 멤버들은 자신이 생성하고 변경한 세션 정보를 다른 인스턴스와 공유한다. 갑자기 장애가 발생하여 특정 멤버가 중지하더라도 여전히 다른 인스턴스에서 세션을 가지고 있기 때문에 사용자 세션은 유지된다. 단, 세션 클러스터는 추가적인 워크로드와 네트워크 트래픽을 동반하며 심지어 장애 전파를 유발하기도 하므로 반드시 세션 공유가 필요할 때만 적용하는 것이 좋다.

 


클러스터 설정 

 

SimpleTcpCluster

 

Tomcat 클러스터는 $CATALINA_BASE/conf/sever.xml 내 <Cluster>를 통해 설정한다. <Cluster>는 <Engine> 혹은 <Host> 내에 위치한다.

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

기본적으로는 설정이 비활성화되어 있다. 위 주석을 풀면 바로 클러스터를 구성할 수 있는데 이때는 아래와 같은 기본값을 사용한다.

Delta Manager 사용
Multipcast IP address: 228.0.0.4
Multicast 포트: 45564
Broadcast IP address: java.net.InetAddress.getLocalHost().getHostAddress()
Listen 포트: 4000~4100 포트 중 사용 가능한 첫번째 포트
JvmRouteSessionIDBinderListener, ClusterSessionListener 설정
JvmRouteBinderValve, ReplicationValve 설정
TcpFailureDetector, MessageDispatch51Interceptor 설정

 

다음은 Tomcat7.0.o.a.catalina.ha.tcp.SimpleTcpCluster 클래스의 checkDefaults 메서드이다.

protected void checkDefaults() {
	if (clusterListeners.size() == 0) {
    	addClusterListener(new JvmRouteSessionIDBinderListener());
        addClusterListener(new ClusterSessionListener());
    }
    if (valves.size() == 0) {
    	addValve(new JvmRouteBinderValve());
        addValve(new ReplicationValve());
    }
    if (clusterDeployer != null) clusterDeployer.setCluster(this);
    if (channel == null) channel = new GroupChannel();
    if (channel instanceof GroupChannel &&
    		!((GroupChannel)channel).getInterceptors().hasNext()) {
        channel.addInterceptor(new MessageDipatch15Interceptor());
        channel.addInterceptor(new TcpFailureDetector());
    }
}

 

별도의 Listener 설정이 없는 경우 JvmRouteSessionIDBinderListener와 ClusterSessionListener 설정을 추가하며, 별도의 Valve 설정이 없는 경우 JvmRouteBinderValve와 ReplicationValve 설정을 추가한다. 이외에도 MessageDispatch15Interceptor, TcpFailureDetector 등의 Interceptor를 기본 설정 한다.

 

Tomcat8.0의 checkDefaults 메서드도 확인해보자.

protected void checkDefaults() {
	if (clusterListeners.size() == 0) {
    	addClusterListener(new ClusterSessionListener());
    }
    if (valves.size() == 0) {
    	addValve(new JvmRouteBinderValve());
        addValve(new ReplicationValve());
    }
    if (clusterDeployer != null) clusterDeployer.setCluster(this);
    if (channel == null) channel = new GroupChannel();
    if (channel instanceof GroupChannel &&
    		!((GroupChannel)channel).getInterceptors().hasNext()) {
    	channel.addInterceptor(new MessageDispatch15Interceptor());
        channel.addInterceptor(new TcpFailureDetector());
    }
}

Tomcat7.0의 checkDefaults 메서드와 비슷하지만 JvmRouteSessionIDBinderListener가 빠져있다. 8.0 버전부터 JvmRouteSessionIDBinderListener가 없어졌기 때문이다.

 

Tomcat 클러스터는 Multicast 방식을 사용하기 때문에 반드시 Multicast가 가능한 네트워크 환경에서 구성해야 한다. 또한 클러스터 그룹 내 호스트는 시간이 모두 같아야 한다. 1번 톰캣 기동이 끝난후, 2번 톰캣을 구동하면 2번 톰캣 로그에는 Replication memeber added가 기록된다. 2번 톰캣 기동 시점에 1번 톰캣에도 역시 Replication member added 로그가 기록된다. 또한 2번 톰캣의 정보도 확인할 수 있다.

 

그리고나서 1번 톰캣을 중지한다면 2번 톰캣에는 아래와 같은 로그가 남게된다.

org.apache.catalina.ha.tcp.SimpleTcpCluster memberDisappeared

 

 

Tribes

Tribes는 Tomcat 초기 클러스터 모듈에서 발전하였으며 현재는 1:1 통신 혹은 그룹 통신을 구현하는 Message Framework로 발전하고 있다.

 

 

Channel

<Channel>은 Membership, Sender, Receiver, Interceptor 등의 내부 요소를 갖는다. o.a.catalina.tribes.group.ChannelCoordinator 클래스를 통해 각 요소별 기본 클래스를 확인할 수 있다.

public class ChannelCoordinator extends ChannelInterceptorBase implements MessageListener {
	private ChannelRecevier clusterReceiver = new NioReceiver();
    private ChannelSender clusterSender = new ReplicationTransmitter();
    private MembershipService membershipService = new McastService();
    // ... 생략 ...
}

 

 

Membership

<Membership>은 멤버간 상호 관리를 담당하며 사용 가능한 클래스는 o.a.catalina.tribes.membership.McastService 이다. 아래는 McastService 클래스이 생성자인데 Multicast port/address/dropTime/frequency 속성의 기본값을 확인할 수 있다.

public McastService() {
	properties.setProperty("mcastPort", "45564");
    properties.setProperty("mcastAddress", "228.0.0.4");
    properties.setProperty("memberDropTime", "3000");
    properties.setProperty("mcastFrequency", "500");
}

멤버가 다른 클러스터 그룹에 속하게 되면 다른 멤버의 존재를 알수도 없고 세션 공유도 할 수 없게 된다. 동일 업무를 처리하는 멤버나 세션 공유가 필요한 멤버끼리는 같은 그룹에 속해야 한다. 그러나 처리하는 업무가 다르거나 세션 공유가 필요치 않은 멤버간에는 그룹을 분리해야 한다. 그렇지 않으면 불필요하게 세션 공유를 시도하게 된다.

 

frequency는 hearbeat 전송 주기이다. o.a.catalina.tribes.membership.McastServiceImpl 클래스의 SenderThread 메서드를 통해 heartbeat를 전송한다.

public class SenderThread extends Thread {
	long time;
    int errorCounter = 0;
    public SenderThread(long time) {
    	this.time = time;
        setName("Tribes-MembershipSender");
    }
    @Override
    public void run() {
    	while (doRunSender) {
        	try {
            	send(true);
                errorCounter = 0;
            } catch (Exception x) {
            	if (errorCounter == 0)
                	log.warn("Unable to send mcast message.", x);
                else log.debug("Unable to send mcast message.", x);
                if ((++errorCounter)>=recoveryCounter) {
                	errorCounter = 0;
                    RecoveryThread.recover(McastServiceImpl.this);
                }
            }
            try { Thread.sleep(time); } catch ( Exception ignore ) {}
        }
    }
} //class SenderThread

 

frequency가 500이면 Sleeping for 1000 milliseconds, 10000이면 Sleeping for 20000 milliseconds 로그가 남는다. 즉, frequency 시간의 두 배만큼 sleeping한다. o.a.catalina.tribes.membership.McastServiceImpl 클래스의 waitForMembers 메서드는 아래와 같다.

private void waitForMembers(int level) {
	long memberwait = sendFrequency * 2;
    if(log.isInfoEnabled())
    	log.info("Sleeping for "+memberwait+" milliseconds to establish cluster membership, start level:"+level);
    try {Thread.sleep(memberwait);} catch(InterruptedException ignore) { }
    if(log.isInfoEnabled())
    	log.info("Done sleeping, membership established, start level:" + level);
}

 

soTimeout은 frequency와 관련이 있다. 만약 mcastSoTimeout이 0이하면 soTimeout은 frequency값으로 설정된다. setupSocket 메서드를 확인해보자.

if (mcastSoTimoute <= 0) mcastSoTimeout = (int)sendFrequency;
if (log.isInfoEnabled())
	log.info("Setting cluster mcast soTimeout to " + mcastSoTimeout);
socket.setSoTimeout(mcastSoTimeout);

 

 

Receiver

<Receiver>는 메시지 수신을 담당하며 ReceiverBase를 상속한 두 종류의 클래스를 사용할 수 있다. 각각 non-blocking 방식의 o.a.catalina.tribes.transport.nio.NioReceiver와 blocking 방식의 o.a.catalina.tribes.transport.bio.BioReceiver이다.

 

 

Sender

<Sender>는 메시지 전달을 담당하며 현재 사용 가능한 클래스는 오직 o.a.catalina.tribes.transport.ReplicationTransmitter 뿐이다. <Sender> 하위의 <Transport>는 PooledSender를 상속한 두 클래스를 사용할 수 있는데 non-blocking 방식의 o.a.catalina.tribes.transport.nio.PooledParallelSender와 blocking 방식의 o.a.catalina.tribes.transport.bio.PooledMultiSender가 있다. 명시적으로 설정하지 않으면 기본적으로 PooledParallelSender를 사용한다.

 

 

TcpFailureDetector

클러스터 그룹내 타 멤버가 정말 중지한 것이 맞는지 TCP Unicast를 통해 확인하는 Interceptor이다. connectTimeout의 단위는 ms이며 기본값은 1000다.

 

 

JvmRouteBinderValve

클러스터 그룹 내 특정 멤버가 장애나 재기동에 의해 중지되면 그룹내 다른 멤버가 중지한 멤버 내 세션들을 이어받아 처리하게 된다. 이때 세션 ID가 jvmRoute를 포함하고 있다면 JvmRouteBinderValve는 이전 멤버의 jvmRoute 값을 새로운 멤버의 jvmRoute로 변경한다.

 

 


클러스터 세션 Manager

 

Delta Manager(org.apache.catalina.ha.session.DeltaManager)

클러스터 그룹 내 모든 멤버 간에 세션을 공유하는 방식으로 All-to-All 공유 방식이라고도 부른다. 소규모 클러스터라면 큰 문제없지만 클러스터 그룹에 참여하는 멤버가 늘어나거나 공유 세션 정보가 많아지면 성능이 떨어질 수 있다. 각 멤버가 모든 상대 멤버의 세션 정보를 가지고 있어야 하기 때문에 더욱 많은 메모리가 필요하며 네트워크 트래픽도 늘어나기 때문이다.

 

 

Backup Manager(org.apache.catalina.ha.session.BackupManager)

Delta Manager는 All-to-All 방식이기 때문에 많은 오버헤드를 동반한다. 그 대안은 Backup Manager이다. 오직 Primary-Backup 두 멤버간에만 세션 공유를 하기 때문에 상대적으로 부하가 적다. 두 멤버 이외에 멤버들ㄷ은 투가 특정 세션을 가지고 있는 지만 알고 있다. 한편 Backup 멤버는 멤버간 통신을 통해 결정한다.