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