복붙노트

[SPRING] 바람둥이 클러스터에있는 Spring Websocket

SPRING

바람둥이 클러스터에있는 Spring Websocket

현재 우리의 응용 프로그램에서는 STOMP를 통해 스프링 웹 소켓을 사용합니다. 우리는 수평으로 확장하려고합니다. 여러 개의 Tomcat 인스턴스에 대해 websocket 트래픽을 처리하는 방법과 여러 노드에서 세션 정보를 유지 관리하는 방법에 대한 모범 사례가 있습니까? 참조 가능한 실용 예제가 있습니까?

해결법

  1. ==============================

    1.귀하의 요구 사항은 2 개의 하위 작업으로 나눌 수 있습니다 :

    귀하의 요구 사항은 2 개의 하위 작업으로 나눌 수 있습니다 :

    업데이트 : Redis를 사용하여 간단한 구현을 작성했습니다. 관심이 있다면 시도해보십시오.

    모든 기능을 갖춘 브로커 (브로커 릴레이)를 구성하려면 다음을 시도해보십시오.

    public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    
        ...
    
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            config.enableStompBrokerRelay("/topic", "/queue")
                .setRelayHost("localhost") // broker host
                .setRelayPort(61613) // broker port
                ;
            config.setApplicationDestinationPrefixes("/app");
        }
    
        @Bean
        public UserSessionRegistry userSessionRegistry() {
            return new RedisUserSessionRegistry(redisConnectionFactory);
        }
    
        ...
    }
    

    import java.util.Set;
    
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.BoundHashOperations;
    import org.springframework.data.redis.core.BoundSetOperations;
    import org.springframework.data.redis.core.RedisOperations;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    import org.springframework.messaging.simp.user.UserSessionRegistry;
    import org.springframework.util.Assert;
    
    /**
     * An implementation of {@link UserSessionRegistry} backed by Redis.
     * @author thanh
     */
    public class RedisUserSessionRegistry implements UserSessionRegistry {
    
        /**
         * The prefix for each key of the Redis Set representing a user's sessions. The suffix is the unique user id.
         */
        static final String BOUNDED_HASH_KEY_PREFIX = "spring:websockets:users:";
    
        private final RedisOperations<String, String> sessionRedisOperations;
    
        @SuppressWarnings("unchecked")
        public RedisUserSessionRegistry(RedisConnectionFactory redisConnectionFactory) {
            this(createDefaultTemplate(redisConnectionFactory));
        }
    
        public RedisUserSessionRegistry(RedisOperations<String, String> sessionRedisOperations) {
            Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
            this.sessionRedisOperations = sessionRedisOperations;
        }
    
        @Override
        public Set<String> getSessionIds(String user) {
            Set<String> entries = getSessionBoundHashOperations(user).members();
            return (entries != null) ? entries : Collections.<String>emptySet();
        }
    
        @Override
        public void registerSessionId(String user, String sessionId) {
            getSessionBoundHashOperations(user).add(sessionId);
        }
    
        @Override
        public void unregisterSessionId(String user, String sessionId) {
            getSessionBoundHashOperations(user).remove(sessionId);
        }
    
        /**
         * Gets the {@link BoundHashOperations} to operate on a username
         */
        private BoundSetOperations<String, String> getSessionBoundHashOperations(String username) {
            String key = getKey(username);
            return this.sessionRedisOperations.boundSetOps(key);
        }
    
        /**
         * Gets the Hash key for this user by prefixing it appropriately.
         */
        static String getKey(String username) {
            return BOUNDED_HASH_KEY_PREFIX + username;
        }
    
        @SuppressWarnings("rawtypes")
        private static RedisTemplate createDefaultTemplate(RedisConnectionFactory connectionFactory) {
            Assert.notNull(connectionFactory, "connectionFactory cannot be null");
            StringRedisTemplate template = new StringRedisTemplate(connectionFactory);
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new StringRedisSerializer());
            template.afterPropertiesSet();
            return template;
        }
    
    }
    
  2. ==============================

    2.수평 적으로 WebSockets을 확장하는 것은 실제로 상태 기반 / 상태 기반 HTTP 전용 응용 프로그램을 수평 적으로 확장하는 것과는 매우 다릅니다.

    수평 적으로 WebSockets을 확장하는 것은 실제로 상태 기반 / 상태 기반 HTTP 전용 응용 프로그램을 수평 적으로 확장하는 것과는 매우 다릅니다.

    수평 적으로 확장 무 상태 (stateless) HTTP 응용 프로그램 : 다른 컴퓨터에서 일부 응용 프로그램 인스턴스를 시작하고 부하 분산 장치를 앞에 놓습니다. HAProxy, Nginx와 같은 꽤 많은로드 밸런서 솔루션이 있습니다. AWS와 같은 클라우드 환경에 있다면 Elastic Load Balancer와 같은 솔루션을 관리 할 수도 있습니다.

    수평 적으로 상태를 유지하는 HTTP 응용 프로그램 : 모든 응용 프로그램을 매번 상태 비 저장으로 설정할 수 있다면 좋겠지 만, 불행히도 항상 가능하지는 않습니다. 따라서 상태 기반 HTTP 응용 프로그램을 처리 할 때는 기본적으로 웹 서버가 다양한 HTTP 요청 (예 : 쇼핑 관련 데이터를 저장할 수있는 데이터를 저장할 수있는 각기 다른 클라이언트의 로컬 저장소 인 HTTP 세션에주의해야합니다. 카트). 글쎄,이 경우 수평 적으로 스케일링 할 때, 내가 말했던 것처럼 로컬 스토리지이므로 ServerA는 ServerB에있는 HTTP 세션을 처리 할 수 ​​없다는 것을 알아야합니다. 즉, ServerA에서 서비스중인 Client1이 ServerB에서 갑자기 서비스를 시작하면 그의 HTTP 세션이 손실되고 장바구니가 사라집니다. 그 이유는 노드 오류이거나 배포 일 수 있습니다. 이 문제를 해결하려면 HTTP 세션을 로컬로만 유지할 수 없습니다. 즉, 다른 외부 구성 요소에 저장해야합니다. 이것은 관계형 데이터베이스와 같이이를 처리 할 수있는 몇 가지 구성 요소이지만 실제로는 오버 헤드가됩니다. 일부 NoSQL 데이터베이스는 Redis와 같이이 키 - 값 동작을 매우 잘 처리 할 수 ​​있습니다. 이제 Redis에 저장된 HTTP 세션을 사용하여 클라이언트가 다른 서버에서 서비스를 시작하면 Redis에서 클라이언트의 HTTP 세션을 가져 와서 메모리에로드하므로 모든 작업이 계속되고 사용자는 자신을 잃지 않습니다 더 이상 HTTP 세션. Spring 세션을 사용하여 Redis에서 HTTP 세션을 쉽게 저장할 수 있습니다.

    수평 적으로 WebSocket 앱 확장 : WebSocket 연결이 설정되면 서버는 클라이언트와의 연결을 유지하여 양방향으로 데이터를 교환 할 수 있어야합니다. 클라이언트가 "/topic/public.messages"와 같은 대상을 청취 할 때 클라이언트가이 대상에 가입한다고합니다. Spring에서 simpleBroker 접근 방식을 사용하면 구독이 메모리에 보관되므로 Client1이 ServerA에서 서비스를 제공하고 ServerB에서 제공하는 ClientS2에 WebSocket을 사용하여 메시지를 보내려면 어떻게됩니까? 당신은 이미 그 해답을 알고 있습니다! Server1은 Client2의 가입에 대해 알지 못하기 때문에 메시지는 Client2에 전달되지 않습니다. 따라서이 문제를 해결하려면 다시 WebSockets 구독을 외부화해야합니다. STOMP를 서브 프로토콜로 사용하면 외부 STOMP 브로커 역할을 할 수있는 외부 구성 요소가 필요합니다. 이 작업을 수행 할 수있는 도구가 많이 있지만 RabbitMQ를 제안합니다. 이제 스프링 등록을 변경하여 구독을 메모리에 보관하지 않도록해야합니다. 대신 구독을 외부 STOMP 중개자에게 위임합니다. enableStompBrokerRelay와 같은 몇 가지 기본 구성으로이 작업을 쉽게 수행 할 수 있습니다. 중요한 점은 HTTP 세션이 WebSocket 세션과 다르다는 것입니다. Redis에서 HTTP Session을 저장하기 위해 Spring Session를 사용하면 WebSocket을 수평 적으로 확장하는 것과 아무런 관련이 없습니다.

    필자는 RabbitMQ를 Full External STOMP Broker로 사용하는 Spring Boot (그리고 훨씬 더 많은 것)와 함께 완벽한 웹 채팅 응용 프로그램을 작성했습니다. GitHub에 공개되어 있으므로 복제하고 컴퓨터에서 응용 프로그램을 실행하고 코드 세부 정보를 확인하십시오.

    WebSocket 연결이 끊어지면 Spring이 할 수있는 일은별로 없습니다. 실제로 재 연결은 재 연결 콜백 함수를 구현하는 클라이언트 측에서 요청해야합니다 (예 : WebSocket 핸드 셰이크 흐름, 클라이언트가 서버가 아닌 핸드 셰이크를 시작해야 함). 이것을 투명하게 처리 할 수있는 클라이언트 측 라이브러리가 있습니다. 그건 SockJS 사건이 아니야. 채팅 응용 프로그램에서이 재 연결 기능도 구현했습니다.

  3. ==============================

    3.여러 노드에서 세션 정보 유지 관리 :

    여러 노드에서 세션 정보 유지 관리 :

    부하 분산 장치로 백업 된 서버 호스트가 2 대 있다고 가정합니다.

    웹 소켓은 브라우저에서 특정 서버 host.eg host1에 대한 소켓 연결입니다.

    이제 host1이 다운되면로드 밸런서 (호스트 1)의 소켓 연결이 끊어집니다. 스프링이로드 밸런서에서 호스트 2로 동일한 websocket 연결을 다시 여는 방법은 무엇입니까? 브라우저가 새로운 websocket 연결을 열지 않아야합니다.

  4. from https://stackoverflow.com/questions/26853745/spring-websocket-in-a-tomcat-cluster by cc-by-sa and MIT license