Feed流系统很常见,不同类型的Feed流产品架构存在差异,本文设计的产品是基于单向关系的时间排序Feed流,类似于微博。
存储
Feed流系统中的存储库主要存放具体的用户Feed消息,而Feed消息具备以下特点:
- 数据量大,很容易达到100 TB,甚至PB级别。
- 数据可靠性要求高,不能丢失数据。
因此根据以上特点,存储库有几个问题是设计的关键:
- 如何能支持100 TB,甚至PB级数据量?
- 数据量与成本成正比,如何能降低成本?
- 如何保证Feed不丢失?
- 确保消息ID在个人发件箱中严格递增,这样读取时只需要范围读取即可。由于个人发布的Feed并发度很低,用时间戳也能满足基本需求,但是当应用层队列堵塞,网络延迟变大或时间回退时,用时间戳还是无法保证严格递增。最好的方法是使用主键自增功能。
最佳的存储库应该是具有主键自增功能的分布式NoSQL数据库,但是在开源系统里没有完全符合特点的数据库,所以有两种存储方案:
表格存储也属于有序性的分布式NoSQL数据库,具有以下优势:
- 单表支持10万亿行+、10 PB+的数据量,再快的数据增长速度都无需担心。
- 数据按主键列排序,可保证有序性和可预期性。
- 单key读写延迟在毫秒级别,可保证响应时间。
- 两种实例类型:高性能实例可提供极佳的读写性能;容量型实例可提供低存储成本。
通过对比分析,表格存储在功能、性能、扩展性以及成本方面都要更加适合存储Feed消息。
使用表格存储后,系统的架构如图所示:
同步
方案选择
Feed消息的同步主要有读扩散和写扩散两种方案:
- 读扩散(拉模式):用户发送消息时存入自己的发信箱,并不主动推送给其他用户。当其他用户获取Feed流时,系统需要去各个用户的发信箱中主动拉取。
- 写扩散(推模式):用户发送消息时存入自己的发信箱,同时会主动推动给其他用户,写入他们的收件箱。当其他用户获取Feed流时,系统只需按顺序读取收件箱中的消息即可。
拉模式和推模式在很多方面完全相反,并且对Feed流产品的用户而言,刷新Feed流(读取)时的延迟敏感度要远远大于发布(写入)。
因此可以看出,推模式优于拉模式,但是推模式也有一个缺点:数据会极大膨胀。针对这个缺点,可以考虑采用推拉结合模式。
推拉结合方案
虽然使用推模式可以满足Feed流系统需求,但随着用户数量增长,数据量也急剧增长。在推模式的工作下,数据量会膨胀得更多。针对这个缺点,可以考虑采用推拉结合的推动方案。
推拉结合是指对大V采用拉模式,而普通用户使用推模式。此时发布Feed和获取Feed流的过程如下:
- 发布Feed
- Feed消息先进入一个队列服务。
- 先从关注列表中读取到自己的粉丝列表,以及判断自己是否是大V。
- 将自己的Feed消息写入个人页Timeline(发件箱)。如果是大V,写入流程到此结束。
- 如果是普通用户,还需要将自己的Feed消息写给自己的粉丝,如果有100个粉丝,那么就要写给100个用户,包括Feed内容和Feed ID。
- 第三步和第四步可以合并在一起,使用BatchWriteRow接口一次性将多行数据写入表格存储。
- 发布Feed的流程结束。
- 读取Feed流
- 先读取自己关注的大V列表。
- 通过GetRange读取自己的收件箱。范围起始位置是上次读取到的最新Feed的ID;结束位置可以是当前时间,也可以是MAX,建议是MAX值。
- 如果有关注的大V,则再次并发读取每一个大V的发件箱。如果关注了10个大V,那么则需要10次访问。
- 合并2和3步的结果,然后按时间排序,返回给用户。
如果使用大V/普通用户的切分,架构存在一定风险。例如某个大V突然发了一个很有话题性的Feed,那么就有可能导致整个Feed产品中的所有用户都没法读取新内容。以粉丝读取流程为例:
- 大V发送Feed消息。
- 大V使用拉模式。
- 大V的活跃粉丝(用户群A)开始通过拉模式读取大V的新Feed。
- Feed内容太有话题性,快速传播。
- 未登录的大V粉丝(用户群B)开始登录产品,登录进去后自动刷新,再次通过读3步骤读取大V的Feed内容。
- 非粉丝(用户群C)去大V的个人页Timeline里面去围观,再次需要读取大V个人的Timeline,同读3。
结果就是,平时正常流量只有用户群A,结果现在却是用户群A + 用户群B + 用户群C,流量增加了好几倍,甚至几十倍,导致读3路径的服务模块被打到server busy或者机器资源被打满,导致读取大V的读3路径无法返回请求。如果Feed产品中的用户都有关注大V,那么基本上所有用户都会卡死在读取大V的读3路径上,然后就没法刷新了。
所以设计时需重点关心下面两点:
- 单个模块的不可用,不应该阻止整个关键的读Feed流路径。如果大V的无法读取,但是普通用户的要能返回,等服务恢复后,再补齐大V的内容即可。
- 当模块无法承受这么大流量的时候,模块不应该完全不可服务,而应该能继续提供最大的服务能力,超过的拒绝掉。
优化方法
- 方法一:不使用大V/普通用户的优化方式,而使用活跃用户/非活跃用户的优化方式,这种优化方式能把用户群A和部分用户群B分流到其他更分散的多个路径上去。而且即使读3路径不可用,仍然对活跃用户无任何影响。
- 方法二:完全使用推模式可以彻底解决这个问题,但会增大存储量,并增长大V微博发送总时间,从发给第一个粉丝到发给最后一个粉丝可能要几分钟时间(一亿粉丝,100万行每秒,需要100秒),还需要为最大并发预留好资源(如果使用表格存储,则不需要考虑预留最大额度资源的问题)。
此外,还有更复杂的基于推荐的Rank流,这里就不展开介绍了,直接网上搜下就可以找到。