知识卡片

Twitter主页时间线的扇出问题揭示写时多做与读时多做的权衡

结构图卡

内容

描述一个系统的负载不能只看总吞吐量,还要看”扇出”——服务一个请求需要引发多少其他 操作。Twitter的两个核心操作(发推文/读主页时间线)有两种实现方式,直接体现了一条 贯穿整个分布式系统设计的权衡:方法一(读时聚合)是发推文只需插入全局推文集合,用户 查看主页时间线时才现场查询他关注的所有人、实时合并结果——写入很轻,但读取要现场做 大量join工作;方法二(写时扇出)是为每个用户维护一份主页时间线缓存(”收件箱”), 发推文时立刻查出所有粉丝、把这条推文写入每个粉丝的收件箱——读取时直接查已经算好的 结果,很轻,但写入要做大量额外工作。Twitter最初用方法一,但扛不住时间线查询的负载, 转向方法二后效果更好,因为发推频率比查询时间线的频率低了近两个数量级,”多花时间在 低频操作(写)、少花时间在高频操作(读)”整体上更划算。但方法二有个致命的隐藏问题: 它假设每条推文的扇出规模是均匀的,而现实中粉丝数分布极不均匀,一个有3000万粉丝的 用户发一条推文,就要触发3000万次收件箱写入且要求几秒内完成。最终的解法是把两种方法 混合:大多数用户走方法二(写时扇出到粉丝收件箱),但少数拥有海量粉丝的”名人”被排除 在扇出机制之外,读取时间线时单独实时查询这些名人的推文再与收件箱结果合并——本质是 按”扇出规模”这个负载参数,对不同用户群体分别选用最适合他们的实现路径。

结构图

flowchart LR
    A[发推文/读时间线] --> B[方法一: 读时聚合]
    B --> B1[写入轻: 直接插入全局集合]
    B --> B2[读取重: 现场查询关注列表+合并]
    A --> C[方法二: 写时扇出]
    C --> C1[写入重: 查出全部粉丝, 写入每人收件箱]
    C --> C2[读取轻: 直接读已算好的收件箱]
    C2 -.粉丝数分布不均, 海量粉丝用户扇出规模失控.-> D[混合方案]
    D --> D1[普通用户走方法二写时扇出]
    D --> D2[名人用户排除扇出, 读时单独查询再合并]

参考来源

- 位置:《数据密集型应用系统设计》第一章《可靠性、可伸缩性、可维护性》"描述负载" (源文件:_epub-src/ch1_split_003.html) - 结论依据:原文详述Twitter两种实现方法(全局推文表查询 vs 每用户收件箱缓存)的 写读代价差异、方法二因粉丝数分布不均导致的极端扇出问题(3000万粉丝需5秒内完成 写入),以及最终转向排除名人用户的混合方案,直接支撑本卡片的权衡结构梳理。 - 原始内容:推特的第一个版本使用了方法1,但系统很难跟上主页时间线查询的负载。所以 公司转向了方法2……在这种情况下,最好在写入时做更多的工作,而在读取时做更少的 工作……一些用户有超过3000万的粉丝,这意味着一条推文就可能会导致主页时间线缓存的 3000万次写入……少数拥有海量粉丝的用户(即名流)会被排除在外。当用户读取主页时间线 时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并。