Skip to main content

框架:系统设计中的 Pattern 与权衡

把任何一道系统设计题拆成 10 种路径 pattern,再用权衡轴、场景、代价、决定四步,讲清 7 个反复出现的决策。

系统设计题千变万化,但拆开来看,几乎每道题都是由少数几种路径拼起来的:一条上传路径、一条读路径、一条后台处理路径、一条推送路径。每种路径都有固定的形状、固定的难点和固定的标准动作。而在每个难点上,你都要做一个选择,并且要把它讲出来、守得住。

这个框架把这两半放进同一个结构里。Pattern 告诉你系统由什么组成;权衡告诉你在每个连接处怎么选。目标只有一个:拿到一道没见过的题,能说出它由哪几个已知的 pattern 组成,怎么把它们连起来,深入时该讨论什么,并且一路上的每个选择都讲得清楚。

网站上其他所有系统设计内容都挂在这个框架上:walkthrough 是 pattern 的完整示例,概念参考是第 3 层的机制,9 周计划是学习它们的顺序。

问题所在

高级系统设计面试里最常见的两种失败,看起来毫不相关。

第一种是背系统,而不是背形状。 你准备了 News Feed、Chat 和短链接。面试官让你设计一个卫星设备管理系统,或者在线判题,或者 feature flag 服务。准备过的系统一个都对不上,于是你从白纸开始,花十五分钟重新发现你本来就知道的东西,因为你把它们归档在 “News Feed” 下面,而不是 “一次写入要在很多别的地方被读到” 下面。

第二种是 “看情况”。 面试官一天要听到这句话三十次。能通过的候选人不会停在这里:他们说清到底看什么情况,选定一边,并说出自己放弃了什么。没通过的候选人要么罗列选项却不做决定,要么选了一边却不说代价。在面试压力下,工作记忆被占满,大多数工程师都会退回到不及格的版本。

这其实是同一个问题的两个高度。第一种是不知道难点在哪里;第二种是走到难点之后不知道怎么说。高级别的回答两者都要:拆题让你快速走到正确的连接处,权衡推理是你在每个连接处要说的话。

Important

核心要点: Pattern 能看出来,决策要推出来。题目会说 “用户上传视频”,但不会说 “这里有倾斜”。所以先用 pattern 拆题,再用决策深入,每个决策都用同样的四步讲清楚:权衡轴、场景、代价、决定。

框架:四层结构

层是什么回答的问题怎么用
第 0 层:积木负载均衡、无状态服务、缓存、数据库、日志 / 队列、对象存储、CDN、ID 生成默认要画哪些框?每道题都有,不需要识别
第 1 层:路径 pattern10 种路径形状,外加 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 分区,加 replicaSQL vs NoSQL · 索引 · 存储引擎
分区与复制partition key + leader/follower 或 leaderless分区 · 复制 · 一致性模型
日志 / 队列按 partition 扩展,consumer group 消费队列与日志 · 投递语义
对象存储 + CDNblob 不可变,CDN 做读路径CDN 与边缘交付 · CDN 演练
ID 生成号段分配或 Snowflake 式唯一 ID 生成
API、鉴权、韧性REST / gRPC,token 鉴权,timeout + retry + circuit breakerAPI 设计 · 认证与授权 · 韧性模式

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-gatherA、D搜索、推荐、附近的司机、typeahead、机票搜索、RAG 召回
P3写后派生(write-then-derive)写一次,要在别处、以别的形态被读到真相来源 + 事件 → 异步生成物化视图(fan-out、索引、计数)A、CFeed fan-out、搜索建索引、CDC
P4事件聚合(event aggregation)计数、统计、排行、趋势、监控、告警;事件量远大于查询量日志 → 窗口聚合 → 汇总存储;批处理对账D、C广告点击、指标监控、top-K、trending、排行榜
P5资源争用(contention)名额、库存、配额有限;“不能超卖”;出价;限流分层削减流量 + 原子扣减 / 单写入者 + 带 TTL 的 holdE、G秒杀、抢票、限流、拍卖、派单锁定
P6多步流程(workflow / saga)下单、支付、状态机;跨多个服务;要能回滚持久的状态机 / orchestrator + 每一跳幂等 + 补偿G、F订单、支付、行程生命周期、agent 循环
P7异步作业(background jobs)后台任务、定时、批量、爬取、转码、重试持久队列 / 调度器 + worker 池 + lease + 重试和 DLQG、E爬虫、任务调度器、通知发送、转码、在线判题
P8实时连接(real-time connections)实时、在线状态、推送、聊天、协同编辑、直播评论连接网关只持有 socket + 路由注册表 + 持久存储作为真相B、GChat、在线状态、直播评论、协同文档
P9大对象(large objects)文件、图片、视频;上传、下载、同步内容与元数据分离 + 分块和 content addressing + 先提交内容再提交指针 + CDNF、C视频平台、文件同步、图片上传
P10Control plane(期望状态下发)配置、部署、发布、固件升级、feature flag、实验配置;“让一批设备 / 机器达到某个状态”版本化的 desired state + agent 上报 actual state + reconcile 循环 + 分批 rollout 和回滚F、G设备 / IoT 管理、部署系统、配置中心、实验平台
扩展昂贵计算(expensive compute)模型推理、GPU、按 token 计费准入控制 + continuous batching + KV cache / prefix cache + 路由E、ALLM 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. 命名相互竞争的权衡轴:你在什么之间做取舍。
  2. 解释打破僵局的场景:这个系统的什么特点偏向某一边。
  3. 说清你接受的代价:你失去了什么,以及为什么在这里可以容忍。
  4. 做出决定:选定一边并把它说出来。

步骤 1:命名权衡轴

不是 “优点和缺点”,那是清单,不是权衡。一个权衡有两个(偶尔三个)相互竞争的属性,改善一个就会削弱另一个。反复出现的轴对,以及它们通常出现在哪个决策下:

权衡轴对你在两者之间做选择通常属于
一致性 ↔ 可用性正确的答案 vs 能给出答案(在分区情况下)D
延迟 ↔ 一致性读得快 vs 读到最新数据D
准确性 ↔ 延迟精确的答案 vs 快速的近似答案D
写入成本 ↔ 读取延迟写入便宜(规范化、pull)vs 读取快(反规范化、push)A
延迟 ↔ 吞吐单个请求快 vs 聚合容量高A
耦合 ↔ 延迟独立服务(网络跳转)vs 进程内调用B
持久性 ↔ 写入速度每次写入都 fsync vs 批量写入并承担丢失风险F
正确性 ↔ 故障时的可用性fail-closed vs fail-openG
成本 ↔ 延迟廉价的基础设施 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;搜索 → P2P3A(push / pull 混合)、C(名人走 pull)
抢票活动页 → P1;搜活动 → P2;抢座 → P5(带 TTL 的 hold + 虚拟排队);下单到支付 → P6P5E(座位的原子 hold)、G(分层 load shedding)
Web crawlerURL frontier + 抓取 worker → P7;网页存储 → P9;去重 → FP7B(按 host 分区,控制抓取礼貌)、F(URL 和内容的去重)、G(重试与死链)
分布式任务调度器任务提交、到期触发、分配执行 → P7P7E(lease + fencing,防止两个 worker 同时执行)、G(at-least-once + 幂等)
Trending topics发帖事件 → 滑动窗口 top-K → P4P4C(热点话题)、D(近似计数可以接受)
在线判题题目页 → P1;提交 → 判题队列 + 沙箱 worker → P7;结果推回 → P8 或轮询;比赛排行榜 → P4P7G(判题超时与重试)、C(比赛开始时的提交洪峰);沙箱隔离是第 3 层的概念
协同文档多人编辑 → P8;文档快照和操作日志 → FP8B(每个文档一个 owner worker)、E(单写入者给操作排序)、D(离线编辑决定 OT 还是 CRDT)
A/B 实验平台实验配置推到所有服务 → P10;分组 → P1(确定性 hash,本地计算);指标统计 → P4P10 + P4F(配置版本化)、D(统计结果的新鲜度)、G(配置服务挂了要用最后一份配置)
机票搜索向多家航司 scatter-gather → P2;报价缓存 → P1P2D(报价 TTL 很短,下单前要重新确认价格)、G(外部依赖超时,返回部分结果)
Kafka / Memcached打开第 0 层的积木积木本身B、D、E、G 全部在一个组件内部回答

什么时候不适用

  • 题目就是积木本身时。 设计一个共识协议、一个存储引擎、一个分布式锁:只有一个组件,没有路径可组合,整场都在第 3 层。把决策当检查清单用,但不要硬列路径表。
  • 关键词误导时。 仪表盘题里的 “实时” 通常是秒级新鲜的聚合(P4),不是长连接(P8)。几千条数据上的 “搜索” 可能只是一个数据库索引,而不是召回漏斗。Pattern 是从信号里识别、再用澄清问题确认的,从来不靠一个词来匹配。
  • 拆得太宽时。 列出六条路径、每条都讲得很浅,听起来像在做综述。选一条主线,深入它的主导决策,其余每条一句话。面试官想听次线,会自己把你拉过去。
  • 这一轮其实考的是产品或 ML 判断时。 在排序质量或实验设计这类轮次里,难点是指标、标签和评估,不是路径。这个框架能帮你搭出 serving 架构,但不会替你选目标函数。
  • 用它代替机制、而不是组织机制时。 说 “这是 P5,决策 E”,却讲不出一次原子的带守卫扣减怎么做,比不用这些标签更糟。Pattern 告诉你往哪里挖,第 3 层才是挖本身。
  • 扩展部分还没定型。 AI 系统设计变化很快。昂贵计算被刻意列为扩展而不是 pattern:它的标准动作还在变。

相关内容