Feed 设计:写扩散的利与弊#
在中小规模场景下,Feed 系统常常会采用 写扩散(Write Fan-out) 的模式,这种方式能快速迭代、快速上线,的确是一个值得推荐的起点。 但如果为了节省缓存,直接将 MySQL 数据库表设计成写扩散(收件箱模型),并在 DB 层直接读取加快访问,就会带来一些实际问题。
问题#
我们的产品现在的feed分发途径是这样的,以一个用户 A 发动态为例,写扩散模式的分发过程如下:
用户 A 发布 Feed
↓
分发 Consumer 查询 A 的粉丝列表
↓
将 Feed 写入所有粉丝的收件箱 (Inbox)plaintext这样做的好处是:
- 粉丝读取时非常快(直接读db收件箱即可)
- 实现简单,业务迭代速度快
- 初期避免了缓存的使用,降低了redis运维成本
这样的流程乍一看是没有什么问题的,但是仔细想想你就能发现:
一:关注前后的历史消息缺失#
这是db层面写扩散最大的致命问题。
-
关注后无法看到历史动态
- 关注用户 A 之后,只能看到关注后的 Feed,收件箱不会保存着用户之前,甚至关注前一秒发送的feed用户期望关注后能在首页立即看到 A 的部分历史动态,否则体验割裂
-
取关后再关注出现消息断层
- 如果中途取关,再次关注时,中间这一段时间的消息永远缺失,这会让用户的动态出现「消息漏洞」,这也是用户不能接受的
二:副本数量爆炸,数据库压力过大#
写扩散意味着一条 Feed 需要写入 所有粉丝的收件箱,这种架构对关系型数据库极不友好消,息副本量会呈指数级增长,如果你也使用的是mysql这样写性能较弱的关系型数据库,feed可能会成为让你的整个系统崩溃的第一个模块。
举个例子: 想象一个日用户活动量在100的网站,每个人都有10个关注,每个人每日会进行10个活动如点赞,收藏等(我们的网站模仿github activity feed,会记录用户活动),这样每日的增长量就会在万级别以上,对于一个中小规模的feed系统负载会有些大
而在 MySQL 环境下,单机写 QPS 稳定值在 1000 左右,如果出现一个500数量级别粉丝用户,一次操作就需要写5000条数据,极容易打爆数据库
问题三:扩展性差,难以支持复杂需求#
DB 层直接写收件箱的模式过于死板,扩展性很差,首先同步写入导致架构难以修改,其次,前期我们的项目对广告,推荐的要求不高,但再向后发展呢?广告/推荐需要的异步实时构建 Feed 流 的需求就很难实现。随着业务发展(如推荐、广告变现需求),DB 层的 Inbox 架构会变成负担。
为了缓解历史消息缺失问题,我们尝试过去构建一个补偿机制去进行处理,比如第一次关注用户时会选取被关注用户的前20个feed发送到关注用户的收件箱中,如果发现是取关后重新关注就会去进行查询并进行消息的补洞
然而这样做会带来更多副本写入,反而加重问题二的数据库压力。
推拉结合#
因此,成熟的 Feed 系统都会采用 推拉结合(Hybrid Push & Pull) 模式:
- 推(写扩散):在缓存层面构建inbox,去保证活跃用户的实时性(高频互动关系)
- 拉(读扩散):解决历史消息、弱关系(推荐,广告)场景,用于inbox层的构建
feed的设计业界已经非常成熟,这里推荐阅读推特系统设计 ↗和bilibili的动态设计 ↗去进行进一步的了解,推特作为业界标杆不用多说b站曾经也是同步写收件箱,中间使用纯拉模式(写扩散)进行过渡,最后实现推拉结合的架构。
总结#
-
如果是一个新项目,还是建议先用推模式
- 快速把系统设计出来,然后让产品去验证、迭代,等客户数大幅上涨到后,再考虑升级为推拉集合模式
- 如果可能,选择 KV 数据库 而不是 MySQL,提高写入能力上限
-
缓存不可或缺
- 成熟系统建议要在缓存层抽象 Inbox
- 异步构建缓存层,可以显著提升灵活性与可扩展性
-
最终形态:推拉结合
- 只用推 or 只用拉都会遇到瓶颈
- 推拉结合模式 是大规模系统的业界最佳解,拉/推模式可以用,但不要只用拉/推模式