在分层架构中,Entry 向客户端提供了 TCP 长连接的接入能力,并对这些长连接的活性进行保活维护(详见 分层架构 IM 系统之架构解读),所以在 Entry 服务内部有两个最核心的数据结构:
-
Map<uid, fd>,其 key 是描述客户端的 uid,其 value 是该客户端与服务端建立的长连接对应的描述符 fd;
-
Map<fd, uid>,其 key 是客户端与服务端之间的长连接对应的描述符 fd,其 value 是客户端的 uid。
这两个 map 是双向的映射关系。当 Entry 需要主动向用户推送消息时,可以通过 Map<uid, fd> 映射结构,获取到 fd,然后基于 fd 推送消息; 当 Entry 收到客户端发送的消息时,可以通过 Map<fd, uid> 映射结构,获取到 uid,知道是哪一个用户发送的消息; 见下图。
Entry 所能承受的 TCP 长连接数是有上限的,当在线客户端数量超过这个上限值后,就需要对入口层 Entry 进行横向扩容,即通过 Entry 集群方式来 cover 越来越多的客户端,不同的 Entry 节点管理着不同的客户端长连接,Entry 服务是一个有状态的服务。这种情况下,当需要主动向用户推送消息时,应该如何处理呢? 使各个 Entry 节点互相通信绝不是一个好的实现方案,见下图。
不管每个 Entry 节点是保存了所有在线客户端的连接状态,还是只保存部分客户端的连接状态,这都不是关键问题,核心问题是每增加一个 Entry 节点,网络通信负载会指数级增加,最终成为瓶颈;如图所示,3个 Entry 节点需要建立 6 条连接,4个 Entry 节点时就需要建立 12 条连接。
比较优雅的解决方案是将客户端的连接状态数据单独集中化存储,见下图。由路由层 Router 集中存储所有在线客户端连接数据;Router 本质上是一个内存数据库,核心是一个很大 Map<uid, EntryIp>,其 key 是在线客户端的 uid,其 value 是该客户端连接的 Entry 节点;当客户端与 Entry 建立连接并登录后,在 Router 中进行注册;当要推送消息给用户时,先在 Router 中获取客户端连接到了哪一个 Entry 节点,然后将消息推送给该 Entry 节点。
将所有在线客户端的连接数据进行集中化存储,每一个 Entry 节点是独立的,Entry 会趋向于无状态化,这为横向扩展 Entry 集群提供了便利。
Entry 内部又是如何设计的呢?Entry 有哪些关键的内部构成组件呢?见下图。
Entry 对外与客户端直接连接,对内与业务逻辑层 Logic 直接交互。
当多个客户端同时并发访问时,通过 “IO多路复用” 机制来管理客户端连接是一个常用的解决方案,在 Linux 系统中一般基于 epoll 模型实现,在 Windows 系统中一般基于 IOCP 模型实现,在 Mac 系统中一般基于 kqueue 模型实现;所谓 “IO多路复用”,就是在一个线程中可以同时等待多个文件描述符(即连接)就绪,哪个就绪(有连接到来、有数据可读、有数据可写)了,就对哪条连接进行操作,一句话描述:在单个线程中实现对多条连接的管理。
客户端发送的请求,全部写入到 “请求队列”,然后由下游的工作线程从 “请求队列” 中获取请求进行处理。在系统设计上, “请求队列” 起到了 IO线程与工作线程交互、对高并发请求进行流量削峰、IO逻辑与业务逻辑解耦等三个关键作用。
工作线程池的业务逻辑处理非常简单,拿到客户端请求后判断,若是心跳请求,则调用 “心跳管理器” 模块来处理心跳(详见 分层架构 IM 系统之 Entry 心跳算法),若是其他请求,则全部通过 RPC 方式交由业务逻辑层 Logic 进行处理;所以 Entry 需要集成 rpc 客户端的 sdk。Logic 业务逻辑处理的结果返回到 Entry后,写入到 “发送队列”,最后仍然通过 “IO多路复用” 机制返回到客户端。
当 Logic 需要主动推送消息到客户端时,Logic 通过 RPC 调用的方式将消息发送给 Entry,此时 Entry 的角色是 RPC 服务端,所以 Entry 同时需要集成 rpc 服务端的 sdk。向客户端推送的消息到达 Entry 后,仍然写入到 “发送队列” ,复用上述流程。
另外,图中的 “在线用户管理器”,其核心就是文章开头部分所描述的两个映射关系,即 Map<uid, fd> 和 Map<fd, uid>。
综述一下 Entry 的核心组成部分:一是实现IO多路复用机制的IO线程;二是请求队列和发送队列;三是实现与 Logic 交互的工作线程,其集成了 rpc客户端和服务端sdk; 四是心跳管理器和在线用户管理器两个逻辑模块。
最后,总结文中关键:
1、Entry 的核心数据结构是两个映射关系,即 Map<uid, fd> 和 Map<fd, uid>;
2、Entry 集群部署时对客户端进行优雅管理的解决方案是集中存储客户端连接状态数据;
3、Entry 内部核心组成包括四部分:IO线程、请求队列和发送队列、工作线程、心跳管理器和在线用户管理器。