WebSocketクラスターソリューション

1.問題の原因

最近プロジェクトに取り組んでいたときに、WebSocketハンドシェイク要求やクラスター内でのWebSocketセッション共有の問題など、複数のユーザー間の通信が必要な問題に遭遇しました。

この期間中、数日間の調査の後、ズールからスプリングクラウドゲートウェイまで、分散型WebSocketクラスターを実装するいくつかの方法を要約し、この記事を要約しました。この分野での考えと研究。

以下は私のシーンの説明です

  • リソース:4台のサーバー。1台のサーバーのみがssl認証済みドメイン名、1台のredis + mysqlサーバー、および2台のアプリケーションサーバー(クラスター)を持っています

  • アプリケーションの公開の制限:シナリオのニーズにより、アプリケーションサイトは公開するためにSSL認定のドメイン名が必要です。したがって、sslで認証されたドメインネームサーバーがAPIゲートウェイとして使用され、https要求とwss(安全に認証されたws)の間の接続を担当します。一般にhttpsアンインストールとして知られているユーザーはhttpsドメインネームサーバー(例:https://oiscircle.com/xxx)を要求しますが、実際のアクセスはhttp+ipアドレスの形式です。ゲートウェイ構成が高い限り、複数のアプリケーションを処理できます

  • 要件:ユーザーがアプリケーションにログインするときは、サーバーとのwss接続を確立する必要があります。さまざまな役割で単一メッセージまたはグループメッセージを送信できます。

  • クラスタ内のアプリケーションサービスタイプ:各クラスタインスタンスは、httpステートレスリクエストサービスとwslong接続サービスを担当します

2.システムアーキテクチャ図

写真

私の実装では、各アプリケーションサーバーがhttpおよびwsリクエストを担当しますが、実際には、wsリクエストによって確立されたチャットモデルをモジュールとして確立することもできます。分散の観点からは、これら2つの実装タイプは似ていますが、実装の利便性の観点から、アプリケーションがhttp+ws要求を処理する方が便利です。以下に説明します

この記事に含まれるテクノロジースタック

  • Eurekaサービスの検出と登録

  • Redisセッション共有

  • Redisメッセージサブスクリプション

  • スプリングブーツ

  • ズールゲートウェイ

  • SpringCloudGatewayゲートウェイ

  • SpringWebSocketは長い接続を処理します

  • リボン負荷分散

  • NettyマルチプロトコルNIOネットワーク通信フレームワーク

  • コンシステントハッシュコンシステントハッシュアルゴリズム

この点に到達できるすべての人が、上記のテクノロジースタックを理解していると思います。そうでない場合は、インターネットにアクセスして、入門チュートリアルを見つけて、それについて学ぶことができます。以下の内容は上記の技術に関連しており、主題はデフォルトですべての人の理解に基づいています...

3.技術的実現可能性分析

以下では、セッション機能について説明し、これらの機能に基づいて分散アーキテクチャでwsリクエストを処理するためのn個のクラスターソリューションをリストします。

WebSocketSession与語HttpSession

Springによって統合されたWebSocketでは、各ws接続に対応するセッションWebSocketSessionがあります。SpringWebSocketでは、ws接続を確立した後、同様の方法でクライアントと通信できます。

protected void handleTextMessage(WebSocketSession session, TextMessage message) {
   System.out.println("服务器接收到的消息: "+ message );
   //send message to client
   session.sendMessage(new TextMessage("message"));
}

 次に、問題が発生します。wsのセッションをredisにシリアル化できないため、クラスターでは、セッション共有のためにすべてのWebSocketSessionsをredisにキャッシュできません。各サーバーには独自のセッションがあります。反対はHttpSessionで、redisはhttpsession共有をサポートできますが、WebSocketセッション共有のソリューションがないため、redisWebsocketセッション共有のパスを取ることは現実的ではありません。

sessinのキー情報をredisにキャッシュできますか?クラスター内のサーバーがredisからセッションのキー情報を取得し、WebSocketセッションを再構築します...誰かがこれを試すことができれば方法、教えてください...

上記は、WebSocketセッションとhttpセッション共有の違いです。一般に、httpセッション共有のソリューションはすでに存在し、非常に簡単です。関連する依存関係が導入されている限り、spring-session-data-redisおよびspring-boot-starter-redis、からデモを見つけることができます。遊ぶためのインターネットとそれを行う方法を知っています。WebSocketセッション共有スキームでは、WebSocketの最下層の実装方法が原因で、実際のWebSocketセッション共有を実現できません。

4.ソリューションの進化

4.1、Netty与春WebSocket

最初は、nettyを使ってWebSocketサーバーを構築しようとしました。nettyには、WebSocketセッションの概念はありません。チャネルと同様に、各クライアント接続はチャネルを表します。フロントエンドのwsリクエストは、nettyによって監視されているポートを通過し、WebSocketプロトコルを介してwsハンドシェイク接続が実行された後、一連のハンドラー(責任連鎖モード)を介してメッセージ処理が実行されます。WebSocketセッションと同様に、接続が確立された後、サーバーにはチャネルがあり、チャネルを介してクライアントと通信できます。

   /**
    * TODO 根据服务器传进来的id,分配到不同的group
    */
   private static final ChannelGroup GROUP = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
 
   @Override
   protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
       //retain增加引用计数,防止接下来的调用引用失效
       System.out.println("服务器接收到来自 " + ctx.channel().id() + " 的消息: " + msg.text());
       //将消息发送给group里面的所有channel,也就是发送消息给客户端
       GROUP.writeAndFlush(msg.retain());
   }

それで、サーバーはnettyまたはspring websocketを使用しますか?以下に、これら2つの実装の長所と短所をいくつかの側面からリストします。

4.2.nettyを使用してWebSocketを実装する

nettyをプレイしたことのある人なら誰でも、nettyのスレッドモデルがnioモデルであり、同時実行量が非常に多いことを知っています。春5以前のネットワークスレッドモデルはサーブレットによって実装され、サーブレットはnioモデルではありませんでした。したがって、春以降5、Springの基盤となるネットワーク実装はnettyを採用しています。nettyだけを使用してWebSocketサーバーを開発する場合、速度は絶対的ですが、次の問題が発生する可能性があります。

  1. システムの他のアプリケーションと統合するのは不便です。rpcを呼び出すと、springcloudで偽のサービス呼び出しの便利さを楽しむことができません。

  2. ビジネスロジックは繰り返し実装する必要があるかもしれません

  3. nettyを使用するには、車輪の再発明が必要になる場合があります

  4. サービスレジストリへの接続方法も面倒です

  5. Restfulサービスとwsサービスは別々に実装する必要があります。nettyでRestfulサービスを実装すると、それがいかに面倒か想像できます。多くの人が春のワンストップのRESTful開発を使用することに慣れていると思います。

4.3.スプリングWebSocketを使用してwsサービスを実装する

Spring WebSocketはspringbootによって十分に統合されているため、springbootでwsサービスを開発するのは非常に便利であり、実践は非常に簡単です。

4.3.1、最初のステップ:依存関係を追加する

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

4.3.2。ステップ2:構成クラスを追加する

@Configuration
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(myHandler(), "/")
        .setAllowedOrigins("*");
}
 
@Bean
 public WebSocketHandler myHandler() {
     return new MessageHandler();
 }
}

4.3.3。ステップ3:メッセージリスナークラスを実装する

@Component
@SuppressWarnings("unchecked")
public class MessageHandler extends TextWebSocketHandler {
   private List<WebSocketSession> clients = new ArrayList<>();
 
   @Override
   public void afterConnectionEstablished(WebSocketSession session) {
       clients.add(session);
       System.out.println("uri :" + session.getUri());
       System.out.println("连接建立: " + session.getId());
       System.out.println("current seesion: " + clients.size());
   }
 
   @Override
   public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
       clients.remove(session);
       System.out.println("断开连接: " + session.getId());
   }
 
   @Override
   protected void handleTextMessage(WebSocketSession session, TextMessage message) {
       String payload = message.getPayload();
       Map<String, String> map = JSONObject.parseObject(payload, HashMap.class);
       System.out.println("接受到的数据" + map);
       clients.forEach(s -> {
           try {
               System.out.println("发送消息给: " + session.getId());
               s.sendMessage(new TextMessage("服务器返回收到的信息," + payload));
           } catch (Exception e) {
               e.printStackTrace();
           }
       });
   }
}

このデモから、SpringWebSocketを使用してwsサービスを実装することの便利さを想像できます。春のクラウドの大家族とよりよく連携するために、私はついに春のWebSocketを使用してwsサービスを実装しました。

したがって、私のアプリケーションサービスアーキテクチャは次のようになります。アプリケーションは、RESTfulサービスとwsサービスの両方を担当します。分割ではサービス呼び出しを行うためにfeignを使用する必要があるため、wsサービスモジュールは分割されません。最初のものは怠惰であり、2番目の分割と分割なしはサービス間のio呼び出しのもう1つの層と異なるので、私はそれをしませんでした。

5.ズールテクノロジーからスプリングクラウドゲートウェイへの変革

WebSocketクラスタリングを実装するには、必然的にzuulからSpringクラウドゲートウェイに移行する必要があります。その理由は次のとおりです。

Zuul1.0バージョンはWebSocket転送をサポートしていません。zuul2.0はWebSocketのサポートを開始し、zuul2.0は数か月前にオープンソースでしたが、バージョン2.0はSpring Bootによって統合されておらず、ドキュメントは完全ではありません。したがって、変換が必要であり、変換も簡単に実行できます。

ゲートウェイでは、ssl認証と動的ルーティングの負荷分散を実現するために、ymlファイルで次の構成のいくつかが必要です。ここでは、事前に落とし穴を回避できます。

server:
  port: 443
  ssl:
    enabled: true
    key-store: classpath:xxx.jks
    key-store-password: xxxx
    key-store-type: JKS
    key-alias: alias
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      httpclient:
        ssl:
          handshake-timeout-millis: 10000
          close-notify-flush-timeout-millis: 3000
          close-notify-read-timeout-millis: 0
          useInsecureTrustManager: true
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
      - id: dc
        uri: lb://dc
        predicates:
        - Path=/dc/**
      - id: wecheck
        uri: lb://wecheck
        predicates:
        - Path=/wecheck/**

httpsオフロードを楽しくやりたい場合は、フィルターも構成する必要があります。そうしないと、ゲートウェイを要求するときにSSL/TLSレコードではなくエラーが発生します。

@Component
public class HttpsToHttpFilter implements GlobalFilter, Ordered {
  private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10099;
  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      URI originalUri = exchange.getRequest().getURI();
      ServerHttpRequest request = exchange.getRequest();
      ServerHttpRequest.Builder mutate = request.mutate();
      String forwardedUri = request.getURI().toString();
      if (forwardedUri != null && forwardedUri.startsWith("https")) {
          try {
              URI mutatedUri = new URI("http",
                      originalUri.getUserInfo(),
                      originalUri.getHost(),
                      originalUri.getPort(),
                      originalUri.getPath(),
                      originalUri.getQuery(),
                      originalUri.getFragment());
              mutate.uri(mutatedUri);
          } catch (Exception e) {
              throw new IllegalStateException(e.getMessage(), e);
          }
      }
      ServerHttpRequest build = mutate.build();
      ServerWebExchange webExchange = exchange.mutate().request(build).build();
      return chain.filter(webExchange);
  }
 
  @Override
  public int getOrder() {
      return HTTPS_TO_HTTP_FILTER_ORDER;
  }
}

このようにして、ゲートウェイを使用してhttpsリクエストをオフロードできます。これまでのところ、基本的なフレームワークが構築されています。ゲートウェイは、httpsリクエストとwssリクエストの両方を転送できます。次のステップは、ユーザー間の多対多セッションの相互通信のための通信ソリューションです。次に、ソリューションの優雅さに基づいた最もエレガントでないソリューションから始めます。

6.セッションブロードキャスト

これは、最も単純なWebSocketクラスター通信ソリューションです。シナリオは次のとおりです。

先生Aは生徒にグループメッセージを送信したいと考えています

  • 先生のメッセージリクエストがゲートウェイに送信され、コンテンツには{私は先生Aです。生徒にxxxメッセージを送信したい}が含まれています。

  • ゲートウェイはメッセージを受信し、クラスターのすべてのIPアドレスを取得して、教師の要求を1つずつ呼び出します。

  • クラスタ内の各サーバーはリクエストを取得し、教師Aの情報に従ってローカルで生徒に関連付けられたセッションがあるかどうかを確認し、sendMessageメソッドを呼び出し、そうでない場合はリクエストを無視します。

写真

セッションブロードキャストの実装は非常に簡単ですが、計算能力を浪費するという致命的な欠陥があります。サーバーにメッセージレシーバーセッションがない場合、ループトラバーサルの計算能力を浪費することに相当します。このスキームでは、同時需要が高くない場合は優先されます。実装が簡単であることを考慮してください。

スプリングクラウドのサービスクラスタ内の各サーバーの情報を取得する方法は次のとおりです。

@Resource
private EurekaClient eurekaClient;
 
Application app = eurekaClient.getApplication("service-name");
//instanceInfo包括了一台服务器ip,port等消息
InstanceInfo instanceInfo = app.getInstances().get(0);
System.out.println("ip address: " + instanceInfo.getIPAddr());

サーバーは、関係マッピングテーブルを維持して、ユーザーのIDをセッションにマップする必要があります。セッションが確立されると、マッピング関係がマッピングテーブルに追加されます。セッションが切断された後、マッピングテーブルの関係を削除する必要があります。 。

7.コンシステントハッシュアルゴリズムの実装(この記事の要点)

私の意見では、この方法が最も洗練された実装であり、この解決策を理解するにはある程度の時間がかかります。辛抱強く見れば、間違いなく何かが得られると思います。繰り返しになりますが、コンシステントハッシュアルゴリズムを理解していない学生は、最初にここを読んで、ハッシュリングが時計回りに検索されると想定してください。

まず、コンシステントハッシュのアイデアをWebSocketクラスターに適用するには、次の新しい問題を解決する必要があります。

  • クラスタノードがDOWNの場合、ステータスがDOWNのノードへのハッシュリングのマッピングに影響します。

  • クラスタノードがUPの場合、対応するノードにマップできない古いキーに影響します。

  • ハッシュリングの読み取り/書き込み共有。

クラスタでは、サービスのUP/DOWNに常に問題があります。

ノードDOWNの問題分析は次のとおりです。

サーバーがダウンすると、サーバーが所有するWebSocketセッションが自動的に接続を閉じ、フロントエンドが通知を受け取ります。これは、ハッシュリングのマッピングエラーに影響します。サーバーのDOWNをリッスンするときに、ハッシュリングに対応する実際のノードと仮想ノードを削除するだけで、ステータスがDOWNのサーバーにゲートウェイが転送されないようにすることができます。

実装方法:ユーレカガバナンスセンターでクラスターサービスのDOWNイベントをリッスンし、ハッシュリングを時間内に更新します。

ノードUPの問題分析は次のとおりです。

ここで、クラスター内にオンラインのサービスCacheBがあり、サーバーのIPアドレスがkey1とcacheAの間にマップされているとします。次に、key1に対応するユーザーは、メッセージを送信するたびにCacheBにアクセスしてメッセージを送信します。その結果、CacheBにはkey1に対応するセッションがないため、メッセージを送信できないことは明らかです。

写真

この時点で、2つの解決策があります。 

スキームAは単純ですが、アクションは大きいです。

eurekaはノードUPイベントをリッスンした後、既存のクラスター情報に従ってハッシュリングを更新します。そして、すべてのセッション接続を切断し、クライアントに再接続させます。この時点で、クライアントは更新されたハッシュリングノードに接続し、メッセージを配信できない状況を回避します。

オプションBは複雑で、アクションは小さいです。

まず、サーバーCacheBがCacheCとCacheAの間でオンラインであると仮定して、仮想ノードがない状況を見てみましょう。CacheCからCacheBにマップされたすべてのユーザーは、メッセージを送信するときにメッセージを送信するセッションを見つけるためにCacheBに移動します。つまり、CacheBがオンラインになると、CacheCとCacheBの間でユーザーが送信するメッセージに影響します。したがって、CacheCからCacheBへのユーザーに対応するセッションからCacheAを切断し、クライアントに再接続させるだけで済みます。

写真

次は、明るい色のノードが仮想ノードであると仮定して、仮想ノードがある場合です。長い括弧を使用して、特定の領域マッピングの結果が特定のキャッシュに属することを示します。1つ目は、Cノードがオンラインでない場合です。誰もがグラフを理解する必要があります。Bのすべての仮想ノードは実際のBノードを指すため、すべてのBノードの反時計回りの部分がBにマップされます(ハッシュリングが時計回りに検索するように規定されているため)。

写真

 次はCノードがオンラインになる状況です。一部のエリアがCで占められていることがわかります。

写真

上記の状況から、ノードがオンラインになると、対応する仮想ノードが同時に多数存在することがわかります。そのため、マルチセグメント範囲のキーに対応するセッションを切断する必要があります(上の図)。具体的なアルゴリズムは少し複雑で、実装方法も人によって異なりますので、自分で実装してみてください。

ハッシュリングはどこに配置する必要がありますか?

  • ゲートウェイは、ハッシュリングをローカルで作成および維持します。wsリクエストが着信すると、ハッシュリングがローカルで取得され、マッピングサーバー情報が取得され、wsリクエストが転送されます。この方法は見た目は良いですが、実際には望ましくありません。サーバーがDOWNの場合、eurekaを介してのみ監視できることを思い出してください。eurekaがDOWNイベントをリッスンした後、ioを介して対応するノードを削除するようにゲートウェイに通知する必要がありますか。 ?明らかに面倒なので、eurekaの責任をゲートウェイに分散させることはお勧めしません。

  • eurekaが作成され、redis共有の読み取りと書き込みに配置されます。このソリューションは実行可能です。eurekaがサービスDOWNをリッスンすると、ハッシュリングを変更し、redisにプッシュします。リクエストの応答時間をできるだけ短くするために、ゲートウェイがwsリクエストを転送するたびにハッシュリングを取得するためにゲートウェイをredisに移行させることはできません。ハッシュリング変更の可能性は確かに非常に低いです。ゲートウェイは、この問題を解決するために、redisのメッセージサブスクリプションモードを適用し、ハッシュリング変更イベントをサブスクライブするだけで済みます。

 これまでのところ、Spring WebSocketクラスターはほぼ構築されており、最も重要なことはコンシステントハッシュアルゴリズムです。最後の技術的なボトルネックがあります。wsリクエストに従って、ゲートウェイは指定されたクラスターサーバーにどのように転送しますか?

答えは負荷分散にあります。Spring Cloud Gatewayまたはzuulはどちらも、デフォルトでリボンを負荷分散として統合します。wsリクエストが確立されたときにクライアントから送信されたユーザーIDに従ってリボンの負荷分散アルゴリズムを書き直し、ユーザーIDに従ってハッシュし、検索するだけで済みます。ハッシュring.ipを実行し、wsリクエストをこのIPに転送すれば完了です。このプロセスを次の図に示します。

写真

ユーザーが次に通信するときは、IDに従ってハッシュし、ハッシュリングで対応するIPを取得するだけで、ユーザーとのws接続が確立されたときにセッションが存在するサーバーを知ることができます。

8.春の雲Finchley.RELEASEバージョンのリボンの欠陥 

実際の操作中に、被験者はリボンに2つの欠陥を発見しました...

  • インターネットで見つかった方法によると、AbstractLoadBalancerRuleを継承し、負荷分散戦略を書き直した後、複数の異なるアプリケーションの要求が混乱します。eurekaに2つのサービスAとBがある場合、負荷分散戦略を書き直した後、AまたはBのサービスの要求は、最終的には1つのサービスにのみマップされます。非常に奇妙な!おそらく、春のクラウドゲートウェイの公式ウェブサイトは、負荷分散戦略を正しく書き直すデモを提供する必要があります。

  • コンシステントハッシュアルゴリズムには、ユーザーIDと同様のキーが必要です。キーに従ってハッシュした後、ハッシュリングを検索し、IPを返します。しかし、リボンは選択関数のキーパラメータを改善せず、デフォルトを直接記述しました。

写真

それについて私たちにできることは何もありませんか?実際、実行可能で一時的な代替方法があります!

次の図に示すように、クライアントは通常のhttpリクエスト(idパラメータを含む)をゲートウェイに送信し、ゲートウェイはIDに従ってハッシュし、ハッシュリングでIPアドレスを見つけ、クライアントにIPアドレスを返します。次に、クライアントはIPアドレスを使用してws要求を行います。

写真

リボンのキー処理が不完全なため、当面の間、リボンにコンシステントハッシュアルゴリズムを実装することはできません。コンシステントハッシュは、クライアントが2つの要求(1つはhttp、1つはws)を行うことによってのみ間接的に実現できます。うまくいけば、リボンはこのバグをすぐに更新します!WebSocketクラスターをもう少しエレガントに実装しましょう。

9.追記 

 これらは過去数日間の私の研究の結果です。この期間中に多くの問題が発生し、問題は1つずつ解決され、2つのWebSocketクラスターソリューションがリストされました。1つはセッションブロードキャストで、もう1つはコンシステントハッシュ法です。

これらの2つのスキームには、さまざまなシナリオに対して独自の長所と短所があります。この記事では、ActiveMQ、Karfa、およびその他のメッセージキューを使用してメッセージプッシュを実現するのではなく、独自のアイデアを使用して、複数のユーザー間の長期的な接続通信を実現したいと考えています。メッセージキューに依存しています。私はあなたに別の考え方を提供したいと思っています。

おすすめ

転載: blog.csdn.net/qq_34272760/article/details/121259313