如何设计一款高性能、高并发、高可用的im综合消息平台是很多公司发展过程中会碰到且必须要解决的问题。比如一家公司内部的通讯系统、各个互联网平台的客服咨询系统,都是离不开一款好用且维护的方便im综合消息系统。
那么,我们应该怎么样来设计一款三高特性的im系统,并能同时支持各个业务线的接入(比如:内部OA通讯、客服咨询、消息推送等等功能)有呢?
im第一版设计的初衷是公司需要一款im消息中间件用于支撑客服咨询业务。
但是,考虑到为了方便日后其他业务线也能接入消息沟通平台,所以一开始就将整个消息中心的能力需求给到中间件团队进行开发,以便除客服外的各业务线接入综合消息中心,从而实现多元的消息实时触达能力。
1)存储端:
在初版的架构下,存储端我们使用tidb、redis作为主要存储:
[1] redis用于存储消息已读未读,缓存连接信息等功能;
[2] tidb作为开源的分布式数据库,选择它是为了方便消息的存储。
2)mq消息总线:
我们使用rocketmq来实现消息总线(PS:即分布式情况下,不同im实例间通过MQ进行消息交互)。
消息总线是整个im的核心,使用rocketmq能支持十万级别的tps。基本所有服务都要从消息总线中消费消息进行业务处理。
3)zookeeper注册中心:各个服务会注册到zk中,方便服务之间内部进行调用,同样也可以暴露服务给外部进行调用。
4)link服务:
link服务主要用于接收客户端的ws(WebSocket协议)、tcp、udp等协议的连接。
同时调用用户服务进行认证,并投递连接成功的消息给位置服务进行消费,存储连接信息。
ws(WebSocket协议)过来的消息先到link再投递到消息总线。
5)消息分发服务:
消息分发服务主要用于接收消息总线推过来的消息进行处理,按照im内部消息协议构造好消息体后,又推送到消息总线中(比如会推给会话服务、消息盒子、link服务)。
6)位置服务:
存储link的(WebSocket协议)连接、tcp连接等信息,并使用redis进行缓存(key为userId),方便根据UserId查询到该用户所登录的客户端连接在哪个link上。
一个用户在相同设备只能登录一个,但可以支持多端登录。
7)用户服务:用于存储所有用户,提供认证查询接口。
8)消息盒子:存储所有消息,提供消息查询、消息已读未读、消息未读数、消息检索等功能。
9)会话服务:管理会话、群聊会话、单聊会话等功能。
使用MQ消息总线的问题
正如上节所分享的那样,我们初版IM架构中,link服务到消息分发服务的消息使用的MQ消息总线。
初版架构设计中,link服务将消息下推给消息分发服务进行处理时,使用的是mq消息总线(通俗了说,IM集群内不同IM实例间的通信是依赖于MQ进行的消息传递),而mq消息总线必然做对有一定的时延(而且时延受制于MQ本身的系统实现和技术策略)。
举个例子:
当两个处于不同IM实例的客户端A和B聊天时,A用户发送消息到link --> 消息总线 --> 消息分发服务 --> 消息总线 --> link --> B用户。
正如上面这个例子,im消息投递流程太长了,并且这样也会大大降低系统的吞吐量。
那么为啥微信使用写扩散不是缺陷,而对于我们的IM架构来说确是缺陷呢?
微信的技术特性:
1)微信号称没有存储用户的聊天记录,全是实时推送;
2)微信聊天记录全部会在我们手机端存储一份,两台手机终端上的聊天记录并不互通,并且互不可见。即时通讯聊天软件app开发可以加蔚可云的v:weikeyun24咨询
我们的IM综合消息中心技术特性:
1)综合消息中心是会有拉取历史聊天记录(服务端拉取)的功能,存储了全量消息;
2)综合消息中心的客户端,需要支持网页版本。
综上所述:
1)写扩散对微信这样有移动端的富客户端版本的即时通讯产品十分友好,每个消息在消息分发的时候给处于这个会话(单聊,群聊)下的所有用户所在客户端先推送消息,没找到连接就针对这个用户写一个离线缓存消息,那么下次该用户登录进来,可以从缓存中拉取到该消息,并且清掉缓存;
2)写扩散对于我们这类通用综合消息平台并不友好,由于接入方大部分是网页版的客户端,所以没有缓存消息的能力,浏览器刷新就没有了任何消息,所以需要实时去服务端拉取历史消息。假设我是写扩散,在一个群聊中有五百个用户,针对这五百个用户在这个会话,我需要去写五百条消息,大大的增加了写io,并且还不能写缓存(得写数据库)。
tidb是目前主流的开源分布式数据库,查询效率高、无需分库分表。
但同样的,tidb存在一些隐藏的问题:
1)tidb在高并发情况下,并发事务会导致事务失败,具体原因不知;
2)tidb排错成本高,公司很少有tidb专业运维,经常遇到不走索引的情况。
在我们初版的IM架构设计中,单聊和群聊是冗余在会话服务中的,并且冗余在同一张表的。
其实单聊、群聊从数据角度来说,还是会有些不同(比如业务属性)虽然都是会话,我们还是需要将这两个服务拆分开,细粒度的服务拆分能更好的把控整体的逻辑。
在生产上暴露出了以下问题:
1)tps没达到预期,吞吐量不能满足公司业务的发展;
2)使用的存储中间件难以维护(主要是tidb),试错成本高,经常在生产暴露问题,并且速度越来越慢;
3)消息写扩散没有太大必要,并大大增加了系统io次数(原因见上一节);
4)一些特性无法支持,比如消息图文检索,消息已读未读。
改版后的各模块情况如下:
1)存储端:存储端我们改用了mysql,针对消息服务单独使用了主从mysql集群(主节点用于写消息、从节点用于消息检索)——;
2)mq消息总线:与第一版相比没有改动;
3)link服务:与第一版相比,改动了link服务到消息分发服务的消息推送方式(由MQ总线方式变更为tcp实时推送);
4)消息分发服务:集成了消息处理能力、路由能力,每台消息分发服务拥有所有link服务的tcp连接;
5)单聊服务:负责单聊会话的管理能力;
6)群聊服务:负责群聊会话的管理能力;
7)用户服务:提供用户认证,登录\注册能力。
针对初版MQ消息总结的问题,升级版架构中,我们将link到消息分发服务改为tcp实时连接,百万客户端连接同一台link机器,消息实时触达能力tps达到16万。
link到消息分发服务的改版是本次设计的亮点之一,完全消除了mq推送的时延性,并且路由简单,几乎实时触达。
举个例子:(当两个处于不同IM实例的客户端A和B聊天时)
1)初版架构中是:A用户发送消息到link --> 消息总线 --> 消息分发服务 --> 消息总线 --> link --> B用户;
2)升级版架构是:用户A --> link --> 消息分发 --> link --> 用户B。
而且:link服务到消息分发服务集群的消息推送使用轮询负载均衡的方式,保证公平,不会导致个别机器负载过高。