目录

架构解读

一、终端层

二、入口层

三、业务逻辑层

四、路由层

五、数据访问层

六、存储层

局部演进

总结文中关键


架构解读

在日活只有几千的时候,IM 系统采用【单体架构】方式进行实现,是完全没有问题的。在单体架构 IM 系统(如下图)这种方式下,如果用户的规模逐步增大,比如几万日活、十几万日活的时候,通常应该如何应对呢?

大家应该会很容易想到,不断横向扩展 server 节点即可,是的,这种方式会非常快捷和有效;但是,随着 server 节点数量的不断增多,大家会感觉到投入和产出的 ROI 越来越低,为什么会这样呢?

横向扩展 server 节点,其实是一种粒度非常粗的解决方案。server 程序在实现的时候,一般会包含用于前端连接的部分、实现业务逻辑的部分和访问数据库与缓存的部分,这三部分到底是哪一部分的资源紧张才需要横向扩展 server 节点呢?日活量增大,有可能是前端连接逻辑需要扩容,也有可能是业务逻辑需要扩容,但是在横向扩展 server 节点时,通常不会考虑这么细。这是单体架构系统一个非常关键的问题。

另外,单体架构系统的技术栈非常单一,只能垂直扩展;举个例子:假设单体架构的系统最初选型用 C++ 语言进行实现,前端连接的部分如果想改用 SpringMVC 进行实现,肯定不行的,访问数据库和缓存的部分如果想改用MyBatis 进行实现,也肯定不行;没办法,只能在 C++ 这个技术栈范围内死磕到底。

同时,单体架构的系统,随着业务迭代越多,其逻辑实现会愈加臃肿;任何一个小地方的改动,都会导致整个系统重新编译和重新部署。

所以,在大家遇到以上问题时,通常会采用【水平分层架构】进行实现。水平分层架构是在单体架构的基础上横向切几刀形成,是在技术的驱动之下,由单体架构演化而来;分层架构的 IM 系统见下图。

分层架构的 IM 系统,整体上包含:【终端层】、【入口层】、【业务逻辑层】、【路由层】、【数据访问层】和【存储层】,该分层架构的 IM 系统运行在日活百万级别的电商场景下;下面分层介绍。

一、终端层

终端层负责与用户进行交互,获取用户的操作行为,转换成访问后端具体接口的动作;终端层具体包括安卓和 IOS 移动手机上运行的客户端,即 APP。

二、入口层

入口层,也叫做入口网关层,即 Entry进程,Entry 的职责只有一条,即负责维护与终端之间的长连接,不负责任何的业务逻辑。为什么要这样设计呢?

我们必须保证 Entry 的实现足够简单,简单到 “一次实现,终生不迭代”,也就是 Entry 实现完进行部署之后,可以做到 100 年不重启;Entry 是负责维护与客户端之间的长连接的,Entry 如果重启,将会影响连接该 Entry 节点的所有的客户端。

对长连接的维护,通常采用 “心跳” 方式;也就是,对于客户端发送的所有请求中, Entry 只负责处理心跳请求,其他请求 Entry 会全部转发给业务逻辑层进行处理。

三、业务逻辑层

业务逻辑层,即 Logic 进程,其负责处理 IM 系统的所有的业务逻辑,比如:登录、点对点消息、群消息、联系人等;可想而知,Logic 进程是因为业务升级迭代,重启最频繁的。

四、路由层

路由层,即 Router 进程,通过名称可以很容易明白它的核心职责是实现用户的路由,怎么理解呢?我们已经知道Entry 负责和客户端建立长连接,为了提高 IM 系统的吞吐量和避免单点问题,Entry 肯定是集群化部署的;客户端与后端建立连接时,会根据策略将客户端分配给特定的 Entry 节点;Entry 节点之间如果通信,代价太大,所以哪一个客户端由哪一个 Entry 节点来管理,必须要有一个中央存储来负责维护,该中共存储即是 Router。

Router 是一个内存数据库,其本质是一个很大的 Map,该 Map 的 key 是用户的 uid,该 Map 的 value 是客户端所连接到的 Entry 的 ip;当 IM 系统要主动向用户推送消息时,可以先查询 Router,如果用户记录不存在,说明该用户不在线;如果用户在线,获取到用户连接到了哪一个 Entry 节点,然后由 Logic 将消息推送到该 Entry 节点。

描述到这里,Entry 的本质也是一个很大的 Map,该 Map 的 key 是用户的 uid,该 Map 的 value 是长连接的文件描述符 fd;通过查找该 Map,获取到用户对应的 socket 的 fd,然后基于 fd 将消息推送给用户。

通过思考路由层和入口层的本质,我们基本可以想清楚完整的消息推送流程的主要框架(后面的技术短文中会详细分析)。

五、数据访问层

数据访问层,即 Das 进程,该层的核心职责即负责访问数据库和缓存。理解该层的核心职责,不是最关键的,关键问题是数据访问层的架构意义何在呢?是不是说,在分层架构系统中,一定需要数据访问层,没有该层整个系统就不能干活了?答案肯定是否定的。

数据访问层是在 “业务逻辑层” 和 “存储层” 之间,大家在设计任何系统的时候都需要注意这种情况,两层之间若还有一层,那么该层的架构意义往往就是起到 “解耦” 的作用,也就是:数据访问层的存在是为了解耦业务逻辑层和存储层。具体怎么理解呢?

我们都知道,存储层是非常复杂的:为了提高抗读的压力,我们会加一个 “缓存”;如果单表的数据量超过限制,我们会 “分库分表”;为了进一步优化读库,我们会对数据库进行 “读写分离”;我们也有可能会将 MongoDB 替换成 MySQL,将 MySQL 替换成 TiDB 等等等等。存储层这一系列的复杂性,如果没有数据访问层的存在,势必会影响到业务逻辑层,而业务逻辑层是进行业务迭代的,如果我们正在进行 “分库分表” 的操作,还有精力迭代业务吗?但是如果有了数据访问层,它统统屏蔽了存储层的复杂性,业务逻辑层只管向数据访问层要数据即可,不用关注存储层的数据存储模式。这就是数据访问层的架构意义。

六、存储层

存储层负责对业务数据进行持久化存储,通常包括数据库和缓存。

局部演进

随着用户日活量的增多,业务规模也在逐步增大(即后端接口数量越来越大),而且业务逻辑也越来越复杂;为了引流,平台几乎每周都会做运营活动,此时 IM 系统的业务逻辑全部集中在 Logic 中实现,会愈加繁杂;此时系统表现出的最大的问题是:非核心的业务(如各类运营活动)影响核心的业务(如收发消息、联系人)。

怎样解决该问题呢?将非核心的业务和核心的业务进行拆分即可。上图 IM 系统我们称之为分层架构1.0,那么分层架构2.0 见下图。

在业务逻辑层中,包含 Logic 和 Extlogic 两个服务:由 Logic 负责处理核心的、实时性较高的、轻量级的业务,比如:用户登录、点对点消息、群消息、消息已读等; 由 Extlogic 负责处理处理非核心的、实时性较低的,重量级的业务,比如:广播系统消息、离线用户召回等;然后由 Logic 通过 RPC 方式远程调用 Extlogic;Extlogic 如果要推送消息到客户端,直接将其推送到 Entry 即可。

业务逻辑层拆分成 Logic 和 Extlogic 之后,各类运营活动的代码直接在 Extlogic 完成,核心的 Logic 逻辑不会受到运营活动代码的侵入;再一个,频繁重启的是 Extlogic 进程,核心的 Logic 进程大大减少了被重启的次数,保证了核心业务的稳定性。

随着业务的不断迭代,仔细分析 Extlogic 的业务,其业务接口的执行逻辑结果如何,并不会影响到 Logic 的执行逻辑,也就是说:Logic 在执行业务逻辑的过程中,并不关注 Extlogic 的执行结果,只是将业务事件通知到 Extlogic 即可。此时,Logic 和 Extlogic 通过 RPC 这样一种高耦合的方式进行通讯就不是太合适。

再一个,自研的内存存储 Router,随着在线用户量的增多,其维护的复杂度也增大,可以通过成熟的组件进行优化。分层架构 IM 系统 3.0 见下图。

在 3.0 版本的 IM 系统中,引入了 MQ 消息中间件:MQ 一方面解耦了 Logic 和 Extlogic,同时解耦了 “平台业务” 对整个 IM 系统的调用;在整个电商平台中,有非常多的业务(比如订单、支付、物流、客服等)需要借助于 IM 系统,实现定制化消息的推送。

另外,引入缓存(Redis),替换 Router,大大降低了对用户在线状态中央存储维护的复杂度。

总结文中关键

1、 单体架构系统,横向扩展 server 节点方案,粒度很粗,ROI 很低;

2、 分层架构 IM 系统,包括【终端层】、【入口层】、【业务逻辑层】、【路由层】、【数据访问层】和【存储层】;

3、 入口层 Entry 只负责维护与终端之间的长连接;

4、 路由层 Router 是一个内存数据库,是用户在线状态的中央存储,其本质是一个很大的 Map;Entry 的本质也是一个很大的 Map;

5、数据访问层 Das 存在的架构意义在于向业务逻辑层屏蔽存储层的复杂性,解耦业务逻辑层和存储层。

6、分层架构 IM 1.0,业务逻辑层通过 Logic 实现所有的业务逻辑;

7、分层架构 IM 2.0,业务逻辑层通过 Logic 实现核心的业务逻辑,通过 Extlogic 实现非核心的业务逻辑;

8、分层架构 IM 3.0,引入 MQ,一方面解耦 Logic 和 Extlogic,一方面解耦电商平台和IM系统;引入 Redis,替换 Router,降低对中央存储维护的复杂度。