7月QCon广州站2022,关注Web 3.0、数据架构选型、数字化转型等热门话题,点击了解 了解详情
写点什么

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

  • 2021 年 2 月 21 日
  • 本文字数:4486 字

    阅读完需:约 15 分钟

到达率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:001357

评论 1 条评论

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

超越竞争文化:致善式创新能否打造手机产业的“海法城”

脑极体

SQL数据库集合运算

正向成长

SQL表联结 SQL集合运算

区块链数字货币商城系统开发模式

薇電13242772558

区块链 数字货币

6个JDK自带JVM调优工具,一次性打包给你说清楚

田维常

jvm调优

SpringBoot-技术专题-Hystrix学习介绍

浩宇天尚

基于Vue实现一个有点意思的拼拼乐小游戏

徐小夕

Java GitHub 开源 H5游戏 H5

解读登录双因子认证(MFA)特性背后的TOTP原理

华为云开发者联盟

算法 totp 密钥

读完某C++神作,我只记住了100句话

MySQL从删库到跑路

c++

即将写入MySQL源码的官方bug解决之路

数据君

MySQL

甲方日常 52

句子

工作 随笔杂谈 日常

这份算法攻略,我拿到了5个大厂的offer

yes

面试 算法 笔试

程序员双十一剁手指南

数据君

腾讯云 程序员

【JVM】肝了一周,吐血整理出这份超硬核的JVM笔记(升级版)!!

冰河

性能优化 内存模型 JVM 堆栈 JVM笔记

2020双十一,阿里云GRTN拉开直播和RTC技术下半场的序幕

阿里云视频云

架构 云直播 直播 流媒体 直播架构

《程序员面试金典》.pdf

田维常

面试

响应式关系数据库处理R2DBC

程序那些事

MySQL R2DBC 程序那些事 响应式系统 响应式数据库

程序员如何判断跳槽岗位是否有坑!

Java架构师迁哥

Java中的线程与C++中的区别

jiangling500

Java c++ 线程

什么是服务器租用?

德胜网络-阳

Java中NullPointerException的完美解决方案

Silently9527

java8 Optional

花四个月和阿里面试官“大战”7回合,成功将其“斩于马下”!复盘面试题及答案!

Java架构追梦

Java 阿里巴巴 面试 java架构

Vokenization:一种比GPT-3更有常识的视觉语言模型

脑极体

.net core增强工作流组件,基于稳定平台,多项目整合开发

雯雯写代码

有点意思的gif动图生成平台开发实战(二)

徐小夕

Java Vue 大前端 GIF React

厉害了!阿里内部都用的Spring+MyBatis源码手册,实战理论两不误

小Q

Java spring 学习 面试 mybatis

直播卖货已成趋势

anyRTC开发者

音视频 WebRTC RTC

支撑2715​亿元海量订单 揭秘京东大促背后的数据库基石

京东科技开发者

数据库 数据仓库 云服务 云数据库

容器和虚拟机到底有啥区别?

网管

容器 虚拟机

微服务架构中的“参天大树”:SpringBoot+SpringCloud+Docker

小Q

Java 学习 容器 面试 微服务

影视剪辑类自媒体运营心得:如何抓住观众的痛点

石头IT视角

到达率99.9%:闲鱼消息在高速上换引擎(集大成)_架构_闲鱼技术_InfoQ精选文章