首先要考虑建立长连接的目的是服务端进行信息推送还是固定客户端与服务端同步调用、又或者是IM即时通讯场景呢?采用什么协议?如何做负载均衡?
为了实现100w长连接,需要使用非阻塞I/O模型:采用如epoll、kqueue等高效的I/O多路复用技术,可以同时处理大量并发连接;异步处理:使用事件驱动或协程模型,避免线程阻塞,提高处理效率。这些在java中,netty都已经实现封装好了,也是目前使用最普遍的技术。
根据场景不同,可考虑使用WebSocket或其他二进制协议,它们比HTTP更高效,更适合长连接;或者考虑使用MQTT或AMQP这样的消息队列协议,它们天生支持长连接和消息推送。
以下设计以WebSocket为例。
建立连接分为两部分。一部分是客户端发送建立连接请求,一部分是服务端进行会话管理。
WebSocket 具有状态特性,这与 HTTP 的无状态特性不同,因此无法像 HTTP 一样通过集群方式实现负载均衡。在长连接建立后,它会与服务端的某个节点保持会话,所以在集群环境下,要确定会话属于哪个节点会有些困难。
解决以上问题一般有两种技术方案:
1)一种是使用类似于微服务注册中心的技术来维护全局的会话映射关系;这种方案,会话映射关系清晰,集群规模较大时更合适。但是实现复杂,强依赖注册中心,有额外运维成本
2)另一种是使用事件广播,由各节点自行判断是否持有会话。这两种方案的对比如下表所示。实现简单更加轻量,但节点较多时,所有节点均被广播,资源浪费。
实现广播的方法有多种,如基于 RocketMQ 的消息广播、基于 Redis 的 Publish/Subscribe、基于 ZooKeeper 的通知等。
广播的实现方案对比:
基于RocketMQ 吞吐量高、高可用、保证可靠 实时性不如Redis
基于Redis 实时性高、实现简单 不保证可靠
基于ZooKeeper 实现简单 写入性能较差,不适合频繁写入场景
整体流程如下:
1)客户端与网关的任何一个节点建立长连接,节点会将其加入到内存中的长连接队列。客户端会定期向服务端发送心跳消息,若超过设定时间还未收到心跳,则认为客户端与服务端的长连接已断开,服务端会关闭连接,清理内存中的会话。
2)当业务系统需要向客户端推送数据时,通过提供的HTTP接口将数据发送至服务。
3)在收到推送请求后,服务会将消息写入RocketMQ。
4)服务作为消费者,以广播模式消费消息,所有节点都能收到消息。
5)节点在收到消息后会判断推送的消息目标是否在其内存中维护的长连接队列里,如果存在则通过长连接推送数据,否则直接忽略。
服务通过多节点构成集群,每个节点负责一部分长连接,实现负载均衡。当面临大量连接时,也可以通过增加节点来分散压力,实现水平扩展。
同时,当节点出现故障时,客户端会尝试与其他节点重新建立长连接,确保服务的整体可用性。
会话管理
在 WebSocket 长连接建立后,会话信息会保存在各个节点的内存中。
SessionManager 组件负责管理会话,它内部使用哈希表来维护 UID 与 UserSession 的关联。
UserSession 表示用户层面的会话,一个用户可能同时拥有多个长连接,因此 UserSession 内部同样使用哈希表来维护 Channel 与 ChannelSession 的关联。
为了防止用户无休止地创建长连接,当 UserSession 内部的 ChannelSession 超过一定数量时,它会关闭最早建立的 ChannelSession,以减少服务器资源的占用。
如果有人恶意创建非法连接,怎么解决?
首先建立长连接需要符合一定条件才可以建立。比如服务端推送,需要先进行用户认证,确认用户的合法性。之后需要进行限流,比如按照IP,单个IP不能超过20个连接。还可以对请求内容大小做限制。如果请求内容过大,则关闭连接。