IM即时通讯系统架构设计

即时通信(Instant Messaging,简称IM)是一种通过网络进行实时通信的系统,允许两人或多人使用网络即时的传递文字消息、文件、语音与视频交流。
微信、QQ基本占领的IM即时通讯系统的半个江湖,易信、钉钉、飞信、旺旺、咚咚、陌陌也各有市场。查询了相关资料后设计了这篇高并发IM通讯系统,不当之处,请指正。(最终架构图在总结章节)

IM不就是聊天工具么? 两个用户(Client),一个Server转发聊天内容就可以完成聊天功能。最早的聊天室就是这样设计开发的。问题在于用户量不断上涨,一台服务器无法应对成千上万的用户请求。如何接入这么多用户,做到高并发?这就是今天要解决的问题,首先应该对系统进行分层,细化功能点确定每层需要完成的功能。
IM系统的三层架构(不包括client):

  • 接入层: 用户的连接接入(协议是tcp、udp、http、socketio等,是短链接、长链接。),接受客户端发送的消息,推送消息到客户端。
  • 逻辑层: 接收接入层接入的消息,对消息进行校验,去重,验证,过滤等业务操作,存储消息内容,本层服务是无状态的,容易扩展。
  • 存储层: 处理逻辑层注册的消息,主要维护消息发送者客户端状态功能,保存发送者接入路由信息(也就是从哪个机器接入,最新的msgid是多少等信息)。本层服务有状态的,设计时应该关注本层的扩展性。

我给这三层对应服务的命名:接入层服务叫tranfer(客户端连接管理,接受转发推送消息模块),逻辑层服务叫logic(业务逻辑模块),存储层服务router(用户在线状态,用户接入路由信息保存等)

消息轮转流程

我接着了解下IM系统的消息轮转流程。首先客户发送信息流程

  1. 客户端连接上tranfer,发送消息到tranfer服务,这个消息请求中包括用户UID、消息内容、发送消息时间、发送设备ID等。然后等待ack响应。
  2. tranfer接收到客户端发送的信息后,将消息转发到logic服务。
  3. logic为消息生成msgid(通常由uid,deviceid,发送时间hash再加上自增id得到,这样msgid就是有序的),logic通过msgid查询消息是否已经发送进行排重,重复响应tranfer消息重复发送。
  4. logic将消息注册到router,更新发送消息用户的在线状态,保存消息的来源(来自哪个tranfer,相当于路由表),把消息来源存储到cache/db。完成后,router给logic服务ack响应。
  5. logic收到ack后,把ack响应给tranfer服务。
  6. tranfer将此带msgid的响应ack返回给客户端,客户端收到ack,即可确定消息发送成功

客户端发送消息成功,接着就是另一边的客户端接收消息,也就是消息投递。这里需要说明的是客户A发送消息到客户B(群B),他们之间就建立了会话,我们叫它sessoin(有些叫channel,room【群】)。

  1. logic从router服务获取session上发送到接收者的用户列表。
  2. logic从route服务查找出用户列表中所有在线用户的tranfer地址。
  3. logic将消息发送到对应的tranfer地址。
  4. tranfer收到消息并将其发送到客户端。
  5. 最终客户端接收到消息内容。

客户端接收消息OK了。但是有个问题,如果用户在线却一直不发送信息,怎么判断他在线呢?所以客户端应该定时发送心跳包给tranfer,以告知transfer客户端的存在,这就是hearbeat。

  1. 客户端发送hearbeat到tranfer,hearbeat包括用户UID、发送消息时间、发送设备ID外,还用户所在的所有session和在session中的最新msgid。
  2. tranfer转发hearbeat到logic
  3. logic到router更新发送消息用户的在线状态,保存消息的来源(来自哪个tranfer,相当于路由表)。
  4. logic到router查询session的最新msgid,如果上报msgid不等于查询msgid,启动下发消息流程。
  5. logic下发未读消息到客户端。

hearbeat心跳时间间隔根据不同的网络环境,心跳发送成功次数,信息发送次数等因素进行动态调整。

了解了IM的消息轮转流程和分层结构,可以给出一个粗略设计图。
Alt text

图中为IM系统进行了分层,每层服务有多点(还没有负载均衡方案,当可以同时提供多点服务),对于数据存储可以使用cache/db两层模式,cache提供读取性能,db用户持久化。在推送下发消息使用消息队列,可以做到解耦、异步、高并发。
Alt text

高可用

图中三层中的服务节点扩展为多节点,不同的客户端连接不同的节点以达到接入能力的提升。接入能力提升,但每个节点还存在着单点问题。如何解决单点问题,达到高可用?

接入层的tranfer服务是客户端的接入口,使用长链接时,服务要保持链接不中断。tranfer是无状态,但要保持本次连接状态(本次连接断开可以连接其它tranfer)。

  1. 对于不分区域的接入情况,可以使用负载均衡器将用户请求分配到不同的tranfer上,如果有一台服务宕机,负载均衡器将该节点移除。这样会出现该节点所有连接会断开一次,客户端重连,再由负载均衡器分配到其他节点上。缺点是宕机会加大其他机器的负荷。(负载均衡的软实现有lvs+keepalived,nginx upstream,haproxy + pacemaker,硬负载有f5、array。根据业务情况选择实现)。
  2. 对于不分区域的接入情况,对于单点宕机时可不可以不增加其它节点负荷。答案是YES,使用transfer服务主备方案,主transfer服务宕机,备接替工作。由于这里使用tcp长连接,主节点的连接还是会中断一次,还好客户端有重连机制,重连连接会到备节点上。transfer的主备切换如何实现呢?有两种方式,第一种:使用vrrp协议 + 接口监视,第二种:使用下面会将到的monitor监控通知。 主备方案缺点很明显,有一半的备节点处于空闲状态,资源浪费严重,优点是主备方案实施相对其它要简单些。在前期(业务快速增长期)使用,暂时不考虑节约成本问题。
  3. 分区域接入场景,对于来自不同地区的用户,地区之间的网络差异明显。对于这样情况,应该按区域部署的transfer服务,不同IDC部署的transfer服务。那问题来了,客户端如何选择接入哪个(几个)transfer呢?通常增加查询transfer地址列表接口,通过客户端ip的到用户区域,返回transfer地址列表,客户端再尝试在地址列表中选择最优地址。

通过以上策略就能是接入层transfer高可用,接下来分析逻辑层。接入层、逻辑层、存储层的通讯通常采用RPC。逻辑层logic服务是无状态,每个节点都可以处理来自transfer的消息。

  1. 最简单的方式就是使用nginx upstream做转发达到负载的功能,logic接收nginx转发消息。需要注意的是nginx本身也需要搭建集群解决单点问题。
  2. 增加moniter服务监控transfer、logic、router的健康情况,transfer、logic、router启动会定时向monitor发送心跳hearbeat,当monitor监控到会logic有节点出现故障,则推送新logic地址列表到transfer,transfer更新本地logic地址列表。transfer使用新的logic地址发送消息。这里存在logic宕机,transfer未及时更新logic列表任然将数据发送到故障节点的问题,所以transfer必须有重试机制和重试队列,重试策略与moniter的心跳发送频率有关,具体策略也按实际策略而定。moniter从设计上是无状态,集群的,集群中每个节点都有相同配置。优点从业务角度做负载均衡,可以自定义易扩展,缺点是增加了服务的耦合性。需要强调的无论如何都需要moniter监控服务,通知transfer更新地址列表接口增加了耦合性。

剩下存储层高可用问题,router服务是存储用户的在线状态,服务本身就是有状态的。

  1. logic通过一致性hash算法将消息散列到不同的router,当有节点故障时,散列该节点的新消息转发到其他节点,当老消息中处理的消息就无法处理。并且节点回复正常是,还需要做数据迁移。(可作为前期版本过度)
  2. 基于上面的问题,可以使用router主备策略,logic转发的消息同时发送到主备,当只有主工作,当主宕机后,moniter(或logic)感知到router宕机,提升备为主。同时告警节点故障,恢复节点是,从运行节点中恢复数据。优点是解决了高可用,最大的缺点是一致性hash使扩容需要迁移数据。

修改后的架构图。
Alt text

服务扩容

随着用户的增长,现有机器无法承载是就需要进行服务扩容,怎样才能快捷方便的进行服务扩容呢?能否做到auto-scaling?
上面讲到接入层上层使用的负载均衡器,当transfer增加节点服务时,启动新节点后,将节点信息配置到负载均衡器并加载。假如负载策略是平均分配,新接入用户就会分配到新的transfer,直到每个节点接入量平衡。当然也可以按其它策略进行分配,例如A机器配置高可以承载更多的接入量,就可以调整分配策略分配更多接入到A。

对于逻辑层而言,增加logic节点,logic注册到monitor,monitor更新本地logic地址列表,通知transfer服务logic地址已改变,最后transfer调整发送策略完成扩展。

存储层服务扩容,比较麻烦,由于之前使用了一致性hash算法,增加删除节点都需要进行数据迁移,扩展能力相对差些。一致性hash算法是根本问题所在,那我们使用逻辑层的monitor + 主备方式可以吗?monitor方式的问题是请求随机发送到router服务上,而不是用户不变时都请求同一服务,这样会大大降低cache的命中率,降低系统性能。
从业务场景出发,router是维护用户状态的,用户数量是有限的,是可以提前预告在线和压力的。微信架构中对于相关设计是单点容灾策略,他整个系统又按用户uid范围进行分Set,每个Set都是一个完整的、独立的子系统。分Set设计目的是为了做灾难隔离,一个Set出现故障只会影响该Set内的用户,而不会影响到其它用户。他使用仲裁节点(类似monitor)判断节点是否正常,再配合嵌入式路由表,将宕机节点请求转移到其它节点。嵌入式路由表是核心,这里就不说原理了,它就是通过维护client(我们这里的logic)与查询服务(我们这里的router)的路由信息一致,其实就是通过在每个报文中带上路由信息,在配合仲裁节点可动态修改路由表,以达到节点的增加删除切换。
Alt text

结论:使用一致性hash算法和预估压力就能满足打部分要求,使用这种方案做好节点的数据迁移(其实不用迁移,cache重新命中就可以)。 后期可以考虑嵌入式路由表节约成本和auto-scaling.

统计、监控与配置

为了时刻知道线上服务的状态,做到提前预警,整个系统还应该有监控。其次,业务数据的统计量也很重要,知道我们下一步该业务中心,以及可能产出的瓶颈。通过这些数据,可能需要去修改服务配置、启动新服务等,配置管理与下发同样重要。

  1. 监控:agent,监控服务所属服务器的状态(CPU、MEM、IO等),核心进程状态(logic、router等),通常核心进程状态监控不应用到生产环境,开启会影响性能。如有必要才开启
  2. 统计:statis,统计点数据上报,业务数据和非业务数据都可以。
  3. 配置: etcd,配置下发功能。

序列号生成器

在消息轮转流程中提到msgid的生成,要求是msgid是线性递增的。序列号生成器需要再系统中独立存在运行。IM中通常按用户来自增长的,可以使用redis的inrc来简单实现。

富文本消息

对于图片、声音、视频的消息,在IM系统中,客户端本地上传富文本到文件服务器,文件服务器返回url,客户端再将url作为普通消息发送(文本类型要标识),接收端收到消息后,再去服务器上下载。 文件服务器可以选择第三方服务,自建建议使用ceph对象存储服务。

网络传输协议

IM系统传输使用UDP、TCP、基于TCP的http这几种协议中的一种或几种。

  • UDP协议实时性更好,但是如何处理安全可靠的传输并且处理不同客户端之间的消息交互是个难题;
  • TCP协议安全可靠的,如何保证单机服务器高并发量,如何做到灵活,扩展的架构。
    业界选择TCP居多,建议选择TCP。

数据传输格式

对传输的数据应该进行压缩,安全性处理。基于这些要求,选择probuffer是再合适不过了(建议)。当然json格式也是可以的选择,json没有压缩报文,但清晰明了。

跨区域网络问题

目前想到的只能走IDC机房专线。

总结

下图是对IM系统认知的总结,总体来说我对im系统还不太了解,还需要继续研究学。
Alt text

参考项目