Весенний Websocket в кластере tomcat

в нашем текущем приложении мы используем Spring Websockets над STOMP. Мы хотим масштабироваться горизонтально. Существуют ли какие-либо рекомендации по обработке трафика websocket на нескольких экземплярах tomcat и как мы можем поддерживать информацию о сеансе на нескольких узлах.Есть ли рабочий образец, на который можно ссылаться?

3 ответов


ваше требование можно разделить на 2 подзадачи:

  1. поддерживать информацию о сеансе на нескольких узлах: вы можете попробовать кластеризацию весенних сеансов, поддерживаемую Redis (см.: HttpSession с Redis). Это очень просто и уже имеет поддержку Spring Websockets (см.:Весенняя Сессия & WebSockets).

  2. обрабатывать трафик websockets через несколько экземпляров tomcat: есть несколько способов сделать это.

    • первый способ: с помощью полнофункционального брокера (например: ActiveMQ) и попробовать новую функцию поддержка нескольких серверов WebSocket (from: 4.2.0 RC1)
    • второй способ: использование полнофункционального брокера и реализация распределенного UserSessionRegistry (например: использование Redis: D ). Реализация по умолчанию DefaultUserSessionRegistry использование памяти в памяти.

Обновлено: я написал простую реализацию с помощью Redis, попробуйте, если вы интересно!--30-->

чтобы настроить полнофункциональный брокер (broker relay), вы можете попробовать:

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;
    }

}

горизонтальное масштабирование WebSockets на самом деле очень отличается от горизонтального масштабирования приложений без состояния / состояния HTTP только на основе.

горизонтальное масштабирование без состояния HTTP app: просто раскрутите некоторые экземпляры приложений на разных машинах и поставьте перед ними балансировщик нагрузки. Существует довольно много различных решений балансировки нагрузки, таких как HAProxy, Nginx и т. д. Если вы находитесь в облачной среде, такой как AWS, вы также можете управлять такими решениями, как Эластичный Балансировщик Нагрузки.

горизонтальное масштабирование Stateful HTTP app: было бы здорово, если бы мы могли иметь все приложения без гражданства каждый раз, но, к сожалению, это не всегда возможно. Таким образом, при работе с HTTP-приложениями с состоянием вы должны заботиться о сеансе HTTP, который в основном является местные хранение для каждого отдельного клиента, где веб-сервер может хранить данные, которые хранятся в разных HTTP-запросах (например, при работе с покупками Телега.) Ну, в этом случае при горизонтальном масштабировании вы должны знать, что, как я уже сказал, это местные storage, поэтому ServerA не сможет обрабатывать сеанс HTTP, который находится на ServerB. Другими словами, если по какой-либо причине Client1, который обслуживается ServerA, внезапно начинает обслуживаться ServerB, его HTTP-сессия будет потеряна (и его корзина исчезнет!). Причиной может быть сбой узла или даже развертывание. Для решения этой проблемы вы не можете сохранить HTTP сеансы только локально, то есть их необходимо хранить на другом внешнем компоненте. Это несколько компонентов, которые могли бы справиться с этим, например, любая реляционная база данных, но это было бы накладными расходами. Некоторые базы данных NoSQL могут очень хорошо обрабатывать это поведение ключа-значения, например Redis. Теперь, когда сеанс HTTP хранится на Redis, если клиент начинает обслуживаться другим сервером, он будет извлекать сеанс HTTP клиента из Redis и загружать его в свою память, поэтому все будет продолжать работать, и пользователь больше не потеряет свой HTTP-сеанс. Вы можете использовать весенний сеанс, чтобы легко сохранить сеанс HTTP на Redis.

горизонтальное масштабирование WebSocket app: когда установлено соединение WebSocket, сервер должен держать соединение открытым с клиентом, чтобы они могли обмениваться данными в обоих направлениях. Когда клиент прослушивает назначение, такое как " /topic / public.сообщения " мы говорим, что клиент подписан на это назначение. Весной, когда вы используете simpleBroker подход, подписки сохраняются в, так что происходит, например, если Client1 обслуживается ServerA и хочет отправить сообщение с помощью WebSocket на Client2 обслуживается ServerB? Ты уже знаешь ответ! Сообщение не будет доставлено в Client2, потому что Server1 даже не знает о подписке Client2. Итак, чтобы решить эту проблему, вам снова нужно экстернализировать подписки на WebSockets. Как вы используете STOMP в качестве субпротокола вам нужен внешний компонент, который может выступать в качестве внешнего брокера STOMP. Есть довольно много инструментов, способных это сделать, но я бы предложил RabbitMQ. Теперь вы должны изменить конфигурацию Spring, чтобы она не сохраняла подписки в памяти. Вместо этого он делегирует подписки внешнему брокеру STOMP. Вы можете легко достичь этого с помощью некоторых основных конфигураций, таких как enableStompBrokerRelay. Важно отметить, что http-сеанса является отличается от сеанса WebSocket. использование весенней сессии для хранения http-сессии в Redis не имеет абсолютно ничего общего с горизонтальным масштабированием WebSockets.

я закодировал полное приложение веб-чата с Spring Boot (и многое другое), которое использует RabbitMQ в качестве полного внешнего брокера STOMP, и это публика на GitHub поэтому, пожалуйста, клонируйте его, запустите приложение на своем компьютере и посмотрите детали кода.

когда дело доходит до WebSocket потеря связи, там не так много, что весна может сделать. На самом деле, переподключение должно быть запрошено клиентской стороной, реализующей функцию обратного вызова переподключения, например (это поток рукопожатия WebSocket,клиент необходимо запустить рукопожатие, а не сервер). Есть некоторые клиентские библиотеки, которые могут обрабатывать это прозрачно для вас. Это не дело SockJS. В приложении чата я также реализовал эту функцию повторного подключения.


поддерживать информацию о сеансе на нескольких узлах:

предположим, что у нас есть хост сервера 2, резервное копирование с помощью балансировщика нагрузки.

Websockets-это подключение сокета из браузера к определенному хосту сервера.например host1

теперь, если host1 идет вниз, соединение сокета с балансировщиком нагрузки-хост 1 сломается. Как spring снова откроет то же соединение websocket с балансировщиком нагрузки на хост 2 ? браузер не должен открывать новое соединение websocket