RFC7 春松客服的 IM 架构设计,在微服务下构建高可用的 WebSocket 服务
架构图
WebSocket在微服场景下的问题
在微服场景下,一个服务会部署多个实例,对于WebSocket服务而言,当用户的连接被负载均衡到多个WebSocket实例上时,需要解决如下问题:
- 1.如何让连接到实例S1上的用户U1能和连接到实例S2上的用户U2对话,可以泛化为如何让连接到任意实例上的用户U1如何与连接到其他任意实例上的用户U2对话;
- 2.当实例S1宕机时,如何恢复U1和U2的对话;
这里的对话,就是双方收发消息
如何解决
第二个问题的根源在于WebSocket连接是有状态的连接,它无法像Session一样能被持久化,进而在多个实例间共享。如果它能被共享,就可以实现WebSocket连接在所有实例之间共享,S1宕机时,可以在其他任意实例上恢复这个连接,从而恢复U1和U2的对话。
而WebSocket连接不能在多个实例间共享,S1宕机时,如何恢复U1和U2的对话呢?要恢复U1和U2的对话必须先建立用户U1的WebSocket连接。建立这个连接也很简单,即客户端检测到连接断开(很容易检测)后,由客户端(多数情况下通过服务端建立连接是不现实的)请求建立新的WebSocket连接即可。连接建立后,后续的过程就可以通过解决第一个问题来完成。
第一个问题的关键在于,连接到实例S1上的用户U1能够收到用户U2发来的消息,并能回复消息给U2,返过来也是一样。假设U1的连接为C1,U2的连接为C2,要让U1和U2对话,在U1发送消息时,只需要S1能感知道U2的连接C2在S2上,然后消息能从S1到达S2即可。U2发送消息时,刚好反过来。这显然可以通过建立U2的ID2与S2的映射来解决,可以进一步泛化为建立Un的IDn与Sn的映射。
S1感知道U2的连接C2在S2上之后,消息如何从S1到达S2呢?S1可以直接与S2连接连接,然后,将消息发送出去,但这种方案有很多问题,最大的问题是连接数会随着实例数量的增长呈指数级增大,因为任何两个实例之间都可能需要建立连接。
这里只要求S1上(U1发送)的消息能够到达S2,可以进一步泛化为任意实例上的消息能到达其他实例Sn即可。这可以使用消息队列来完成,S1将U1发送的消息,投递到消息队列,通过广播其他实例都能收到这个消息,从消息中提取接收消息的用户ID,如果当前实例持有对应的连接就消费这条消息,否则不做任何处理即可。更进一步,还可以结合MQ的特性,只让S2消费发送给U2的消息,这不是本文的重点,这里不述。
需不需要一致性哈希
一致性哈希算法解决的主要问题是:会由于节点数量变化,导致变化导致大量甚至全部的"key % 节点数量"的结果在节点数量变化前和变化后不匹配以前节点位置。典型的场景是Redis分片,节点数量变化可能导致大量缓存无法命中,而使用一致性哈希算法时失效的只是上一个节点到失效节点间的少量缓存数据。
除了上面第一和第二问题外,其实还有另外一个问题,就是如何保证多个WebSocket实例间的负载均衡,这主要体现在新增节点(假设为S3)时。新增节点并不影响现已建立的连接,U1的消息会通过C1发送给S1,经由S1到达S2,S2再通过C2发送给U2。之所以称其为问题,是因为如果没有新的连接到达,S3就会一直空闲着,而增加S3就是需要它来承载一部分连接。这个问题可以通过扩展微服务的负载均衡功能解决,从既有节点断开部分连接,当客户端请求建立连接时,又会经过负载均衡,将其分配到新的节点即可。
不需要引入一致性哈希,引入一致性哈希会使程序变得复杂。上述内容中引入了消息队列,需要额外的负载均衡处理。而使用一致性哈希算法,除可能需要自己实现一致性哈希算法外,新增节点时,也需要自己剔除部分连接;节点宕掉时,需要更新啥希环;以及哈希环应该位于整个架构中的位置等都需要考虑。而一致性哈希对解决第一个问题并没有什么帮助。