系统设计 34:内容平台跨模块保证的四条路径
发布按钮返回成功、时间线出现一张卡片、搜索可以找到正文、好友收到通知,是四个不同的确认。把上传、时间线、搜索、消息和通知组合到同一张图上,并不会使这些确认自动同步。本篇用四条跨模块路径检查它们在哪里可以延迟,在哪里必须拒绝继续。
教学平台支持图片与短文本发布、关注时间线、公开内容搜索、私信和站内通知。排除广告竞价、推荐模型训练、视频转码和真实外部推送。核心不变量是未提交的上传不可见、已确认删除的内容不通过新读取泄露、客户端重连不会重复展示同一消息。搜索和通知允许延迟,但延迟不能变成权限旁路。
为每种状态确定权威来源
元数据采用 posts(id, owner, object_key, state, version, visibility),状态依次为 UPLOADING、LIVE、DELETED。对象上传与数据库无法仅凭两次成功响应成为原子事务,因此只有对象校验完成、元数据进入 LIVE 后才允许读者看到。未引用对象由扫描任务延迟回收,回收前检查上传会话和引用,避免删除仍在提交的对象。
发布接口 POST /posts 携带创建幂等键;上传会话另有 POST /uploads 与 POST /uploads/{id}/complete。删除 DELETE /posts/{id} 更新版本与墓碑;评论或点赞必须引用内容 ID 并经过可访问性检查。通知事件保存 (post_id, version, recipient, event_id),不把完整正文复制进所有投递任务。
时间线和搜索是派生视图,只提供候选 ID 与排序信息。真正返回正文前批量检查权威状态和权限。这样删除确认可以先保证“新读取不返回”,物理清理索引、时间线和对象允许稍后完成。若搜索结果本身包含敏感标题或片段,过滤必须发生在结果序列化之前;只在点击详情时鉴权仍会泄露摘要。
flowchart TD
C[客户端] -->|上传字节| O[(对象存储)]
C -->|提交上传与发布幂等键| P[发布服务]
P -->|原子写内容状态及 Outbox| D[(元数据权威库)]
D -.->|版本化发布删除事件| E[事件分发]
E -.->|候选 ID| F[时间线物化]
E -.->|文本及版本| S[搜索索引]
E -.->|收件人工作项| N[通知队列]
C -->|列表或搜索| R[聚合读服务]
R -->|取候选| F
R -->|取候选| S
R -->|批量授权与状态过滤| D
N -->|投递前再次检查| D
最小架构可以先在同一应用与数据库内实现发布、授权和消息,后台线程处理派生视图。只有搜索查询或扇出吞吐与事务写入产生冲突时,才把派生路径独立扩容。相比一开始拆成十个服务,这样保留了较短的事务边界;代价是进程故障影响更多功能,需要隔离线程池、队列与资源预算。
算容量时不要漏掉放大项
假设每天二十万次发布、每张图片 1 MB、平均三十个关注者,原图每日 200 GB、三十天两份 12 TB;图片读取每天四百万次,平均传输 200 KB 缩略图,则每日约 800 GB 出口。正文与状态每篇 1 KiB,三十天三份约 18.432 GB,远小于图片与出口,不能只依据数据库体积估算成本。
发布平均 200000/86400≈2.31 post/s、八倍峰值 18.52。写扇出产生 18.52×30≈555.6 条/s 时间线写入。每项 80 B,日新增 480 MB;若平均关注者变成三百,写放大增至 5556 条/s、日新增 4.8 GB,图片上传量并未改变。热点作者拥有百万关注者时,平均数失去指导意义,应对热点作者改读取合并或延迟物化。
目标暂设发布 p99 小于 500 ms,时间线读 p99 小于 250 ms,公开内容在三秒内进入搜索,通知 99% 十秒内完成。不同路径分别测量:发布不等全部扇出才确认,搜索新鲜度从元数据提交到可查询计时,通知延迟包含排队。元数据可用而搜索滞后时,允许发布与详情继续工作,并展示搜索延迟状态;不能伪装成强一致搜索。
四条路径逐项审查
发布路径先上传对象,再完成校验,最后原子提交 LIVE 元数据及可靠事件记录。上传成功但元数据失败只产生不可见孤儿;元数据成功但响应丢失由幂等键回查原内容,不能再生成一份。事件重复送达靠处理 ID 去重,派生视图还要核对版本,去重本身不能解决旧事件晚到。
删除路径先在权威库写入更高版本墓碑,再确认客户端。时间线、搜索和通知可能暂时仍有候选,但所有新读取与待投递任务必须重新检查。异步消费者把索引 v2 删除后,收到 v1 发布应拒绝,防止内容复活。已经发出的邮件、截图或客户端缓存不在服务端可撤回保证内;若需要外部通知,产品契约应明确这条边界。
断线路径以每个会话或收件箱的稳定序号恢复,客户端提交最后已展示序号。服务端重发可能包含已收到但未确认的消息,客户端按消息 ID 去重。游标只代表该会话的进度,不是整个系统的全局时间;消息删除与权限变化仍按权威规则处理,不能拿离线消息缓存绕过当前权限。
积压路径要区分创建事件和收件人工作项。一个热点作者的发布能扩成大量投递,消费者追不上时优先延迟非关键通知,控制每租户预算,并保留重放位点。若创建事件已可靠保存,队列服务暂时不可用不一定要求回滚内容发布;但延迟必须可见,并且日志保留期不能早于恢复完成。
sequenceDiagram
participant C as 作者
participant D as 权威库
participant Q as 派生事件队列
participant S as 搜索索引
participant R as 搜索读取
C->>D: 发布 p1,version=1
D->>Q: 发布事件 v1
Q->>S: 索引 v1
C->>D: 删除 p1,version=2
D-->>C: 删除确认
Note over Q,S: 消费积压,索引仍有旧片段
R->>S: 获得候选 p1
R->>D: 检查当前状态
D-->>R: DELETED,禁止序列化片段
Q->>S: 删除 v2
Q->>S: 迟到发布 v1,被版本条件拒绝
两种跨模块保证的代价
一种方案等所有模块更新完成才确认发布或删除,用户能获得更直观的同步结果,但任何搜索、通知或扇出故障都会拖累核心写入。另一种方案只让权威状态与事件记录同步提交,派生模块异步更新,在读取时执行当前授权。本题选择后者,新增代价是读放大和对权威授权路径的依赖。
若每次列表有二十个候选,逐项 RPC 会放大延迟;应批量查询状态并限制候选数量。授权缓存不是天然禁止,但其可见性上界必须与删除承诺相容;若要求删除确认后所有新读立即拒绝,普通 TTL 缓存不能独立完成该保证。权威状态不可用时返回受控错误,不以旧索引结果作为“降级成功”。
Google SRE 数据完整性章节提醒多存储之间的引用和恢复关系会影响整体正确性。这里的具体状态机和四路径评审是独立教学设计;引用并不表示采用该公司的产品架构。灾难恢复也要恢复墓碑和事件位点,只恢复对象文件可能使已删内容重新进入索引。
运行 python3 examples/system-design/labs/34/run.py。模型拒绝未上传完成的内容,重复发布仅形成一个时间线候选;删除后旧候选仍存在,未经授权过滤的负例返回 p1,而安全路径为空;断线游标重放删除事件后索引停在 v2,旧 v1 不能覆盖。JSON 输出和源码哈希位于 examples/system-design/evidence/34/。这是事件顺序与授权检查模型,没有真实对象存储、消息服务器、搜索引擎或浏览器连接,不证明生产跨系统事务。
面试追问应沿保证传播展开:搜索积压时删除如何生效;通知已经发出还能撤回什么;数据库故障时哪些模块能继续服务;恢复对象快照后怎样避免墓碑丢失。回答只说“最终一致”还缺少具体允许的中间状态。
[PATTERN] 派生视图负责候选与性能,权威状态负责可见性;异步允许晚到,不能允许较旧事件改变较新的业务事实。
实验附件与导航
可运行实验源码 · 本次原始结果系列导读;容量和数据承诺分别沿用系列的方法,本文数字为独立教学假设。






