组里做的IM需要优化,同时这也是我毕设的选题,所以我最近抽空总结了下组里IM的现有流程和架构,其中,就通信流程和同步方式的学习,我获益颇深。

但是,对于缓存失效,性能优化(集群,缓存,异步,批处理),代码抽象等地方还不是很好,还需要我下一步继续改进

一. 通信流程

二. 基础功能

用户在上线之后会把信息存入redis中,便于后续收发消息。存储的信息包括用户的channel,ack,token,seq,在线人数等

1. 单聊(C2C)

单聊逻辑比较简单,当sender通过长连接把消息发送给server之后,server再把消息(通过RPC/RESTful)转发给route,route将消息和序列号落库之后,再通过redis的缓存,转发给receiver的channel中

2. 群聊(C2G)

我们把群聊和单聊的逻辑基本抽象成一样的,把群也作为一个接受者

群聊的接收方为groupId,第二步业务处理会把receiver的groupId通过查库的方式获得群里面的所有成员,然后再通过Server把消息转发给所有群成员,然后再通过channel发送消息

3. 推送(S2C)

推送并不能通过groupId进行写扩散的优化,所以会存在写入大量msg,并且会大量更新receiver的ack_seq和msg_seq,所以推送需要额外优化

三. 消息同步

1. 落库方式

msg_seq保存的是消息的seq,以receiverId为粒度自增。last_ack_seq保存的是确认的seq,之所以服务端保存ack是因为如果不保存,当用户换手机之后,就不能获得未读消息了

  1. group:msg_seq
  2. group_user: last_ack_seq
  3. message: msg_seq
  4. user: last_ack_seq msg_seq

单聊:消息落库,用户msg_seq根据userId为粒度自增,落库更新(1%),用户ack_seq更新(1%)

群聊:消息落库:群聊msg_seq根据groupId为粒度自增,落库更新(1%),用户群聊ack_seq更新(1%)。**[当群人数很多,同时看到消息的时候,瞬间的ack确认,即是是1%的概率更新数据库也有可能把库打掉]**

2. 同步流程

3. 缓存失效

因为msg_seq&ack_seq在Redis中存储,只有1%的概率会同步到DB中,当Redis失效的时候,如何保证用户一定能同步到未读的信息,这也是一个问题。

以下假设建立在user1离线的情况下

假如在一次C2C中,user1的msg_seq刚好落库,并且此时数据库中的user1_msg_seq是100,同时Redis中的user1_msg_seq也是100。之后user1正常作为receiver接收消息,不过此时并没有触发落库机制

不知道过了多久,数据库中的user1_msg_seq=100(因为没有被1%的几率触发),Redis的user1_msg_seq是200,说明此时user1已经又接收到了100条数据,但是还没有同步到DB中,此时Redis宕机

这时,user1的100条数据是丢失的,所以,之前的解决办法是,我们需要重新从数据库中获取旧的seq,同时给其增加10000的步长(对于1%的落库来说,1w几乎能保证一定比宕机的redis的seq大)

对于ack_seq来说,则不存在这种问题,因为我们的ack是从客户端获取的,换句话说,我们服务端的ack是无状态的,所以不用考虑1w的步长

4. 消息乱序

如果sender发送消息的顺序是1,2,但是receiver接收消息的顺序是2,1,那该如何处理?

我们需要在发送消息时携带上该消息的createTime,然后客户端根据createTime重新调整部分范围内的顺序

5. 优化方案

  1. 同步消息可以需要多次少量同步,不用一次把所有未读消息同步完,类似于分页查询
  2. 可以选择MQ做缓冲,其实对于推送来说,因为不需要及时性,所以可以通过MQ进行削峰填谷的作用
  3. 对于长连接的发送消息来说,额外的落库操作也可以通过另外开辟一个线程落库,或者使用消息队列异步落库(同步消息时可能会有延迟),但是消息发送和存储的一致性就没办法保证
  4. 落库不能用百分率,因为百分之一对应1w,这个成本是很大的,可以考虑使用固定步长
  5. ack不能每次都落库(1%),也需要用步长来落库[参考微信]

四. 方案取舍

1. 服务器选型

Tomcat还是Netty:暂定基于Netty的原生连接,为了简单易用,选择Yeauty作为WebSocket的脚手架,方便Tomcat到Netty上的迁移。但是如果使用Yeauty,就固定了传输格式,享受不了Netty的自定义网络传输红利

2. 通信协议

  • 长连接:TCP下的WebSocket还是基于TCP自己解析,暂定基于TCP的ProtoBuf
  • 短连接:目前使用RESTful,后期希望改进为RPC

3. 读写扩散

读扩散还是写扩散,目前是基于写扩散。因为对于我们的业务来说,当群聊人数控制得当,且并发不是很高的情况下,写扩散更容易编写。

同时,把群Id也作为receiver,防止群聊人数过多,增加写扩散的写入压力。同时一次消息也只存入一次msg的实体类,对于ack_seq和msg_seq都是随机更新

五. 架构问题

1. 服务部署

集群部署,将Netty作为集群部署

在多台服务器中如何路由到receiver的channel中——需要前置一个route进行路由

每台服务器的注册和服务发现问题——需要注册中心,可能是Redis,也可能是Zookeeper

2. 业务抽象

需要把基本的聊天管道,和建群,好友等模块的业务抽象出来,然后对外提供SDK

3. 消息可达

当发送方发送成功或者接送放接收成功消息都会给服务端发送ack,以此来表明自己接收成功

六. 监控问题

目前只能监控在线人数,还是有所欠缺的