今天给大家分享一个 X/ Twitter 早期系统架构演变的故事,内容来自《数据密集型应用系统设计》这本书,具体数据来自 X/ Twitter 在 2012 年 11 月发布的分享。
推特的两个主要业务是:
-
发布推文(Tweets)。用户可以向其粉丝发布新消息(平均 4.6k 请求 / 秒,峰值超过 12k 请求 / 秒)。
-
主页时间线(Timeline)。用户可以查阅他们关注的人发布的推文(300k 请求 / 秒)。
仅仅处理峰值每秒 12000 次写入还是很简单的,然而推特的扩展性挑战并不是主要来自推特量,而是来自扇出(fan-out)- 每个用户关注了很多人,也被很多人关注。
扇出:从电子工程学中借用的术语,它描述了输入连接到另一个门输出的逻辑门数量。 输出需要提供足够的电流来驱动所有连接的输入。在事务处理系统中,我们使用它来描述为了服务一个传入请求而需要执行其他服务的请求数量。
大体上讲,这一操作有两种实现方式。
方法一:发布推文时,只需将新推文插入全局推文集合即可。当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。在下图所示的关系型数据库中,可以编写这样的查询:
SELECT tweets.*, users.*
FROM tweets
JOIN users ON tweets.sender_id = users.id
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_user
X/ Twitter 的第一个版本使用了方法一,但是随着用户的增长,系统很难跟上主页时间线查询的负载(300k 请求 / 秒)。
方法二:为了应对高查询负载,为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱,如下图所示。
当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。 因此读取主页时间线的请求开销很小,因为结果已经提前计算好了。
X/ Twitter 公司转向了方法二之后的效果更好,因为发推频率比查询主页时间线的频率几乎低了两个数量级,所以在这种情况下,最好在写入时做更多的工作,而在读取时做更少的工作。
然而方法二的缺点是发推文现在需要大量的额外工作。平均来说,一条推文会发往约 75 个关注者,所以每秒 4.6k 的发推写入,变成了对主页时间线缓存每秒 345k 的写入。而且这个平均值隐藏了用户粉丝数差异巨大这一现实,一些用户有超过 3000 万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的 3000 万次写入!及时完成这种操作是一个巨大的挑战,推特尝试在 5 秒内向粉丝发送推文。
在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可伸缩性的一个关键负载参数,因为它决定了扇出负载。其他应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。
X/ Twitter 轶事的最终转折:现在已经稳健地实现了方法二,推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并,如方法一所示。这种混合方法能始终如一地提供良好性能。
总结
系统架构具有复杂性和动态性,需要随着业务需求、技术革新等外部环境变化而不断调整和完善,而非仅仅通过预先设计就能够达到最优状态。推荐大家阅读《数据密集型应用系统》这本书,它可以帮助我们了解如何设计、构建和维护可靠、可扩展且易于维护的数据密集型应用系统。