题目与适用场景
请设计一个支持关注关系的个性化信息流系统。用户可以发布文字或图片帖子、关注或取关作者,并浏览按 相关性排序的首页信息流。删除帖子、改为私密、屏蔽作者和取关应及时影响后续读取;点赞、评论和推荐 模型只作为排序信号,本题不展开媒体转码、评论树、广告竞价或模型训练。
本题假设有 5000 万 DAU,每位活跃用户每天读取 20 页,每页 20 条;每天产生 1000 万条帖子。信息流 读取 p99 小于 200 毫秒,普通作者发帖到活跃粉丝可见的 p99 小于 5 秒。普通作者平均有 200 名符合 投递条件的粉丝,头部作者最多有 5000 万粉丝。数字与时限是面试约束,不代表任何现有产品的实际规模 或承诺。
这道题适合中高级后端、基础设施和系统设计面试。公开面试记录会直接追问写扩散、读扩散、大 V、游标 分页、缓存和一致性;一手工程资料还显示,真实信息流会把候选召回、聚合、过滤和多阶段排序拆开,并 保留可降级的已物化内容。因此回答重点是如何根据成本与正确性组合这些机制,而非背一个固定架构图。
面试官考察什么
第一项是能否先量化读写不对称。5000 万 DAU 乘每天 20 页等于 10 亿次读取,平均约 1.16 万 QPS; 若峰值为平均值的 5 倍,就是约 5.8 万 QPS。1000 万条帖子平均约 116 次创建每秒,但普通作者的 写扩散会把它放大到每天约 20 亿条时间线引用,即平均约 2.31 万次插入每秒。只比较原始发帖 QPS 会 低估真正的写入压力。
第二项是能否说明推、拉与混合策略的适用边界。纯写扩散让读取便宜,却可能因一个 5000 万粉丝作者 产生 5000 万次插入;纯读扩散让发帖便宜,却要求每次读取合并大量关注对象。高质量回答会预计算普通 作者的候选引用,对高扇出作者在读取时拉取,并让阈值由活跃粉丝数、发帖率、队列预算和延迟目标共同 决定。
第三项是能否把帖子真源、候选集合与最终展示分开。时间线只保存轻量引用,不复制正文。聚合器先合并 预计算收件箱、头部作者近期帖子和推荐候选,再做权限、删除、屏蔽与去重,最后分层排序和填充。排序 模型的结果不能绕过可见性规则。
第四项是能否处理动态排序下的分页与一致性。分数会随互动和模型版本变化,单独使用 offset 或分数 会造成重复与跳项。答案要给出短期冻结的 feed session 或带排序版本的复合游标,并明确新帖、删除、 取关和权限变化在翻页期间如何表现。
最后是可恢复性和可证伪性。事件总线、缓存和扇出工作进程都会失败或重复执行。候选人需要用幂等引用、 积压监控、回源降级、墓碑过滤和修复扫描让系统收敛,并用大 V 突发、乱序事件、取关竞态、删除泄漏和 缓存故障验证设计。
回答前要确认的问题
- 信息流来源是什么? 本题以关注作者的帖子为主,可混入少量推荐候选;广告、群组和话题流不展开。
- 排序契约是什么? 默认相关性排序,要求内容新鲜且可解释地混合来源;不承诺严格时间顺序。
- 谁能看到帖子? 帖子有公开、仅粉丝和私密状态;屏蔽、删除和权限收紧必须优先于缓存命中与排名。
- 发帖后谁需要读到自己内容? 作者应立即读到自己的帖子;活跃普通粉丝目标为 5 秒内可见,离线
粉丝可在下次访问时补齐。
- 分页需要怎样稳定? 一次连续滚动会话中尽量无重复、无跳项;新帖可在刷新或新会话出现,删除和
权限收紧必须在当前会话立即生效。
- 关注关系规模如何分布? 分布高度倾斜,普通作者平均 200 名符合投递条件的粉丝,头部作者可达
5000 万;不能用平均值掩盖热点。
- 数据保留多久? 每位活跃用户预计算最近 500 条候选引用,帖子正文按产品政策保留;更老内容可
在读取时从作者索引和推荐存储补充。
- 多地域要求是什么? 读取就近服务,帖子在作者主地域提交后异步复制;跨地域新鲜度允许秒级,
可见性收紧走高优先级失效通道。
30 秒回答框架
“我会把帖子真源、关注图和每用户候选收件箱分开。发帖事务写帖子与 outbox;普通作者的事件由扇出 工作进程幂等写入活跃粉丝收件箱,头部作者只写作者近期索引,读取时再拉取。Feed 聚合器合并预计算、 头部作者和推荐候选,先做权限、删除、屏蔽和去重,再用轻量召回分数与精排生成短期 feed session, 游标按 session 和位置翻页。正文只在最后批量补全。删除和权限收紧写权威墓碑并走高优先级失效, 读取始终复核。队列积压时保留旧物化流并对近期关注作者做有界回源,监控发帖到可见延迟、重复率、 删除泄漏和各阶段 p99。”
分步深入分析
第一步:用容量推导架构预算
每日读取页数为:
50,000,000 DAU × 20 pages/day = 1,000,000,000 feed reads/day
1,000,000,000 / 86,400 ≈ 11,574 average read QPS
11,574 × 5 peak factor ≈ 57,870 peak read QPS每日帖子创建平均约 10,000,000 / 86,400 ≈ 116 QPS。若所有普通作者平均向 200 名活跃粉丝 投递,则每天产生约 20 亿条候选引用,平均 2,000,000,000 / 86,400 ≈ 23,148 次插入每秒。 单个 5000 万粉丝作者发一条就会超过普通流量的许多秒预算,因此必须绕开同步全量扇出。
每位 5000 万活跃用户保留 500 条候选,共 250 亿条引用。若每条引用把 postid、authorid、 初始分数、时间和标志粗略按 64 字节计算,逻辑下限约 1.6 TB,尚未包含索引、存储引擎开销和副本。 这说明时间线应只保存引用、限制长度,并按活跃度分层存储;复制正文会迅速放大空间和失效成本。
第二步:定义所有权、API 与最小数据模型
帖子服务拥有正文、作者、创建时间、可见性、版本和删除墓碑;社交图服务拥有 following 与 followers 邻接索引;Feed 服务拥有候选引用、会话和展示记录。对象存储与 CDN 承载图片,时间线只 保存图片引用。核心接口可以是:
POST /v1/posts create a post with Idempotency-Key
DELETE /v1/posts/{post_id} write a deletion tombstone
PUT /v1/users/{id}/following/{author} follow an author
DELETE /v1/users/{id}/following/{author} unfollow an author
GET /v1/feed?cursor=...&limit=20 read one ranked page
GET /v1/posts/{post_id} hydrate one visible postposts 按 postid 保存权威状态,并有 (authorid, createdat, postid) 近期索引。follows 至少 能按关注者列出关注对象,也能按作者分片列出粉丝。feedinbox 按 viewerid 分区,排序键包含粗排 分数、创建时间和 postid;feedsessions 短期保存排序版本、候选 ID 序列与过期时间。展示事件和 互动事件进入独立日志,供排序特征与实验使用,不在读请求里同步回写所有候选。
发帖请求使用调用方幂等键。帖子与 outbox 事件在同一数据库事务提交,成功后作者可直接从帖子真源 看到内容。事件总线按至少一次传递,(viewerid, postid) 唯一或条件覆盖让重复扇出收敛;事件携带 帖子版本,旧事件不能复活已删除或已收紧权限的帖子。
第三步:为普通作者和头部作者使用混合扇出
普通作者发帖后,扇出服务读取符合条件的活跃粉丝分片,批量把轻量引用写入各自收件箱。工作进程使用 分片游标和事件 ID 记录进度;超时重试同一批,不能因重复消息生成两条候选。离线或长期不活跃用户不必 立即物化,可在其下次访问时按关注图补建近期候选。
头部作者的帖子只进入 (authorid, createdat) 近期索引和热点缓存。读取者关注了这些作者时,聚合器 并行拉取每位头部作者的有限条近期内容,与收件箱合并。阈值不应硬编码成一个永久粉丝数:应估算 eligibleactivefollowers × postsperwindow 的写入成本,并与预计读取合并成本、队列余量和 5 秒 新鲜度预算比较。作者突然爆红或连续发帖时,控制面可以把其切到拉取模式;已入队的批次允许幂等完成 或取消,不能同时无界执行两种路径。
纯拉取仍会在用户关注大量头部作者时产生读扇出。聚合器要限制每个来源的候选数、并发和截止时间, 缓存作者近期索引,并在某个来源超时时返回其他合格候选。头部内容还可按地域或关注群组预聚合,但只有 测量表明合并成本是瓶颈时才增加这层复杂度。
第四步:把候选生成、过滤、排序和补全拆开
读取路径分四段:
- 从用户收件箱取一批普通作者候选,并从头部作者索引和推荐服务取有界候选;
- 按
post_id去重,检查关注关系、屏蔽、帖子墓碑、当前可见性和地域合规; - 先用便宜分数缩小候选,再用较重模型计算互动概率、时效、来源质量和负反馈,最后应用多样性与频控;
- 对最终 20 条批量读取帖子正文、作者摘要和聚合计数,生成响应并异步记录展示。
Meta 的公开工程说明把 Feed 聚合器描述为收集候选、对象和特征后再排序,并使用多阶段模型缩小计算; Pinterest 的公开设计也把未展示候选池、内容生成和已物化信息流分开。面试中不需要复刻具体实现,但这 证明“先复制完整帖子,再在缓存里排序”缺少必要边界。
安全与产品规则在排序前后都要守住。前置过滤减少无效计算,补全时再次检查版本可以挡住排序期间发生的 删除或权限收紧。模型分数只是排序输入,不能把不可见内容重新加入结果。互动计数允许最终一致;作者、 正文、可见性和墓碑来自权威帖子版本。
第五步:用 feed session 稳定动态排序分页
offset 会在新内容插入或分数变化时重复和跳过。单纯以 (score, post_id) 做 keyset 也不够,因为 精排分数在下一页请求前可能变化。首屏请求可以生成最多 500 个排序后的候选 ID,并写入 30 分钟的 feedsessionid;不透明游标包含 session、下一位置、查询指纹和签名。后续页面按位置读取,再对候选 做实时可见性检查和补位。
新帖在用户刷新或开始新 session 时出现,不插入当前列表中间。删除、屏蔽和权限收紧立即过滤,所以 当前 session 可能少一条;聚合器从 session 后续候选补足页面。若 session 缓存丢失或过期,服务端返回 可识别的游标过期结果,让客户端保留已展示 ID 并刷新,不能静默把旧位置套到新排名上。
如果 500 条冻结序列的存储成本不可接受,可保存排名 epoch 和复合 keyset,并让客户端提交已展示摘要; 这种方案存储更省,但重复、模型切换和删除补位更难。应根据连续滚动时长、允许的重复率和会话存储预算 选择,不把游标编码格式当成核心答案。
第六步:让关注、删除和权限变化正确收敛
关注成功后,用户下一次读取可直接拉取新作者的近期帖子,同时异步回填有限候选。取关与屏蔽先更新 权威社交图,读取路径立即过滤该作者,再异步清理收件箱;这样无需等待数百万条物理删除才能阻止展示。 关注事件与发帖事件乱序时,候选中携带的关系版本只能用于优化,最终可见性仍以读取时权威规则为准。
删除或改为私密时,帖子服务在提交新版本与墓碑的同一事务写高优先级 outbox。缓存失效、搜索清理和 收件箱清理都可以异步,但 Feed 补全必须批量读取当前版本并过滤。热墓碑缓存缩短检查延迟,持久帖子 记录防止缓存丢失后旧内容重现。后台扫描负责删除过期引用;物理清理不承担授权职责。
点赞和评论引起的分数变化不应重新写入所有粉丝收件箱。互动事件更新聚合计数和特征,活跃用户在下次 请求时重排候选;极热内容可更新共享特征缓存。这样接受短暂排序陈旧,避免每次互动再次产生全网扇出。
第七步:设计降级、恢复与多地域边界
扇出队列积压时,先保护帖子写入与已物化流读取。Feed 可以返回上一份合格候选,并对用户最近活跃的 关注作者做有界回源;页面应允许新鲜度下降,权限检查不能降级。若某作者导致队列热点,立即切换为拉取 模式并限制其未完成批次。恢复后按事件时间和版本幂等追赶,监控最老未处理事件,避免只看队列长度。
候选缓存故障时,可从持久收件箱读取较少候选或返回已保存 session;排序服务超时则使用确定性的时效 分数和多样性规则。帖子或社交图权威存储不可用时,系统可以返回已验证且仍在安全 TTL 内的内容,超过 权限缓存期限就应失败或缩小结果,不能以可用性为由展示可能已撤权的内容。
多地域下,帖子由作者主地域接受写入并产生全局 ID,事件异步复制到读取地域。普通内容允许秒级新鲜度, 作者本人通过主地域读后写保证立即可见。删除、屏蔽和权限收紧使用更高优先级复制,并在全局墓碑服务 确认前限制旧副本展示。跨地域主动—主动写关注图会引入关系冲突;只有明确要求后再增加冲突解决规则。
第八步:用指标与故障注入证明边界
核心指标包括发帖成功率、发帖到首个/95% 活跃粉丝可见延迟、按分片的扇出吞吐与最老积压、头部作者 拉取耗时、候选数、去重率、过滤率、粗排和精排 p99、补全批量大小、缓存命中、游标过期率、页面重复 率、空页率,以及删除或撤权内容泄漏数。最后一项应始终为零,并有合成探针主动验证。
故障注入至少覆盖:同一发帖事件重复和乱序;5000 万粉丝作者连续发帖;作者在推送中途从普通模式切到 头部模式;关注、取关与发帖并发;翻页期间删除或改私密;墓碑缓存丢失;一个粉丝分片变慢;事件总线 停机后追赶;排序服务超时;feed session 节点丢失;地域复制延迟。每个场景都断言页面无越权内容、 候选引用可收敛、延迟降级可观测,并且恢复不会重复放大写入。
高质量示范回答
“我先按 5000 万 DAU、每人每天 20 页计算,平均约 1.16 万次 feed 读取每秒,5 倍峰值约 5.8 万。 1000 万条日发帖只有约 116 QPS,但普通作者平均 200 名活跃粉丝会放大为平均约 2.31 万条候选插入 每秒;5000 万粉丝作者的一条帖子不能同步全推。
我会把帖子真源、社交图和候选收件箱分开。发帖事务写帖子和 outbox。普通作者的事件按粉丝分片异步 写入每用户收件箱,viewerid + postid 幂等;头部作者只写作者近期索引,Feed 读取时拉取并合并。 推拉阈值按活跃粉丝、发帖率、队列预算和读取成本动态决定。时间线只存引用,正文和图片在最后批量补全。
读取时,聚合器从收件箱、头部作者和推荐源取有界候选,先按当前关注、屏蔽、墓碑和可见性过滤、去重, 再做便宜召回、精排、多样性与频控。首屏把最多 500 个候选冻结成 30 分钟 feed session,游标携带 session 和位置;新帖等刷新出现,删除和权限收紧在每页补全时立即过滤并补位。
删除与改私密提交权威版本和高优先级失效事件,缓存和收件箱可以稍后清理,但每次展示必须复核。队列 积压时返回旧物化流,并对近期作者做有界回源;排序超时降级到时效分数。权限检查永不降级。最后用 发帖到可见延迟、最老积压、各排序阶段 p99、重复率和撤权泄漏监控系统,并注入大 V 突发、重复事件、 取关竞态、删除、缓存与地域故障验证恢复。”
常见错误
- 只比较原始读写 QPS。 发帖只有约 116 QPS,粉丝扇出才是主要写入量;应把写放大和头部尾部
分布一起计算。
- 纯写扩散到所有粉丝。 一个 5000 万粉丝作者会制造 5000 万次插入;应对高扇出作者读时拉取。
- 把“大 V 阈值”写成永久常量。 粉丝活跃度、发帖频率和队列余量都会变化;应基于成本与 SLO
动态分类并保留切换保护。
- 在时间线复制完整正文。 编辑、删除和权限变化会产生巨大的失效面;收件箱只保存轻量引用,展示
前补全并复核版本。
- 用
offset分页动态排名。 插入与重排会造成重复和跳项;使用短期 feed session 或带版本的
复合游标。
- 只在异步任务中过滤取关和删除。 清理延迟会泄漏内容;权威关系、墓碑和可见性必须在读取时检查。
- 声称事件总线提供端到端 exactly-once。 工作进程、存储和重试仍可能重复;用事件 ID、帖子版本
与候选唯一键实现幂等收敛。
- 降级时跳过权限检查。 陈旧排序和较少候选可以接受,越权展示不可接受;安全过滤有独立预算和
失败策略。
- 只测正常流量。 设计的关键风险来自头部作者、积压、乱序和权限竞态;故障注入必须覆盖这些边界。
追问及应对
追问一:普通作者突然获得 5000 万粉丝并连续发帖,怎样避免两种路径重复?
控制面按版本把作者切换到拉取模式。扇出任务读取作者模式版本,只允许开始前已认领的有限批次完成; 候选以 (viewerid, postid) 幂等,因此已推送与读取拉取合并后只保留一条。队列对单作者设配额,切换 期间的发帖直接进入头部作者索引。
追问二:用户取关后,旧 feed session 里还有该作者帖子怎么办?
session 只冻结候选顺序,不冻结授权。每页补全都批量检查当前关注与屏蔽关系;已取关作者被过滤,并从 后续候选补位。异步清理收件箱用于节省空间,不能承担即时权限语义。
追问三:怎样支持严格按时间排序?
复用同一候选来源和权限过滤,把排序键改为 (createdat, postid),使用带上界时间的 keyset 游标。 当前会话固定 snapshot_time,新帖在刷新后出现。头部作者与普通收件箱仍需归并,但无需精排 session。
追问四:排序模型发布后重复率突然升高,如何定位?
先按模型版本比较候选去重前后数量、session 生成 ID、游标位置和已展示记录,确认重复来自候选源、排序 输出还是会话丢失。若新模型给同一帖子不同来源别名,统一到权威 post_id;若 session 缓存抖动,回滚 模型也不会修复,应恢复会话存储并让客户端显式刷新。
追问五:如何判断混合扇出阈值是否合适?
离线重放真实关注度和活跃度分布,分别估算每位作者的预计算写入、队列等待、读取合并与缓存命中成本; 在线逐步调整阈值,观察发帖到可见 p99、Feed 读取 p99、总存储写入和降级率。阈值应带滞回,避免作者 在两种模式间频繁抖动。