微信:我们绝不丢消息!(第49讲)
《架构师之路:架构设计中的100个知识点》
49.消息可靠投递
有水友问我说,总感觉微信不丢消息,它是怎么做到的? 之前做过几十年IM架构,今天和大家聊聊消息的可靠投递。
IM系统中,报文分几种类型? 答,三种:
1. 请求报文(Request)
2. 应答报文(Acknowledge)
3. 通知报文(Notify)
R:客户端主动发送给服务器的报文; A:服务器被动应答客户端的报文,一个A对应一个R; N:服务器主动发送给客户端的报文;
路人架构师如何设计消息投递流程? 一个没做过IM系统的路人架构师,他可能会这么设计消息投递流程:用户A给用户B发送“你好”:
1. client-A向im-server发送msg:R; 2. im-server回复client-A一个msg:A; 3. 如果此时client-B在线,则im-server向client-B发送msg:N;
画外音:如果client-B不在线,im-server存储离线消息。
上述消息投递流程存在什么问题? 流程图中会发现,发送方client-A收到msg:A后,只能说明im-server成功接收到了消息,并不能说明client-B接收到了消息。
在若干场景下,可能出现msg:N包丢失,例如: 1. 服务器崩溃,msg:N包未发出; 2. 网络抖动,msg:N包被网络设备丢弃; 3. client-B崩溃,msg:N包未接收;
结论是悲观的:接收方client-B是否有收到msg:N,发送方client-A完全不可控。
那怎么办呢?
应用层的消息可靠投递,必须通过应用层的超时、重传、确认来保证。
首先,加入应用层的确认机制。
要想让发送方client-A确保接收方client-B收到了消息,必须让接收方client-B回复client-A一个消息的确认。这个应用层的确认流程,与消息的发送流程类似:
4. client-B向im-server发送ack:R; 5. im-server回复client-B一个ack:A; 6. im-server向client-A发送ack:N;
至此,发送“你好”的client-A,在收到了ack:N报文后,才能确认client-B真正接收到了“你好”。
你会发现,一条“你好”的发送,分别包含上下两个半场,即msg的R/A/N三个报文,ack的R/A/N三个报文,这是IM系统中消息投递的核心。
还可能存在什么问题? 复杂的网络环境下,msg:N,ack:N这两个报文都可能丢失(服务器奔溃、网络抖动、客户端奔溃),此时client-A都收不到期待的ack:N报文,即client-A不能确认client-B是否收到“你好”,但这两个报文的丢失对应的业务影响又大有不同:
1. msg:N包丢失,业务结果是client-B没有收到消息;
2. ack:N包丢失,业务结果是client-B收到了消息,只是client-A不知道而已;
结论仍然是悲观的:client-A无法知晓具体是哪种情况。
那怎么办呢?
接下来,要加入超时与重传。
client-A发出了msg:R,收到了msg:A之后,在一个期待的时间内,如果没有收到ack:N,client-A会尝试将msg:R重发。
可能client-A同时发出了很多消息,故client-A需要在本地维护一个等待ack队列,并配合timer超时机制,来记录哪些消息没有收到ack:N,以定时重发。
一旦收到了ack:N,说明client-B收到了“你好”消息,对应的消息将从“等待ack队列”中移除。
消息的重传会引入什么新的问题?
超时与重传机制可能导致client-B收到重复的消息。
那怎么办呢?
最后,client-B要引入消息的去重机制。
发送方client-A生成一个消息去重的msgid,保存在“等待ack队列”里,同一条消息使用相同的msgid来重传,供client-B去重,而不影响用户体验。
也就是说,系统层面,client-B其实收到了很多消息,而产品体验层面,用户并不知道。系统,就是在背后默默保证你体验的那个人!
总结:
1. IM系统通过超时、重传、确认、去重的机制来保证消息的可靠投递; 2. 一个“你好”的发送,包含上半场msg:R/A/N与下半场ack:R/A/N的6个报文;
3. 等待ACK队列,是超时重传的关键,重传的消息msgid相同;
4. 无法做到系统层面的不丢不重,只能做到业务层面的不丢不重;
知其然,知其所以然。
思路比结论更重要。
==全文完==
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-03-30,如有侵权请联系 cloudcommunity@tencent 删除服务器系统微信client队列