在Spring服务中如何扩展 WebSockets? -Aleksandr


假设我们有一个简单的聊天应用程序,其中前端通过休息和用于聊天的 WebSockets 与后端通信。我们意识到应用程序的一个实例开始无法应对负载。
扩展使用 WebSockets 的微服务并非易事。通过在默认循环负载均衡器下简单启动另一个实例,我们可能会遇到一个用户连接到实例 A 而第二个用户连接到实例 B 的情况。现在,我们的后端必须以某种方式了解将传入消息发送到何处。
可能想到的第一个选项是编写一个智能负载均衡器,将用户从同一个聊天重定向到同一个实例。
这里可能会出现几个问题:

  1. 如果用户同时与多人交流,那么对于每次聊天,您都需要打开一个新的 WebSocket 连接。
  2. 如果聊天的人太多,那么一个后端实例可能应付不来。具体来说,对于聊天而言,这不太可能,但聊天示例过于简单化了。在现实生活中,这个问题并不少见。

所以让我们走一条不同的路。幸运的是,除了 WebSockets 的内存中代理之外,Spring还有一个 BrokerRelay 将队列的处理委托给第三方代理。
现在是选择经纪人的时候了。有很多选择,我们不会考虑所有。最受欢迎的:
  1. Apache Kafka不太适合,因为它不是为大量动态生成的队列设计的。
  2. Redis PUB/SUB 可以最好地处理这种负载,但是对于 Spring,您无法开箱即用。
  3. 其他选项是 RabbitMQ 和 ActiveMQ。

@Configuration
public class WebSocketConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue")
            .setClientLogin(
"guest")
            .setClientPasscode(
"guest")
            .setSystemLogin(
"guest")
            .setSystemPasscode(
"guest")
            .setRelayHost(
"127.0.0.1")
            .setRelayPort(61613);
    }
}


如果我们选择 RabbitMQ,路由命名可能是一个问题。如果我们使用标准斜杠路径,那么我们可以看到消息:

2021-07-16 10:54:44.765 [error] <0.983.0> STOMP error frame sent:
Message: "Invalid destination"
Detail:
"'/clusters/list-userc3f44bd2-bbff-c237-04de-4cd9375fe344' is not a valid topic destination\

关键是 RabbitMQ 不允许在标准路由后使用斜杠。因此,如果我们向路由/topic/or发送消息/queue/,则名称中不应有其他斜杠。这里的简单方法是通过用点替换斜线来重命名前端和后端的所有目的地。
如果前端(或通过 WebSockets 连接的另一个应用程序)具有不同的发布周期,情况就会变得更加复杂。或者您需要保持与其他版本的兼容性。在这种情况下,您可以编写一个拦截器来替换消息中的目标。
但是您应该记住 Spring 在将订阅发送到 MessageChannel 之前保存订阅,因此您必须在发送到代理本身并从中接收的阶段替换目的地。您可以在Spring 文档中查看整个通信模式。
这里的另一种解决方案是使用不同的代理。比如ActiveMQ就没有这个问题。
 
如果您的应用程序在控制器中为委托给外部代理 ( , ) 的路由使用@SubscribeMapping注释,这也将是一个问题。/topic//queue/
Spring收到具有此类目的地的消息时,它会保存订阅,然后将消息发送到代理,绕过控制器。因此,用@SubscribeMapping注释的方法将永远不会被调用。
这里有两种解决方案。第一个解决方案不需要更改业务逻辑,而第二个解决方案:
  1. 您可以添加一个 BeanPostProcessor,它将使用此注释扫描所有方法并在SessionSubscribeEvent事件上调用它们。这里的主要问题是SessionSubscribeEvent在订阅消息发送到代理之前引发。因此,当我们调用向主题发送内容的方法,但用户自己尚未订阅该主题时,这种情况是可能的。这个问题的解决方法比较棘手,需要通过Interceptor等待dispatch事件。
  2. 您可以替换@SubscribeMapping与控制器@GetMapping REST注释。因此,初始状态不是通过 WebSockets 获得,而是通过REST 获得。这样的解决方案还需要在前端进行更改

 
结论

如果您在构建应用程序时没有考虑到使用 WebSockets 的微服务扩展可能会更加复杂。主要的解决方案归结为使用外部消息代理。首次连接消息代理时,应用程序可能无法正常工作。但是只有两个主要问题(路由命名,@SubscribeMapping),并且都有一个解决方案。