系统设计题千变万化,但拆开来看,几乎每道题都是由少数几种路径拼起来的:一条上传路径、一条读路径、一条后台处理路径、一条推送路径。每种路径都有固定的形状、固定的难点和固定的标准动作。而在每个难点上,你都要做一个选择,并且要把它讲出来、守得住。
这个框架把这两半放进同一个结构里。Pattern 告诉你系统由什么组成;权衡告诉你在每个连接处怎么选。目标只有一个:拿到一道没见过的题,能说出它由哪几个已知的 pattern 组成,怎么把它们连起来,深入时该讨论什么,并且一路上的每个选择都讲得清楚。
网站上其他所有系统设计内容都挂在这个框架上:walkthrough 是 pattern 的完整示例,概念参考是第 3 层的机制,9 周计划是学习它们的顺序。
问题所在
高级系统设计面试里最常见的两种失败,看起来毫不相关。
第一种是背系统,而不是背形状。 你准备了 News Feed、Chat 和短链接。面试官让你设计一个卫星设备管理系统,或者在线判题,或者 feature flag 服务。准备过的系统一个都对不上,于是你从白纸开始,花十五分钟重新发现你本来就知道的东西,因为你把它们归档在 “News Feed” 下面,而不是 “一次写入要在很多别的地方被读到” 下面。
第二种是 “看情况”。 面试官一天要听到这句话三十次。能通过的候选人不会停在这里:他们说清到底看什么情况,选定一边,并说出自己放弃了什么。没通过的候选人要么罗列选项却不做决定,要么选了一边却不说代价。在面试压力下,工作记忆被占满,大多数工程师都会退回到不及格的版本。
这其实是同一个问题的两个高度。第一种是不知道难点在哪里;第二种是走到难点之后不知道怎么说。高级别的回答两者都要:拆题让你快速走到正确的连接处,权衡推理是你在每个连接处要说的话。
Important
核心要点: Pattern 能看出来,决策要推出来。题目会说 “用户上传视频”,但不会说 “这里有倾斜”。所以先用 pattern 拆题,再用决策深入,每个决策都用同样的四步讲清楚:权衡轴、场景、代价、决定。
框架:四层结构
| 层 | 是什么 | 回答的问题 | 怎么用 |
|---|---|---|---|
| 第 0 层:积木 | 负载均衡、无状态服务、缓存、数据库、日志 / 队列、对象存储、CDN、ID 生成 | 默认要画哪些框? | 每道题都有,不需要识别 |
| 第 1 层:路径 pattern | 10 种路径形状,外加 1 个扩展(昂贵计算) | 这道题由哪几条路径组成? | 从题目原文的信号里识别,画出框图 |
| 第 2 层:决策 | 7 个反复出现的决策:5 个在路径内部,2 个在路径之间;每个都用权衡四步来论证 | 每条路径上要选什么?怎么守住这个选择? | 深入讨论时的检查清单,资深信号主要来自这里 |
| 第 3 层:概念 | 具体机制:consistent hashing、saga、watermark、fencing token…… | 这个决策具体怎么实现? | 被追问细节时调用 |
两条贯穿始终的规则:
- Pattern 对应的是路径,不是整个系统。 不要说 “Instagram 是 feed 类题”,而要说 “Instagram 有一条大对象上传路径、一条 fan-out 路径、一条读路径和一条搜索路径”。组合由此变得自然。
- Pattern 能看出来,决策要推出来。 Pattern 可以从题目的措辞里认出来;决策认不出来,要靠问出那个问题才能暴露(“关注数的分布是什么样的?”)。
第 0 层:积木
这些组件出现在几乎每一个设计里,各自有默认的扩展方式。画框图时直接用,不必解释为什么需要它们,把话留给路径。
| 积木 | 默认扩展方式 | 深入阅读 |
|---|---|---|
| 负载均衡 + 无状态服务 | 水平扩展,服务本身不存状态 | 负载均衡与服务发现 |
| 缓存 | cache-aside,按内存(工作集)而不是 QPS 定规模 | 缓存模式 |
| 数据库(关系型 / KV / 宽列) | 按访问模式选型,按 key 分区,加 replica | SQL vs NoSQL · 索引 · 存储引擎 |
| 分区与复制 | partition key + leader/follower 或 leaderless | 分区 · 复制 · 一致性模型 |
| 日志 / 队列 | 按 partition 扩展,consumer group 消费 | 队列与日志 · 投递语义 |
| 对象存储 + CDN | blob 不可变,CDN 做读路径 | CDN 与边缘交付 · CDN 演练 |
| ID 生成 | 号段分配或 Snowflake 式 | 唯一 ID 生成 |
| API、鉴权、韧性 | REST / gRPC,token 鉴权,timeout + retry + circuit breaker | API 设计 · 认证与授权 · 韧性模式 |
Note
面试信号: 题目要你设计的就是积木本身时(分布式 KV store、Kafka、Memcached、分布式锁、ID 生成器),这道题就是 “把一个积木打开来讲”。此时的内容是分区、复制、quorum、version vector、repair 和 consensus(Quorum 与冲突解决 · 反熵 · 共识),全部在一个组件内部回答。这类题在 senior 以上越来越常见。分布式 KV Store 演练是这一类的示范答案,分布式数据库、队列、锁和缓存是其余部分。
第 1 层:10 种路径 Pattern
| # | Pattern | 识别信号(题目里会出现的话) | 标准形状 | 主导决策 | 代表系统 |
|---|---|---|---|---|---|
| P1 | 点查读(key lookup) | 按 ID 取;读远多于写;数据基本不变 | 多层缓存 + KV 存储;写路径保证 ID 正确 | C、D | 短链、商品页、个人主页 |
| P2 | 检索与排序(retrieval & ranking) | 搜索、推荐、附近、联想、比价;不能按 key 直接取 | 索引 + 漏斗:召回 → 排序 → 过滤;或向多个数据源 scatter-gather | A、D | 搜索、推荐、附近的司机、typeahead、机票搜索、RAG 召回 |
| P3 | 写后派生(write-then-derive) | 写一次,要在别处、以别的形态被读到 | 真相来源 + 事件 → 异步生成物化视图(fan-out、索引、计数) | A、C | Feed fan-out、搜索建索引、CDC |
| P4 | 事件聚合(event aggregation) | 计数、统计、排行、趋势、监控、告警;事件量远大于查询量 | 日志 → 窗口聚合 → 汇总存储;批处理对账 | D、C | 广告点击、指标监控、top-K、trending、排行榜 |
| P5 | 资源争用(contention) | 名额、库存、配额有限;“不能超卖”;出价;限流 | 分层削减流量 + 原子扣减 / 单写入者 + 带 TTL 的 hold | E、G | 秒杀、抢票、限流、拍卖、派单锁定 |
| P6 | 多步流程(workflow / saga) | 下单、支付、状态机;跨多个服务;要能回滚 | 持久的状态机 / orchestrator + 每一跳幂等 + 补偿 | G、F | 订单、支付、行程生命周期、agent 循环 |
| P7 | 异步作业(background jobs) | 后台任务、定时、批量、爬取、转码、重试 | 持久队列 / 调度器 + worker 池 + lease + 重试和 DLQ | G、E | 爬虫、任务调度器、通知发送、转码、在线判题 |
| P8 | 实时连接(real-time connections) | 实时、在线状态、推送、聊天、协同编辑、直播评论 | 连接网关只持有 socket + 路由注册表 + 持久存储作为真相 | B、G | Chat、在线状态、直播评论、协同文档 |
| P9 | 大对象(large objects) | 文件、图片、视频;上传、下载、同步 | 内容与元数据分离 + 分块和 content addressing + 先提交内容再提交指针 + CDN | F、C | 视频平台、文件同步、图片上传 |
| P10 | Control plane(期望状态下发) | 配置、部署、发布、固件升级、feature flag、实验配置;“让一批设备 / 机器达到某个状态” | 版本化的 desired state + agent 上报 actual state + reconcile 循环 + 分批 rollout 和回滚 | F、G | 设备 / IoT 管理、部署系统、配置中心、实验平台 |
| 扩展 | 昂贵计算(expensive compute) | 模型推理、GPU、按 token 计费 | 准入控制 + continuous batching + KV cache / prefix cache + 路由 | E、A | LLM serving |
容易混淆的边界
- P1 vs P9:都是 “内容不可变、热度倾斜、缓存就是架构”。P1 是小 key 的读,难点在 ID 正确性和缓存;P9 是大文件,难点在上传管道、处理管道和元数据一致性。
- P3 vs P4:P3 把一次写变成很多份视图(1 → N);P4 把很多事件变成少数几个数字(N → 1)。
- P5 vs P6:P5 是很多请求抢一个东西;P6 是一个请求跨很多服务。秒杀下单两者都有:抢库存是 P5,扣库存之后的支付和发货是 P6。
- P6 vs P7:P6 关心的是跨服务的正确性(补偿、一致);P7 关心的是大量任务的吞吐和调度(谁来做、何时做、做失败了怎么办)。
- P8 vs P10:P8 是把消息送到在线的客户端;P10 是让设备收敛到一个状态,设备可以长时间离线,回来后补齐的是状态,而不是一条条重放消息。
P1 点查读(key lookup)
- 识别信号:按 ID 或短 key 取数据;读写比在 100:1 以上;数据创建后基本不变。
- 核心矛盾:读路径的规模极大,缓存就是架构;但正确性在写路径上,比如任何两个 URL 不能共用同一个短码。
- 标准动作:
- 先估算,得出 “没有数据库能直接服务这个读量”,从而确定 CDN、应用缓存、KV 存储这几层。
- 用号段分配的计数器加 base62 生成 ID,从构造上消除冲突,而不是 “先检查再写入” 加重试。
- 缓存的规模由工作集的内存大小决定,而不是 QPS。
- 数据不可变时,缓存几乎不用失效,TTL 设得略短于过期时间即可。
- 点击分析这类副作用放进异步事件流(变成一条 P4 路径),不拖慢主路径。
- 代价:写入后有短暂的不一致窗口,对这类产品通常可以接受。
- 常见陷阱:把 ID 方案当作细节;缓存宕机时发生 cache stampede,没有提前说出请求合并(request coalescing)。
- 面试追问:为什么用 302 而不是 301?应用服务器崩溃时手里的 ID 号段怎么办?Redis 在峰值宕机会怎样?
- 锚点:短链接:读写比 100:1,缓存就是架构。
- 继续阅读:缓存模式 · 唯一 ID 生成
P2 检索与排序(retrieval & ranking)
- 识别信号:搜索、推荐、附近、联想、比价;结果不能按 key 直接取出,必须在一个大集合里找。
- 核心矛盾:语料库大到无法给每个条目打分,所以必须是漏斗;而每个阶段的工作和衡量指标都不同,混在一起就会出问题。
- 标准动作:
- 先用算术说明为什么需要漏斗(一亿个条目 × 每次打分的成本,远超延迟预算)。
- 召回阶段按 recall 评判,宽而重叠;排序阶段按 precision 评判,使用新鲜特征。
- 可见性(权限)在召回时就限定,而不是排序之后再过滤,否则既浪费算力,又可能返回空页面。
- 特征一个定义、两处物化,防止 training-serving skew。
- 搜索按文档分片,用 hedged request 和部分结果控制尾延迟。
- 地理 “附近” 查询是空间索引上的检索(geohash 要扫描 9 个格子)。
- 数据源在外部时(机票、酒店),检索变成 scatter-gather:并行请求、设超时、返回部分结果,并缓存短 TTL 的结果。
- 代价:多个阶段要分别维护;推荐系统自带反馈回路偏差,要靠探索来对冲。
- 常见陷阱:“直接用 Elasticsearch”;把召回当作更差的排序器;可见性只在排序后过滤;地理查询只扫描一个格子。
- 面试追问:training-serving skew 是什么?一个私密帖子泄露给了非好友怎么办?为什么要扫描九个格子?
- 锚点:信息流的追问 “从按时间改成按相关性排序”:先合并出候选帖子,再加一层打分。
- 继续阅读:帖子搜索 · 推荐系统 · 完整排序系统 · 地理空间匹配 · 倒排索引 · 两阶段排序 · ANN 索引 · 地理空间索引
P3 写后派生(write-then-derive)
- 识别信号:一次写入要在别处被看到,或要以另一种形态被查询:新帖子进入粉丝的 feed,商品更新进入搜索索引,一条记录要被两个方向查询。
- 核心矛盾:写入的成本与 fan-out 的规模成正比,而 fan-out 的规模遵循幂律,按平均值设计一定会在最极端的那个用户身上崩溃。
- 标准动作:
- 先问倾斜的分布,而不是平均值。
- 写路径只做权威记录和发一个事件,fan-out 放在异步的 worker 里。
- 普通用户在写入时 push,极端用户在读取时 pull,两条路径在读取时合并并去重。
- 用基于日志的消息流,因为重放是修复 fan-out bug 的手段。
- 数据模型围绕查询来设计,每个热查询都是一次单 partition 读,所以同一份关系常常存成两张互逆的表。
- 代价:要维护两条路径和一个阈值策略;读侧会出现新的热点(被 pull 的名人账号)。
- 常见陷阱:只为平均用户设计;在 inbox 里存排序分数(排序应在读取时计算);合并之后忘记去重。
- 面试追问:名人发帖时会发生什么?为什么需要两张关注表?用户发完帖刷新却看不到自己的帖子怎么办?
- 锚点:信息流:发一条帖子,异步 fan-out 到每个粉丝的 inbox。
- 继续阅读:热 key · SQL vs NoSQL · 队列与日志
P4 事件聚合(event aggregation)
- 识别信号:计数、统计、排行、趋势、监控、告警;事件洪流式写入,查询的是少数几个汇总数字。
- 核心矛盾:新鲜度与正确性。快速的流式路径能回答 “现在大概是多少”,但可能悄悄出错;计费这类场景要求 “什么是真的”。
- 标准动作:
- 把两者交给不同的负责方:流式路径负责新鲜度;基于不可变原始日志的批处理路径负责正确性,并允许覆盖流式结果(Lambda)。也可以只保留流式路径,靠重放日志修正(Kappa)。
- 流式内部处理好 event time、watermark 和迟到数据。
- Exactly-once 靠 dedup、原子 checkpoint 和事务型 sink 三者配合,并说明它在副作用离开框架的地方就结束了。
- 热点 key 用两阶段聚合:先加 salt 分散,再合并。
- 不要求精确时用近似结构(Count-Min Sketch + Space-Saving 做 top-K)。
- 时间序列系统的容量上限是 series 的数量(cardinality),而不是样本数,要在摄入时限制 cardinality。
- 代价:两条路径要维护并保持一致。
- 常见陷阱:做成一根 Kafka 到数据库的管子;说 exactly-once 却不说明边界;告警路径本身静默地失败。
- 面试追问:迟到事件怎么办?一个广告占了全部流量的 10% 怎么办?为什么 cardinality 才是指标系统的瓶颈?
- 锚点:广告点击聚合:点击事件洪流 → 按时间窗口聚合成计数。
- 继续阅读:指标监控 · Top K · 连续活跃聚合 · 流处理 · 近似计数 · 时序存储与基数
P5 资源争用(contention)
- 识别信号:名额、库存、配额有限;“不能超卖”;大量请求同时争同一个东西;出价;限流。
- 核心矛盾:难点是争用,而不是吞吐。正确性不是一个可以打开的开关,而是你把原子操作、key 和 TTL 放在哪里的结果。
- 标准动作:
- 让大多数请求在最便宜的一层就输掉:CDN、bot 校验、网关、准入 token / 虚拟排队,最后才到被分片的库存。
- 用原子的、带守卫的扣减,而不是先读后写。
- 用带 TTL 的 hold,防止崩溃把资源锁死。
- 需要严格顺序时(拍卖出价),用单写入者 actor 串行化。
- 多个资源一起获取时按固定顺序加锁,防止死锁。
- 决定失败的方向:对零容忍超卖的库存,宁可少卖,也不要用异步复制加 failover;限流器要明确 Redis 挂了是 fail-open 还是 fail-closed。
- 代价:一个需要重点保护的单点;部分请求被拒绝。
- 常见陷阱:在百万 QPS 下用数据库行锁;先读计数器再写计数器(两个网关都看到 “还剩 1 个”);hold 没有 TTL。
- 面试追问:两个顾客抢最后一件商品怎么办?一个 Redis shard 在刚卖光之后 failover 会怎样?限流器底下的 Redis 挂了怎么办?
- 锚点:Flash Deal:防超卖、削峰,大多数请求在前面几层就被挡掉。
- 继续阅读:限流器 · 拍卖 · 限流算法 · 分布式锁 · 分布式锁与 fencing · 隔离级别与 MVCC
P6 多步流程(workflow / saga)
- 识别信号:下单、支付、退款、行程生命周期;一个业务操作跨多个服务;中途失败要能回滚。
- 核心矛盾:跨服务的部分失败。不能用一个大事务锁住所有服务,但结果必须一致。
- 标准动作:
- 跨服务不用 2PC,用 saga:拆成一连串本地事务,每一步带一个补偿动作。
- Orchestrator 的状态必须持久化(它本身就是一个状态机)。
- 先重试,最后才补偿;不可补偿的步骤(发货、扣外部款)放在最后。
- 每一跳都带从业务操作推导出的 idempotency key。
- 用 outbox 解决 “写库” 与 “发消息” 之间的双写问题。
- 和外部系统(支付网关)之间做定期对账,因为外部无法保证 exactly-once。
- Agent 循环也是这个形状:每一次状态转换都 checkpoint,不可逆的动作前加审批关卡。
- 代价:最终一致;补偿逻辑复杂;要存储 idempotency key。
- 常见陷阱:“直接用分布式事务”;orchestrator 的状态放在内存里;补偿本身不幂等。
- 面试追问:补偿本身失败了怎么办?支付网关超时但其实扣款成功了怎么办?orchestrator 在中途崩溃会怎样?
- 锚点:订单处理:支付、库存、物流、通知四步,任何一步失败都要 saga 补偿。
- 继续阅读:分布式事务 · Agent 编排 · 幂等与 outbox
P7 异步作业(background jobs)
- 识别信号:后台任务、定时任务、批量处理、爬取、转码、发送通知、判题;“提交之后稍后完成”。
- 核心矛盾:大量任务要在有限的 worker 上可靠地执行:不丢、不重复执行出错、不被一个大租户或一批毒消息拖垮。
- 标准动作:
- 任务先持久化进队列或调度器,再返回 “已接受”。
- Worker 用 lease(visibility timeout)领取任务;lease 过期任务自动回到队列;执行结果带 fencing token,防止过期的 worker 覆盖结果。
- 投递是 at-least-once,所以任务处理必须幂等。
- 失败用指数退避加 jitter 重试,超过次数进 DLQ。
- 按优先级和租户分队列,防止低优先级或大租户饿死别人(通知系统:关键告警不能排在营销消息后面)。
- 定时和延迟任务用 timing wheel 或分桶的延迟队列,而不是轮询数据库。
- 多步作业(转码:切片 → 并行转码 → 写 manifest)是一个 DAG,每一步都是独立可重试的作业。
- 代价:任务状态的存储;重复执行的可能性始终存在,只能靠幂等吸收。
- 常见陷阱:用
SELECT ... WHERE run_at < now()轮询数据库做调度;lease 没有 fencing,两个 worker 同时执行;重试没有退避,造成雪崩;没有 DLQ,一条毒消息卡住整个队列。 - 面试追问:Worker 执行到一半崩了怎么办?同一时刻有一百万个任务到期怎么办?某个租户塞满了队列怎么办?
- 锚点:分布式任务调度器:worker 执行到一半挂了,lease 过期,任务回到队列。
- 继续阅读:通知系统 · 投递语义 · 时间轮与延迟队列 · 分布式锁与 fencing
Tip
为什么要单独练: 异步作业很少是题目的主角,所以备考材料常常把它散落在通知、转码和调度器里。但在高频题里(爬虫、调度器、在线判题、固件升级),它是出现最多的 pattern 之一。要把它当作一个 pattern 来练,而不是别的系统的附属细节。
P8 实时连接(real-time connections)
- 识别信号:实时、在线状态、推送、聊天、协同编辑、直播评论。
- 核心矛盾:连接是有状态的,绑定在某台服务器上,但投递路由必须是全局的。真正的负载往往是 “一亿个长连接”,而不是消息量。
- 标准动作:
- 比较 WebSocket、SSE、long polling,再做选择。
- 连接服务器只持有 socket;用 registry 或 pub/sub 回答 “这个用户的 socket 在哪台机器”。
- 投递是 at-least-once + 客户端去重,用 idempotency key 变成用户视角的 exactly-once。
- 顺序只在会话内保证:同一个会话的消息落在同一个 partition owner 上。
- 持久化存储才是真相,推送通知只是唤醒信号;离线用户上线后从存储补齐。
- 协同编辑:每个文档一个 owner worker 做单写入者;离线编辑的需求决定 OT 还是 CRDT。
- 代价:连接集群、持久化的投递管道和离线推送是三个相互耦合的系统。
- 常见陷阱:默认选 WebSocket;把推送当作投递机制;连接服务器部署时一次性断开所有连接,引发重连风暴。
- 面试追问:用户两台设备同时发送,顺序怎么保证?连接服务器重启时会发生什么?群聊有一万人时消息怎么 fan-out?
- 锚点:聊天系统:“这个用户的 socket 在哪台机器?”
- 继续阅读:协同编辑器 · 实时连接模式 · 负载均衡与服务发现
P9 大对象(large objects)
- 识别信号:文件、图片、视频;上传、下载、同步、备份、转码。
- 核心矛盾:内容与元数据的性格相反:内容不可变、体积巨大;元数据可变、需要事务一致。
- 标准动作:
- 把内容和元数据拆成两个系统分别设计。
- 内容用 hash 做 content addressing,按固定大小的 chunk 存放,同时得到断点续传、去重、增量同步和廉价的版本。
- 上传时先提交内容、最后提交指针,这样崩溃留下的是垃圾,而不是损坏。
- 处理(转码、缩略图)是派生数据,交给 P7 的作业;视频按 “分段 × 画质” 并行转码,最后写 manifest。
- 分发靠 CDN,设计的对象是 miss 流:请求合并、shield 层、第二次命中才准入。
- 变化用版本化的新 URL 表达,purge 只是应急手段。
- 文件同步:设计的是 “同步记账”(sync cursor、冲突时两份都保留),而不只是传输。
- 代价:垃圾回收变得棘手:引用计数分布在分片的元数据层上,GC 要跨 shard 协调。
- 常见陷阱:“一个 blob store 加一个 CDN 加 FFmpeg”;一次 POST 上传整个文件;只设计缓存这个方框;设计了文件传输而不是文件同步。
- 面试追问:从上传到播放走一遍管道;CDN 缓存失效怎么做;一百万用户同时请求一个新对象时 origin 会怎样?
- 锚点:文件存储与同步:先传内容,最后提交指针,崩溃留下的是垃圾,不是损坏。
- 继续阅读:视频流媒体 · CDN · CDN 与边缘交付
P10 Control plane(期望状态下发)
- 识别信号:配置、部署、发布、固件升级(OTA)、feature flag、实验配置;“让一批设备或机器达到某个状态”;设备可能长时间离线。
- 核心矛盾:control plane 要可靠地改变成千上万个 data plane 节点的状态,但节点会离线、会失败、改错了要能撤回,而且 control plane 本身不能成为 data plane 的故障点。
- 标准形状:
- Desired state:版本化,存在一个小而强一致的存储里,是唯一的真相。
- Actual state:由 agent 上报,是派生出来的视图。
- Reconcile 循环:比较两者的差异,推动节点往期望状态走;循环本身幂等,可以随时重跑。
- Rollout 控制器:分批发布,每批之间有 health gate,触发就停止并回滚。
- 标准动作:
- 声明式而不是命令式:下发 “应该是什么状态”,而不是 “执行这条命令”。命令式丢一条就会状态错乱。
- 每次修改递增版本号;并发修改用版本号做 CAS。
- Agent 用 pull(长轮询)为主、push 通知为辅,离线回来后对比版本补齐差异,而不是重放所有历史命令。
- 分批 rollout(1% → 10% → 50% → 100%),health gate 来自 data plane 的指标;节点本地保留上一个可用版本,失败自动回退。
- Static stability:control plane 整体不可用时,data plane 用最后一份有效配置照常工作。
- 连接是间歇的(卫星过境窗口)时,在窗口外把要下发的批次预先算好,窗口内只负责发送。
- 代价:两套状态之间总有一段收敛延迟;rollout 让发布变慢。
- 常见陷阱:一条条下发命令;一次性全量下发;data plane 每次请求都去 control plane 读配置,control plane 一挂全挂;没有回滚路径。
- 面试追问:设备离线三天回来怎么办?发布到 10% 时错误率上升会发生什么?两个运营方同时修改同一台设备的配置怎么办?control plane 挂了,data plane 会怎样?
- 锚点:下面的卫星设备管理示例:离线三天回来,补的是差异,不是重放。
- 继续阅读:共识 · 反熵 · 韧性模式
扩展:昂贵计算(expensive compute)
- 识别信号:模型推理、GPU、按 token 计费、输出不确定。
- 核心矛盾:核心资源是 GPU 内存,而不是请求路由;成本取决于 token 数,而不是请求数。
- 标准动作:LLM serving 的预算是 KV cache 而不是权重,所以用 continuous batching 和 paged KV cache 提高利用率,用 prefix caching 和按前缀路由降低 prefill 成本;用准入控制和排队保护 GPU(这是 P5 的变体)。
- AI 系统的其他部分都是已有 pattern 的组合:
- RAG = P2(混合召回 + rerank)+ 分别评估召回和生成
- Agent = P6(带 checkpoint 的状态机)+ 预算、审批关卡、沙箱
- 向量数据库 = P2 的索引,在 recall / 延迟 / 内存三者之间取舍
- 常见陷阱:静态 batching;只调 prompt 而召回根本没找到答案;没有预算的 agent 循环。
- 继续阅读:LLM Serving · RAG 系统 · 向量数据库 · Agent 编排 · ANN 索引
第 2 层:7 个决策
Pattern 给你框图,决策才是面试打分的地方。5 个决策在路径内部;2 个在路径之间,负责把组合粘在一起。
路径内部的 5 个决策
| # | 决策 | 要问的问题 | 典型选项 | 锚点 |
|---|---|---|---|---|
| A | 工作放在哪里做 | 写的时候算、读的时候算,还是异步算?在不在关键路径上? | push / pull;预计算 / 现算;同步 / 异步 | 短链接:重定向不能等分析事件写完,点击事件异步丢进队列,不挡主路径 |
| B | 分区与归属 | 按什么 key 分?哪份状态由谁拥有? | 按用户 / 文档 / 地理 / 设备分区;单 owner 的 worker;有状态 vs 无状态 | KV store:consistent hashing,加一个节点只搬约 1/N 的数据 |
| C | 倾斜(skew) | 幂律在哪里?少数极端的 key 怎么办? | 给少数单独一条路径;salt;分层缓存;两阶段聚合 | 信息流:极少数大 V 占了不成比例的关注量 → 大 V 改成读时拉 |
| D | 正确性预算 | 结果可以多旧(stale)、多不准(approximate)、多 “临时”(provisional)? | 强一致 / 最终一致 / read-your-writes;近似结构;先给流式结果再用批处理修正 | KV store:网络分区时,写入必须成功(AP)还是宁可拒绝(CP)? |
| E | 顺序与争用 | 哪些操作必须排队?谁来做串行化? | 原子操作;单写入者;条件更新 / CAS;lease + fencing token | 限流器跑在多台机器上:两个网关都看到 “还剩 1 个” → 检查和扣减必须原子 |
路径之间的 2 个决策(连接的胶水)
| # | 决策 | 要问的问题 | 典型选项 | 锚点 |
|---|---|---|---|---|
| F | 真相来源与派生 | 哪份数据是真相?哪些可以从它重建? | 事件日志;outbox;物化视图;重放;版本号 | 广告点击:不可变的原始日志是真相,聚合结果都能用批处理从它重算 |
| G | 失败、重试与过载 | 部分失败怎么办?过载时先丢掉什么?往哪个方向失败? | idempotency key;at-least-once;saga 补偿;load shedding;优先级隔离;fail-open / fail-closed | 订单处理:下游已经处理了,ack 却丢了 → 幂等 key 从客户端一路透传到每个下游 |
锚点来自短链接、分布式 KV Store、信息流、限流器、广告点击聚合和订单处理这几篇演练。记住这一个点,决策就会跟着回来。
Important
核心要点: 几个 pattern 之间几乎总是通过两样东西连起来:事件(日志或 outbox)和派生视图。所以决策 F 和 G 是组合的关键:上游写真相、发事件,下游消费事件、生成视图,连接处要保证事件不丢(outbox)、重复投递无害(幂等)。
把决策说出口:四个步骤
说出是哪个决策还不够,你要论证它。上面每个决策都用同样的四步。四步都做到,你就完成了推理;漏掉一步,你就没有。
- 命名相互竞争的权衡轴:你在什么之间做取舍。
- 解释打破僵局的场景:这个系统的什么特点偏向某一边。
- 说清你接受的代价:你失去了什么,以及为什么在这里可以容忍。
- 做出决定:选定一边并把它说出来。
步骤 1:命名权衡轴
不是 “优点和缺点”,那是清单,不是权衡。一个权衡有两个(偶尔三个)相互竞争的属性,改善一个就会削弱另一个。反复出现的轴对,以及它们通常出现在哪个决策下:
| 权衡轴对 | 你在两者之间做选择 | 通常属于 |
|---|---|---|
| 一致性 ↔ 可用性 | 正确的答案 vs 能给出答案(在分区情况下) | D |
| 延迟 ↔ 一致性 | 读得快 vs 读到最新数据 | D |
| 准确性 ↔ 延迟 | 精确的答案 vs 快速的近似答案 | D |
| 写入成本 ↔ 读取延迟 | 写入便宜(规范化、pull)vs 读取快(反规范化、push) | A |
| 延迟 ↔ 吞吐 | 单个请求快 vs 聚合容量高 | A |
| 耦合 ↔ 延迟 | 独立服务(网络跳转)vs 进程内调用 | B |
| 持久性 ↔ 写入速度 | 每次写入都 fsync vs 批量写入并承担丢失风险 | F |
| 正确性 ↔ 故障时的可用性 | fail-closed vs fail-open | G |
| 成本 ↔ 延迟 | 廉价的基础设施 vs 快速的响应 | C |
| 运维简单 ↔ 最优性 | 更少的活动部件 vs 尽可能好的性能 | 任意 |
面试中的动作:走到决策点时,先说 “这里的权衡是在 X 和 Y 之间”,再做选择。这一句话就是 “看情况” 和结构化推理之间的分水岭。
步骤 2:解释打破僵局的场景
这个系统的某些特点让某一边更重要。它几乎总是来自下面五个约束之一,而其中大部分正是 pattern 的识别信号已经告诉你的:
- 访问模式。 读多(P1)、写多(P4)还是突发(P5)。决定你优化读路径还是写路径。
- 规模维度。 什么东西大:用户数、数据量、请求速率、连接数(P8)、fan-out(P3)?起约束作用的维度决定选择。
- 故障容忍度。 这个组件出错或变慢会怎样?面向用户(不可容忍)还是内部(可降级)?
- 一致性要求。 要此刻正确(金额、库存)、最终正确(信息流),还是近似正确(分析)?
- 变更速率。 低变更 → 激进缓存。高变更 → 缓存变成一致性问题。
面试中的动作:“在这个系统里,[场景] 意味着我们应该偏向 [某一边]。” 场景是把偏好变成决定的那个理由。
步骤 3:说清代价
说清代价,显式地、大声地说出来,正是 SDE III 的答案区别于 SDE II 的地方。SDE II 选定一边并解释它为什么好;SDE III 选定一边,解释它为什么在这个场景下好,并说清因此而破裂的是什么。上面每张 pattern 卡片都写了代价,原因就在这里。
面试中的动作:“代价是 [我们失去的东西]。我们接受它,因为 [为什么在这里可以容忍]。”
步骤 4:做出决定
前三步是分析,这一步是交付物。停在步骤 3 的候选人听起来像还在犹豫。说 “所以我会选 X”。假设不确定时,就有条件地决定:“假设读多写少,我选 X;如果写多,就选 Z”。有条件的推荐仍然是推荐,拒绝做选择才是唯一不及格的动作。
三个决策,完整论证
短链接(P1)里的决策 A:缓存放在哪里?
- 权衡轴:延迟 ↔ 一致性。边缘缓存省掉回源往返,但更难失效;应用层缓存稍慢,但失效简单。
- 场景:短链接创建后实际上不可变,创建一次、永远重定向。通常反对激进缓存的理由在这里几乎不适用。
- 代价:被删除的 URL 在 TTL 过期前仍会从缓存里重定向。可以接受:删除是管理操作,5 分钟的过时窗口没问题。
- 决定:两层都缓存,用长 TTL。
信息流(P3)里的决策 A 和 C:push 还是 pull?
- 权衡轴:写入成本 ↔ 读取延迟。push 让每条帖子变成 N 次写;pull 让每次刷 feed 变成 N 次读。
- 场景:feed 被读的次数约是帖子被写的 100 倍,所以应该在写入时付成本。但是关注数服从幂律(决策 C)。
- 代价:一个 1000 万粉丝的帖子就是 1000 万次写。对普通用户可以接受,因为 fan-out 是异步的、发帖人不用等;对长尾的极端用户不可接受。
- 决定:普通用户 push,超过关注阈值的少数人 pull,读取时合并并去重。
限流器(P5)里的决策 D 和 G:计数器放 Redis 还是 Postgres?
- 权衡轴:延迟 ↔ 持久性。限流器在每个请求的路径上;但状态丢了,客户端就会白得一次突发。
- 场景:每秒 10 万请求时,存储必须在 1ms 内响应;而限流状态是临时的,丢失意味着短暂超额放行,而不是数据损坏。
- 代价:Redis 重启会重置限额,客户端能突发 5–10 秒。这远比给每个请求永久加 10ms 便宜。同时说清往哪个方向失败(G):普通 API 选 fail-open,昂贵或容易被滥用的接口选 fail-closed。
- 决定:Redis,配合原子的检查并扣减。
第 3 层:概念
概念是面试官追问到决策之下时你要用的机制:“fencing token 具体怎么工作?” 不要一上来就讲概念。先讲决策和代价,被问到再往下走一层。不说明服务于哪个决策的概念,就是在报名词。
- 常见系统设计决策是第 2 层和第 3 层之间的桥:每个反复出现的选择的默认答案,以及应该让你换掉默认的触发条件。
- 网站上每篇概念参考讲一个机制。上面的 pattern 卡片链接了每个 pattern 依赖的那几篇。
怎么用:按面试阶段走
面试的结构不变:问题与澄清 → 需求与估算 → High level → Deep dive → 收尾。四层框架不是另一套流程,而是告诉你每个阶段该找什么、该做什么。
| 面试阶段 | 在这一阶段做什么 |
|---|---|
| 1 问题与澄清 | 从题目原文里找 pattern 信号;问出决定架构的那个问题(“数据是什么形状”“谁是真相”“分布是什么样的”) |
| 2 需求与估算 | 先估算(容量估算),用数字说明哪里是瓶颈。这就是之后每个权衡第 2 步要引用的场景 |
| 3 High level | 第一句说出 “我看到几条路径”:写、读、异步、连接各有哪些,每条配一个 pattern,最难的一条作为主线;然后用第 0 层的组件画框图;再用决策 F 和 G 处理 pattern 之间的接口(谁是真相,事件怎么流,失败怎么重试) |
| 4 Deep dive | 按主线 pattern 的主导决策逐个展开,每个决策用四步论证;次线 pattern 各说一两句它带来的约束;面试官的追问也在这一阶段 |
| 5 收尾 | 故障模式、说明你跳过了什么以及为什么、运维(监控、告警、发布) |
Note
面试信号: “列路径、配 pattern” 不是面试里单独的一步。没有人会停下来宣布 “我现在开始配 pattern”。你在 High level 的第一句话里说出路径,然后直接画框图。
示例 1:卫星设备管理(详细示范)
题目(假设版本):设计一个系统,管理大量卫星(或卫星用户终端)。设备上报遥测;运营方下发指令和固件;卫星只在过境窗口内能和地面站通信;系统要监控设备健康并告警。
网站上没有任何一篇是 “卫星系统”。这正是重点。
High level 开头:列路径,配 pattern
| 路径 | Pattern | 说明 |
|---|---|---|
| 运营方设定期望状态(模式、参数、固件版本) → 等窗口 → 设备收敛 | P10 Control plane(主线) | 期望状态是真相,设备可以离线很久 |
| 固件分发与分批升级 | P9 + P10 + P7 | 固件包是大对象;rollout 是 control plane;每台设备的升级是一个作业 |
| 遥测上报 → 时间序列 + 每台设备的最新状态 | P4 + P3 | 洪流式写入,聚合并派生出 “设备当前状态” 视图 |
| 健康检测与告警 | P4 | 规则 / 阈值作用在聚合结果上 |
| 地面站天线的时间窗口分配 | P5 + P7 | 天线是稀缺资源,窗口是按时间调度的任务 |
High level:画在积木上,连起来
- Desired state 存在一个小而强一致的存储里,每次修改都递增版本号(F)。
- Planner 在窗口开始前,按 “desired 与 reported 的差异” 生成本次窗口要上传的指令批次(A:工作在窗口外做完)。
- 窗口内,地面站把批次发出去;设备执行后回报新的 actual state 和已执行的指令序号。
- 遥测进入日志,流式聚合后写入时间序列库,同时更新每台设备的 reported state 视图(F:reported 是派生出来的)。
Deep dive:深入主线(P10 的主导决策 F、G,加上 B、E)
- B 分区与归属:按设备分区,每台设备的指令队列只有一个 owner,避免两个调度器给同一颗卫星发冲突指令。
- E 顺序:同一台设备的指令带单调递增的序号,设备只执行比已执行序号大的指令;两个运营方同时改同一台设备的 desired state,用版本号做 CAS,后来的人要基于最新版本修改。
- G 失败与重试:指令天然要重发(窗口内可能丢包),所以必须幂等;错过窗口就进入下一个窗口;设备离线三天回来,补齐的是 “当前 desired state 与它的差异”,而不是重放三天的所有指令。
- G 发布安全:固件按 1% → 10% → 50% → 100% 分批,每批之间有 health gate(遥测里的错误率、重启次数),触发就自动停止并回滚;设备本地保留上一个可用版本(A/B 分区),升级失败自动回退。
- F 静态稳定:control plane 整体不可用时,设备照常按最后一份有效配置工作,不会因为管理系统宕机而失控。
完整论证其中一个决策,看四步怎么落到一个新系统上:地面站主动 push 指令,还是设备在接触时 pull? 权衡轴:新鲜度 ↔ 对错过接触的鲁棒性。场景:接触只发生在短而可预测的窗口里,设备可能连续错过几个窗口。代价:声明式、接触时拉取,意味着一次修改要等下一个窗口才生效,收敛延迟从几分钟到几小时。可以接受,因为另一个选择(命令式 push)只要丢一条指令,状态就会错乱。决定:声明式的 desired state,每次接触时 reconcile。
Deep dive 结尾:次线各一两句
- 遥测(P4):cardinality 是容量上限,按设备 × 指标数估算;告警路径本身要被监控。
- 天线调度(P5 + P7):窗口是可预测的(轨道已知),可以提前离线排期;冲突时按优先级(紧急指令 > 例行遥测下载)。
示例 2:网约车
同样的流程用在一道熟悉的题上,只保留开头和关键决策:
| 路径 | Pattern |
|---|---|
| 司机每隔几秒上报位置 → 写入地理索引 | P3 |
| 乘客叫车 → 匹配附近司机 | P2(主线) |
| 锁定选中的司机 | P5(主线) |
| 行程 → 支付 | P6 |
| 计价、热区统计 | P4 |
Deep dive 落在 B(按地理分区)、C(市中心密集格子是热点)和 E(一个司机不能被两个请求同时拿到)。常见陷阱是整场都在讲地图,从没回答 E。P2 这一半的深入版本见地理空间匹配演练。
更多拆解
| 题目 | 路径 → Pattern | 主线 | 关键决策 |
|---|---|---|---|
| 图片分享(Instagram 类) | 上传图片 → P9;发帖进 feed → P3;刷 feed → P1;搜索 → P2 | P3 | A(push / pull 混合)、C(名人走 pull) |
| 抢票 | 活动页 → P1;搜活动 → P2;抢座 → P5(带 TTL 的 hold + 虚拟排队);下单到支付 → P6 | P5 | E(座位的原子 hold)、G(分层 load shedding) |
| Web crawler | URL frontier + 抓取 worker → P7;网页存储 → P9;去重 → F | P7 | B(按 host 分区,控制抓取礼貌)、F(URL 和内容的去重)、G(重试与死链) |
| 分布式任务调度器 | 任务提交、到期触发、分配执行 → P7 | P7 | E(lease + fencing,防止两个 worker 同时执行)、G(at-least-once + 幂等) |
| Trending topics | 发帖事件 → 滑动窗口 top-K → P4 | P4 | C(热点话题)、D(近似计数可以接受) |
| 在线判题 | 题目页 → P1;提交 → 判题队列 + 沙箱 worker → P7;结果推回 → P8 或轮询;比赛排行榜 → P4 | P7 | G(判题超时与重试)、C(比赛开始时的提交洪峰);沙箱隔离是第 3 层的概念 |
| 协同文档 | 多人编辑 → P8;文档快照和操作日志 → F | P8 | B(每个文档一个 owner worker)、E(单写入者给操作排序)、D(离线编辑决定 OT 还是 CRDT) |
| A/B 实验平台 | 实验配置推到所有服务 → P10;分组 → P1(确定性 hash,本地计算);指标统计 → P4 | P10 + P4 | F(配置版本化)、D(统计结果的新鲜度)、G(配置服务挂了要用最后一份配置) |
| 机票搜索 | 向多家航司 scatter-gather → P2;报价缓存 → P1 | P2 | D(报价 TTL 很短,下单前要重新确认价格)、G(外部依赖超时,返回部分结果) |
| Kafka / Memcached | 打开第 0 层的积木 | 积木本身 | B、D、E、G 全部在一个组件内部回答 |
什么时候不适用
- 题目就是积木本身时。 设计一个共识协议、一个存储引擎、一个分布式锁:只有一个组件,没有路径可组合,整场都在第 3 层。把决策当检查清单用,但不要硬列路径表。
- 关键词误导时。 仪表盘题里的 “实时” 通常是秒级新鲜的聚合(P4),不是长连接(P8)。几千条数据上的 “搜索” 可能只是一个数据库索引,而不是召回漏斗。Pattern 是从信号里识别、再用澄清问题确认的,从来不靠一个词来匹配。
- 拆得太宽时。 列出六条路径、每条都讲得很浅,听起来像在做综述。选一条主线,深入它的主导决策,其余每条一句话。面试官想听次线,会自己把你拉过去。
- 这一轮其实考的是产品或 ML 判断时。 在排序质量或实验设计这类轮次里,难点是指标、标签和评估,不是路径。这个框架能帮你搭出 serving 架构,但不会替你选目标函数。
- 用它代替机制、而不是组织机制时。 说 “这是 P5,决策 E”,却讲不出一次原子的带守卫扣减怎么做,比不用这些标签更糟。Pattern 告诉你往哪里挖,第 3 层才是挖本身。
- 扩展部分还没定型。 AI 系统设计变化很快。昂贵计算被刻意列为扩展而不是 pattern:它的标准动作还在变。
相关内容
- 计划:9 周系统设计面试备战:学习这些 pattern 的顺序,每周对应一到两个 pattern。
- 参考:常见系统设计决策:反复出现的选择的默认答案和切换条件,是第 2 层的查表页。
- 演练:设计信息流系统:奠基的演练,以 P3 为主线,完整论证了决策 A 和 C。
- 清单:面向资深大厂面试的 70 题 LeetCode 精选:同样是 “先识别题型,再选方法” 的思路,用在 coding 轮。