深度剖析IM消息乱序的根本原因及优化方法

本文深度剖析即时通讯IM消息乱序的问题,梳理IM客户端时间戳不可信、多端并发、群消息扇出重试三大核心陷阱,拆解微信、钉钉、企业微信的时序设计方案,并从锚点设计、多端去交错、幂等兜底、可观测性等维度,给出IM项目消息时序性保证的落地优化策略。

消息息乱序问题的由来

凌晨一点,运营甩来一张截图:群里两条消息显示顺序是 A → B,但 A 的语义明显在回复 B —— 时间穿越了。

翻消息表,按 msgSeq 排能对上业务因果,按 ctime 排却整个倒过来。这就是”消息时序错乱”最常见的样子。

很多人第一反应是”加个时间戳排序不就行了”,但用谁的时间戳?

  • 1)客户端的会被用户改、会跨时区、会被 NTP 校准;
  • 2)服务端多个网关实例本地时钟也对不齐。

你以为在解决一个排序问题,碰到的其实是分布式共识问题 —— 一群机器要对”哪条消息在前”达成一致,而手上没有一只共同的表。

在中等规模 toB IM 项目,”消息时序”的坑集中在三处:

  • 1)客户端时钟不可信 —— 用户改时间、跨时区、NTP 漂移、睡眠唤醒;
  • 2)多端并发发送   —— 同一发送方在手机和 PC 同时发,谁先到服务端不确定;
  • 3)扇出与重试乱序 —— 群消息扇出顺序不定、MQ 重投把”旧消息”塞回队尾。

三个陷阱单独看都不致命,叠加起来”对方收到的顺序”就和”发送方以为的顺序”对不齐了。

IM 最伤信任的体验问题不是”丢消息”,是”顺序错” —— 丢一条可以补,顺序错了整段对话读不通。

消息时序的三个可验证设计目标

先把”对方收到的顺序正确”翻译成三个能写进测试用例的目标:

  • 1)同一会话内强保序:A 给 B 连发 3 条,B 端看到的必须是 A 发出的顺序 —— 硬约束。
  • 2)跨会话弱保序:A 给 B 发一条、又给 C 发一条,两条之间不需要全局保序 —— 强行做全局有序是为假需求买单。
  • 3)多端最终一致:同一接收方在手机和 PC 上看到的同一会话顺序必须一致,即使中间出现过临时乱序。

第三点要留意:分布式系统没办法保证所有端在任意时刻顺序都一样,能保证的是收敛 —— 等消息到齐后各端排序一致。把目标定成”任意时刻都一样”,会掉进永远填不平的坑。

陷阱1:不可信的客户端时间戳

新人做消息排序,第一反应几乎都一样:消息体不是带了 ctime(client time)吗,拿它 sort 一下。

1)测试机上跑得好好的,上线后用户跨时区出差本地时间跳 8 小时,消息全跑到聊天列表最顶;
2)iOS 用户为薅签到把时间调到去年;
3)安卓深度睡眠唤醒后被 NTP 强校准 1~2 秒,同一秒内几条消息次序乱掉。

根因一句话:客户端时钟不在服务端控制范围内。 拿它做排序主键,等于把决定权交给一个随时漂移的对抗性环境。

锚点挪到服务端要先想清楚用什么字段。stime(服务端时间戳)和 msgSeq(会话内单调序号)各管一段。

主流 toB IM 两个都打:stime 用于跨会话排序,msgSeq 用于会话内强保序。只用 stime 不够 —— 服务端多实例间本地时钟也有几毫秒到几十毫秒漂移,这是陷阱二的伏笔。打锚点在序号服务里大致如下:

// 序号服务给消息打锚点on_conversation_topic_consume(msg):    
lock =redis_lock("conv:seq:"+ msg.conversation_id, timeout=5s)
// 会话粒度的锁
try:        
msg.msgSeq =next_sequence(msg.conversation_id)// 单调递增        
msg.stime  =server_now_ms()
finally:        
lock.release()publish_to_router(msg)

msgSeq 怎么生成又是一道选择题,三种主流实现各有取舍:

深度剖析IM消息乱序的根本原因及优化方法

没有最优解,只有演进路径:早期用 Redis INCR,单会话写入压力变大后切号段。客户端 ctime 也不是彻底没用 —— 它能和 clientMsgId 一起做去重辅助 key,边界是参与去重可以、排序不行。

陷阱2:多端并发的时序裁决把锚点收到服务端,陷阱一就解决了?

没那么快。

用户 A 同时登录 PC 和手机,PC 上敲完一句话按回车,几乎同一瞬间手机误触发出一条语音,两条几乎同时到达服务端 —— 哪条先?

直觉是在网关接收那一刻打时间戳谁小谁先。

但 PC 走 WiFi、手机走 4G,两条消息很可能落到两个不同的网关实例上,实例间本地时钟差几毫秒到几十毫秒,打的本地时间根本没有可比性;退回去用客户端时间戳,陷阱一又在等着。多端并发场景里”谁先发”本身就不成立 —— 没有任何一只表能权威回答它。

既然没有共同的表,那就只设一只表。

把”打时序锚点”收敛到一个会话级单点(通常就是序号服务):

  • 1)网关只做转发不打时序;
  • 2)同一会话消息用 HASH(conversationId) 路由到同一处理点;
  • 3)序号服务内部用锁串行化分配保证 msgSeq 严格单调。就像电影院检票,观众从不同门进场但座位按票号定。
深度剖析IM消息乱序的根本原因及优化方法

上图是多端并发发送的串行化路径。MQ 的 HASH 分区让”同会话进同 partition”,序号服务的会话锁负责按序分配。

这里藏着一个反直觉的取舍:这两条消息谁的msgSeq更小本质上是不确定的,取决于受网络延迟影响的 MQ 入队顺序。但这没关系:只要顺序定下来全系统都承认它,体验就是一致的 —— 一致比符合直觉更重要。

本文来自即时通讯网(52im.net)

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

赞 (0)

相关推荐