web-dev-qa-db-ja.com

Tomcatクラスター内のSpringWebsocket

現在のアプリケーションでは、STOMPではなくSpringWebsocketを使用しています。水平方向のスケーリングを検討しています。複数のTomcatインスタンスでWebSocketトラフィックを処理する方法、および複数のノードでセッション情報を維持する方法に関するベストプラクティスはありますか?参照できる実用的なサンプルはありますか?

21
Robin Varghese

要件は、2つのサブタスクに分けることができます。

  1. 複数のノード間でセッション情報を維持する:Redisに裏打ちされたSpring Sessionsクラスタリングを試すことができます( Redisを使用したHttpSession を参照)。これは非常に単純で、すでにSpring Websocketをサポートしています( Spring Session&WebSockets を参照)。

  2. 複数のTomcatインスタンスを介したWebSocketトラフィックの処理:これを行うにはいくつかの方法があります。

    • 最初の方法:フル機能のブローカー(例:ActiveMQ)を使用して、新しい機能を試してください 複数のWebSocketサーバーをサポート (from:4.2.0 RC1)
    • 2番目の方法:フル機能のブローカーを使用し、分散UserSessionRegistryを実装します(例:Redis:Dを使用)。インメモリストレージを使用したデフォルトの実装DefaultUserSessionRegistry

更新: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;
    }

}
18

WebSocketを水平方向にスケーリングすることは、実際には、ステートレス/ステートフルHTTPのみに基づくアプリケーションを水平方向にスケーリングすることとは大きく異なります。

ステートレスHTTPアプリを水平方向にスケーリングする:異なるマシンでいくつかのアプリケーションインスタンスを起動し、それらの前にロードバランサーを配置するだけです。 HAProxy、Nginxなどのさまざまなロードバランサーソリューションがあります。AWSなどのクラウド環境を使用している場合は、Elastic LoadBalancerなどのマネージドソリューションを使用することもできます。

ステートフルHTTPアプリを水平方向にスケーリングする:すべてのアプリケーションを常にステートレスにすることができれば素晴らしいのですが、残念ながらそれが常に可能であるとは限りません。したがって、ステートフルHTTPアプリを処理するときは、HTTPセッションに注意する必要があります。これは、基本的に、Webサーバーが保持されるデータを格納できるさまざまなクライアントごとのローカルストレージです。さまざまなHTTPリクエスト間(ショッピングカートを処理する場合など)。さて、この場合、水平方向にスケーリングするときは、私が言ったように、それは[〜#〜]ローカル[〜#〜]ストレージであるため、ServerAはそうではないことに注意する必要がありますServerB上にあるHTTPセッションを処理できる。つまり、何らかの理由でServerAによって提供されていたClient1が突然ServerBによって提供され始めた場合、彼のHTTPセッションは失われます(そして彼のショッピングカートはなくなります!)。理由は、ノードの障害または展開である可能性があります。この問題に対処するには、HTTPセッションをローカルでのみ保持することはできません。つまり、HTTPセッションを別の外部コンポーネントに保存する必要があります。これは、リレーショナルデータベースなど、これを処理できるいくつかのコンポーネントですが、実際にはオーバーヘッドになります。 Redisなどの一部のNoSQLデータベースは、このKey-Value動作を非常にうまく処理できます。これで、HTTPセッションがRedisに保存されているため、クライアントが別のサーバーからサービスを受け始めると、クライアントのHTTPセッションがRedisからフェッチされてメモリに読み込まれるため、すべてが引き続き機能し、ユーザーが自分のHTTPセッションを失うことはありません。 HTTPセッションはもうありません。 Spring Sessionを使用して、HTTPセッションをRedisに簡単に保存できます。

WebSocketアプリを水平方向にスケーリングする:WebSocket接続が確立されると、サーバーはクライアントとの接続を開いたままにして、双方向でデータを交換できるようにする必要があります。クライアントが「/topic/public.messages」などの宛先をリッスンしている場合、クライアントはこの宛先にサブスクライブされていると言います。 Springでは、simpleBrokerアプローチを使用すると、サブスクリプションはメモリに保持されます。たとえば、Client1がServerAによって提供されていて、送信したい場合はどうなりますか。 ServerBによって提供されているClient2へのWebSocketを使用したメッセージ?あなたはすでに答えを知っています! Server1はClient2のサブスクリプションについてさえ知らないため、メッセージはClient2に配信されません。したがって、この問題に対処するには、WebSocketサブスクリプションを外部化する必要があります。 STOMPをサブプロトコルとして使用しているため、外部STOMPブローカーとして機能できる外部コンポーネントが必要です。これを実行できるツールはかなりたくさんありますが、RabbitMQをお勧めします。ここで、サブスクリプションをメモリ内に保持しないように、Spring構成を変更する必要があります。代わりに、サブスクリプションを外部のSTOMPブローカーに委任します。これは、enableStompBrokerRelayなどのいくつかの基本的な構成で簡単に実現できます。注意すべき重要なことは、HTTPセッションはWebSocketセッションとは異なるということです。 Spring Sessionを使用してHTTPセッションをRedisに格納することは、WebSocketを水平方向にスケーリングすることとはまったく関係ありません。

RabbitMQを完全な外部STOMPブローカーとして使用するSpringBoot(およびその他多数)を使用して完全なWebチャットアプリケーションをコーディングしました。これは GitHubで公開 なので、クローンを作成し、マシンでアプリを実行してください。コードの詳細を参照してください。

WebSocketの接続が失われると、Springができることはあまりありません。実際には、再接続は、再接続コールバック関数を実装するクライアント側によって要求される必要があります(これはWebSocketハンドシェイクフローであり、clientはサーバーではなくハンドシェイクを開始する必要があります)。これを透過的に処理できるクライアント側ライブラリがいくつかあります。それはSockJSの場合ではありません。チャットアプリケーションでは、この再接続機能も実装しました。

18
Jorge Acetozi

複数のノード間でセッション情報を維持します。

ロードバランサーでバックアップされた2台のサーバーホストがあるとします。

Websocketは、ブラウザから特定のサーバーHost.egHost1へのソケット接続です。

ここで、Host1がダウンすると、ロードバランサーからのソケット接続-ホスト1が切断されます。ロードバランサーからホスト2への同じWebSocket接続をどのようにスプリングが再開しますか?ブラウザは新しいWebSocket接続を開くべきではありません

1
LoVIn