cdan 2019-07-03
简介
这些产品都是 Feed 流类型产品,由于 Feed 流一般是按照时间“从上往下流动”,非常适合在移动设备端浏览,最终这一类应用就脱颖而出,迅速抢占了上一代产品的市场空间。
Feed 流是 Feed + 流,Feed 的本意是饲料,Feed 流的本意就是有人一直在往一个地方投递新鲜的饲料,如果需要饲料,只需要盯着投递点就可以了,这样就能源源不断获取到新鲜的饲料。 在信息学里面,Feed 其实是一个信息单元,比如一条朋友圈状态、一条微博、一条咨询或一条短视频等,所以 Feed 流就是不停更新的信息单元,只要关注某些发布者就能获取到源源不断的新鲜信息,我们的用户也就可以在移动设备上逐条去浏览这些信息单元。
当前最流行的 Feed 流产品有微博、微信朋友圈、头条的资讯推荐、快手抖音的视频推荐等,还有一些变种,比如私信、通知等,这些系统都是 Feed 流系统,接下来我们会介绍如何设计一个 Feed 流系统架构。
Feed 流本质上是一个数据流,是将 “N 个发布者的信息单元” 通过 “关注关系” 传送给 “M 个接收者”。
Feed 流系统是一个数据流系统,所以我们核心要看数据。从数据层面看,数据分为三类,分别是:
针对这三类数据,我们可以有如下定义:
设计 Feed 流系统时最核心的是确定清楚产品层面的定义,需要考虑的因素包括:
上述是选择数据存储系统最核心的几个考虑点,除此之外,还有一些需要考虑的:
Feed 流系统设计
上一节,我们提前思考了 Feed 流系统的几个关键点,接下来,在这一节,我们自顶向下来设计一个 Feed 流系统。
1. 产品定义
第一步,我们首先需要定义产品,我们要做的产品是哪一种类型,常见的类型有:
接着,再详细看一下这几类产品的异同:
类型关注关系是否有大 V时效性排序微博类单向有秒~ 分时间抖音类单向 / 无有秒~ 分推荐朋友圈类双向无秒时间私信类双向无秒时间
上述对比中,只对比各类产品最核心、或者最根本特点,其他次要的不考虑。比如微博中互相关注后就是双向关注了,但是这个不是微博的立命之本,只是补充,无法撼动根本。
从上面表格可以看出来,主要分为两种区分:
用户数很少的时候,就比较简单,这里我们主要考虑 亿级用户 的情况,因为如果系统能支持亿级,那么其他量级也能支持。为了支持亿级规模的用户,主要子系统选型时需要考虑水平扩展能力以及一些子系统的可用性和可靠性了,因为系统大了后,任何一个子系统的不稳定都很容易波及整个系统。
2. 存储
我们先来看看最重要的存储,不管是哪种同步模式,在存储上都是一样的,我们定义用户消息的存储为存储库。存储库主要满足三个需求:
所以,存储库最重要的特征就是两点:
综上,可以选为存储库的系统大概有两类:
特点分布式 NoSQL关系型数据库(分库分表)可靠性极高高水平扩展能力线性需要改造水平扩展速度毫秒无常见系统Tablestore、BigtableMySQL、PostgreSQL
所以,结论是:
如果使用 Tablestore,那么存储库表设计结构如下:
主键列第一列主键第二列主键属性列属性列列名user_idmessage_idcontentother解释消息发送者用户 ID消息顺序 ID,可以使用 timestamp。内容其他内容
到此,我们确定了存储库的选型,那么系统架构的轮廓有了:
3. 同步
系统规模和产品类型,以及存储系统确定后,我们可以确定同步方式,常见的方式有三种:
用图表对比:
类型|推模式|拉模式|推拉结合模式
写放大|高|无|中
读放大|无|高|中
用户读取延时|毫秒|秒|秒
读写比例|1:99|99:1|~50:50
系统要求|写能力强|读能力强|读写都适中
常见系统|Tablestore、Bigtable 等 LSM 架构的分布式 NoSQL|Redis、memcache 等缓存系统或搜索系统 (推荐排序场景)|两者结合
架构复杂度|简单|复杂|更复杂
介绍完同步模式中所有场景和模式后,我们归纳下:
如果选择了 Tablestore,那么同步库表设计结构如下:
主键列第一列主键第二列主键属性列属性列属性列列名user_idsequence_idsender_idmessage_idother解释消息接收者用户 ID消息顺序 ID,可以使用 timestamp + send_user_id,也可以直接使用 Tablestore 的自增列。发送者的用户 IDstore_table 中的 message_id 列的值,也就是消息 ID。通过 sender_id 和 message_id 可以到 store_table 中查询到消息内容其他内容,同步库中不需要包括消息内容。
确定了同步库的架构如下:
4. 元数据
前面介绍了同步和存储后,整个 Feed 流系统的基础功能完成了,但是对于一个完整 Feed 流产品而言,还缺元数据部分,接下来,我们看元数据如何处理:
Feed 流系统中的元数据主要包括:
我们接下来逐一来看。
4.1 用户详情和列表
主要是用户的详情,包括用户的各种自定义属性和系统附加的属性,这部分的要求只需要根据用户 ID 查询到就可以了。
可以采用的分布式 NoSQL 系统或者关系型数据库都可以。
如果使用 NoSQL 数据库 Tablestore,那么用户详情表设计结构如下:
主键顺序第一列主键属性列 -1属性列 -2…字段名user_idnick_namegenderother备注主键列,用于唯一确定一个用户用户昵称,用户自定义属性用户性别,用户自定义属性其他属性,包括用户自定义属性列和系统附加属性列。Tablestore 是 FreeSchema 类型的,可以随时在任何一行增加新列而不影响原有数据。
4.2 关注或好友关系
这部分是存储关系,查询的时候需要支持查询关注列表或者粉丝列表,或者直接好友列表,这里就需要根据多个属性列查询需要索引能力,这里,存储系统也可以采用两类,关系型、分布式 NoSQL 数据库。
如果使用 Tablestore,那么关注关系表设计结构如下:
Table:user_relation_table
主键顺序第一列主键第一列主键属性列属性列Table 字段名user_idfollow_user_idtimestampother备注用户 ID粉丝用户 ID关注时间其他属性列
多元索引的索引结构:
Table 字段名user_idfollow_user_idtimestamp是否 Index是是是是否 enableSortAndAgg是是是是否 store是是是
查询的时候:
4.3 推送 session 池
思考一个问题,发送者将消息发送后,接收者如何知道自己有新消息来了?客户端周期性去刷新?如果是这样子,那么系统的读请求压力会随着客户端增长而增长,这时候就会有一个风险,比如平时的设备在线率是 20%~30%,突然某天平台爆发了一个热点消息,大量休眠设备登陆,这个时候就会出现“查询风暴”,一下子就把系统打垮了,所有的用户都不能用了。
解决这个问题的一个思路是,在服务端维护一个推送 session 池,这个里面记录哪些用户在线,然后当用户 A 发送了一条消息给用户 B 后,服务端在写入存储库和同步库后,再通知一下 session 池中的用户 B 的 session,告诉他:你有新消息了。然后 session-B 再去读消息,然后有消息后将消息推送给客户端。或者有消息后给客户端推送一下有消息了,客户端再去拉。
这个 session 池使用在同步中,但是本质还是一个元数据,一般只需要存在于内存中即可,但是考虑到 failover 情况,那就需要持久化,这部分数据由于只需要指定单 Key 查询,用分布式 NoSQL 或关系型数据库都可以,一般复用当前的系统即可。
如果使用 Tablestore,那么 session 表设计结构如下:
主键列顺序第一列主键第二列主键属性列列名user_iddevice_idlast_sequence_id备注接收者用户 ID设备 ID,同一个用户可能会有多个设备,不同设备的读取位置可能不一致,所以这里需要一个设备 ID。如果不需要支持多终端,则这一列可以省略。该接收者已经推送给客户端的最新的顺序 ID
5. 评论
除了私信类型外,其他的 feed 流类型中,都有评论功能,评论的属性和存储库差不多,但是多了一层关系:被评论的消息,所以只要将评论按照被被评论消息分组组织即可,然后查询时也是一个范围查询就行。这种查询方式很简单,用不到关系型数据库中复杂的事务、join 等功能,很适合用分布式 NoSQL 数据库来存储。
所以,一般的选择方式就是:
主键列顺序第一列主键第二列主键属性列属性列属性列字段名message_idcomment_idcomment_contentreply_toother备注微博 ID 或朋友圈 ID 等消息的 ID这一条评论的 ID评论内容回复给哪个用户其他
如果需要搜索评论内容,那么对这张表建立多元索引即可。
6. 赞
最近几年,“赞”或“like”功能很流行,赞功能的实现和评论类似,只是比评论少了一个内容,所以选择方式和评论一样。
如果选择了 Tablestore,那么“赞表”设计结构同评论表,这里就不再赘述了。
系统架构中加了元数据系统后的架构如下:
7. 搜索
到此,我们已经介绍完了 Feed 流系统的主题架构,Feed 流系统算是完成了。但是 Feed 流产品上还未结束,对于所有的 feed 流产品都需要有搜索能力,比如下面场景:
这些内容搜索只需要字符匹配到即可,不需要非常复杂的相关性算法,所以只需要有能支持分词的检索功能即可,所以一般有两种做法:
使用搜索引擎,将存储库的内容和用户信息表内容推送给搜索系统,搜索的时候直接访问搜索系统。
使用具备全文检索能力的数据库,比如最新版的 MySQL、MongoDB 或者 Tablestore。
所以,选择的原则如下:
如果使用 Tablestore,那么只需要在相应表上建立多元索引即可:
系统架构中加了搜索功能后的架构如下:
8. 排序
目前的 Feed 流系统中的排序方式有两种,一种是时间,一种是分数。
我们常用的微博、朋友圈、私信这些都是时间线类型的,因为这些产品定义中,需要我们主动关注某些人后才会看到这些人发表的内容,这个时候,最重要的是实时性,而不是发布质量,就算关注人发布了一条垃圾信息,我们也会被动看到。这种类型的产品适用于按照时间线排序。这一篇我们介绍的架构都是基于时间类型的。
另外一种是不需要关注任何人,我们能看到的都是系统希望我们看到的,系统在后台会分析我们的每个人的爱好,然后给每个人推送差异化的、各自喜欢的内容,这一种的架构和基于时间的完全不一样,我们在后续的推荐类型中专门介绍。
9. 删除 Feed 内容
在 Feed 流应用中有一个问题,就是如果用户删除了之前发表的内容,系统该如何处理?因为系统里面有写扩散,那么删除的时候是不是也要写扩散一遍?这样的话,删除就不及时了,很难应对法律法规要求的快速删除。
针对这个问题,我们在之前设计的时候,同步表中只有消息 ID,没有消息内容,在用户读取的时候需要到存储库中去读消息内容,那么我们可以直接删除存储库中的这一条消息,这样用户读取的时候使用消息 ID 是读不到数据的,也就相当于删除的内容,而且删除速度会很快。除了直接删除外,另外一种办法是逻辑删除,对于删除的 feed 内容,只做标记,当查询到带有标记的数据时就认为删除了。
10. 更新 Feed 内容
更新和删除 Feed 处理逻辑一样,如果使用了支持多版本的存储系统,比如 Tablestore,那么也可以支持编辑版本,和现在的微博一样。
11. 总结
上面介绍了不同子功能的特点和系统要求,能满足需求的系统主要有两类,一类是阿里云的 Tablestore 单系统,一类是开源组件组成的组合系统。
架构实践
上面我们介绍了 Feed 流系统的设计理论,具体到不同的类型中,会有不同的侧重点,下面会逐一介绍。
朋友圈
朋友圈是一种典型的 Feed 流系统,关系是双写关系,关系有上限,排序按照时间,如果有个人持续产生垃圾内容,那就只能屏蔽掉 TA,这一种类型就是典型的写扩散模型。
我们接下来会在文章《朋友圈类系统架构设计》中详细介绍朋友圈类型 Feed 流系统的设计。
微博
微博也是一种非常典型的 Feed 流系统,但不同于朋友圈,关系是单向的,那么也就会产生大 V,这个时候就需要读写扩散模式,用读扩散解决大 V 问题。同时,微博也是主动关注类型的产品,所以排序也只能是时间,如果按照推荐排序,那么效果就会比较差。
接下里会在文章《微博类系统架构设计》中详细介绍微博类型 Feed 流系统的设计。
头条
头条是最近几年快速崛起的一款应用,在原有微博的 Feed 流系统上产生了进化,用户不需要主动关注其他人,只要初始浏览一些内容后,系统就会自动判断出你的喜好,然后后面再根据你的喜好给你推荐你可能会喜好的内容,训练时间长了后,推送的内容都会是你最喜欢看的。
后面,我们会在文章《头条类系统架构设计》中详细介绍头条类型 Feed 流系统的设计。
私信
私信也算是一种简单的 Feed 流系统,或者也可以认为是一种变相的 IM,都是单对单的,没有群。我们后面也会有一篇文章《私信类系统架构设计》中做详细介绍。
总结
上面我们介绍了 Feed 流系统的整体框架,主要是产品定义、同步、存储、元数据、评论、赞、排序和搜索等内容,由于篇幅有限,每一章节都介绍的比较简单。读者如果对某一部分看完后仍然有疑问,可以继续再文后提问,我会继续去完善这篇文章,希望未来读者看完这篇文章后,就可以轻轻松松设计出一个亿级规模的 Feed 流系统。
另外,我们也欢迎有兴趣的读者一起来完成这个系列,帮忙实现朋友圈、微博、头条或者私信类型的文章,有任何问题都欢迎来讨论。
本文转载云栖社区。
更多干货、咨询 点击“了解更多“