第 1 章 / 共 57 章 · 约 6 分钟
第 1 章:系统设计面试框架
本页目录
系统设计面试通常 45–60 分钟,题目往往只有一句话:“设计一个短链接服务”“设计 Twitter 首页 Feed”。这类题没有标准答案。面试官主要看三件事:你能不能把模糊的问题拆清楚,能不能在约束下做出合理取舍,能不能边做边把思路讲明白。
框架不是用来背的模板,它的作用是让你不漏掉关键环节,并且把时间花在最能拿分的地方。本章介绍一套五步流程,以及贯穿全程的估算方法和不同职级的评价差异。
总览:五步与时间分配
以 45 分钟为例(去掉开场寒暄和结尾提问,实际可用约 35–40 分钟):
| 步骤 | 建议时长 | 产出 |
|---|---|---|
| 1. 需求澄清 | ~5 分钟 | 3–5 条功能需求 + 量化的非功能需求 |
| 2. 核心实体 | ~2 分钟 | 系统里的主要“名词” |
| 3. API 设计 | ~5 分钟 | 对外接口签名 |
| 4. 高层设计 | ~10–15 分钟 | 能跑通所有功能需求的架构图 |
| 5. 深挖 | ~10–15 分钟 | 针对瓶颈和非功能需求的优化 |
查看图表源码
flowchart LR
accTitle: 答题框架:五个步骤与非功能需求
req["需求"] --> ent["实体"] --> api["API"] --> hld["高层设计"] --> deep["深挖"]
req --> nfr["非功能需求"]
nfr --> hld
nfr -->|"决定深挖往哪个方向走"| deep
第 1 步:需求澄清
功能需求用“用户可以……”的句式写,只保留 3–5 条核心功能。以 Twitter Feed 为例:
- 用户可以发推文(文字 + 图片)
- 用户可以关注其他用户
- 用户可以查看关注对象的推文,按时间倒序排列
同时明确说出不做什么(out of scope),比如“私信、广告、搜索先不考虑”。这样既省时间,也能让面试官看到你有范围意识。
非功能需求要量化,而且要和这道题相关:
- 规模:DAU 多少?读写比多少?(Feed 类系统通常读远多于写)
- 延迟:写成“Feed 加载 p99 < 300ms”,而不是“要快”
- 可用性与一致性:发推后几秒内别人能看到即可,说明可以接受最终一致
- 持久性:已发布的推文不能丢
常见误区:
- 问一长串与设计无关的问题,10 分钟就过去了。
- 面试官说“你来定”时犹豫不决。正确做法是直接给出合理假设并说出来:“我假设 2 亿 DAU,读写比约 100:1。”
第 2 步:核心实体
列出系统中的主要对象即可,例如 User、Tweet、Follow、Media。这一步不需要写完整表结构,字段可以在高层设计时按需补充。它的作用是和面试官对齐用词,同时为 API 设计打基础。
第 3 步:API 设计
默认用 REST;需要服务端主动推送时考虑 WebSocket 或 SSE;内部服务之间可以提 gRPC。
POST /v1/tweets body: { text, media_ids } → { tweet_id }
POST /v1/users/{id}/follow
GET /v1/feed?cursor=xxx&limit=20 → { items, next_cursor }
几个加分细节:
- 当前用户的身份从认证 token 中获取(请求头里的 JWT 或 session),不要让客户端在请求体里自己填
user_id,否则任何人都能冒充别人。 - 列表接口用游标分页(cursor)。数据频繁插入时,offset 分页容易出现重复或遗漏,深翻页也更慢。
- 写接口考虑幂等,例如让客户端带一个 idempotency key,防止网络重试导致重复发帖。
第 4 步:高层设计
目标是先拿出一个能工作的方案:逐条走通功能需求的数据流,不要一开始就追求完美。
查看图表源码
flowchart TD
accTitle: Twitter Feed 的高层设计
client["Client"] --> lb["LB / Gateway"]
lb --> tweet["Tweet Service"]
lb --> feed["Feed Service"]
tweet --> tdb[("Tweet DB")]
feed --> fcache[("Feed Cache")]
tdb -.->|"异步"| fcache
讲解时沿着请求路径叙述:“用户发推 → 网关鉴权 → Tweet Service 写库 → 异步更新粉丝的 Feed 缓存……”。遇到需要优化的地方,先标注“这里稍后深挖”,保持节奏,别在这一步陷进细节。
第 5 步:深挖
深挖的方向由非功能需求决定:规模带来扩展问题,延迟要求带来缓存问题,可用性要求带来单点故障问题。每个深挖点可以按下面的结构展开:
- 问题:例如“某个大 V 有 1 亿粉丝,写扩散(fan-out on write)一次就要写 1 亿条”
- 方案:至少给两个选项,例如写扩散、读扩散、二者混合
- 权衡:延迟、存储、实现复杂度、一致性
- 决定:结合本题需求选一个,并说明理由
Senior 及以上的候选人应该主动提出深挖点,同时也要灵活跟随面试官感兴趣的方向。
粗略估算(Back-of-the-Envelope)
估算只有一个目的:支撑设计决策。如果算出来的数字不会改变你的方案,就一句话带过。
| 量 | 近似值 |
|---|---|
| 一天的秒数 | 86,400 ≈ 10⁵ |
| 每天 100 万次请求 | ≈ 12 QPS |
| 每天 1 亿次请求 | ≈ 1,200 QPS |
| 峰值 QPS | 平均值 × 2~5(视业务而定) |
| 内存访问 / SSD 随机读 / 同机房往返 / 跨洲往返 | ~100ns / ~100μs / ~0.5ms / ~100ms 以上 |
示例:2 亿 DAU,每人每天刷 20 次 Feed → 每天 40 亿次读 ≈ 4×10⁹ ÷ 10⁵ = 4 万 QPS(精确值约 4.6 万),峰值按 3 倍算约 12–14 万 QPS。结论:单个数据库扛不住,需要缓存加读副本。这就是用估算推动设计决策。
存储:每天新增 5,000 万条推文,每条约 1KB → 50GB/天 → 一年约 18TB(不含图片视频)。说明几年的数据单机放不下,需要考虑分片;媒体文件则应放到对象存储。
不同职级的期望
| 维度 | Mid(L4/E4) | Senior(L5/E5) | Staff(L6/E6+) |
|---|---|---|---|
| 主导程度 | 在提示下推进,面试官引导较多 | 自己掌控节奏,很少需要提示 | 全程主导,像在主持一场技术评审 |
| 高层设计 | 能做出可行方案 | 方案完整,每个组件都有选择理由 | 方案简洁,主动避免不必要的复杂度 |
| 深挖 | 1–2 个点,讲清基本原理 | 多个点,能比较方案、讲清权衡 | 一眼找到最关键的难点,并讨论演进、运维、成本 |
| 技术深度 | 知道常见组件的用途 | 了解组件内部机制和适用边界 | 能结合实战经验指出方案在生产环境中的坑 |
注意,Staff 不等于“用更多组件”。很多时候 Staff 级别的表现恰恰是说出:“这里一个 Postgres 就够了,原因是……”。
常见失分点
章节测验
容量估算练习
误差 ±30% 以内算对
写 = 生成一个短链;读 = 一次跳转
- DAU
- 1 亿
- 每人每天写入
- 0.1
- 读 : 写
- 100 : 1
- 每次写入 / 读取的大小
- 500 B
- 保留期
- 5 年
- 峰值 / 平均
- × 3
- 单台应用服务器
- 5,000 QPS
- 单台存储服务器
- 10 TB · 3 副本