IM系统为了保证消息必达以及发送可靠,消息数据是先落盘后推送or同步的。消息从发
送方发出后,经过服务端转发,服务端会先将消息保存到消息库,完成消息的持久化保存
后,对于在线的接收方,会直接选择通过长链接在线推送。但在线推送并不是一个必达路
径,只是为了保证“即时”,采用的一个更优的消息传递路径。对于在线推送失败或者离线的
接收方,会通过主动拉取的方式获得服务端所有未同步消息。通过推拉结合的手段,保证消
息发出后,接收方一定能够看到。
消息存储方案(已更新为kbase)
读扩散:消息存储模型中,每个会话的消息队列中保存了这个会话的全量消息。读扩散
的消息同步模式下,每个会话中产生的新的消息,只需要写一次到其用于存储的会话队列
中,接收端从这个会话队列中拉取新的消息,所有聊天参与方共享同一个消息队列。优点是
消息只需要写一次,相比写扩散的模式,能够大大降低消息写入次数,特别是在群消息这种
聊天参与方特别多的场景下。但其缺点也比较明显,接收端去同步消息的逻辑会相对复杂和
低效。 接收端需要对每个会话都拉取一次才能获取全部消息,并且在实际的线上生产环境
中,数据还会分库分表,读被大大的放大,可能会造成性能的瓶颈。虽然目前快手IM的场景
下,我们利用会话增量更新的机制(每次某个会话有更新,才去同步该会话的消息队列),
大大缩小了读扩散带来的性能损耗,但是还是无法完全避免。(业内最佳实践:QQ)
存储策略
IM系统在消息数据存储方面使用了Redis、Mysql和HBase,三级存储逐级查询。其中Redis
和Mysql中存储经常被访问的热数据,HBase中存储全量数据,一方面是作为Mysql的冷
备,另一方面是作为归档数据源进行使用。
首先对于Redis中的数据,仅仅通过设置过期时间是完全不够的,设置太短,失去了缓存的
意义,设置太长,缓存存储空间根本不够。这里,我们在ç(下图中也叫position),一个是
write offset,代表当前会话对的消息队列最后一条消息的SequenceId,一个是read
offset, 代表当前用户已经读到的SequenceId。当两个用户的read offset都已经达到某个
位置的时候(会话是有方向的性的,因此两个用户的数据彼此的独立的),我们认为这个位
置之前的消息,双方都已经同步到了端上,短时间内不会有大量的访问场景,因此就可以从
缓存中删除了。 即便后续由于用户删除数据或者卸载重新安装,也只需要和DB同步一次,
就可以同步到所有数据,不会造成太大的服务压力。
对于MYSQL中的数据,最早我们是没有进行归档的。随着数据量越来越大,我们库的shard
也越来越多(100个)。MYSQL本身在水平扩容上就有一些局限性,集群一大,运维成本
又非常高。同时,IM的消息存储场景,是非常适用于NOSQL的:没有复杂的事务(最多就
是行级事务),存储的数据结构以及查询场景也相对简单。我们曾尝试直接将作为冷备的
HBase切换称主存储介质,但是在切换的过程中,我们发现HBase的scan性能并不是非常稳
定,完全应用于在线业务有一定的风险,最终我们决定将MYSQL做成一个二级缓存,储存
大部分热数据,HBase存储全量数据,但只承担冷数据的流量。
对于MYSQL,我们采用的数据存储策略是:针对每个会话的消息队列数据采用FIFO淘汰机
制,我们只保留最近的N条(单聊目前是80,群聊是12000,为什么是这么多,日后的文章
中会有解释),超过N条的消息,我们会通过消息归档功能进行裁剪,从而将消息队列的长
度保持在一个较为稳定的数目,进而将整个数据库的存储资源稳定在一个可控的范围。这里
需要注意的是,消息队列的长度取的过长,会导致MYSQL的容量没有减少多少;取的过
短,落在HBase上的流量又会太高。因此选取合理的队列长度是策略有效性的保障。
组件
1.消息ID生成器:保证会话纬度消息的连续性和唯一性;依赖的生成组件为MYSQL或者
REDIS,内部设计有防回退策略,能够在集群发生宕机时,切换集群并且不发生消息ID
回退。
2.消息存储:无状态服务,可水平扩展,依赖的各个存储介质的存储策略见上文。
3.消息归档:部署分布式定时归档服务集群,管理归档任务,在不影响在线服务性能的前
提下,高性能地进行消息归档,控制Mysql存储资源的增长。
4.安全存储:落库消息进行加密,保证存储安全。
对于在线的终端,可以由消息服务器主动推送至在线终端。对于离线终端,登录后会主动向
服务端同步消息。每个终端会在本地保留有所有会话的最近一条消息的SequenceId,当某
个会话发生了更新时,客户端会在适当的时机向服务端同步该会话的消息,保证本地消息的
完整性。