写点什么

到达率 99.9%:闲鱼消息在高速上换引擎(集大成)

2021 年 2 月 21 日

到达率99.9%:闲鱼消息在高速上换引擎(集大成)

背景

在 2020 年年初的时候接手了闲鱼的消息,当时的消息存在一些反馈:“闲鱼消息丢失”、“消息用户头像乱了”、“订单状态不对”。闲鱼消息的稳定性是个亟待解决的问题,我们调研了集团的一些解决方案,例如钉钉的 IMPass。直接迁移的成本和风险都是比较大,包括服务端数据需要双写、新老版本兼容等。


那基于闲鱼现有的消息架构和体系,如何来保证它的稳定性?治理应该从哪里开始?现在闲鱼的稳定性是什么样的?如何衡量稳定性?希望这篇文章,能让大家看到一个不一样的闲鱼消息。

行业方案

消息的投递链路大致分为三步:发送者发送,服务端接收然后落库,服务端通知接收端。特别是移动端的网络环境比较复杂,可能你发着消息,网络突然断掉了;可能消息正在发送中,网络突然好了,需要重发。


null


在如此复杂的网络环境下,是如何稳定可靠的进行消息投递的?对发送者来说,它不知道消息是否有送达,要想做到确定送达,就需要加一个响应机制,类似下面的响应逻辑:


  1. 发送者发送了一条消息“Hello”,进入等待状态。

  2. 接收者收到这条消息“Hello”,然后告诉发送者说我已经收到这条消息了的确认信息。

  3. 发送者接收到确认信息后,这个流程就算完成了,否则会重试。


上面流程看似简单,关键是中间有个服务端转发过程,问题就在于谁来回这个确认信息,什么时候回这个确认信息。网上查到比较多的是如下一个必达模型,如下图所示:


null


null


[发送流程]

  1. A 向 IM-server 发送一个消息请求包,即 msg:R1

  2. IM-server 在成功处理后,回复 A 一个消息响应包,即 msg:A1

  3. 如果此时 B 在线,则 IM-server 主动向 B 发送一个消息通知包,即 msg:N1(当然,如果 B 不在线,则消息会存储离线)


[接收流程]

  1. B 向 IM-server 发送一个 ack 请求包,即 ack:R2

  2. IM-server 在成功处理后,回复 B 一个 ack 响应包,即 ack:A2

  3. 则 IM-server 主动向 A 发送一个 ack 通知包,即 ack:N2


一个可信的消息送达系统就是靠的 6 条报文来保证的,有这个投递模型来决定消息的必达,中间任何一个环节出错,都可以基于这个 request-ack 机制来判定是否出错并重试。看下在第 4.2 章中,也是参考了上面这个模型,客户端发送的逻辑是直接基于 http 的所以暂时不用做重试,主要是在服务端往客户端推送的时候,会加上重试的逻辑。

闲鱼消息的问题

刚接手闲鱼消息,没有稳定相关的数据,所以第一步还是要对闲鱼消息做一个系统的排查,首先对消息做了全链路埋点。


null


基于消息的整个链路,我们梳理出来了几个关键的指标:发送成功率、消息到达率、客户端落库率。整个数据的统计都是基于埋点来做的。在埋点的过程中,发现了一个很大的问题:闲鱼的消息没有一个全局唯一的 ID,导致在全链路埋点的过程中,无法唯一确定这条消息的生命周期。

消息唯一性问题


null


之前闲鱼的消息是通过 3 个变量来唯一确定一个消息

• SessionID: 当前会话的 ID

• SeqID:用户当前本地发送的消息序号,服务端是不关心此数据,完全是透传

• Version:这个比较重要,是消息在当前会话中的序号,以服务端为准,但是客户端也会生成一个假的 version


以上图为例,当 A 和 B 同时发送消息的时候,都会在本地生成如上几个关键信息,当 A 发送的消息(黄色)首先到达服务端,因为前面没有其他 version 的消息,所以会将原数据返回给 A,客户端 A 接收到消息的时候,再跟本地的消息做合并,只会保留一条消息。同时服务端也会将此消息发送给 B,因为 B 本地也有一个 version=1 的消息,所以服务端过来的消息就会被过滤掉,这就出现消息丢失的问题。


当 B 发送消息到达服务端后,因为已经有 version=1 的消息,所以服务端会将 B 的消息 version 递增,此时消息的 version=2。这条消息发送给 A,和本地消息可以正常合并。但是当此消息返回给 B 的时候,和本地的消息合并,会出现 2 条一样的消息,出现消息重复,这也是为什么闲鱼之前总是出现消息丢失和消息重复最主要的原因。

消息推送逻辑问题

之前闲鱼的消息的推送逻辑也存在很大的问题,发送端使用 http 请求,发送消息内容,基本不会出问题,问题是出现在服务端给另外一端推送的时候。如下图所示,


null


服务端在给客户端推送的时候,会先判断此时客户端是否在线,如果在线才会推送,如果不在线就会推离线消息。这个做法就非常的简单粗暴。长连接的状态如果不稳定,导致客户端真实状态和服务端的存储状态不一致,就导致消息不会推送到端上。

客户端逻辑问题

除了以上跟服务端有关系外,还有一类问题是客户端本身设计的问题,可以归结为以下几种情况:


  • 多线程问题 反馈消息列表页面会出现布局错乱,本地数据还没有完全初始化好,就开始渲染界面

  • 未读数和小红点的计数不准确 本地的显示数据和数据库存储的不一致。

  • 消息合并问题 本地在合并消息的时候,是分段合并的,不能保证消息的连续性和唯一性。


诸如以上的几种情况,我们首先是对客户端的代码做了梳理与重构,架构如下图所示:


null

我们的解法 - 引擎升级

进行治理的第一步就是,解决闲鱼消息的唯一性的问题。我们也调研了钉钉的方案,钉钉是服务端全局维护消息的唯一 ID,考虑到闲鱼消息的历史包袱,我们这边采用 UUID 作为消息的唯一 ID,这样就可以在消息链路埋点以及去重上得到很大的改善。

消息唯一性

在新版本的 APP 上面,客户端会生成一个 uuid,对于老版本无法生成的情况,服务端也会补充上相关信息。


null


消息的 ID 类似a1a3ffa118834033ac7a8b8353b7c6d9,客户端在接收到消息后,会先根据 MessageID 来去重,然后基于 Timestamp 排序就可以了,虽然客户端的时间可能不一样,但是重复的概率还是比较小。

- (void)combileMessages:(NSArray<PMessage*>*)messages {    ...
// 1. 根据消息MessageId进行去重 NSMutableDictionary *messageMaps = [self containerMessageMap]; for (PMessage *message in msgs) { [messageMaps setObject:message forKey:message.messageId]; }
// 2. 消息合并后排序 NSMutableArray *tempMsgs = [NSMutableArray array]; [tempMsgs addObjectsFromArray:messageMaps.allValues]; [tempMsgs sortUsingComparator:^NSComparisonResult(PMessage * _Nonnull obj1, PMessage * _Nonnull obj2) { // 根据消息的timestamp进行排序 return obj1.timestamp > obj2.timestamp; }];
...}
复制代码

重发重连


null


基于 #2 中的重发重连模型,闲鱼完善了服务端的重发的逻辑,客户端完善了重连的逻辑。


  1. 客户端会定时检测 ACCS 长连接是否联通

  2. 服务端会检测设备是否在线,如果在线会推送消息,并会有超时等待

  3. 客户端接收到消息之后,会返回一个 Ack


已经有小伙伴发表了一篇文章:《向消息延迟说bybye:闲鱼消息及时到达方案(详细)》,讲解了下关于网络不稳定给闲鱼消息带来的问题,在这里就不多赘述了。

数据同步

重发重连是解决的基础网络层的问题,接下来就要看下业务层的问题,很多复杂情况是通过在业务层增加兼容代码来解决的,闲鱼消息的数据同步就是一个很典型的场景。在完善数据同步的逻辑之前,我们也调研过钉钉的一整套数据同步方案,他们主要是由服务端来保证的,背后有一个稳定的长连接保证,大致流程如下:


null


闲鱼的服务端暂时还没有这种能力,原因详见 4.5 的服务端存储模型。所以闲鱼这边只能从客户端来控制数据同步的逻辑,数据同步的方式包括:拉取会话、拉取消息、推送消息等。因为涉及到的场景比较复杂,之前有个场景就是推送会触发增量同步,如果推送过多的话,会同时触发多次网络请求,为了解决这个问题,我们也做了相关的推拉队列隔离。


null


客户端控制的策略就是如果在拉取的话,会先将 push 过来的消息加到缓存队列里面,等拉取的结果回来,会再跟本地缓存的逻辑做合并,这样就可以避免多次网络请求的问题。之前同事已经写了一篇关于推拉流控制的逻辑,《如何有效缩短闲鱼消息处理时长》,这里也不过多赘述了。

客户端模型

客户端在数据组织形式上,主要分 2 种:会话和消息,会话又分为虚拟节点、会话节点和文件夹节点。


null


在客户端会构建上图一样的树,这棵树主要保存的是会话显示的相关信息,比如未读数、红点以及最新消息摘要,子节点更新,会顺带更新到父节点,构建树的过程也是已读和未读数更新的过程。其中比较复杂的场景是闲鱼情报社,这个其实是一个文件夹节点,它包含了很多个子的会话,这就决定了他的消息排序、红点计数以及消息摘要的更新逻辑会更复杂,服务端告知客户端子会话的列表,然后客户端再去拼接这些数据模型。

服务端存储模型


null


在 4.3 中大概讲了客户端的请求逻辑,历史消息会分为增量和全量域同步。这个域其实是服务端的一层概念,本质上就是用户消息的一层缓存,消息过来之后会暂存在缓存中,加速消息读取。但是这个设计也存在一个缺陷,就是域环是有长度的,最多保存 256 条,当用户的消息数多于 256 条,只能从数据库中读取。


关于服务端的存储方式,我们也调研过钉钉的方案,是写扩散,优点就是可以很好地对每位用户的消息做定制化,比如钉的逻辑,缺点就是存储量很很大。闲鱼的这套解决方案,应该是介于读扩散和写扩散之间的一种解决方案。这个设计方式不仅使客户端逻辑复杂,服务端的数据读取速度也会比较慢,后续这块也可以做优化。

我们的解法 - 质量监控

在做客户端和服务端的全链路改造的同时,我们也对消息线上的行为做了监控和排查的逻辑。

全链路排查


null


全链路排查是基于用户的实时行为日志,客户端的埋点通过集团实时处理引擎 Flink,将数据清洗到 SLS 里面,用户的行为包括了消息引擎对消息的处理、用户的点击/访问页面的行为、以及用户的网络请求。服务端侧会有一些长连接推送以及重试的日志,也会清洗到 SLS,这样就组成了从服务端到客户端全链路的排查的方案,详情请参考《消息质量平台系列文章|全链路排查篇》

对账系统

当然为了验证消息的准确性,我们还做了对账系统。


null


在用户离开会话的时候,我们会统计当前会话一定数量的消息,生成一个 md5 的校验码,上报到服务端。服务端拿到这个校验码之后再判定是否消息是正确的,经过抽样数据验证,消息的准确性基本都在 99.99%。

核心数据指标

我们在统计消息的关键指标的时候,遇到点问题,之前我们是用用户埋点来统计的,发现会有 3%~5%的数据差;所以后来我们采用抽样实时上报的数据来计算数据指标。


消息到达率=客户端实际收到的消息量/客户端应该收到的消息量


客户端实际收到的消息的定义为消息落库才算是 该指标不区分离线在线,取用户当日最后一次更新设备时间,理论上当天且在此时间之前下发的消息都应该收到。


null


最新版本的到达率已经基本达到 99.9%,从舆情上来看,反馈丢消息的也确实少了很多。

未来规划

整体看来,经过一年的治理,闲鱼的消息在慢慢的变好,但还是存在一些待优化的方面:

• 现在消息的安全性不足,容易被黑产利用,借助消息发送一些违规的内容。

• 消息的扩展性较弱,增加一些卡片或者能力就要发版,缺少了动态化和扩展的能力。

• 现在底层协议比较难扩展,后续还是要规范一下协议。

• 从业务角度看,消息应该是一个横向支撑的工具性或者平台型的产品,规划可以快速对接二方和三方的快速对接。


在 2021 年,我们会持续关注消息相关的用户舆情,希望闲鱼消息能帮助闲鱼用户更好的完成二手交易。


参考资料:

http://www.52im.net/thread-464-1-1.html


本文转载自:闲鱼技术(ID:XYtech_Alibaba)

原文链接:到达率99.9%:闲鱼消息在高速上换引擎(集大成)

2021 年 2 月 21 日 13:00824

评论 1 条评论

发布
用户头像
技术是好了,可这咸鱼现在变了,各种奸商,就跟58的中介一样被满屏占据
2021 年 02 月 22 日 18:40
回复
没有更多了
发现更多内容

执法办案信息化建设,情报研判管控分析平台搭建解决方案

t13823115967

智慧公安

区块链食品溯源--为食品安全保驾护航

135深圳3055源中瑞8032

数字货币交易所系统开发功能方案

系统开发咨询:I76-883I-5I52 邓森

差点跳起来了!全靠这份“Java核心知识笔记”我成功拿到美团offer

比伯

Java 程序员 架构 计算机 编写

【经验分享】遵循10步法,应用系统发布效率大不同!

嘉为蓝鲸

敏捷 运维自动化 部署 发布流程 应用发布

得物(毒)APP,8位抽奖码需求,这不就是产品给我留的数学作业!

小傅哥

Java 小傅哥 编程开发 七日更 数学逻辑

Polkadot系列(三)——如何实现共享安全性

QTech

区块链 polkadot 跨链

Fair World智能合约APP系统软件开发

开發I852946OIIO

系统开发

小白干货奇遇记

熊斌

个人成长 七日更

做音视频最好用的几款跨平台框架

anyRTC开发者

flutter uni-app ios android WebRTC

Gridea+GitHub搭建个人博客

Simon

GitHub Pages 博客 七日更

向我看齐!京东智联云成 2020 TOP100 Summit“技术标兵”

京东科技开发者

DevOps 云原生 数字化

Windows安装MySQL5.7教程

Simon

MySQL windows 安装 七日更

合成游戏app系统开发软件技术

系统开发咨询:I76-883I-5I52 邓森

智慧社区综合信息服务平台搭建,智能社区建设解决方案

t13823115967

智慧社区系统开发

盘点 2020 | 鲜衣怒马少年时,不负韶华行且知!

程序员的时光

程序员 成长 编程之路 计算机 盘点2020

OLAP计算引擎怎么选?

数据社

OLAP 七日更

dForce挖矿APP系统开发|dForce挖矿软件开发

开發I852946OIIO

系统开发

AWS云上安全最佳实践

雪雷

安全 AWS 云安全

突破程序员基本功的16课

田维常

程序员

入门参考:从Go中的协程理解串行和并行

soolaugust

go Go Concurrency Patterns 七日更

智慧城市平安智慧社区平台建设,公安防控管理平台

WX13823153201

发布会直播技术及业务实践

vivo互联网技术

分布式 服务器 直播技术

智慧警务大数据可视化平台搭建,警情分析研判系统

135深圳3055源中瑞8032

智慧公安重点人员管控系统开发,预警研判系统搭建

135深圳3055源中瑞8032

Spring 源码学习 09:refresh 大概流程

程序员小航

spring 源码 源码阅读

甲方日常72

句子

工作 随笔杂谈 日常

生产环境全链路压测建设历程17:某快递A股上市公司的生产压测案例之前言

数列科技杨德华

全链路压测 七日更

【理论篇】浅析分布式中的 CAP、BASE、2PC、3PC、Paxos、Raft、ZAB

merlinfeng

大数据 分布式

四币连发交易所系统开发技术

系统开发咨询:I76-883I-5I52 邓森

堪称完美!阿里架构师用60个实战案例讲明白了Spring Boot

Java架构追梦

Java 架构 面试 微服务 springboot

打造 VUCA 时代的 10 倍速 IT 团队

打造 VUCA 时代的 10 倍速 IT 团队

到达率99.9%:闲鱼消息在高速上换引擎(集大成)-InfoQ