IM系统经验总结
背景
IM系统伴随着下面几个问题:
- 实时性:网络协议、带宽、机器的演变升级
- 投递可靠性:这里是指消息能够在应用层被对等方接收
- 一致性
- 安全性
投递可靠性
消息为什么会丢
使用的TCP协议作为传输层的协议,它是可靠的全双工协议,仅仅只靠它就能保证对等方正常接收消息了吗?
先说结论:仅仅靠TCP的可靠性是不行的,原因主要是以下三点:
- 网络不是完美的
- 服务问题:应用出现故障导致收发失败
- 服务器出现故障
补充:send()成功只能表明上层应用成功将消息发送到发送缓冲区里,在连接断开的时候直接回收连接和释放缓冲区数据;对于接收方在未检测到TCP断开时,会正常处理缓冲区里的数,但存在断开时,上层应用未来得及正常从缓冲区里取出完整的数据,例如RST状态下。
消息的丢失情况,主要分以下几个维度:
- 发送方发消息失败
- 接收方收消息失败
- 接收方发送ack成功,发送方接收来自接收方ack失败
- 接收方发送ack失败
网络和服务问题
情景描述:弱网的情况下,服务端与客户端之间的传输会出现问题
解决:
- 网络不佳或者请求服务器等待响应超时,通常会提示发送方失败,发送方再次调用接口发送。
- 服务端需要根据针对请求的唯一 ID,供服务端去重使用
·
服务器出现故障
情景描述1:网络层已经投递成功,但是接收方在处理过程中出现了失败导致没有看到消息,服务端并不知道对等方是否真正处理了消息
解决:
- IM服务器在推送消息时,用SID标识本次请求,并将该消息加入”待ACK消息列表”
- 接收方在处理完消息后,返回ACK,服务端在收到后将“待ACK消息列表”删除掉
- ack丢失:如果在一段时间没有收到接收方的ack
- 可能的原因:1. 网络原因 2. 接收方自身崩溃导致处理失败
- 服务端超时重传
- 接收方去重:接收方可能收到了消息处理了,可能没有处理
情景描述2:当消息写入db后,下一步便是准备去通知接收方,可能在代码走到socket推送或者已经写入到内核缓冲区时,服务器出现宕机或者断电。在服务器恢复正常后,客户端重连,但是服务端这期间的消息通知不会重发
解决:
- 当接收方重新上线时,本地存储的时间戳(版本号)返回给服务端,服务端将这个版本号之后的消息推送(返回)给接收方。
一致性
描述:消息的顺序,不影响对client的语义了解。使用类似全局的生成的有序唯一消息ID,使得消息在一定程度内保证了有了时序性,为什么是一定程度上呢,因为伴随了以下几个问题:
- 服务端是多线程接收消息,可能先发的消息比后发的消息要先生成msgId落入库中
- 服务端都是集群化部署,每个节点的性能上存在差异,可能在找到网关的长连接下推时存在早接收的消息晚下推了的情况
其他问题:需不需要保证全局唯一的msgId
整流
大部分情况下是允许小范围的消息乱序,人在看到或听到消息视图时,会进行自己的一定理解包容当时的聊天场景,但有时可能需要保证消息的绝对时序,操作要保证1->2->3的顺序执行,否则会出错。
- 解决:
- 针对需要保证绝对时序的消息,将这些消息定义一个公有的packageId,然后为包内的每条消息加上seqId
- 服务端整流:在一定超时时间内将它们根据seqId进行排序,然后找到gateway顺序下推
- 客户端整流:先发的消息后到达,根据收到的msgId进行排序
其他问题
Q:需不需要保证全局唯一的msgId?
A:维护一个公有全局的分布式唯一自增ID会存在性能上的问题,在IM情景单聊便维护在一个会话维度内的自增ID,群聊则维护一个在群里的分布式唯一自增ID