系统设计 19:信息流候选排序与权限撤销
早上预先给读者生成的候选列表,下午依然排着一条帖子;但帖子已经被删除,读者也不再关注另一条帖子的作者。只把新帖子送进新的候选列表,无法修复早上的旧列表。信息流的难处不是给一条帖子算出多少分,而是派生列表中的旧引用何时可以展示。
本篇的问题是 Facebook Newsfeed 类型的关注信息流,而不是介绍某家公司实际采用的架构。上一章时间线的推拉与热点作者只按时间归并;这里在候选生成后加入确定性排序,并让删除与权限变更越过旧候选生效。以下规模、时限和特征分数均为教学假设;实验使用真实 SQLite 内存数据库和固定候选列表,不是机器学习训练或线上排名效果。
先界定什么不能错
用户关注作者,作者发布公开或仅关注者可见的内容。GET /feed 返回当前用户能看的最多 20 条帖子,并给出继续读取的游标。作者能删除帖子、将可见范围收紧;读者取消关注后,不应通过旧收件箱继续读到仅关注者可见的帖子。候选可以延迟抵达,排序可以不是全局最优,但已经完成撤销的请求,不得靠旧候选继续泄露内容。
这个承诺要写清边界:本设计只保证撤销接口成功返回之后开始的新读请求,对当前权威状态做完检查才返回;撤销前已经开始并做完权限检查的在途响应不在承诺内。若需要连在途响应都阻断,就要引入更重的同步、会话中断或延迟确认协议。权限检查服务不可用时返回明确的业务错误,不把没有查到授权误报为“信息流为空”。删除正文副本、CDN 与搜索索引的物理清理仍要跟踪,但它们的积压不能成为继续展示内容的理由。OWASP Authorization Cheat Sheet强调按请求校验权限并默认拒绝;下面的读取门禁是本题对这一原则的具体设计,而非该文档对本系统的性能保证。
练习目标设为:从 API 入口到返回 p99 小于 150 ms;普通新帖 95% 在发布后 5 s 内成为候选;删除和权限收紧提交后的新请求不得展示撤销结果。p99 的测量窗口暂定滚动 24 小时,观测点在 API 网关入站至响应结束;两个新鲜度指标分别按发布提交时间和撤销确认时间计算。不存在这些目标的实测达成记录。排除训练排序模型、广告投放、实时反作弊、跨地域强一致和完整 GDPR 物理清除流程;这些不应借“排序服务”之名偷塞到核心承诺里。
从读写量找到预计算的代价
设每天新增 100 万帖、每帖事实文本与元信息合计 300 B,保留 30 天、事实表三份副本。逻辑量 100 万帖/天 × 300 B/帖 × 30 天 = 9 GB,三副本为 27 GB,不含图片、索引、日志和备份。一天 2000 万次首页读取,平均 2000 万/86400 = 231.48 request/s,假设峰值系数 8,约 1851.85 request/s。一天 100 万次发帖,平均 11.57 post/s,峰值系数 6 得 69.44 post/s。每个普通作者平均给 100 个读者写候选、每条引用假设 24 B,峰值写约 69.44 × 100 = 6944 reference/s,即 166656 B/s;一日写 2.4 GB 引用,保留三天、三副本约 21.6 GB,均为十进制 GB。
读时拉取另有成本。若每次从关注图与候选服务拉 100 条,按每条含特征引用平均 80 B 计,峰值内部传输预算约 1851.85 × 100 × 80 = 14.8 MB/s,尚未算网络协议、重试与批量权威状态检查。每次逐条走 100 次远程权限 RPC,延迟可能先于带宽成为瓶颈;所以门禁应批量查、做边界缓存时也必须声明撤销的同步语义。候选缓存即使命中 90%,不能因此省去本设计承诺的权威校验。上述数值是容量算式,非吞吐测量,稳态吞吐不能推出 p99。
敏感性不是只乘一个峰值系数:若平均关注者从 100 提到 1000,写候选峰值增为约 69444 reference/s,三天复制引用增至约 216 GB;一个拥有百万粉丝的作者仅发一帖便产生百万次候选写,不能假定均匀分摊。改为对热点作者读时拉取会节省其写入,代价是每次打开首页都要再取其近期帖。若读请求频率提高十倍而发布不变,读时拉的批量查询与门禁而非写扇出更可能成为瓶颈。是否划分热点作者,应依据真实访问分布重新测量,而不是给出一个通用粉丝阈值。
接口、事实与派生数据各司其职
写入契约可从 POST /posts 开始:传 author_id、正文、初始 visibility 和客户端幂等键,成功返回 post_id 与提交版本;DELETE /posts/{id} 写入墓碑,PATCH /posts/{id}/visibility 提交新可见性与版本,不能仅删缓存。关注关系由独立 PUT/DELETE /following/{author_id} 维护。GET /feed?cursor=...&limit=20 读候选、批量校验当前帖子及关注状态,然后排序、截断并返回 post_id, version, score, cursor;正文要在相同授权上下文下获取,不能让另一个无门禁的详情接口绕过检查。
权威模型是 post(post_id, author_id, body_ref, visibility, deleted, version, created_at) 与 follow(reader_id, author_id, state, version)。派生模型是 candidate(reader_id, post_id, materialized_at, feature_snapshot, source_version),可由作者流补充。候选只负责“可能相关”,既不是帖子事实,也不是权限凭证;删除候选可异步重试,查询时仍须重新鉴权。具体存储引擎、事务隔离和多地域权威读取尚未验证;跨记录撤销时采用什么提交原语,必须在真实组件上另做故障实验,不能由本篇的单连接 SQLite 实验推出。
教学排序式为 2 × affinity + freshness:两个字段都是示例整数而非学习特征,输出按得分降序、post_id 升序打破并列;实验先排完有限候选,再逐条鉴权,最后才返回。把当前读者的关联与实时互动作为特征可能改变排序,但不能改变授权门禁。排序前取有上界的候选窗口(例如 100 条)并过滤,再取 Top-20;如果先截断为 20 条才过滤,全部被删时第一页可能为空而第 21 条其实可见。若 100 条内仍不满一页,返回不足一页及 has_more/继续游标,或有界地继续扫描,不能无限补查。游标至少绑定读者、过滤条件与候选水位;跨页发生删除时允许页长变化,若要求稳定快照,需要额外会话版本,不能只在游标中保存末尾分数。
flowchart LR
W[发帖 删除 权限变更] --> D[(权威帖子与关注状态)]
D --> E[候选事件或作者近期流]
E --> C[(物化候选库)]
U[读者请求] --> G[信息流接口]
G --> C
G --> E
G -->|批量核对当前可见性| D
G -->|仅可见候选参加打分| R[确定性排序与有界分页]
R --> U
最小版其实只需事实表、关注图与读时有界归并;当关注规模和读流量让这一条路径超过预算,才引入物化候选。此后“存储一份排序后的 Top-20”也是可选优化,而不是排序与存储必须绑在一起:热点作者增量、特征更新、删除都会使静态顺序过时。把整页最终排序结果长期缓存,换来更便宜的读,同时多出读者维度的失效与安全验证负担。
一次权限收紧如何越过旧候选
假设物化候选里已有 p1、p2、p3。实验先将 p2 删除,再把 p1 从公开改为私有,旧候选列表保持不变。访客只能读到 p3,作者 alice 还能读到自己的 p1。取消关注后的好友权限传播属于更完整的候选设计,本实验只验证公开/私有及作者例外。
sequenceDiagram
participant A as 作者与读者
participant D as 权威状态
participant C as 旧候选库
participant F as 信息流读取
A->>D: 删除p2 收紧p1权限
D-->>A: 状态提交成功
Note over D,C: 清理事件滞留 候选仍有p1 p2
A->>F: 发起新的首页读取
F->>C: 获取有界候选
C-->>F: p1 p2 p3
F->>D: 批量校验帖子与关注状态
D-->>F: p2已删 p1仅作者可见
F-->>A: 过滤并返回p3
若校验服务超时,不能把旧列表当安全回退;应返回可观测错误,短时限流或等待权威恢复。如果期望“删除提交后所有节点的新读”都满足承诺,还必须验证读路径不会读落后的权限副本;可以强读权威主路径,或者把撤销版本作为读取门禁的下界并等待达标,代价是故障时读服务受限。物化候选恢复时以事实帖子与关注关系重建、核对水位和版本,再灰度切换;候选索引可回退,撤销事实不能回退。需要监测删除提交至被过滤的年龄、门禁不可用率、候选可见率和过量扫描,不只看排序 p99。
取舍要围绕承诺而不是组件名
| 方案 | 适用条件与收益 | 放弃或回退的理由 |
|---|---|---|
| 读时从事实表和关注图拉取、即算即排 | 规模小、读请求少;路径短、权限容易贴近事实 | 大关注列表和高读量使聚合贵;实测超过 150 ms 预算再加候选库 |
| 写时物化候选,读时过滤再排序 | 首页读取多、更新相对少;读时只处理小窗口 | 扇出、重放、热点作者及旧候选治理;热点写过载时改为混合拉取 |
| 写时预计算最终 Top-20 并直接返回 | 内容稳定且没有敏感权限时读极便宜 | 删除、取消关注和特征更新让旧页不可信;本题拒绝绕过权威检查 |
用固定数据打破“排序好了就安全”
执行 python3 -B examples/system-design/labs/19/feed.py:三个固定候选按 2*affinity+freshness 排序,初始为 p2,p1,p3;p2 删除、p1 私有化后,访客结果为 p3,作者 alice 的结果为 p1,p3。--unsafe 跳过读取门禁,旧列表仍泄露 p1、p2,程序识别到反例后退出 2。--authority-down 删除权威表后调用同一个 feed 入口,SQLite 查询失败,程序返回 AUTHORITY_UNAVAILABLE、空候选数组并退出 3。版本、源码哈希、命令、原始输出及退出码见 examples/system-design/evidence/19/RUN.md。
这是对固定排序和实际 SQLite 查询门禁的本地验证,不是缓存失效协议或端到端性能实验;真实事务提交与读副本一致性、用户私密内容的审计仍为未验证设计。面试里若追问“删除后旧候选怎么办”,应先给出新读请求的权限承诺与在途例外,再谈过滤、清理和恢复;追问“排序模型升级会影响删除吗”,答案应是不能:新模型改变分数,不能替代权威门禁。下一章社交内容搜索讨论同一删除事件进入倒排索引后为何还会有可搜索窗口。
参考资料
- OWASP Authorization Cheat Sheet:逐请求校验与默认拒绝的安全原则;具体新请求时间边界由本文教学契约定义。
examples/system-design/labs/19/feed.py与examples/system-design/evidence/19/RUN.md:本篇 SQLite 实验和本 checkout 的原始结果。






