关注页 Feed 架构设计
Twitter 架构演进
Twitter 的两个主要业务是:
- 发推文: 用户可以向其粉丝发布新消息(平均 4.6k 请求 / 秒,峰值超过 12k 请求 / 秒)。(2012年[2])
- 看推文: 用户可以查阅他们关注的人发布的推文(300k 请求 / 秒)。
1. 读扩散
读扩散处理每秒 12K 次发推文还是很简单的, Twitter 的挑战主要是 300K 查看推文请求.
看推文时,首先查找关注的所有人,查询这些被关注用户发布的推文, 并按时间顺序合并
SQL 伪代码:
SELECT * FROM posts
WHERE user_id IN (SELECT user_id FROM follows WHERE follower_id = :user_id)
ORDER BY created_at DESC
[1]
2. 写扩散
- 发推文时: 查找所有粉丝, 写入所有粉丝的 timeline 中
- 看推文时: 读取 timeline
3. 演进
- 推特第一版使用读扩散, 系统很难承受 300K看推文的负载
- 推特第二版使用写扩散, 一条推文平均推送 75 个粉丝, 所以 timeline 的写压力大约 345K, 读压力是 300K
写扩散看起来很美好, 但是隐患是一些用户有超过 3000 万的粉丝, 如果该用户发推文, 那么写入 3000 万用户的 timeline (而 Twitter 尝试在 5 秒内向粉丝发送推文)
最终: Twitter 已经稳健地实现了写扩散,逐步转向了读写混合模式
Twitter 在技术分享[2]中没有透露实现细节, 下面是我的架构设计
读写混合模式 架构设计
1. 名词解释
- 粉丝收件箱/timeline: 关注页的信息流(微博、朋友圈).
- 发件箱: 作者发布的内容
- 写扩散: 作者发布消息后, 立即推送给粉丝的 "粉丝收件箱"
- 读扩散: 作者发布消息后, 不推送给粉丝. 粉丝获取信息流时, 遍历关注列表中的作者, 拉取 "发件箱" 中的新内容.
- 大V: 粉丝数超过 10 万的用户定义为大V, 大V 不可降级为小V.
2. 作者发布内容时
- 小V : 采用写扩散模式, 写入 所有粉丝 的 "粉丝收件箱"
- 大V : 仅写入 活跃粉丝 的 "粉丝收件箱", 离线粉丝通过读扩散获取
流程图:
2.1. 什么是 "活跃粉丝"?
- 粉丝登录时, 查询自己关注的所有大 V, 注册到当天的 "活跃粉丝" 中
- 大 V 发内容时, 查今天的 "活跃粉丝" 和昨天的 "活跃粉丝",合并 2 个结果作为大 V 活跃粉丝 (1-2天 内登录的都是活跃粉丝)
<---- 昨日 ---->|<---- 今日 ---->
[ 活跃粉丝 ][ 活跃粉丝 ]
<------------------------->
合并后的大V活跃粉丝 (1-2天内登录)
好处:
- 大 V 查询对 关系表 的压力降低到 0 QPS
- 减少大 V 写扩散对 "粉丝收件箱" 的压力
1-2 天登录的用户有点多, 如何减少活跃粉丝量?
缩短时间, 比如 10-20 分钟存在心跳的用户
3. 粉丝登录时
- 查询用户关注的大V列表
- 查询"发件箱"
- 写入新内容到"粉丝收件箱"
- 计算红点
流程图:
4. 粉丝读取内容时
- 查询 "粉丝收件箱"
"粉丝收件箱" 数据结构 (Redis ZSet):
# Score: 内容更新时间戳
# Value: 内容ID
content_id_1 -> 1704240000 (对应 2024-01-03 00:00:00)
content_id_2 -> 1704153600 (对应 2024-01-02 00:00:00)
...
5. 粉丝关注作者时
主线逻辑:
- 查询作者最近发布内容(大V有缓存), 写入 "粉丝收件箱" (100ms)
分支逻辑:
- 判断作者是否大V: (1ms)
如果是:
- 修改粉丝 followBigV 表 (1ms)
如果不是, 且粉丝数不超 10 万:
- 结束
如果不是, 且粉丝数超 10 万:
- 该作者标记为大V (1ms)
异步:
- 查询作者所有粉丝 (10s)
- 为每个粉丝
followBigV
(关注的大V) 添加此新晋大V (100S)
6. 粉丝取消关注时
主线逻辑:
- 从粉丝收件箱删除作者内容 (大V 有缓存) (100ms)
- 如果是大 V, 从粉丝关注的大V列表删除作者
详细设计
1. 大V列表如何存储?
- 每个用户关注的大V列表, 因为数量有限, 存储在单个 key 中
- 预估: 用户关注 100 个大V, 该 key 约为 1KB
followBigV
数据结构:
{
// 用户关注的大 V ID 列表
"bigVIds": ["10001", "10002", "10003", "10004", "10005"],
// 上次登录刷新时间
"lastRefreshTime": "2024-01-01 00:00:00"
}
2. 如何降低"粉丝收件箱"的存储成本?
- 采用冷热数据分离.
冷备:
- 将 1 个月 未活跃用户的 "粉丝收件箱" 数据从 Redis 迁移到 DB, 标记冷备.
- 存在冷备标记
cold:<UserID> -> '1'
表示用户数据被冷备
用户再次登录加载冷备:
- 检查冷备标记.
- 若标记存在 则从 DB 读取数据重新加载到 Redis
用户登录流程图:
3. "粉丝收件箱"的写性能优化
- 采用批量写入 (Pipeline) 能带来 1-2 个数量级的提升 (Redis 单机可达 200-300 万 QPS)
参考
[1] DDIA:
http://ddia.vonng.com/#/ch1
[2] Twitter Timelines at Scale
https://www.infoq.com/presentations/Twitter-Timeline-Scalabil...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。