本文由 ITPub 根据封宇在【第十届中国系统架构师大会(SACC2018)】现场演讲内容整理而成。
1、引言
瓜子业务重线下,用户网上看车、预约到店、成交等许多环节都发生在线下。瓜子 IM 智能客服系统的目的是要把这些线下的活动搬到线上,对线下行为进行追溯,积累相关数据。系统连接用户、客服、电销、销售、AI 机器人、业务后台等多个角色及应用,覆盖网上咨询、浏览、预约看车、到店体验、后服、投诉等众多环节,各个角色间通过可直接操作的卡片传递业务。
例如,用户有买车意向时,电销或 AI 机器人会及时给用户推送预约看车的卡片,用户只需选择时间即可完成预约操作。整个系统逻辑复杂,及时性、可靠性要求高,涉及 IM 消息、业务卡片、各种实时统计。此次演讲,从数据架构层面讲解系统遇到的挑战及解决办法。
补充说明:本文对应的演讲 PPT 详见《瓜子 IM 智能客服系统的数据架构设计(PPT) [附件下载]》。
学习交流:
– 即时通讯 / 推送技术开发交流 5 群:215477170 [推荐]
– 移动端 IM 开发入门文章:《新手入门一篇就够:从零开发移动端 IM》
(本文同步发布于:http://www.52im.net/thread-2807-1-1.html)
2、分享者
封宇:瓜子二手车高级技术专家,中国计算机学会专业会员。2017 年 2 月入职瓜子二手车,主要负责瓜子即时消息解决方案及相关系统研发工作。在瓜子期间,主持自研消息系统用于支持瓜子内效工具呱呱,满足瓜子两万多员工移动办公需求;作为项目经理,负责瓜子服务在线化项目,该项目对瓜子二手车交易模式及流程带来深远影响。
在入职瓜子二手车之前,封宇曾供职于 58 同城、58 到家、华北计算技术研究所,参与到家消息系统、58 爬虫系统以及多个国家级军工科研项目的架构及研发工作。在消息系统、后端架构、存储架构等方面有丰富经验。
封宇分享的其它 IM 技术资料:
《一套海量在线用户的移动端 IM 架构设计实践分享(含详细图文)》
《即时通讯安全篇(七):用 JWT 技术解决 IM 系统 Socket 长连接的身份认证痛点》
《移动端 IM 中大规模群消息的推送如何保证效率、实时性?》
《一个低成本确保 IM 消息时序的方法探讨》
《从零开始搭建瓜子二手车 IM 系统(PPT) [附件下载]》
《瓜子 IM 智能客服系统的数据架构设计(PPT) [附件下载]》
《瓜子 IM 智能客服系统的数据架构设计(整理自现场演讲,有配套 PPT)》
3、正文概述
今天分享的题目是“瓜子 IM 智能客服数据架构设计”,这个系统和旺旺比较像。
简单说一下分享的几大部分内容:
第一部分:项目背景,项目背景需要稍微讲一下,有助于大家理解;
第二部分:是系统架构,为什么要简单讲一下系统架构呢?因为不讲业务的架构都是耍流氓,所以说我们讲存储,都要知道系统是怎么回事;
第三部分:重点讲一下存储,在这块我们讲分享一下我们的实践经验,以及演进过程,更多的是采到的一些坑,我们怎么解决的。
4、项目背景
比较熟悉的都知道瓜子二手车没有中间商赚差价,其实瓜子在中间要做很多事情。
首先二手车很难,它不是一个标品,我们要去收车,收车都是在社会上去收,通过网站收,你要验车。我们要把它放到我们的网站上,有些车要收到我们的场地,有些车可能是在用户家里楼下停着。如果一个用户来买车,在网上点了之后,你可能下单要去看这个车,我们的销售要去跟到线下去陪你看车选车试驾,还有一系列的复检等等,有很多的事情。
现在瓜子这块我们做的还不够好,为什么不够好? 我们站在瓜子内部的角度来看这个事情,有很多的行为发生在线下,我们很难防就会带来很多问题。比如飞单,有些去跟别的企业串,这个车就没有在平台上卖,这些问题都很难解决。
第二个就是销售到底跟用户在做什么,他没准骂那些用户或者什么的,回头企业发现的投诉很难查。那我们这个项目的一个重要的问题,就要解决一个线上化,就是把这些线下的行为搬到线上,我们利用通讯,在 IM 过程当中传递一些业务,这样把整个的一些业务线上化固化下来。
第三个是电商化,我比较喜欢用京东,我觉得他的物流和客服都很好,瓜子也是希望做成电商,还有一个就降本增效快一点,如果现在你用瓜子,你会发现你到网上去浏览,就会有电销人员给你打电话,这个是很烦的,对用户的体验很不好,再一个对瓜子来说成本也很高,我们也养了很多的电销,所以说我们就启动了这样一个项目来解决这些问题。
刚才提到了很复杂,整个系统串联了很多角色,有用户、销售、电销、评估师,还有 AI 和机器人。做系统要善于抽象,我们先有一个基于通讯的即时通讯系统。第二我们是要把通讯系统集成到业务,或者叫把这些业务搬到通讯系统上面来,核心就是这样子。
这是截了几个图,我们看第一个图,我们上边就有一个车,下边有个预约,用户事实上是可以直接在聊天界面里面去点预约了。这个就是我说的跟一般的客服不一样的,它可以做一些叫做导购或者叫营销也好,直接通过这样一个途径,因为瓜子的获客成本很高,每个访问瓜子的用户我们都希望及时跟他沟通,这个图有助于大家理解。
5、系统架构
整个系统不是一个单一的系统,它结合了一些业务,我们可以把它拆分成这么几个层次。最上面是一个端,那端首先就是瓜子的 APP, 当然还有毛豆的,现在瓜子的业务有好几个,除此之外有些员工用的,比如说电销的、客服的、售后的、金融的、我们可能都是一些 APP 或者是一些桌面系统,这是我们的客户端。
第二是一个路由层,我们要打通这些业务。要让这些业务在这个系统里边及时的传递,所谓传递刚才前面有个图案,比如说你想约车了,那客服就给你或者电销就给你发一个约车的卡片,你就可以直接选时间约,这是传递业务。再往下边是一些业务层,那就是原来瓜子有很多有什么业务就涉及什么业务,最底下是一个存储。这次后边主要讲的就是站在存储层的角度来看整个系统,重点会讲存储层怎么对路由层进行支持。
6、存储架构
存储这块会讲大概几个点,包括:
1)数据库的拆分;
2)消息怎么存,消息里边也会特别提一下群的模型,大规模的群是比较麻烦的;
3)还有一些存储逻辑以及业务怎么在上边 run 起来;
4)最后是统计分析,实时计算这样一些设备。
6.1 数据库的拆分
这个图是现在的一个数据库的图,我们看着这些就是数据库已经分得很好了,比如通讯的数据库,有调度的有卡片有分析。
我在这里介绍一下“调度”这个新出现的名词,它是干嘛的?就是你在这个系统里边点开一个车也好,点开的人也好,聊天时用户看到的是一个瓜子的客服,后边瓜子内部实际上是一个瓜子的客户团队非常大的团队来支持你,所以说到底你跟哪个客服聊天,是有一些策略的。
我们感觉这样一个拆分实际上是顺理成章的,但是事实上根本就不是。我举个例子,2015 年大概是京东内部有一个分享,刘强东分享流露出来了,说有一个二手车企业一年还是一个月,我忘了,卖了两辆车出去,估值就到了 2 亿美金。简直不敢相信。
我分享这个例子并不是说话有什么不对,当然我相信他也不是说的瓜子,因为瓜子 A 轮它不止这个数,我想说最初企业是很小的,业务量是很小的,我们根本就不可能是这样一个数据库的结构,就跟沈老师说的一样,其实它就是一个库也没有什么 IM,没有什么调度,这些卡片可能就是一个卖车的一个数据库。
我是去年的 2 月份进入瓜子的,快两年了,那时候瓜子的业务量非常小,具体我也不知道,就是一个数据库,一个数据库其实是非常好的。因为很多人来了之后就说要拆库,一个数据库的好处是写业务很快,十几个人快速的就把系统就搭起来了。
我们事实上库拆成这么几个也经历了一个过程,最初是做 IM 只有一个 IM 的库,后来有了调度加了一个库,再后来有什么卡片,有分析逐步得往外扩。
这个库它其实是有一定的成本,如果你拆分得不好,你会去做很多接口,比如说你像关联查一下,发现不是我团队的库也要做各种各样接口,产生了分布式的事物的一致性的问题,都产生了。所以说数据库的拆分,尤其垂直拆分,实际上是随着你不同的阶段,你选择不同的拆分方式,以后随着系统的扩大瓜子业务扩大这个系统它会拆的更多数据库,但拆的更多,对你的运维监控这些团队的挑战都会带来一些成本,也会带来一些挑战。我们现在把它拆成了这样一个库,各司其职。
(原图来自《现代 IM 系统中聊天消息的同步和存储方案探讨》)
6.2 消息怎么存
下面就重点说一下消息,这块我们怎么存?
我们看一下左边这个图,左边这个图是一般来说很容易理解的消息,怎么存的方式?
以前桌面系统经常这么干:比如说 A 要给 B 发一个消息,他怎么发?他就说 A 用户端,A 这个端我发一个消息,如果 B 在线,我就把消息直接发给他,他给我一个确认,这个过程就存储好就结束了。
其实我服务当中不需要存这个消息,如果是 A 发给一个 C 这个 C 不在线怎么办?
我们也有策略: A 把这个消息发给了 C,C 如果没有确认说我收到这个消息,我就把这个下边的第二步,我就把它存到一个离线的数据库里边等着你 C 什么时候上线,你就把这个消息拉回去,这个过程就完结了,这个消息我就给送到了,所以说这个时候的存储非常简单,我就一个离线库,存一下某个人的消息就好了。
但是这种模式其实是有很多问题的,真正使用的时候,很多产品现在是移动端的手机端,网络首先是不稳定,长期处于一个 C 的状态,如果你都去监控它的状态,送没送到,再存储性能会很差。
(原图来自《现代 IM 系统中聊天消息的同步和存储方案探讨》)
第二个现在的端有很多:有桌面的、有手机的、有 APP 端、还有 PAD 端,有好几个端。如果都用这种模式,需要为每个端都这里判断去看传输数据其实也是很困难的。
我们就变了一个方式:第 1 步来了消息,我们就把消息存到存储库,你只要发消息我就先给你存下来,第 2 步,同时我还存到一个同步库里边。
这两个库要稍微解释一下,存储和同步库分别来做什么?
存储库比较好理解,你什么时候都能从库里边还原你的消息,把它读回去,比如说你换了手机,你都可以把消息拉回来。
这个同步库是什么意思?同步库就是说你没有换手机,也没有重新装系统,就是你可能有一段时间离线,离线起来之后,就说我比如假设一个消息序列,1 到 100 发给你了,就 A 发给 C,1 到 100 了。结果但是 C 从第 70 号消息的时候,他就离线了,他就不在线。这样子,C 这个端上线后,第四步把 70 号消息传给这个服务端,说我有 70 消息同步库就知道,把 70 到 100 的消息发给 C。这样子消息就是可以送达这个端,这两个概念稍微是会有一点模糊,但是没有关系,后边我会接着展开来讲。
也就说一个消息,我们会存一个消息同步库和一个存储库,这个实际上是一个消息同步库,它是一个模型,我下一张 PPT 应该会讲用什么东西来存它。
如果我们把它理解成一个邮件,你很好理解。我们的邮件,有一个收件箱,同步库就像一个收件箱,不管是谁发给你的邮件,群发地也好,单发的也好,反正我都给你放一份,在收件箱里面放一份,A 把这个邮件放进去了消息,你从另一个要取出来,你不管用这个手机也好用你的苹果系统笔记本或者 windows 本也好,也都要去收这个邮件,收的过程就是什么? 比如说刚才说第一苹果系统笔记本,B1 说我之前收了前两封 B 游标,它本地有一个消息最大的,说我在 2 号消息,我收到了,那他把 2 号消息传给服务端,服务端就说好,后边 2 到 20 号消息都可以收走了,这样子可以保证这个消息不重不漏地送给客户端去。
B2 说我之前这个端其实收了十封了,我就从 11 分开始收,B3 说就收过一封,我就从第二封邮件开始收就好了,这样子就解决了,消息就送过去了。这里有几个问题,我们同步的过程就是理论上是可以了,但是有几个问题,第一个就是扩散写扩散读,在这块跟存储很相关,扩散写和扩散读有很多讨论。
举个例子:是什么地方产生的?比如我如果是一个单聊,我们两个人聊天没有问题,我肯定把消息都写给你了。但是事实上很多时候我们是在一个群里边聊天,给我们销售,我们的评估师或者机器人,他们都在里边聊,用户也在里边聊,这个消息我是为每人写一份,还是我为整个会话也就是这个群,我只写只存一份,你们都来读。一个会话的消息,或者说你自己手里的收件箱,我先说结论,我们是在消息的同步库里边采用的扩散写,后边还有一个存储库,存储库里边我们是采用的扩散读的方式,在同步库里边,我们每人都写了一份,这样子读的时候很方便。而在存储的时候,由于我们的存储速度慢一些,我们是只写了一份数据,这一个会话只有一条消息。
第二个点是事实上在同步过程中,我们遇到了一些问题,有很多的策略需要我们考虑。就是一个消息 TimeLine,有时候不同的端有不同的同步策略,比如说有些场景下,我们要求它的每一个端都收到这一条消息,那就是我们的通知,在公司发的优惠券什么的,每个端都要送达。有些场景人他是希望说你的手机收了,那你打开桌面,我们就不再给你送这个消息了,按照刚才这一个同步模型,有一些困难的每个端可能都会去搜这个消息,所以说我们就准备了三个这样的存储的号。
那比如说这个图上的 B1B2B3, 当前消息的我们另外存在一个最大消息的号。第三个号你所有的里边搜的消息最靠前的一块就说比较像 B2 这样通过这三个位置的组合,我们可以确定你收取消息的位置或者一个策略,这是这个模型,我们存储采用什么? 我们采用了 Redis cluster, 我们用了 SortedSet 结构。
我专门把它提出来了,因为我们其实在这还踩过一个坑,我要存这个消息了,怎么都得知道这个结构的效率,我们查了一下 SortedSet 效率还可以,就说它是一个 ln 这样子一个效率,所以说在同步库,如果我们每个用户只存一部分消息,它的性能是非常高的。这个结构本质上是个跳表,跳表结构其实很复杂,我想在这会上讲清楚很难,最后放了两个图,就是一本书。
我们知道这些结构,比如像二叉树或者一些红黑树,检索都有比较好的索引策略,跳表也类似。它比较类似什么,就像我们比如说一本书上有一千页,我想翻到 856 页怎么翻? 其实我们有一种方法去前面去找索引去定位什么东西,还有我大概翻到 800 页,逐步修正。跳表结构本质上比较像翻书,我觉得是翻到一个大的页,先翻 800 页,翻到 850, 在逐渐翻到 860 页。
下面分享一下这块我们遇到了一个什么问题!我们 SortedSet 存储,存消息,而我们存消息为了全局一致性,用了一个思路。这个算法我们消息是一个长整型,就是上班卡,我先讲的是没关系,先讲下面精度丢失的问题,我们的消息是一个长整型。这个场景下总共是 64 位,所以说 snowflake 这个算法,它的前面第一位不用,它其实表示正整数它是有意义的。用 41 表示一个时间区间,这里面产生一些 ID 代表了大概有六七十年或者三四十年,反正是肯定是够用了。
中间十位是一个工作机编号,他可以支持 1024 个台机器,我们现阶段用不了这么多机器,最后的 12 位是一个毫秒内的一个序号,所以说构成了我们消息的 ID 因此消息是很长的一串,算下来得 18 位的整数。
放到 SortedSet 里边之后,我们后来就发现一些问题,发现这个时间靠的近的消息,我们区分不出来它的先后顺序。就这深挖下去,发现 SortedSet 它十个字实际上是个 double 类型的,下边这个图是 double 类型的描述,它的精度只有 52 位,上边长整型它是有 63 位的精度,这里边就有 11 位的差距。所以说在那个毫时间很接近的消息,它的精度丢失,我们检索拉取的时候,这些顺序就出了问题。
因此我们采用了一个策略,也可以借鉴一下,根据我们的当时的负载量以及机器数,这个最终保证了我们几乎遇不到这种精度丢失的问题,就把精度主动的转换降低了。这个 case 上说明就是我们选择存储的时候,数据类型很重要,你得根据你的业务类型看一下。
我们还在这同步的时候遇到一些问题,就是这个问题更多出现在我们内部的一个工具,我们有很多的人数比较大的群,因为我们的消息像一个收件箱,他的大小是有限制的,有些大群它疯狂的刷消息,那这样子这个群里边可能就有成百上千上万的消息,因为我们收件箱大小有限制,我们就会把之前更早的消息淘汰掉,导致一些单聊比较重要的消息就丢失了,这个是我们的遇到的问题,后边的 PPT 会有解决方案。
第二个问题就是还有一些 web 端,我们 web 端,其实本地的缓存是很难用的,这个就是我们用户一打开之后,它有多少未读数,只能先通过我们把消息拉回去算一下,新拉到多少消息,才能计算出它有多少未读数。这个实际上对我们也是一个挑战,很不友好。
6.3 消息的落库存储
接下来我们讲一下存储这块,我们存储消息要落库了,我们怎么存消息?
我们刚才提到了,是按照每个会话你看到的每个人跟你聊天的一个维度来存储。我们也是采用了分库的策略,分库比较简单。
这举个例子:事实上这个库不止这么多个,我们把一个他的消息绘画的 ID 除以四,取它的模来确定它到底放到哪个库里边,刚才提到了我们很多 ID 是用 snowflake 算法来生成的,我们有个方法来防止它的生存不均,看一下。这是一个我们防止它的 ID 分布不均的一个方案。我们看到最后有一个 12 位的序列号,如果你不加任何干预,他每次都从零开始,事实上当你并发比较小的时候,你会发现它后边都是零,就最后几位都是你这样子,如果都是零,你用是你用固定的取模的算法,他就绝对是不平均了。
更多有关消息 ID 的算法和策略,可以详细读读以下文章:
《美团技术分享:深度解密美团的分布式 ID 生成算法》
《融云技术分享:解密融云 IM 产品的聊天消息 ID 生成策略》
《微信技术分享:微信的海量 IM 聊天消息序列号生成实践(算法原理篇)》
《微信技术分享:微信的海量 IM 聊天消息序列号生成实践(容灾方案篇)》
比如说你的数据库不够了,你到时要扩容的时候你就发现很困难,你不知道以前的数据,你只能把以前的数据全部翻出来,简直是灾难,所以说我们需要人为的干预,我们就是用取模的方式,把分库进行特殊的处理,加上一些分库的行为,在这个里边我们用了一个 ID 生成的时间,给他一个最后八位的一个遮罩,他在 128 个数据库的时候,它分布会很平均。
说一下怎么扩容:刚才之前提到的最开始业务量很少,但是前几天瓜子一天已经卖出 1 万辆的车了,所以说这个量现在我们是逐步的会在起来,当成交 1 万辆,用户量是非常大的。我们怎么扩容,这就是扩容的基本方法。我们最初有 db0123 这样几个库,我们看一下左边有就是这个图的左边,一个 msg:chatid=100 和 msg:chatid=104, 以前 chatid 除以四的时候,100 和 104 这两个数据,这两个数据它都会并重 db0 这个库。
我们分库的时候怎么做?第一步我们把 db0123 这样的库同时进项,我就要主从同步,反正在搞 db4567,db0 和 db4 数据一样的 db1 和 db5 数据也一样,相对应的一样。我们把分库策略改成除以八求余,之后的结果就出现什么?
我们就发现按照新的分库策略,chatid104 还在 db0 里面,chatid100 它由于除了之后他就到 db4 了,这样子我们就相当于是从四个库直接就变成了八个库,之后的过程就是分库规则上线之后,我们再由 DBA 把不属于库的其他数据给删掉,这个库的扩容就搞定了,所以说业务可以接着跑,但是这样子是不是就解决问题了?
其实没有简单,远远没有,因为你像我们的数据库前面的数据库是 MySQL, 为了安全,他有一重两重可能还有扩库,简直这个数据库越来越多,它运维和 DBA 他就不干了,说你这库越来越多,我怎么维护,这个受不了了。
所以说还有一个就是这种关系型数据库,它可以支持像事务有好多事务关联性查询这些非常丰富的逻辑,但事实上我们一个消息都最多的一个数据量的应用,它用不上这么复杂,它是很简单的,我要么根据消息 ID 查某一条消息,要么根据一个消息要检索这个范围之间的消息,真的用不上这么复杂的一些逻辑,所以说存储用关系型数据库并不是说特别适合,那我们就去研究了。
我们首先一查发现有一个时序型的数据库是一个 OpenTSDB, 他的应用场景跟这个业务很相似。OpenTSDB 它内部是用 Hbase 来实现的,我们觉得 Hbase 就很好。我们为什么选择 Hbase? 因为有团队维护,非常现实。选用了 Hbase 之后,这个接下来如果用过 Hbase 的同学就知道,除了我们要分片,这些做好了之后,最关键的是要设计 Rowkey 设计是需要结合业务,而且需要设计得非常精妙,我们的 Rowkey 结果就是一个会话 chatid ID,Chatid 可以分散 region,msgid 时间有序用于范围检索。
而如果我们用这个你去咨询,你会经常的一个场景,我打开了看到的最新的消息,我往下划一划才是加载更老的消息,这个结构正好一来了之后,检索你最新的消息,你往下滑的时候,我们就接着去查后边的消息,这样子非常快,而如果当地什么都没有,你重新新装一个非常方便,你直接来 Hbase 里边查询最新的 Rowkey 你就找到你最新的消息了,这个就解决了。
还有一点就是 region, 就像分库一样,Hbase 做的比较好,它可以自己帮你维护这个分片,但是我们不建议这么搞自己维护分片,当你像这种消息的数据它存储量是很小的,它很小会导致默认给你一个 region, 但是这样一个读写瓶颈就来了,所以说我们需要提前规划我们分库的 region.
下面是一些群,群有一些特殊的地方,在我们的二手车 APP 上,这种大规模的群比较小,但是我想分享的是我们在内部通讯里边群遇到的一些问题也带上来,就一起把它跟大家交流一下。
第一个就是刚才提到的减少存储量。这个是下面的存储库,比如有很多群,可能有 2000 多人有,如果我发一条消息就存 2000 份,那简直是灾难,所以说我们只能存一份,因此我们看这个图就是左边的蓝色之后,我们只存了一份,标明了这个消息 ID,标明了这是哪个会话或者是哪个群的。
因为存了一份,第二个问题就带来了:如果今天加群的人,昨天加群的人其实看到的消息应该是不一样的。
正常业务是这样,有时候你还可以看到最近多少条的逻辑怎么实现,就是我们在再给它扩展一个数据库表,这个表是关系型数据库里的,记录上群的号码,记录上这个人的 ID, 记录上他加群的时间,加群的时间我们可以通过一个函数把它运算。所以说 msg ID 的策略很重要,我们经过加群时间,由于它是一个时间的函数,我们可以跟这个加群的时间进行一个映射关系,这样子我通过加群时间能够大概定位到他从哪条消息可以检索,如果你需要去做策略,也可以说上面看多少条,下边看多少条都可以做。第三个就是有一个会话排序的问题,这种对话的场景里边,我们可以看到会有很多的会话,所以说这是一个策略的选择。
第一种做法:你可以为每个人建一个会话,他每有一条消息,你就把他的最后时间更新一下,这个过程就能满足会话的排序,但事实上我们能不能这么做? 我觉得我们不能这么做,因为有些时候消息很多,而且有些时候用户很大,我们发一条消息。有一千个人要去更新他的状态,不管你用多少都是扛不住的,所以会话的策略,我们也是在缓存转存会话的最后一条消息量。当用户要来拉取他的会话列表,或者更新他的会话列表的时候,由服务器端给他预算好了之后返回给他,我们用的时候正常情况下与客户端它本地是可以收到消息,如果你在线他是自己知道调整这个数据的。拉取会话的行为,当它发生离线了再次打开,这个时候需要更新一下,如果这个频率比较低这样一个取舍,我们的存储模型也就出来了,所以说其他很多业务都是在发生的时候我们就跟踪她的状态,而这个会话排序我们是在比如说读取的时候我们才可以建立这个过程。
后边的已读未读,这个点不再细讲,没有什么特征。我们知道缓存里为每条消息都建了一个存储结构,说这条消息哪些人已读哪些未读,在比较短的时间把它淘汰。消息撤回这块提一下,之前有个小同学这么干,这个消息怎么撤回? 在关系型数据库里边,这个消息要撤回,我在表里边把这条消息标记上,这条消息是撤回来的,这个做法有没有问题? 一点都没有问题。
之后他又来了个需求,说我就想看一下这些没有撤回的消息拿出来怎么办? 这个同学也是刚毕业没多久,就调整,就想到了建索引,他就把索引建好了,就可以这么去拉取数据,结果跑一段时间数据库报警了。这不行,怎么回事? 因为撤回的消息跟正常没撤回的消息比例是失衡的非常小一间隔索引,所以毫无意义,而且还消耗了写消息的性能,因此我们撤回消息后来两种做法,第一是把它从这个消息库里边删掉,挪到一个撤回的消息表里边,这是显而易见的。还有一种做法就是我们也打标记,但是不做索引,我也不支持你过滤接受,而我是无差别的拉出来之后在存储的逻辑层那边把它过滤掉,这样子做。
下边有提到了,我们讲存储结构不光是一个简单的一个数据库这样一个简单的概念,它其实在 db 到业务之间还会有一些叫做约定也好,规范也好,或者降低复杂度也好,因为你直接让业务去处理它是不好的,所以我们有存储的逻辑,这样逻辑层做一些基本的逻辑。
这里跟大家分享一下:瓜子它 APP 的地位还不够高,我第一次用的时候一点它要登陆,因此我们要做一些匿名的策略,我们希望匿名的状态下你已经能建立沟通了,如果你觉得可以我们再接着聊,卖车也好,买车也好,所以说匿名就对我们这个业务带来一个挑战,匿名的时候,我们可能给他分了一个 ID, 他聊着聊着觉得可以了,它就登录了,登录了之后,他实名的时候,他实名有可能是新创建的一个,也可能他之前就登过,但是由于忘了,或者是时间久了过期了,这个时候他在这一次的业务过程当中,他就两个 ID, 如果一直让它成为两个 ID 其实对后边的电销人员是很郁闷的,说我们开始跟我聊了一下,过会变了个人其实是一个人,前面的业务也中断了,所以说我们对这个消息层面我们就进行了一个 Merge, 这个我们并没有说你实名,我们就把你的数据给搬家,按照这个实名的就是匿名有一个时间序列,实名是不是也有一个,我们并没有这么搞,我们还是两个,而是在存储中间的一个层次进行拉取的过程,在需要 Merge 的时候,我们在存储逻辑上给他 Merge。
但是匿名到实名远没有简单,只是一个延伸,事实上你这个消息里面的匿名很好做,但是你的业务匿名到实名很难,还有我们经常遇到这个问题,机器人给他发了一个东西,匿名状态,后来他登陆了,他一打开,拉回去了之后,这个消息还在他那里,他变成实名了。他进行操作,这个时候业务的匿名到实名其实是更难的,如果有做这样想法的,提前想好,更多的是业务层面的理论都是。
这个是消息的最后一部分了,实际上还会遇到一些问题,事实上消息同步是非常复杂的一个事情,我们后来越做越觉得它复杂。这个有些人会出现一个什么情况,比如说我用 A 手机收了几条消息,我在 B 手机上又收了几条消息,过一段时间我在 A 手机上又来收几条消息。
你看刚才那种模型就会导致中间出现很多断层,中间出现了很多,就像 Client 右边这个图,就 345 的消息他并没有收到,但是服务端其实是所有消息都有的。这时候我们做了一些策略,我们为每个消息严格的编号,msg index:1,2,3, 每个消息严格的编号。如果是这样子,客户端知道了之后,他就知道我原来少了 345 这三个号对不对?我就可以到服务端去说,我缺 345 这几个消息,你给我解索出来。
有没有这么容易?客户端可能觉得这个是很容易的,但是到了服务端事情不是这么回事,345 是你自己编的一个号,而我们的消息之前说了 snowflake 这样一个唯一的编号,那你拿着 345 并不能找到你到底是哪个消息 ID,所以说我是不是服务端要用哪个消息建立这么一个索引,还是应该是一个编号到 msg ID 索引?
可以做,但是存储量工作量非常的大,那我们怎么干?
我们在逻辑层里边做了一些事情,服务端每次返回客户端的消息,我们把这个消息把它做成一个链表的结构,当你来拉取,因为是反向消息好多是吧? 拉去 2 号消息的时候,我就说,这个我告诉你,你的下一条消息是 msg7, 你拉取,也可以一段一段拉取没关系。你拉到 msg6 的时候,我告诉你说你的消息 msg5, 我客户端说原来少这个消息 5, 这样子客户端可以通过这个消息 ID 到服务端来检索消息,由于是消息 ID 不管我们是 OK 也好,或者我们的关系型数据库是基于索引也好,都很容易做,现成的,也不需要再维护其他的索引关系。所以说这也是一个策略的点。这种断层我们就解决了。
但是还有问题,有时候消息非常多,如果你一次都把这些就是我们刚才说的同步库的消息收过去,过程其实是很慢的,尤其在深度用户的时候这个方案不好。
有一种做法:你把这个消息压缩一下送过去,简直传递。但是其实还是不好,客户端要渲染,要计算数量很慢,这就是刚才提到的扩散读和扩散写的问题,最早有一个,所以说后来更好的办法是说同步库里边也并不是去同步的具体的消息,你可以去做这个用户有哪些变更的会话,这么一个会话,它有多少未接收的数据,记录好这个数字有多少未读。这样客户端可以把这些数据拉到本地,你看到了有多少未读的会话之后,你点进去的时候,你再照这个存储库里边反向的通过这个来拉取你的消息,再加上我们刚才说的中间空荡的一个补齐的策略,一个列表补齐的策略,这样的体验非常好。
所以说我们就解决了我们这个项目,我们就解决了消息的问题。后边我们看一下消息解决了还没有完,我们要推广这个项目,我们要落地,需要做业务,因为只是传一个消息没有意义,对观众来说我们就在做业务了,我们承载业务的就是叫我们的业务卡片,最初那个图里边我们看到的那些传过去可以直接操作的这个东西,应该我们还申请了专利,当时去查了一下没人这么搞的,但是由于没人这么搞,其实我们在实施过程中遇到一些问题,下面我们看一下这个图。
这个图右边有一个卡片的代理,右图有个绿色的卡片代理是我们对这个业务设置的一个特殊的东西,我们在推广的时候遇到很多问题,我们这些业务原有的业务部门,他们都做得有接口是现成的,由于你把它搬到了 IM 交互里面有几种方法:第一你们全部给我改一下,这个是很困难,有些业务说他不愿意,我们就设计了这么一个代理的产出的卡片的所有响应试点,先打到我们的一个代理模块,代理模块再去适配你原有的业务逻辑,这样子代理模块,知道用户操作行为到底是什么样,成没成功,成功了或者没成功,他再通过调度,通过他们的通道通知相关业务的各方结果,这是一种策略。
同时它要高可靠,比如说我们预约看车就相当于下班了,就相当于这个是很重要的业务。你这个不行,链条太长了,风险太高,宁可我们加点东西都可以,那就是左边这个逻辑,这业务服务愿意说我自己改一下,可控性我得自己把控,不能说因为通讯有问题,我的业务就不跑了。那就是他改一下,来触发调度的一些逻辑。这个是我们在整个推广的过程当中最重要的一个策略,实际上也是探索出来的,因为不光是一个技术问题,还是个组织结构问题。
简单提一下调度问题,因为不是重点,你怎么知道到底是哪个客户来服务你?我们提出了一个场景的概念,就是你每次从各种入口进到对话界面的时候,这些入口我们是有状态的。A 入口 B 入口,比如你约车还是砍价什么之类的,我们它先把这个场景到调度去注册一下,说我从这儿进来,同时调度会有一些大数据来支撑,原来你是谁谁,你就从这个场景进来,我们认为你可能是要干什么事情。这样给他返回场景里,他再次跟通道间发生关系的时候,带上这个场景,我们这个调度就知道把你推给具体的谁,是销售也好,机器人也好,是我们具体的解决投诉的客服或者解决什么电销的客服了。
6.4 分析统计
接下来再提一下分析统计,像瓜子的规模已经比较大了,我们现在有超过一千个研发团队,但是在大数据这块投入也比较多,但是事实上现在公司都是数据驱动化,这个团队的力量依然是很有限的,它支持各种各样的业务线,非常吃力的还是很忙,所以我们在分析统计有两块,第一块是 T + 1 的分析是离线的,还有一块是实时的一个分析。
我们这个项目于对实时统计的要求很高,比如及时回复率这些各种各样的统计要求实时的监控报警,怎么做呢? 这是我们整个系统另一个角度的一个架构,和我们一些跟消息相关的或跟一些业务调度相关的,我们都走了一个 kafka, 就是通道的以及送给客服的一些销售评估师等等,他们业务线的这些逻辑,我们通过 kafka 传递一些比较多的数据。我们在想能不能用借用 kafka 来简单的实现技术,事实上是可以,这概念是一个流式数据库,我们最初的结构就是图左边这一块,整个系统中间走的消息都通过了一个 kafka.
我们可以保证业务在上面跑,其次 kafka 它缓存的这段数据,我们是可以对他进行流式计算,我们整体的架构是上图这样。
7、最后
我简单重复一下我们的过程:
第一:我们这个系统通过数据层面展示了一个通讯,就是即时通讯的这样一个系统,大概是怎么做的,数据库怎么存的;
第二:是把我们通讯的能力应用到业务系统,我们解决了技术上或者组织上遇到了一些什么困难;
第三:是我们找一个比较简单的方法,处理我们的一些离线计算,当然他做 T + 1 也是可以的。
谢谢大家。
附录:更多有关 IM 架构设计的文章
《浅谈 IM 系统的架构设计》
《简述移动端 IM 开发的那些坑:架构设计、通信协议和客户端》
《一套海量在线用户的移动端 IM 架构设计实践分享(含详细图文)》
《一套原创分布式即时通讯 (IM) 系统理论架构方案》
《从零到卓越:京东客服即时通讯系统的技术架构演进历程》
《蘑菇街即时通讯 /IM 服务器开发之架构选择》
《腾讯 QQ1.4 亿在线用户的技术挑战和架构演进之路 PPT》
《微信后台基于时间序的海量数据冷热分级架构设计实践》
《微信技术总监谈架构:微信之道——大道至简(演讲全文)》
《如何解读《微信技术总监谈架构:微信之道——大道至简》》
《快速裂变:见证微信强大后台架构从 0 到 1 的演进历程(一)》
《17 年的实践:腾讯海量产品的技术方法论》
《移动端 IM 中大规模群消息的推送如何保证效率、实时性?》
《现代 IM 系统中聊天消息的同步和存储方案探讨》
《IM 开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?》
《IM 开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议》
《IM 开发基础知识补课(四):正确理解 HTTP 短连接中的 Cookie、Session 和 Token》
《WhatsApp 技术实践分享:32 人工程团队创造的技术神话》
《微信朋友圈千亿访问量背后的技术挑战和实践总结》
《王者荣耀 2 亿用户量的背后:产品定位、技术架构、网络方案等》
《IM 系统的 MQ 消息中间件选型:Kafka 还是 RabbitMQ?》
《腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面》
《以微博类应用场景为例,总结海量社交系统的架构设计步骤》
《快速理解高性能 HTTP 服务端的负载均衡技术原理》
《子弹短信光鲜的背后:网易云信首席架构师分享亿级 IM 平台的技术实践》
《知乎技术分享:从单机到 2000 万 QPS 并发的 Redis 高性能缓存实践之路》
《IM 开发基础知识补课(五):通俗易懂,正确理解并用好 MQ 消息队列》
《微信技术分享:微信的海量 IM 聊天消息序列号生成实践(算法原理篇)》
《微信技术分享:微信的海量 IM 聊天消息序列号生成实践(容灾方案篇)》
《新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践》
《一套高可用、易伸缩、高并发的 IM 群聊、单聊架构方案设计实践》
《阿里技术分享:深度揭秘阿里数据库技术方案的 10 年变迁史》
《阿里技术分享:阿里自研金融级数据库 OceanBase 的艰辛成长之路》
《社交软件红包技术解密(一):全面解密 QQ 红包技术方案——架构、技术实现等》
《社交软件红包技术解密(二):解密微信摇一摇红包从 0 到 1 的技术演进》
《社交软件红包技术解密(三):微信摇一摇红包雨背后的技术细节》
《社交软件红包技术解密(四):微信红包系统是如何应对高并发的》
《社交软件红包技术解密(五):微信红包系统是如何实现高可用性的》
《社交软件红包技术解密(六):微信红包系统的存储层架构演进实践》
《社交软件红包技术解密(七):支付宝红包的海量高并发技术实践》
《社交软件红包技术解密(八):全面解密微博红包技术方案》
《社交软件红包技术解密(九):谈谈手 Q 红包的功能逻辑、容灾、运维、架构等》
《即时通讯新手入门:一文读懂什么是 Nginx?它能否实现 IM 的负载均衡?》
《即时通讯新手入门:快速理解 RPC 技术——基本概念、原理和用途》
《多维度对比 5 款主流分布式 MQ 消息队列,妈妈再也不担心我的技术选型了》
《从游击队到正规军(一):马蜂窝旅游网的 IM 系统架构演进之路》
《从游击队到正规军(二):马蜂窝旅游网的 IM 客户端架构演进和实践总结》
《IM 开发基础知识补课(六):数据库用 NoSQL 还是 SQL?读这篇就够了!》
《瓜子 IM 智能客服系统的数据架构设计(整理自现场演讲,有配套 PPT)》
>> 更多同类文章 ……
(本文同步发布于:http://www.52im.net/thread-2807-1-1.html)