본문 바로가기
개발/SpringBoot

SpringBoot Embedded Tomcat 세션 클러스터링

by 궁즉변 변즉통 통즉구 2022. 5. 30.
반응형

SpringBoot를 내장 톰캣으로 실행하고 만약 세션을 사용한다면 세션 클러스터링 설정이 필요하다. 토큰이나 Redis를 사용하는 경우에는 불필요하겠지만 내장 톰캣의 세션을 그대로 이용한다면 세션 클러스터링을 통해 세션 공유 설정을 해야지만 여러 대의 was로 서비스가 가능할 것이다.

 

테스트 환경

- SpringBoot 2.6.7, Tomcat 9.0.62, JDK 11

 

1. 의존성 설정

먼저 build.gradle에 tomcat-catalina-ha를 의존성으로 추가한다.

implementation 'org.apache.tomcat:tomcat-catalina-ha:9.0.62'

 

2. Java Config 설정

@Configuration을 통해 내장 톰캣에 대한 세션 클러스터링 Java Config 파일을 작성한다. 아래에 MultiCast 포트로 설정한 45564는 서버에서 TCP/UDP 포트 오픈이 필요하고, Receiver 포트로 설정한 5000은 TCP 포트 오픈이 필요하다.

@Configuration
public class TomcatClusterConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(final TomcatServletWebServerFactory factory) {
        factory.addContextCustomizers(new TomcatClusterContextCustomizer());
    }
}
class TomcatClusterContextCustomizer implements TomcatContextCustomizer {
    @Override
    public void customize(final Context context) {
        context.setDistributable(true); 

        DeltaManager manager = new DeltaManager();
        manager.setExpireSessionsOnShutdown(false);
        manager.setNotifyListenersOnReplication(true);
        context.setManager(manager);
        configureCluster((Engine) context.getParent().getParent());
    }
    private void configureCluster(Engine engine) {
        //cluster
        SimpleTcpCluster cluster = new SimpleTcpCluster();
        cluster.setChannelSendOptions(6);

        //channel
        GroupChannel channel = new GroupChannel();
        //membership setting
        McastService mcastService = new McastService();
        mcastService.setAddress("228.0.0.4");
        mcastService.setPort(45564); // TCP&UDP port 오픈 필요
        mcastService.setFrequency(500);
        mcastService.setDropTime(3000);
        channel.setMembershipService(mcastService);

        //receiver
        NioReceiver receiver = new NioReceiver();
        receiver.setAddress("auto");
        receiver.setMaxThreads(6);
        receiver.setPort(5000); // TCP port 오픈 필요
        channel.setChannelReceiver(receiver);

        //sender
        ReplicationTransmitter sender = new ReplicationTransmitter();
        sender.setTransport(new PooledParallelSender());
        channel.setChannelSender(sender);

        //interceptor
        channel.addInterceptor(new TcpPingInterceptor());
        channel.addInterceptor(new TcpFailureDetector());
        channel.addInterceptor(new MessageDispatchInterceptor());
        cluster.addValve(new ReplicationValve());
        cluster.addValve(new JvmRouteBinderValve());
        cluster.setChannel(channel);
        cluster.addClusterListener(new ClusterSessionListener());
        engine.setCluster(cluster);
    }
}

 

3. application.yml 로그 설정

톰캣 관련 로그를 상세히 보기 위해 application.yml에 톰캣 설정 로그를 DEBUG로 설정한다.

logging:
  level:
    org.apache.tomcat: DEBUG
    org.apache.catalina: DEBUG

 

4. Local 환경 테스트

위의 설정을 포함한 간단한 SpringBoot 어플리케이션을 Local에 8080, 8081 포트로 2개를 실행한다. 그리고 같은 브라우저에서 각각 localhost:8080, localhost:8081을 접속 했을 때 JSESSIONID가 동일하게 공유되는 것을 확인할 수 있다. 

로그 상으로 대략 아래와 같은 로그들이 출력된다.

 

5. AWS  EC2 테스트

AWS EC2에서 테스트를 진행했는데 동작을 하지 않았다. 좀더 찾아봐야겠지만 AWS EC2환경에서는 multicast 지원이 안된다는 얘기도 있고 어떻게 설정을 해야하는지 몰라서 일단 톰캣 세션의 클러스터를 multicast를 사용하지 않는 방법(StaticMembership)으로 다시 설정을 변경하고 테스트를 해봤다.

 

6. Java Config 설정2 - StaticMembership(not Multicast)

@Configuration
public class TomcatStaticClusterConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(final TomcatServletWebServerFactory factory) {
        factory.addContextCustomizers(new TomcatStaticClusterContextCustomizer());
    }
}
class TomcatStaticClusterContextCustomizer implements TomcatContextCustomizer {
    @Override
    public void customize(final Context context) {
        context.setDistributable(true);

        DeltaManager manager = new DeltaManager();
        manager.setExpireSessionsOnShutdown(false);
        manager.setNotifyListenersOnReplication(true);
        context.setManager(manager);
        configureCluster((Engine) context.getParent().getParent());
    }
    private void configureCluster(Engine engine) {
        //cluster 
        SimpleTcpCluster cluster = new SimpleTcpCluster();
        cluster.setChannelStartOptions(3);
        cluster.setChannelSendOptions(8);

        //channel 
        GroupChannel channel = new GroupChannel();

        StaticMembershipInterceptor staticMembershipInterceptor = new StaticMembershipInterceptor();

        /** [WAS1] 설정 기준
        // 대상 정보 - was2정보
        StaticMember staticMember = new StaticMember();
        staticMember.setPort(4056);
        staticMember.setSecurePort(-1); // default
        staticMember.setHost("172.31.45.3");
        staticMember.setUniqueId("{0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2}");
        staticMembershipInterceptor.addStaticMember(staticMember);

        //receiver(현재 자신의 정보) - was1
        NioReceiver receiver = new NioReceiver();
        receiver.setAddress("172.31.44.193");
        receiver.setMaxThreads(6);
        receiver.setPort(4055);  // was1: 4055, was2: 4056
        channel.setChannelReceiver(receiver);
         */

        /** [WAS2] 설정 기준 */
        // 대상 정보 - was1정보
        StaticMember staticMember = new StaticMember();
        staticMember.setPort(4055);
        staticMember.setSecurePort(-1); // default
        staticMember.setHost("172.31.44.193");
        staticMember.setUniqueId("{0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1}");
        staticMembershipInterceptor.addStaticMember(staticMember);

         //receiver(현재 자신의 정보) - was2
         NioReceiver receiver = new NioReceiver();
         receiver.setAddress("172.31.45.3");
         receiver.setMaxThreads(6);
         receiver.setPort(4056);  // was1: 4055, was2: 4056
         channel.setChannelReceiver(receiver);


        channel.addInterceptor(staticMembershipInterceptor);

        //sender
        ReplicationTransmitter sender = new ReplicationTransmitter();
        sender.setTransport(new PooledParallelSender());
        channel.setChannelSender(sender);

        //interceptor
        channel.addInterceptor(new TcpPingInterceptor());
        channel.addInterceptor(new TcpFailureDetector());
        channel.addInterceptor(new MessageDispatchInterceptor());
        cluster.addValve(new ReplicationValve());
        cluster.addValve(new JvmRouteBinderValve());
        cluster.setChannel(channel);
        cluster.addClusterListener(new ClusterSessionListener());
        engine.setCluster(cluster);
    }
}

일단 was1, was2 기준으로 각각 주석으로 구분해서 설정하고, ec2 2대에 각 업로드해서 was1로 접속해보니 was2번 아래와 같이 세션 정보 받았다는 로그는 찍힌다. 

일단 위와 같이 로그까지 확인했는데 각 EC2의 public IP가 달라서 실제 동작 확인은 할 수 없었다. 그래서 좀더 테스트 해보는 차원에서 EC2 2개를 타켓 그룹으로 묶고 앞에 ALB를 붙여서 was1로 접속 확인 후 was1번 내리고, was2로 접속 했을 때 JSESSIONID가 변하지 않는 것을 확인했다. 다시 was1을 올리고 was2를 내렸을 경우에도 JSESSIONID가 변하지 않는것을 확인했다.

ALB 테스트
Session 전송 Log

참고:

https://oingdaddy.tistory.com/149

https://shonm.tistory.com/641

 

반응형

댓글