系统设计 00:系统设计题究竟交付什么
系统设计题的交付物不是一张画满组件的架构图,而是一份能被反驳的决策记录:要求是什么,数字如何得到,写入何时算成功,故障时哪些承诺还能成立。图只负责让这份记录里的数据路径清楚可见。
以“用户保存一个链接,取得分享地址,后来可以撤销”为贯穿练习。它不是任何产品的内部实现。这里先确定交付物,后续章节再分别研究需求、规模和组件。
先定下要交付的答案
教学场景只支持创建、查看、撤销;用户登录已有,接收方凭分享地址访问。不做推荐、搜索、离线同步和全站爬取。不可违反的约束是:创建接口超时后重试不得产生两份不同的分享;撤销确认后,新的读取不能再返回内容。地址可能被转发,不能把“知道链接”称作身份认证;如果内容有隐私要求,必须增加接收者鉴权,这会改变读路径。
用于练习的服务目标是:按 30 天滚动窗口,成功读取占有效读取请求的比例不低于 99.9%,读取 p99 小于 250 ms,创建 p99 小于 500 ms。可用性目标需要说明是否剔除非法请求;这里按鉴权通过、格式正确且尚未撤销的请求定义分母。p99 是成功请求样本分布的分位值,不是平均值;即使写下目标,也不意味着设计已达到目标。撤销一致性是功能约束,不能被可用性百分比冲掉。
候选人最后至少交付五样东西:带排除项的不变量与 SLO、带单位的峰值和保留期假设、接口和数据主键、可以画出确认点的最小路径、何时演进及怎样验证的条件。每项都能被追问替换一个前提。
数字让“先单机”有讨论依据
以下数字全是教学假设:10 万位创作者,每人每天创建 2 个链接;每个链接当天有 10 次读取,故写入 20 万次/日、读取 200 万次/日,读写比 10:1。全天均匀平均分别约 2.31 write/s、23.15 read/s;把峰值按均值的 8 倍预算,约 18.52 write/s、185.19 read/s。这是预测到达率,不是压测所得容量。
每条元数据含业务记录 800 B、索引 160 B、日志分摊 160 B;按 365 天、3 份完整副本估算:200000 条/日 × (800+160+160) B/条 × 365 日 × 3 = 245280000000 B ≈ 245.28 GB ≈ 228.43 GiB。这里故意把索引和日志算进同一个 365 天保留期,以暴露预算假设;实际日志可能短留,也可能多个索引叠加,必须按部署重算。若响应有效载荷均为 1024 B,峰值下只算有效载荷的出口约 185.19 × 1024 × 8 ≈ 1.52 Mbit/s,不是 1.52 MB/s,也没有包括 TCP/TLS 和回源等开销。
只更改一项假设:峰值从 8 倍变为 40 倍。读峰值变成 925.93 read/s,写峰值 92.59 write/s,保留容量不变。为了展示决策翻转,暂定某个读路径预算为 500 read/s;它只是练习阈值,不是测过的机器上限。8 倍时不用急着加读副本;40 倍时要先测数据库读、应用线程和网卡谁先到界,再考虑缓存、只读副本或增加实例。单凭估算无法决定选哪一个。
最小架构只有一个事实来源
接口草案:POST /shares 携带目标 URL 和客户端生成的幂等键,返回 share_id;GET /shares/{share_id} 读取目标;DELETE /shares/{share_id} 需要所有者凭证,成功后不再公开读取。GET 是安全方法,不能借一次“访问”隐含撤销;HTTP 方法的安全与幂等含义可参见 RFC 9110 §9.2.1–9.2.2。POST 不天然可重试:若需要重试,必须由接口自行实现幂等语义,而不是仅凭方法名称推断(RFC 9110 §9.2.2)。
关系模型草案为 shares(share_id 主键, owner_id, target_url, state, created_at, revoked_at, version)、requests(owner_id, idempotency_key, share_id, UNIQUE(owner_id, idempotency_key))。share_id 不暴露自增计数,生成时碰撞要依赖唯一约束拒绝而非概率承诺;requests 的键需明确有效期及重用行为。按 share_id 精确读取;按所有者列举不是本章功能,不为未出现的访问模式先造索引。表行和请求去重记录需要在同一提交边界生效;具体数据库事务行为尚未做组件实测,这里仅是设计契约。
flowchart LR
C[创建者或读者] -->|POST 创建 / GET 读取 / DELETE 撤销| A[HTTP 服务]
A -->|事务写 shares 与 requests;按 share_id 读取| D[(主数据存储)]
D -->|提交结果 / 状态与目标 URL| A
A -->|创建确认 / 内容或已撤销| C
创建的确认点是数据和去重键都提交之后;读取直接检查状态;撤销在存储提交后返回成功。这里没有消息队列、异步回填或缓存,避免在强撤销约束上提前制造第二份事实。内容可以另存对象存储,但仅当负载形态变成大文件时再引入;此练习的 URL 元数据不需要。
用失败时间线检验确认点
sequenceDiagram
participant C as 客户端
participant A as HTTP 服务
participant D as 主数据存储
C->>A: POST /shares,幂等键 K
A->>D: 同一事务写分享行与 K 映射
D-->>A: 提交成功,share_id=S
Note over A,C: 返回途中连接中断,客户端超时
C->>A: 重试 POST /shares,仍使用 K
A->>D: 读取 K 对应已提交的 S
D-->>A: S
A-->>C: 返回 S,不创建第二个分享
C->>A: DELETE /shares/S
A->>D: 设置 state=revoked 并提交
D-->>A: 提交成功
A-->>C: 撤销确认
C->>A: GET /shares/S
A->>D: 读取状态
D-->>A: revoked
A-->>C: 不返回目标 URL
如果提交失败,服务不能返回“创建成功”。如果连接在成功提交后断开,重复请求用相同键找回既有 share_id。并发重试需要数据库唯一约束及事务在真实组件里验证;光画时序图不能证明它。撤销与正在途中的读取还要约定并发可见性:这里承诺的是“撤销确认之后开始的新读取”;撤销确认前已经读取目标但迟到的响应不在该承诺内,若不接受须改契约。
一个诱人的替代方案是先把内容放进缓存或异步队列,立即向客户端返回成功:创建的表面延迟会下降,但队列丢失或消费失败时成功响应失真;撤销后缓存未失效时还会泄露旧目标。另一个选择是读取时使用只读副本;它改善主库读负载,却要解决复制延迟下的撤销可见性。先让读取经过主数据源,直到实测证明读瓶颈存在。需要扩展时,先定义撤销状态的失效上界、回退到权威读取的条件,并注入延迟检查承诺是否仍成立。
一次可复核的最小练习
用 Python 3.12.3 标准库做固定输入算术模型,仓库中保存了 examples/system-design/labs/00/capacity.py 和 examples/system-design/evidence/00/capacity.md,记录命令、退出码及失败输入。本地运行得到:峰值 8 倍时 185.19 read/s < 500 read/s;只把峰值改为 40 倍时 925.93 read/s > 500 read/s;--peak -1 被拒绝,退出码 2。实验只核算计算和阈值翻转,不曾压测服务、验证数据库事务或声称达到 p99 SLO。
面试练习可以分配 5 分钟确认需求、8 分钟算规模和写接口、12 分钟讲最小路径、15 分钟解释超时和撤销、5 分钟反向核对承诺。这是练习安排,不是面试评判标准。追问“读取增加到 40 倍、撤销仍必须立即生效”时,回答应从测量点、回源机制和强撤销边界重新推导,而不是在原图上直接添一个 CDN。
可迁移的规则是:先写下不可违反的读写承诺,再找唯一确认点;规模数字负责排除不必要的复杂度,瓶颈证据负责决定增加哪种复杂度。下一篇研究这些承诺如何从模糊需求中提取。
参考资料
- RFC 9110, HTTP Semantics, §9.2:安全与幂等方法。这里只用它支持 HTTP 方法语义,不用它证明本例数据库事务。
- Design Gurus, Grokking the System Design Interview 公开目录。仅供选题;本篇结构、数字、图、模型均独立撰写。






