图解系统设计 中文图解教程全量版 · 20 章
从 GitHub 36.8 万星《System Design Primer》出发 · 中文原创图解重制

图解系统设计

给进阶工程师的“图文版系统设计课”:先把面试到底怎么考讲清楚,再把 DNS、CDN、负载均衡、缓存、数据库、异步… 一个个拆成看得懂的图;最后回到经典大题,带你用四步法现场解题。

📦 知识底座 System Design Primer(CC BY 4.0) 🎯 面向系统设计面试 📊 图解为主 · 文字只讲要点 🧭 20 章 · 全部图解完成
LEARNING PATH

全站学习路径

五站式结构:先方法论,再打地基,然后把“积木”一个个看懂,最后用真题限时演练。第 3 站的 负载均衡 是本期完整图解试点。

① 方法论 四步法 + 心算估算 已图解 ✓ ② 打地基 性能 / 延迟 / CAP 已图解 ✓ ③ 攒组件 DNS→CDN→LB→缓存… ◆ 全站 20 章全部图解完成 ④ 刷例题 短链 / 时间线 / 爬虫… 已图解 ✓ ⑤ 附录速查 数字 / 清单 / Anki 已图解 ✓ 每一站都可独立查阅:先把「方法论」过一遍 → 组件当“词典”按需翻 → 例题用来限时演练。
序章 · HOW TO USE

怎么用本站

系统设计面试没有“标准答案”,考的是结构化的工程判断。本站的拆法有三步:

  • 先学套路:拿到任何题,都先走四步——① 厘清需求与约束 → ② 画高层架构 → ③ 拆核心组件 → ④ 估算并扩展(见 方法论章)。
  • 组件当“词典”:DNS、CDN、负载均衡、缓存、数据库…每个都是面试里的“单词”,遇到不会的随时回来看图。
  • 例题限时演练:经典大题单独成章,先自己写 20 分钟,再看本站的逐步拆解对照。
全站状态:全部 20 章已完成图解(方法论 → 地基 → 组件 → 例题 → 附录),图文均为本站原创中文重写,知识底座来自 System Design Primer(CC BY 4.0)。
组件篇 · CHAPTER 06

负载均衡 Load Balancer

面试出现频率最高的一课:一台机器不够用了,你第一个要认识的角色。读完约 8 分钟,含 6 张图解。

负载均衡 Load Balancer

🔥 面试频率:极高 难度 ★★☆ 前置知识:无 知识点:L4/L7 · 算法 · 健康检查 · 无状态
先背这一句(面试开场 30 秒)“在多个服务实例前面加一个‘分流的门卫’:按策略把请求均匀、可靠地分给健康的机器,从而扛住更大的流量,并容忍单机故障。”

为什么需要它:单机走不远的三个理由

先想一个残酷的现实:一台服务器的处理能力是有限的。用户变多时,单机方案会在三件事上先后崩溃:

  • 流量上限:CPU、内存、带宽总有一个先被打满,之后请求开始排队、超时、报错。
  • 单点故障:机器宕机、机房断电、磁盘损坏——任何一件事发生,整个服务就“团灭”。
  • 没法平滑扩容:换更强的机器(纵向扩展)有物理天花板且贵;买新机器(横向扩展)又需要有人把流量“分”过去。

负载均衡(Load Balancer,简称 LB)就是为第三件事而生的“分发器”:它站在用户和后端机器之间,把请求按策略转发给多台机器中的一台。

方案 A · 单机部署 用户 1 用户 2 用户 3 单台服务器 应用 + 数据库 一把梭 ⚠ 流量一翻倍就扛不住 ⚠ 机器一挂 = 全站 502 100% 的请求都压在同一台 扩展只能“换更大的机器” 方案 B · 负载均衡部署 用户 1 用户 2 用户 3 负载均衡器 LB 按策略分发 + 健康检查 服务器 1 服务器 2 服务器 3 应用 副本 应用 副本 应用 副本 坏一台只损失 1/N 容量,其余照常服务
图 6-1单机 vs 负载均衡。右图里每台后端机器只承担一部分流量;某台挂了,LB 会把请求发给剩下的机器——代价是整体容量少 1/N,而不是全站宕机。
两个概念先分清:“横向扩展”= 加机器;“负载均衡”= 让加进来的机器真正被用上。两者通常成对出现。

它站在架构的哪里

“负载均衡”不是一个固定位置的角色,而是一种会反复出现的思想:只要有一群“能互相替代的实例”在前面共享流量,前面就可能蹲着一个 LB。下图把一张典型 Web 架构里出现 LB 的位置都标了出来:

用户客户端 · 浏览器 / App / IoT ① 入口负载均衡(对外入口) 域名解析到它 / 弹性 IP 由它持有 L7:按 URL / Host / 设备类型 分流 Web A Web B Web C 无状态副本 无状态副本 无状态副本 静态资源常先走 CDN(见 §5) ② 服务间负载均衡(内部) Web 调微服务时也要“选一台” K8s Service / Service Mesh 都干这事 订单服务 ×2 支付服务 ×3 用户服务 ×2 主库(写) 强一致 · 单写入口 只读副本 ×N 读流量分到这里 Redis / 缓存 热点数据放这里 ③ 数据层同样讲分摊:写走主库,读走副本与缓存——由连接代理(如 ProxySQL)按“读写”自动分发 负载均衡的思想贯穿每一层:分摊流量 · 冗余容灾 · 健康检查 · 故障自动切换
图 6-2LB 出现的三个典型位置。① 对外入口(最常考);② 服务与微服务之间;③ 数据读写分离。面试画架构时,习惯把 LB 画成“一个把箭头分叉的菱形”即可。

只认包,还是看懂请求:L4 vs L7

负载均衡器收到一个网络请求后,能“看多深”决定了它是哪一型。想象请求是一个多层信封:最外面是 TCP/IP 的“收发地址”,里面才是 HTTP 的“信件内容”:

TCP/IP 头:源 / 目的 IP + 端口 HTTP:GET /api/order?id=1 Cookie / 业务内容… 一个请求 (层层包裹) L4 · 传输层负载均衡 只拆开“信封外层”:看 IP : 端口 转发 不改内容 · 转发极快 · 延迟低 适合:TCP/UDP 等任意协议、数据库连接、纯性能敏感场景 代表:LVS / DPVS、HAProxy(L4 模式)、云 NLB L7 · 应用层负载均衡 拆到“信件正文”:看 URL / Host / Cookie / Header 按内容路由 · 可改包、压缩、缓存、灰度 适合:HTTP/HTTPS/gRPC、微服务网关、需要按业务分流 代表:Nginx、HAProxy(L7 模式)、云 ALB / SLB
图 6-3L4 与 L7 的差别 = 拆信拆到哪一层。L4 快而“笨”,L7 聪明而慢一点。面试里提到“网关 / 路由 / 灰度发布”基本都默认在说 L7。
维度L4(传输层)L7(应用层)
看什么IP、端口、协议号URL、Host、Cookie、Header、报文内容
性能更快(内核/DPDK 转发)较慢(要解析并加工报文)
能力转发;支持任意 TCP/UDP 协议按业务路由、重写、压缩、缓存、限流、灰度、TLS 卸载
适用数据库连接、游戏、高吞吐网关Web/API/微服务入口
典型产品LVS、DPVS、HAProxy L4、AWS NLBNginx、HAProxy L7、AWS ALB、云 SLB

分发算法:怎么决定“给谁”

LB 拿到一个请求后,选哪台后端?有几种典型算法,面试至少要把前三种说清楚:

轮询 Round Robin 请求按 1、2、3、4… 轮流排给 A、B、C、A… 第 1 个请求 第 2 个请求 第 3 个请求 服务器 A 服务器 B 服务器 C 收到 2 收到 2 收到 2 数量均匀,但不管机器快慢与负载 最少连接 Least Connections 新请求给“当前连接数最少”的那台 当前连接 5 当前连接 2 ← 新请求 当前连接 4 新请求 服务器 A 服务器 B 服务器 C 连接 +1 → 3 长短请求混在一起时更均衡
图 6-4轮询 vs 最少连接。轮询只保证“数量公平”,不感知负载;最少连接把新请求派给当下最闲的机器,长短请求混合时更稳。
算法一句话典型场景
轮询 RR轮流派发,数量公平各机器能力接近、请求耗时均匀
加权轮询按权重(机器配置高低)分配比例新旧机器混跑、不同规格实例
最少连接派给连接数最少的机器长短请求混合(如 Web/API)
最短响应时间统计每台近期响应耗时,选最快对延迟敏感的场景
IP / 一致性哈希按来源 IP 或 key 固定映射到某台需要“同一用户/同一 key 总去同一台”(会话、缓存亲和)

挂了怎么发现:健康检查

LB 的“健康检查”决定了一个谎言是否成立——它说自己只把请求分给“健康的机器”。做法是周期性探测每台后端:主动发 TCP 握手 或 GET /healthz,连续失败 N 次就把机器摘除,恢复后再自动加回:

时刻 t₁ · 服务器 A 宕机 负载均衡器 A(挂了) B C 摘除出列 ✗ 探测失败 ×3 ✓ 正常 ✓ 正常 流量只发给 B、C —— 用户无感知 时刻 t₂ · A 恢复上线 负载均衡器 A(恢复) B C 自动重新入列 ✓ 探测通过 三台重新平分流量,容量恢复 100% 探测方式:TCP 端口探活 / HTTP 状态码 + 内容校验 / 自定义脚本
图 6-5健康检查 = 摘除 + 回归。这是 LB 高可用的“自愈”机制,也是面试里“如何优雅下线一台机器”(先摘除再重启再放回)的标准答案来源。

别忘了让后端“无状态”

LB 有一个“天敌”:有状态的后端。如果登录态、购物车存在某台服务器的内存里,那么“同一个用户两次请求被分到不同机器”就会出 bug——于是人们发明了会话保持(Sticky Session):让 LB 记住“这个用户永远去 A”。这能解决一时,却把 LB 变成了新的单点与调度枷锁。

更优雅的做法是把状态拿出来:存进所有机器共享的 Redis / 数据库。后端从此“无状态”,谁都能接任何请求,LB 也彻底自由:

会话保持 Sticky Session 同一用户(Cookie 带 A) LB:记住映射 该用户 → 只发 A A B C 内存里存着它的购物车 A 一挂 → 会话丢失 → 用户重新登录、购物车清零 LB 还要维护映射表,扩容 / 故障更难处理 无状态 Stateless(推荐) 任何用户请求 LB:随便转发 谁闲着给谁 A B C 状态不在这 状态不在这 共享状态存储(Redis / DB) A 挂 → 请求无缝转给 B / C,用户无感
图 6-6会话保持 vs 无状态。面试的“黄金法则”:让后端无状态,负载均衡、水平扩容、故障恢复才真正简单。状态请统一放进 Redis / 数据库。
考点预告:如果面试官追问“同一个用户的多次请求被分到不同机器怎么办?”,标准演进路线就是——先讲会话保持及其代价,再主动给出无状态化 + 共享存储的更优解。主动讲出演进,比背一个结论分高。

面试应答要点

  • 先给结构:“我按『入口分流 → 后端多副本 → 健康检查 → 无状态化』来讲这个系统的负载均衡设计。”
  • 画图从粗到细:第一张图只画 客户端 → LB → N 台应用;等面试官要求再展开 L4/L7 与算法细节。
  • 默认三板斧:无状态、水平扩展、健康检查——说到扩容必带这三件套,几乎不会错。
  • 别只背名词:能说出“轮询为什么不够、最少连接解决什么、哈希让谁受益”,比报出一串算法名更有说服力。

高频追问 Q&A

Q1同一个用户两次请求被分到不同机器,会出什么问题?

如果后端有状态(登录态/购物车在本地内存),第二次请求可能“不认识”这个用户,表现为掉登录、数据串。解法两条路:会话保持(绑定到固定机器,代价是故障与扩容都变复杂);或后端无状态化 + 状态进 Redis/DB(推荐,见上图)。

Q2健康检查一般怎么实现?

主动探测:LB 周期性请求每台后端的探活接口(TCP 端口连接、或 GET /healthz 校验状态码与响应内容),连续失败 N 次自动摘除,恢复后自动加回。更细的做法:把健康检查做成业务探针——连数据库、依赖都正常才报 healthy,避免“进程活着但服务已不可用”的假活。

Q3L4 和 L7 到底选哪个?

性能敏感或协议不是 HTTP(数据库连接、长连接、游戏)→ L4;需要按 URL/Host/Cookie 路由、灰度、限流、缓存、TLS 卸载 → L7。面试话术:“入口网关用 L7 做精细化路由,内部纯转发可以下放到 L4 提速”——L4/L7 常常同时存在于一个系统里。

Q4负载均衡器自己挂了怎么办?

LB 自己不能是单点。常见方案:① 主备 + 虚拟 IP(VIP):主 LB 持有 VIP,挂了备机接管(keepalived/云浮动 IP);② 多活 + ECMP/BGP:多台 LB 同时活着,上游路由器把包散给它们;③ DNS 层面的全局负载均衡(GSLB)做跨机房兜底。面试答出“LB 也要主备/多活 + 健康检查自己”即可。

Q5有了 Kubernetes Service,还要学负载均衡吗?

要。K8s Service 只是把“负载均衡”这件事内置化了(底层靠 iptables/IPVS/网关实现),概念一模一样:分发、健康检查(就绪探针)、无状态副本。出了 K8s,云上 ALB/NLB、自建 LVS/Nginx/HAProxy 仍是同一套思路——学会原理,工具随便换。

延伸阅读与来源

本节为本站原创图解;知识结构与概念出处为 System Design Primer(Copyright 2017 Donne Martin,CC BY 4.0)的对应章节,可对照精读:

序章 · METHOD

系统设计面试方法论 Four-Step Method & Estimation

系统设计面试没有标准答案,但有标准打法——用四步法把一场开放式对话拆出节奏:先问清边界,再画骨架、钻细节,最后用数字和取舍收口。

系统设计面试方法论

🎯 一切大题的起手式前置知识:无
先背这一句“任何系统设计题都按四步走:① 厘清需求与约束 → ② 画高层设计 → ③ 拆核心组件 → ④ 估算与扩展,最后主动给权衡。”

系统设计面试到底在考什么

它不是知识问答,也没有唯一正确答案:面试官丢给你一个模糊需求,看你能不能自己把边界问出来、把骨架画出来、把关键组件想清楚,并且让每个结论都经得起一句“为什么”。原仓库把这类面试定性为一场开放式的对话——而且期望的是你来主导这场对话,不是等面试官一个个提问。

准备节奏可以参考它的学习指引(Study guide):时间线短,就以主题广度为目标、挑几道题练手;时间中等,追求广度加初级深度、刷很多题;时间充裕,再加高级深度、把大部分题做透。多数岗位只需要“广度 + 一套熟练的四步打法”,这也是本章的全部目的。

四步法:把开放题变成有节奏的对话

四步不是瀑布式的死流程,而是你主导对话的脚手架:每一步都有明确产出,方便你随时向面试官汇报进度、拿到反馈再往前走,而不是闷头画一张没人跟得上的大图。

1 厘清需求与约束 STEP 1 · 场景 / 约束 / 假设 · 谁在用、多大规模、怎么用 · 读:写比、每秒请求、数据量 · 延迟 / 可用性 / 数据保留期 2 画高层设计 STEP 2 · 主要组件与连接 · 主组件 + 连线,一页纸 · 每个组件给一句职责 · 讲清一条完整请求路径 3 拆核心组件 STEP 3 · 核心组件逐块深挖 · 挑 1~2 个关键组件深挖 · 数据模型 / API / 存取路径 · 例:短链的哈希与碰撞 4 估算与扩展 STEP 4 · 扩展 · 取舍 · 收尾 · 逐个试:缓存 / 分片 / LB · 报瓶颈,讲清代价与取舍 · 给优先级并收口结论 面试是「开放式对话」——由你主导节奏:边画边讲、边讲边问,别等面试官提问 需求一变就退回第 1 步重锁量级;每个结论都先给答案、再给理由 输出 · 澄清问题清单 输出 · 一页纸高层草图 输出 · 核心组件细化 输出 · 瓶颈清单 + 权衡
图 M-1四步法全景把一次开放对话切成四段:问需求、画骨架、钻核心、谈扩展取舍——每步都有明确的输入与产出,走不动就退回上一步。

第 1 步回答“系统是什么、边界在哪”,产出是澄清问题清单;第 2 步产出一页纸的高层架构草图(主组件与连线,边画边证明每个组件为什么存在);第 3 步对最关键的 1~2 个组件深入下去——原仓库的例子是 URL 缩写服务,要谈哈希与 Base62、碰撞怎么办、SQL 还是 NoSQL、数据库模型;第 4 步回头确认瓶颈与限制,逐个尝试负载均衡、水平扩展、缓存、数据库分片。

注意第 4 步的原话语气:论述可能的解决办法与代价——每一件事都需要取舍。面试加分点从来不是“方案多完美”,而是你能主动把权衡讲出来。

TIP 四步法不是答题模板,是“救场绳”:卡壳了、被追问了、需求变了,都能明确说出“我现在在第几步、下一步要做什么”。这一句汇报本身就能展示结构化思维。

开场先问什么:澄清问题清单

四步法的第 1 步决定后面所有数字和选型,所以提问是重头戏。下面 5 个维度是提问的主骨架——对应原仓库“第一步”的官方问题清单(谁使用、怎样使用、多少用户、系统作用、输入输出、每秒请求、数据量、读写比率),整理成更好记的“5 问”:

维度要问清的问题影响哪个决策
读写模式读多还是写多?比例大致多少?缓存 / 消息队列 / 读写分离的取舍方向
规模量级多少用户?每秒、每天多少请求?存量数据多大?单机够不够,要不要分布式与分片
延迟要求可接受的读 / 写延迟?有没有 P99 等硬指标?缓存、CDN、异步处理是否必须
可用性要求允许停机多久?SLA 是几个 9?副本数、故障切换、一致性代价
数据生命周期数据保留多久?能不能丢?要不要过期清理?存储选型、TTL 与清理任务、备份策略

别只问一轮。把答案整理成“候选假设”报给面试官确认(“我按读:写 = 100:1、日增 1 亿条来估,可以吗?”)——既展示结构化思考,也防止方向错了整段白画。

心算 Back-of-the-envelope:让每个结论都有数字

第 2~4 步里你会反复遇到“要不要缓存、要不要分片、扛不扛得住”,答案要靠数量级说话。原仓库在第四步之后单列了“预估计算量”(Back-of-the-envelope calculations),并指向 2 的次方表与延迟数两张附录。心算的纪律是:先立假设 → 快速算 → 结论写成“带假设、可修正”的一句话。下面用“设计短链服务”的每秒写请求做一次完整示范(数字均为示范假设,重在看方法):

第 0 步 · 先立假设(大方估 → 讲清楚 → 可随时修正) 日新增短链 1 亿条 / 天 读写比:读 : 写 ≈ 100 : 1 单条记录 ≈ 500 B 数据保留期 5 年 ① QPS —— 每秒要扛多少请求 写 = 1 亿 ÷ 86,400 秒 ≈ 1,157/s ≈ 1.2k 写/s(平均) 读 = 写 × 100 ≈ 12 万读/s —— 峰值按 3~5× 留余量 写 ≈ 1.2k/s 读 ≈ 12 万/s ② 存储 —— 5 年要放多少数据 5 年总量 = 1 亿/天 × 365 × 5 ≈ 1.8 × 10^11 条 单条 ≈ 500 B → 约 90 TB —— 单机放不下:分片或对象存储 ≈ 90 TB 单机放不下 ③ 带宽 —— 每秒要过多少流量 写入口 = 1.2k/s × 500 B ≈ 0.6 MB/s(几乎可忽略) 读回源 = 12 万/s × 约 1 KB ≈ 120 MB/s ≈ 1 Gbps 上限 → 缓存/CDN 挡 写 ≈ 0.6 MB/s 读 ≈ 120 MB/s 结论:压力不在「写带宽」,而在「读 QPS ≈ 12 万/s」与「5 年存储 ≈ 90 TB」 → 于是第 4 步的取舍方向清楚了:读走缓存 / CDN,存储做分片——每一步判断背后都要有数字
图 M-2Back-of-the-envelope 心算拆解示范题:设计短链服务,从每秒写请求出发顺次估 QPS、存储、带宽;数字为示范假设,重在看“先立假设、只算量级、结论导向取舍”的方法。
TIP 心算三原则:① 先取整再算(86,400 ≈ 8.6×10⁴);② 只报量级与假设,不装精确;③ 数字错了当场重算——面试官更在意你是否知道“这个数该用来决定什么”。

随手可查的心算常数(延迟数字)

下表数字全部取自原仓库附录《每个程序员都应该知道的延迟数》,背熟几个关键锚点,估算就有了参照物:

操作延迟一句话感觉
L1 缓存引用0.5 ns纳秒级,最快
L2 缓存引用7 ns约 14× L1
主存引用100 ns约 20× L2
同一数据中心往返500 μs亚毫秒,本地调用无感
SSD 随机读 4 KB150 μs毫秒内
SSD 顺序读 1 MB1 ms吞吐约 1 GB/s
磁盘寻道10 ms约 20× 数据中心往返
磁盘顺序读 1 MB30 ms约 120× 主存引用
跨洲往返(CA→NL→CA)150 ms物理极限,只能靠缓存 / 就近部署

由这些数字还能推出几条吞吐参照,估算带宽时直接用:磁盘顺序读约 30 MB/s、1 Gbps 以太网约 100 MB/s、SSD 顺序读约 1 GB/s、主存约 4 GB/s;同一数据中心内每秒约可往返 2,000 次。

30 分钟怎么花:面试时间盒

一场系统设计面试通常约 30 分钟。把它切成四个阶段,每阶段对应四步法的一步,就能保证“开头有时间问清、结尾有时间收口”:

5′ 澄清需求 10′ 高层设计 10′ 核心组件 5′ 扩展收尾 ① 澄清需求与约束 · 谁用、怎么用、多大规模 · 读:写比、数据量、QPS · 延迟 / 可用性 / 保留期 别:不澄清就开画、抠实现细节 产出 · 澄清问题清单 ② 高层设计 · 主组件 + 连线,一页纸 · 每个组件讲一句职责 · 完整讲一遍再深入 别:一上来就钻进单个组件细节 产出 · 高层架构草图 ③ 核心组件深挖 · 挑 1~2 个关键组件 · 数据模型 / API / 路径 · 用估算数字支撑选型 别:平均用力,每个都只画一半 产出 · 核心组件设计 ④ 扩展与权衡收尾 · 逐项试:缓存 / 分片 / LB · 讲代价,给取舍优先级 · 30 秒总结 + 一句反问 别:新开话题或时间到戛然而止 产出 · 瓶颈与权衡结论 时间盒是「锚」不是枷锁:第 1 步之前绝不画图;面试官追问就多在 ③ 待一会儿,但记得回 ④ 收口 想不起结构时,回到四步法的任一步都能续上话——别让冷场吃掉时间
图 M-330 分钟面试时间盒5′ 澄清 → 10′ 高层 → 10′ 核心 → 5′ 收尾,色块宽度即时间占比;时间不够时优先保“高层完整 + 一处深入”。
WARN 三个最伤分的开局:不问需求直接开画;只报方案不报取舍;全程“感觉撑得住”却没有一个数字。三者都违背了四步法的初衷。

五个高频问答

Q1开场先问什么?哪些话别说?

先围绕使用场景连问:谁用、怎么用、大概多少用户、每秒 / 每天多少请求、数据量多大、读写比例如何——再补三道约束题:延迟能不能容忍、可用性要几个 9、数据会不会过期或丢失。别一上来追着问技术选型(“用 Redis 还是自研?”),别反问面试官“你想要什么方案”把球踢回去,更别在规模没锁定前就开画。问出能影响架构走向的开放问题,把具体方案选择留到画图阶段边画边验证。

Q2心算数字算错了怎么办?

正常——心算的目的本来就不是精确,而是把数量级(order of magnitude)算对。报数时主动展示过程与假设(“按日增 1 亿条、读:写 100:1 估,写约 1.2k/s”);被指正就大方承认、当场重算,并补一句新数字会影响哪个决策(如“存储翻倍,分片就要提前”)。宁可给区间“大概 10² TB 量级”,也不要假装精确到个位——面试官在意的始终是方法。

Q3画图应该画到什么程度?时间不够呢?

优先级永远是把高层骨架画完整:客户端 → 负载均衡 → 应用服务 → 存储 / 缓存,带好连线方向,并能指着图讲通一条读写路径;之后再挑一个最核心的组件深入。图的美观与细节最后再说。配合时间盒:10 分钟给高层、10 分钟给核心,宁可“全链路清晰 + 一处深入”,也不要平均用力、处处半吊子。每个框给一句“为什么需要它”,比画得好看重要得多。

Q4业务域完全没接触过,怎么硬答?

系统设计考的是通用组件与取舍,业务只是外壳。先用澄清问题把外壳剥掉:谁产生数据、谁消费、频率与规模、有哪些硬约束;然后把问题翻译成你熟悉的数据流——写入端、存储端、读取 / 分发端——再按四步法搭。可以坦诚说“我没实际做过这个场景,但按我的假设应该是……”,并把假设一条条列出来请面试官纠正。诚实 + 结构化,远胜硬装懂。

Q5最后怎么收尾最加分?

留 3~5 分钟收口:用三句话总结“架构是什么、关键决策为什么、已知瓶颈是什么”;主动给一两个可继续的改进方向(如“若读延迟成为问题,我会加 CDN / 缓存”)并排好优先级;最后问一句“你还希望我在哪部分再深入?”,把剩余时间的控制权交还给面试官。切忌时间到了戛然而止,或在结尾抛出全新的设计——那等于把没有论证过的方案扔给面试官。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:How to approach…、Back-of-the-envelope。

地基篇 · §1

性能 vs 扩展性 Performance vs Scalability

面试里“系统怎么扛住更大流量”的第一问。先把两个词分开:性能是单台机器快不快,扩展性是加机器以后系统能不能真的变强。

性能 vs 扩展性

难度 ★☆☆ 面试频率:高 关联:§6 负载均衡
先背这一句(开场 30 秒)“优化顺序是:先纵向(换更强的机器),到天花板或成本划不来时再横向(加机器);而横向扩展的前提是服务无状态、数据能分片。”

两个词,两条路

性能(Performance):单台服务器在单位时间内能处理多少请求、单个请求多快返回。指标:QPS、P99 延迟、吞吐。扩展性(Scalability):系统能否通过“加资源”持续变强——加机器(横向)或换大机器(纵向)后,吞吐是否近似线性增长。

一个常见的面试陷阱是:把“系统慢”直接归因于“机器不够”。慢可能是代码问题、慢查询、锁竞争——先诊断再扩容,否则加多少机器都白搭。

纵向 vs 横向

纵向扩展 · 换更大的机器 一台大服务器 更多 CPU / 内存 / 磁盘 不用改代码,但不便宜 ⚠ 有物理天花板 · 换机要停机 横向扩展 · 加机器 用户 用户 用户 负载均衡器 服务器 1 服务器 2 服务器 3 无状态副本 无状态副本 无状态副本 容量近似线性增长 · 可随时弹性伸缩 ⚠ 前提:无状态 + 数据层能分片
图 1-1纵向 vs 横向。纵向简单但撞天花板;横向是互联网的主流玩法,但它有两个硬前提——应用无状态(谁接请求都一样)与数据能分片(存储不再是单点)。
维度纵向扩展横向扩展
做法换更大的机器(CPU/内存/磁盘)增加同构机器实例
代码改动基本不用需无状态化、可水平伸缩的设计
上限单机物理天花板理论上无限(钱与复杂度说了算)
成本曲线越往上越陡(大机器贵得离谱)近似线性,还能按量付费
停机影响换机/升级通常要停机滚动发布、随时增减实例
故障半径单点,挂了全挂坏一台只是容量少 1/N

三层各自怎么扩展

一个典型 Web 系统分三层,每层“能不能横着长”难度完全不同,面试要分层答:

横向扩展的难度是逐层升高的 Web A Web B Web C 应用层:最容易横着长 只要无状态化(状态进 Redis/DB) 前面挂负载均衡即可无限加实例 缓存 1 缓存 2 缓存层:需要“分片”设计 多节点缓存要定“数据在哪个节点” (一致性哈希,见 §10 与例题 T4) 主库(写) 强一致 · 单写入口 只读副本 数据层:最难横着长 先读写分离(读加副本),写是单点 再往上要分片/换分布式数据库
图 1-2三层各自的扩展路径。面试答扩展题的分层模板:Web 层加实例(最容易)→ 缓存层分片 → 数据层先读写分离再分片(最难,留到最后讲)。

什么时候“先纵后横”

  • 起步阶段:用户少,直接上集群是过度设计——一台高配机器最省钱省事。
  • 遭遇单机瓶颈后:先纵向(顺手的事),同时做无状态化与健康检查,为横向铺路。
  • 规模与成本拐点:当“加一台机器”比“换更大机器”更划算、或需要容灾时,切横向。
面试话术:“我的默认路径是『单机起步 → 无状态化 + LB → 缓存 + 读写分离 → 分片与异步』,每一步只解决当下最疼的瓶颈。”——这句话几乎能接住所有“怎么扩展”类问题。

高频追问 Q&A

Q1什么时候该用纵向而不是横向?

阶段 0→1 的早期、对延迟极敏感又无法分片的单体(如某些强一致核心)、以及临时性流量尖峰(云上直接升配最快)。横向不是免费的:引入分布式后的一致性、排错、运维复杂度都上升。

Q2为什么无状态是横向扩展的前提?

如果用户会话/数据存在某台机器内存里,请求被 LB 分到别的机器就“失忆”了(见 §6)。把状态挪到共享存储后,任何实例都能服务任何请求,加实例、故障替换才真正生效——所以面试里“横向扩展”和“无状态化”总是一起出现。

Q3数据库为什么不能像 Web 层那样随便加机器?

Web 层无状态,请求不依赖历史;数据库有状态且要保证一致性——多写节点会引入写冲突与同步问题。所以数据库的扩展是“设计出来”的:先读写分离(读横向),写压力再大就得分片或换分布式数据库,每一步都有代价。

Q4加机器后吞吐翻不了倍,卡在哪?

大概率卡在共享瓶颈:数据库单点、缓存命中率下降导致的回源、热点 key、锁竞争、网络带宽。这就是为什么设计时要“分层看瓶颈”,每层独立伸缩,别只盯应用服务器。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0):Performance vs scalability、Horizontal scaling、Load balancer。

地基篇 · §2

延迟 vs 吞吐 Latency vs Throughput

两个总被混用的词:延迟看单个请求有多快,吞吐看整体每秒能处理多少。心算、容量估算、压测报告都建立在把它们分开之上。

延迟 vs 吞吐

难度 ★☆☆ 面试频率:中 心算地基 · 常配 A1 速查表
先背这一句(开场 30 秒)“延迟是『一个请求从发出到返回』的时间,吞吐是『单位时间能完成的请求数』;提升吞吐靠并发与流水线,但延迟往往受物理极限(光速、磁盘寻道)约束。”

水管比喻:把系统想成管道

水管 = 系统 水(请求)从入口流到出口 请求进 响应出 延迟 Latency = 一滴水走完全程的时间 取决于管长、中间环节(网络/磁盘/CPU 排队) 吞吐 Throughput 单位时间流过多少水 管越粗 = 吞吐越高 加并发、加机器、加带宽 管越长 = 延迟越高 跨机房、跨洲、链路环节多 两者常此消彼长:吞吐上去了,单个请求可能要排队,延迟跟着涨(所以看 P99 而不是只看平均)
图 2-1水管比喻。延迟由“管长与中间环节”决定,吞吐由“管径与并发”决定。面试里问“系统慢”要先分清是延迟问题(单请求慢)还是吞吐问题(并发高扛不住)。

延迟数字金字塔:心里要有数量级

系统设计的判断力,很大程度来自“对数量级有感觉”。下面这组数字(完整版见 附录 A1)是面试心算的基准:

一个请求会经过的“时间黑洞”(对数尺度感受) CPU 缓存 / 内存 ≈ 0.5ns – 100ns(几乎免费) 同机房网络往返 ≈ 0.5ms SSD 顺序读 1MB ≈ 1ms 磁盘寻道 ≈ 10ms 跨洲网络往返 ≈ 150ms 怎么用这组数字? ① 缓存命中(内存 ~100ns)vs 查库(磁盘 ~10ms) 相差约 10 万倍 —— 这就是缓存的“物理意义” ② 同机房 0.5ms vs 跨洲 150ms —— 多机房要就近读 ③ 数据库连接别跨机房 —— 一次调用白扔几十毫秒 压测报告看什么? 看 P50 / P99 而不是平均值:平均值会被 少数慢请求拉高;P99 是“真实用户感知” 吞吐看 QPS / RPS,同时盯错误率与排队
图 2-2延迟数量级金字塔。面试中“为什么加缓存”“为什么分片”“为什么就近部署”都能用这组数字一句话解释。数值以 A1 速查表 为准。

吞吐为什么上不去:串行 vs 并发

  • 串行处理:一个请求走完全程下一个才开始——吞吐 = 1 ÷ 单请求延迟,极低。
  • 并发处理:多请求同时在途(多线程/多进程/异步 IO)——吞吐 ≈ 并发数 ÷ 平均延迟(受系统利用率与排队约束)。
  • 瓶颈法则:吞吐被最慢环节限制(数据库、锁、带宽),加并发不能突破瓶颈,只会让请求排队更久。
一句话记忆:想降延迟 → 缩短链路(缓存、就近、异步旁路);想提吞吐 → 加并发与加资源,但先找到那个最慢的“瓶颈环节”。

高频追问 Q&A

Q1为什么平均延迟低,用户还是觉得卡?

平均值被“大多数快请求”拉低,掩盖了尾部慢请求。正确指标是 P99/P95:99% 的请求在 X 毫秒内返回。尾延迟通常来自 GC、锁、网络抖动与排队,优化重点往往是削掉那 1% 的尖峰。

Q2加并发数是不是吞吐就线性涨?

不是。到达一定并发后,系统进入饱和区:请求开始排队,吞吐不再涨、延迟反而飙升(Little's Law:L = λW)。所以压测要看“吞吐-延迟”曲线,找到最佳工作点,并用限流保护系统不被打进饱和区。

Q3缓存为什么能把延迟从 10ms 干到 100us 级别?

查磁盘 ~10ms(寻道+读),命中内存缓存 ~100ns–250us 量级——相差约 10 万倍到 100 倍。真实收益取决于命中率与数据规模,但方向性结论不变:把热数据留在内存里,是性价比最高的延迟优化(详见 §10)。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0):Latency vs throughput 与附录「Latency numbers every programmer should know」。

地基篇 · §3

可用性与一致性 Availability vs Consistency · CAP

分布式系统最绕不开的一对矛盾:机器会挂、网络会断,这时候你是继续服务但给旧数据,还是宁可报错也不给错数据?这一章把 CAP、一致性模式与可用性数字一次讲清。

可用性与一致性(CAP)

🔥 面试易错点 难度 ★★☆ 关联:§9 数据库 · §10 缓存
先背这一句(开场 30 秒)“CAP 不是『三个里任选两个』,而是:网络分区(P)一旦发生,你必须在『保持可用(A)』与『保持强一致(C)』之间做取舍——选 CP 还是 AP,由业务说了算。”

先理解为什么绕不开

在单台机器上,可用性和一致性根本不是问题:数据就在本地,写进去立刻能读到。问题出在分布式:数据要复制到多台机器(为了容灾与扩展),机器之间靠网络通信,而网络是会断的。

一旦某两台机器“失联”(分区发生),它们各自都还活着、都能服务请求,但无法同步数据。这时你被迫回答一个问题:用户读到的那份数据,允不允许不是最新的?

  • 选一致性 C:我宁可拒绝这次读/写(或等分区恢复),也绝不返回旧数据——适合钱、库存、投票这类“错了就要命”的场景。
  • 选可用性 A:我继续响应,先给“可能旧一点”的数据,等网络恢复再把账算平——适合动态、feed、评论数这类“旧几秒无所谓”的场景。

CAP 三角:三个愿望只能实现两个

C A P 一致性 Consistency 可用性 Availability 分区容忍 Partition CP 派 分区时宁可拒绝/等待 也不给旧数据 例:etcd / ZooKeeper 单写主库 + Quorum AP 派 分区时继续服务 数据最终一致 例:Cassandra / DynamoDB 多写多读 + 冲突调和 口诀:P 是分布式的前提,真正权衡的是 C 与 A —— 先谈 P,再谈 CP 还是 AP
图 3-1CAP 三角。单机不需要选;一旦上了分布式,P(分区容忍)是必须接受的前提,你实际是在 C 与 A 之间按业务取舍。面试把这句话说对,就避开了最大误区。
最常见的扣分回答:“CAP 就是一致性、可用性、分区容忍三选二。”——错。CA 组合只存在于“不会分区”的理想单机里;分布式世界只在 CP 与 AP 之间选。

一致性模式:强、弱、最终

强一致性 写入(同步) 副本 1 副本 2 写必须全部成功才算完成 任何读都看到最新值 代价:写慢、可用性降 例:单写主库同步复制、 etcd / ZooKeeper 适合:余额、库存、投票 弱一致性 写 1 读 1 写后读,可能读到旧值 系统不保证多久收敛 例:多设备间语音留言、 缓存过期前的旧数据 几乎不额外付出性能代价, 但要接受“数据会旧” 适合:展示类、非关键读 最终一致性 写主 副本 A 副本 B 异步复制,短暂不一致 之后自动收敛到最新 例:DNS、CDN、 Cassandra、邮箱同步 适合:读扩散的互联网场景
图 3-2强 / 弱 / 最终一致。面试话术:先默认“最终一致够用”,再根据业务把必须强一致的部分(钱、库存)单独拎出来用强一致方案——这是大厂真实做法。

可用性数字:几个 9 意味着什么

可用性每年停机时间每天停机时间体验
99%(2 个 9)3.65 天14.4 分钟个人项目勉强可接受
99.9%(3 个 9)8.77 小时1.44 分钟一般 SaaS 的门槛
99.99%(4 个 9)52.6 分钟8.6 秒金融/交易常用目标
99.999%(5 个 9)5.26 分钟0.86 秒运营商级,代价极高

想要 99.99%+,就不能靠“少出故障”,而要靠冗余 + 自动故障转移:一台挂了,另一台立刻顶上,用户无感。典型手段:

  • 主备 + 心跳:备机热备,主挂自动切(数据库主从、LB 主备 + 虚拟 IP)。
  • 多副本 + 读流量分摊:挂了也不降级,只是容量少 1/N(见 §6 负载均衡)。
  • 多机房 / 多区域:单机房整体不可用时的最后防线(成本与复杂度最高的那一档)。
面试加分句:“我会先说清楚目标可用性(几个 9)再设计冗余——为了从 99.9% 到 99.99%,成本可能要翻好几倍,先问业务值不值。”

高频追问 Q&A

Q1CAP 里的 C 和数据库事务的 C(ACID)是一回事吗?

不是。ACID 的 C 指事务内约束不被破坏(一致性约束),范围在一笔事务;CAP 的 C 指多副本之间数据的最新性/线性一致,范围在分布式复制。聊 CAP 时别拿 ACID 的 C 来杠。

Q2“最终一致”要多久才一致?

没有硬性保证,取决于复制延迟:同机房毫秒级,跨机房秒级甚至更久(DNS 这类可能以 TTL 计)。面试这么说就够:最终一致的窗口 = 复制链路的延迟 + 重试/冲突调和的时间,所以强一致需求必须单独设计。

Q3设计订单系统时,库存扣减到底选 CP 还是 AP?

库存超卖比“短暂不可用”更致命,一般选 CP:单写主库 + 事务/行锁(强一致),并用 Redis 预扣 + 异步对账来扛峰值。而“商品详情页浏览次数”这种数据就放 AP 体系,最终一致完全够。面试示范:按数据分类给策略,而不是整站一刀切。

Q4主从复制的“从库读到旧数据”属于哪种不一致?

属于读己之写不一致(弱一致的典型)。解法:写后短时间内读主库(read-your-writes)、或让关键读走主库、或版本号判断。这是面试里“数据库读写分离有什么坑”的标准答案来源。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:Availability vs consistency、CAP theorem、Consistency patterns、Availability patterns。

组件篇 · §4

DNS 域名系统 Domain Name System

把 "www.example.com" 翻译成机器能连的 IP 地址——DNS 是互联网的全球电话簿,靠「分层委托 + 逐级缓存」扛住全网的解析流量,还兼职做粗粒度的流量调度。

DNS 域名系统

难度 ★☆☆面试频率:中
先背这一句“DNS 是把域名翻译成 IP 的全球电话簿:用户只记名字,机器只认地址;它还兼职做流量调度(多 A 记录轮询、地理分流)。”

1. 名字 vs 地址:DNS 在解决什么问题

网络层只认 IP 地址:想访问一个网站,TCP 连接必须落到类似 203.0.113.10 的四段数字上。但让用户去记数字既不现实也不稳定——服务器迁移、扩容都会换 IP。DNS(域名系统)就是夹在中间的翻译层:它维护一张分布式的「大表」,key 是 example.com 这样的域名,value 是对应的 IP 以及一批附加元数据。

关键在设计约束:这张表太大了、查询量也太大了,任何一台服务器都装不下也扛不住。所以 DNS 选择了两个朴素手段:按层级分工(每个层级只负责自己辖区内的「指路」)与逐级缓存(就近记住结果,避免每次都问到底层)。这正是下面图 4-1 里那趟旅程能这么快完成的原因。

2. 一次解析的完整旅程

按 TTL 缓存 · 把 IP 还给浏览器 ① 浏览器 / 本机缓存 查缓存未命中,交给解析器 ② 递归解析器 运营商 / 公共 DNS(8.8.8.8) ③ 根服务器 指路:.com 顶级服务器在哪 ④ 顶级服务器(.com) 指路:example.com 的权威 NS 是谁 ⑤ 权威服务器 给出 example.com 的 A 记录 = IP ⑥ 缓存答案并回传 结果按 TTL 存活一段时间 ① → ② 递归查询:客户端把整个解析问题委托给递归解析器,等一个最终答案 ② → ⑥ 迭代查询:解析器逐级追问 根 → 顶级 → 权威,每一级只回答「下一级该问谁」
图 4-1一次完整的域名解析旅程:浏览器只委托一次(递归查询),真正跑腿的递归解析器再逐级追问根、顶级、权威服务器(迭代查询),最后把 A 记录带回并按 TTL 缓存。

先纠正一个常见误解:浏览器并不会「自己满世界问」。它只做两件事——先查自己的缓存(浏览器缓存、操作系统缓存),没命中就把域名交给系统里配置好的递归解析器(通常由运营商或公共 DNS 提供),然后坐等答案。承担逐级询问的是递归解析器,它走的是「迭代」路线:

  • 根服务器 Root:不存储任何具体域名,只负责回答「.com 这类顶级域归谁管」;逻辑上全球共 13 组,通过 Anycast 分布在世界各地的大量镜像上。
  • 顶级域服务器 TLD(如 .com、.cn):回答「example.com 的权威服务器是谁」,通常以 NS 记录形式给出。
  • 权威服务器 Authoritative:真正持有该域名记录、给出最终答案的一方——通常由域名注册商或托管 DNS 服务(Cloudflare、AWS Route 53 等)代为配置。
为什么这么快:绝大多数查询根本走不到根。浏览器、操作系统、递归解析器每一层都按 TTL 缓存结果,热域名的第二次查询往往在本机缓存就命中了。

3. 记录类型:DNS 里装的远不止 IP

DNS 记录可以理解为「域名的各种用途条目」。面试最常问的是 A 与 CNAME,但整套常用类型必须能脱口而出:

A · 地址记录 域名 → IPv4 地址,最常见的记录 例 example.com → 203.0.113.10 AAAA · IPv6 地址记录 域名 → IPv6 地址(128 位) 例 example.com → 2001:db8::1 CNAME · 别名记录 域名别名,指向另一域名再解析 例 www.example.com → example.com MX · 邮件交换记录 收信邮件服务器(数值小者优先) 例 example.com → mail 优先 10 TXT · 文本记录 任意文本:域名校验、SPF 反垃圾等 例 v=spf1 … ~all(邮件发件校验) NS · 权威服务器记录 声明本域 / 子域由谁做权威解析 例 example.com → ns1.cloudflare.com
图 4-2常见 DNS 记录类型一览:A / AAAA 直接给出地址;CNAME、MX、NS、TXT 则分别负责别名、邮件、辖区与校验信息。

几个面试加分点:

  • A 记录直接给 IP,一步到位;AAAA 是它的 IPv6 版本。
  • CNAME 是「别名」而非地址:www.example.com 指向 example.com 后,解析器还要再查一次真正的 A 记录,因此多一跳;RFC 规定同一名字上 CNAME 不能与其他记录共存,裸域(zone apex,即不带前缀的 example.com 本身)也不能用 CNAME——所以 apex 通常配 A/AAAA,或使用厂商提供的 ALIAS/ANAME 伪记录。
  • MX 带优先级数字,越小越优先,可配多台邮件服务器做冗余。
  • TXT 常用于验证「这个域名确实是你的」(如申请 TLS 证书、配置 SPF/DKIM 时)——DNS 因此也承担了不少身份校验任务。

4. DNS 也能做负载均衡

既然解析的「返回值」握在权威服务器手里,它就可以在回答里做文章:同一域名配置多条 A 记录,然后按策略决定每次返回哪一条。Cloudflare、Route 53 等托管 DNS 服务正是以这种方式集中地路由流量:

递归解析器 替客户端发起解析 www.example.com 的权威 DNS 持有 3 条 A 记录 · 按策略轮流返回 ① ② ③ 服务器 A 同一服务 · 203.0.113.11 服务器 B 同一服务 · 203.0.113.12 服务器 C 同一服务 · 203.0.113.13 轮询(Round-robin):同一域名有多条 A 记录,权威 DNS 循环返回 → 各台流量近似均摊 加权轮询适配集群大小 / A/B 测试,权重=0 让维护中的机器退出;另有按地理位置、按延迟路由
图 4-3DNS 层面的负载均衡:同一域名轮询返回不同 A 记录,把流量摊到多台服务器——粗粒度、有缓存延迟,但零额外组件成本。

常用策略及适用场景对比如下:

策略原理典型用途
轮询 Round-robin多条 A 记录按顺序轮流返回多台同质服务器均摊流量
加权轮询 Weighted RR按权重比例决定返回哪条不同容量集群配比;A/B 测试;把权重调 0 让维护中的机器退出流量
基于地理位置按用户出口位置就近返回多机房就近接入(CDN 常用)
基于延迟返回实测延迟最低的节点多可用区 / 多云择优

注意定位:DNS 负载均衡是入口级、粗粒度的调度。它不感知后端进程健康状态(除非运维手动摘除记录),也不保证会话保持;更关键的是解析结果会被各级缓存按 TTL 保留,权重调整不会「实时」生效。生产架构里的分工通常是:DNS 负责把流量引到正确的机房 / 集群入口(含容灾切换),真正的负载均衡器在 IP 之后做细粒度分发与健康检查——两者配合而非互替(详见后文 CDN 与负载均衡章节)。

5. TTL 与缓存:为什么改记录「不生效」

图 4-1 里反复出现的 TTL(存活时间)决定了每条记录能在各级缓存里活多久——短的只有几十秒,长的可达数天。它直接影响两个体验:

  • 解析快不快:TTL 越长,缓存命中率越高,递归解析器越少打扰根与权威服务器;
  • 变更生效快不快:TTL 越长,改完记录后旧值「赖在缓存里」的时间也越长,这就是常说的 DNS 传播延迟(propagation delay)——不是网络慢,而是旧缓存没到期。
工程惯例(变更前先降 TTL):计划切换 IP / CDN 前,先把 TTL 调小到 60 秒左右并等待旧值过期,再执行真正的记录变更,让新值快速传遍全网;稳定后再把 TTL 调回。排查时用 dig example.com +trace 逐级观察每层缓存。

6. 缺陷与坑

  • 解析本身有延迟:虽然各级缓存极大缓解,但冷查询仍要发起网络往返;冷启动页面常因此慢一拍。
  • 管理复杂、责任分散:域名体系通常由注册局、托管服务商、权威服务器等多方共同维护,出问题时的排查链条很长。
  • 是集中攻击的靶子:DNS 是公共咽喉,2016 年 10 月针对 Dyn 的大规模 DDoS 曾让大量用户因「解析不到 Twitter 的 IP」而无法访问 Twitter——服务本身没挂,入口却瘫痪了。
面试要点:说 DNS 的缺点时别只背「慢」,要点出「解析结果被缓存、权威服务器被攻击即可全局瘫痪、运维依赖多方」这三层,再补上对策:多级缓存 + 权威托管商的 Anycast/防 DDoS + 关键域名留多个解析路径。

QA · 高频追问

Q1 从输入 URL 到发起连接,DNS 环节完整经历了什么?

① 浏览器先查自己的缓存与操作系统缓存(含 hosts),未命中则交给系统配置的递归解析器;② 递归解析器缓存未命中后开始「迭代询问」:根服务器只指路到 .com 顶级服务器,顶级服务器再指路到 example.com 的权威服务器,权威服务器最终给出 A 记录;③ 答案沿链路回传,解析器、操作系统、浏览器各自按 TTL 缓存一份;④ 浏览器拿到 IP 后发起 TCP / TLS / HTTP。核心记忆点:客户端只做一次「递归委托」,逐级跑腿都是递归解析器的事。

Q2 A 记录和 CNAME 记录该怎么选?

需要直接对应 IP、或者要配置的是裸域(zone apex,如 example.com)时用 A / AAAA;想让一个名字跟着另一个名字走(例如把 www 统一指向主域、或指向 CDN 提供的 CNAME 目标)时用 CNAME。注意 CNAME 是别名:会额外多一次解析,且 RFC 不允许同一名字上 CNAME 与任何其他记录共存,裸域也不能用 CNAME——因此 apex 要么配 A 记录,要么用托管商的 ALIAS/ANAME 伪记录。

Q3 改了 DNS 记录,为什么半天不生效?

根因是各级缓存仍按旧的 TTL 在存活:你的电脑、递归解析器甚至公司内网 DNS 都可能还握着旧 IP;此外权威侧主从同步也需要时间。工程解法:变更前先把 TTL 调小(如 60 秒),等旧值在全网过期后再改记录,最后把 TTL 恢复;验证时用 dig 直接查权威服务器(绕过缓存),再逐层看递归解析器何时刷新。

Q4 能靠 DNS 完成全部负载均衡吗?

不能,二者是不同粒度的工具。DNS 负载均衡适合:摊流量到多台同质服务器、按地域/延迟就近接入、容灾时整体切换机房或摘除故障 IP。但它无健康检查、不感知后端单机故障;结果被 TTL 缓存导致调整滞后;粒度也粗,做不到按连接或按会话分发。所以生产上 DNS 之后通常还有真正的负载均衡器负责细粒度分发与健康检查——DNS 负责「把流量引对门」,LB 负责「进门后分给谁」。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:

组件篇 · §5

CDN 内容分发 Content Delivery Network

图片、CSS、JS 这些静态资源占了站点流量的大头。CDN 的思路很朴素:把内容搬到离用户近的地方,让“最后一公里”变快。

CDN 内容分发网络

难度 ★★☆ 面试频率:高 关联:§12 通信 · §10 缓存
先背这一句(开场 30 秒)“CDN 把静态内容缓存到全球边缘节点,用户就近获取;源站只在节点未命中(回源)时被访问——既降延迟又给源站挡流量。推模式主动预热,拉模式按需回源。”

没有 CDN 时,跨半个地球的请求有多痛

没有 CDN · 请求直连源站 北京用户 伦敦用户 源站(美国) 所有流量都压到这里 直连 绕大半个地球 跨洲往返 ≈ 150ms+ · 源站带宽被打爆 高峰期图片加载慢、还容易被 DDoS 有 CDN · 就近命中边缘节点 北京用户 伦敦用户 东京用户 边缘 北京 边缘 伦敦 边缘 东京 命中即返回 命中即返回 命中即返回 源站(美国) 只处理边缘未命中的回源 回源(少量) 就近返回 ≈ 10-50ms · 源站流量大减
图 5-1没有 CDN vs 有 CDN。CDN 是“内容的最后一公里加速器”:命中边缘节点时,用户延迟从 150ms 级降到几十毫秒甚至更低,同时源站只承担“没命中”的回源流量。

推模式 vs 拉模式:内容怎么进 CDN

推模式 Push CDN 源站 所有 CDN 节点 内容更新时主动推送 源站决定“什么时候更新什么” 更新即时,节点内容可控 代价:所有节点都要存/推, 小站点流量不大时很浪费 适合:内容量小但更新频繁、要强控 拉模式 Pull CDN 源站 边缘节点 用户请求 未命中 →回源 取回并缓存 → 下次直接命中 代价:第一次请求慢(冷启动) 适合:静态资源多、读多、懒加载场景
图 5-2推 vs 拉。现实中主流是拉模式(按需回源 + TTL 过期),因为简单且贴合“用户访问什么才缓存什么”;推模式用于必须即时更新的少量核心内容(如活动页、版本发布)。

缓存失效与回源放大的坑

  • TTL 过期:节点缓存带 TTL,过期后回源取新——更新频率决定 TTL 长短。
  • 主动失效/刷新:内容下线或改错时,调 CDN 的 purge 接口强制清掉指定 URL。
  • 版本化文件名:app.a1b2c3.js 这类带哈希的文件名,让“新版本”天然是“新 URL”,不用清缓存——前端资源的标准做法。
  • 回源风暴:热点内容 TTL 同时过期 → 所有节点同时回源 → 源站被打穿。解法:错峰过期(TTL 加随机抖动)、源站套缓存/限流。
面试高频追问:“CDN 适合放动态内容吗?”传统 CDN 擅长静态;动态/个性化接口一般不进 CDN(会引入缓存一致性问题)。折中是边缘计算(把少量动态逻辑下沉到节点)或只缓存“按用户分片”的非敏感数据(如公共列表页)。

高频追问 Q&A

Q1CDN 和浏览器缓存、服务端缓存什么关系?

它们是缓存金字塔的邻居(详见 §10):浏览器缓存最近(本机)、CDN 次之(边缘节点)、服务端缓存最远但可控性最强。请求先问浏览器,再问 CDN,最后才到源站——每层命中一次,源站压力就小一分。

Q2https 站点用 CDN 安全吗?

安全,但要正确配置:把证书放到 CDN 节点(边缘做 TLS 卸载),源站到 CDN 之间用回源鉴权/私有证书加密。坑在于:若回源链路没加密或 CDN 缓存了敏感响应头,会引入泄露面——敏感接口要么不进 CDN,要么严格按 URL/Header 缓存策略控制。

Q3为什么说 CDN 还能防 DDoS?

边缘节点分散且带宽巨大,攻击流量被摊到全球节点上“吸收”,到不了源站;配合 WAF 规则能挡掉大部分应用层攻击。但源站 IP 一旦泄露(如被直接解析到),防护就失效——所以别把源站 IP 暴露在 DNS 记录里。

Q4什么场景“不该”用 CDN?

① 个性化内容(每个用户不同,缓存命中率趋近 0);② 必须强一致的数据(缓存旧值不可接受);③ 极低频的内部 API(加了 CDN 反而多一跳);④ 已有内网/专线的高性能内部系统。面试时说“CDN 不是万能的,先看命中率与一致性要求”就很加分。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0):Content delivery network(Push/Pull CDNs)。

组件篇 · §7

反向代理 / Web 服务器 Reverse Proxy & Web Server

所有请求进系统的“第一道门”。反向代理本身不写业务,却决定了系统的入口体验:TLS、压缩、静态资源、限流都在这一层。与 §6 负载均衡常被混为一谈,这里把边界讲清。

反向代理 / Web 服务器

难度 ★☆☆ 面试频率:中 关联:§6 负载均衡 · §13 安全
先背这一句(开场 30 秒)“反向代理替后端服务器接客:对外统一入口,对内隐藏后端细节——Nginx/HAProxy 这类组件在这里做 TLS 卸载、静态文件、压缩、缓存与限流;它和负载均衡经常是同一台机器上的两个角色。”

正向代理 vs 反向代理:一个替用户跑,一个替服务器挡

正向代理 · 替用户(客户端)办事 用户 正向代理 目标网站们 代理知道“用户是谁” 网站只看到代理的 IP 用途:翻墙/访问控制/缓存/ 企业出口审计、抓包调试 “替用户去访问别人” 反向代理 · 替服务器(源站)挡门 用户们 反向代理 后端服务器们 IP 对外不可见 代理知道“服务器是谁” 用户只知道代理的域名 用途:统一入口、隐藏后端、 TLS/压缩/静态/限流/灰度
图 7-1正向 vs 反向。方向看“替谁挡”:正向代理站在用户前面替用户出门,反向代理站在服务器前面替服务器接客。企业里的“代理”多是正向;网站入口的 Nginx 是反向。

反向代理能干的事(一图流)

入口反代 域名、TLS 证书、 HTTP/2、限流、 WAF、访问日志 所有请求先进这里 内容处理 静态文件直出 gzip/brotli 压缩 响应缓存(proxy_cache) 这些活别让业务代码干 转发到后端 按路径/域名路由 负载均衡 + 健康检查 灰度/金丝雀分流 转发时“脱一层皮”: 典型产品:Nginx / HAProxy / Caddy / 云负载均衡(SLB/ALB) 还有一类“网关”(API Gateway):在反代基础上加鉴权、路由到微服务、限流配额(见 §8)
图 7-2反代把“流量进出的脏活”集中在一层。让业务服务只关心业务:请求先被反代剥掉 TLS、压掉重复头、滤掉静态请求,剩下真正的动态请求才到后端。

反向代理 vs 负载均衡:别再说“一样”

对比负载均衡(§6)反向代理
核心职责把请求分给多台后端(分发/健康检查/容灾)作为统一入口做协议处理(TLS/压缩/路由/静态)
是否必须多后端是——单台后端没有“均衡”可言否——单台后端也能用反代(入口/安全收益)
侧重可用性 & 容量:谁挂了、谁闲着入口治理:谁来、走哪、带什么
现实关系同一个进程常同时干两件事:Nginx 既做反代又做负载均衡;面试中把“分发的活”归 LB、“入口的活”归反代即可

高频追问 Q&A

Q1什么时候该上反向代理?

需要统一 TLS 证书与入口、要直接吐静态文件、要多服务按路径路由、要限流或灰度——任何一个理由都够了。几乎任何对外 Web 服务的第一版就该有(云上一般用托管 LB/网关,自建则 Nginx)。

Q2反代把请求转发给后端时,用户 IP 丢了怎么办?

反代通常加 X-Forwarded-For / X-Real-IP 头带上原始 IP,后端取它做限流与审计。安全坑:必须只信任反代写入的这个头(否则用户伪造头骗过基于 IP 的限流),必要时用“反代→后端”的内网信任模型保证。

Q3Nginx 挂了怎么办?它不也是单点吗?

入口层自己也要高可用:主备 + 虚拟 IP(keepalived/云浮动 IP)、或多活 + 上层路由 ECMP;容器环境则让网关控制器(Ingress)管多副本。原则与 §6 Q4 一样:任何“必须活着”的组件都要有自己的冗余与故障转移方案。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0):Reverse proxy (web server)、Load balancer vs reverse proxy。

组件篇 · §8

应用层 · 微服务 Application Layer & Microservices

从“一台服务器跑整个应用”到“几百个服务各自独立部署”,中间隔着的不是炫技,而是一整套关于故障隔离、伸缩、团队组织的权衡。这一章讲清应用层为什么最好无状态、微服务拆了什么又付出了什么。

应用层与微服务

难度 ★★☆ 面试频率:高 关联:§6 LB · §9 数据库 · §11 异步
先背这一句(开场 30 秒)“应用层最好无状态:状态交给存储,实例就能随便加减、故障随便替换;团队与规模到了一定程度,按业务边界拆微服务,让每个服务独立部署、独立伸缩、独立故障——但拆分不是免费的。”

单体不是原罪:先理解它为什么撑不住

  • 部署耦合:改一行代码也要整包发布,任何模块的缺陷都可能拖垮全体。
  • 伸缩浪费:某个功能(如报表)吃 CPU,却要为整个单体多开机器。
  • 团队踩踏:几十人改同一个代码库,合并冲突与发布窗口成为日常痛苦。
  • 技术栈锁死:想给某个模块换语言/框架,几乎不可能。

但注意:单体的部署与事务最简单、最不易错。所以行业共识是——先用单体把业务跑通,等痛点真实出现(部署太频繁/团队太大/局部需要独立伸缩)再拆。

单体 vs 微服务

单体 · 一个进程装下所有 一个应用进程 用户模块 订单模块 支付模块 报表模块 一起部署 · 一起伸缩 · 一起挂 开发简单 · 事务好做 · 调用零成本 规模一大:发布如履薄冰、伸缩浪费 微服务 · 每个服务独立部署 用户服务 订单服务 支付服务 库存服务 通知服务 报表服务 独立部署 · 独立伸缩 · 故障隔离 代价:分布式事务、链路排错、运维爆炸
图 8-1单体 vs 微服务。微服务的本质是“把部署单元变小”:服务间用网络调用(RPC/REST,见 §12)替代进程内函数调用——这是所有分布式痛点的来源,也是独立伸缩与故障隔离的代价。

服务间调用:失败是会传染的

单体里函数调用失败=异常;微服务里服务调用失败=网络错误,而网络错误会连锁放大:A 调 B 超时,A 的线程被占住,流量继续涌来,A 的线程池被打满,A 自己也开始超时——这就是“雪崩”。面试里聊微服务必带三件套:

  • 超时与重试上限:给每次调用设超时;重试要带退避与上限,否则故障时全员重试等于自轰。
  • 熔断(Circuit Breaker):下游连续失败达到阈值,上游直接快速失败(不再等超时),让下游有时间恢复。
  • 舱壁/隔离(Bulkhead):不同下游用独立线程池/连接池——报表服务挂了不该拖垮支付链路。
金句:“分布式系统里没有‘try-catch 就能兜底’的事——超时、重试、熔断、降级要成体系设计,还要有 trace 才能定位是谁拖慢了谁。”

服务发现:新实例上线,谁来告诉调用方?

注册中心 服务注册表(谁在哪儿) 订单服务 实例1 订单服务 实例2 10.0.1.8:8080 10.0.1.9:8080 支付服务(调用方) ① 注册 + 心跳续约 ② 挂了 → 心跳断 → 摘除 ③ 调用方拉取/订阅地址列表 ④ 新实例上线自动加入 无需改调用方配置 代表:K8s Service / Consul / Nacos / Eureka
图 8-2服务发现。实例数量是动态的(扩容、发布、故障替换),调用方不能写死 IP。注册中心维护“谁活着、在哪”,实例启动注册、心跳续约、失联自动摘除——这也是 §6 “健康检查”思想在服务网格层的延伸。

什么时候“别”上微服务

  • 团队 < 2 个披萨能喂饱(十几人以内):单体的开发效率更高。
  • 事务强一致是核心卖点(账务、库存强扣减):跨服务事务是地狱,先留在单体/模块化单体。
  • 业务边界还不清晰:拆错边界的代价远大于不拆。
面试话术:“我不会为了微服务而微服务。合理的路径是:模块化单体起步 → 当『部署频率、团队规模、局部伸缩』三者出现真实痛点时,按业务边界逐个拆出,配合服务发现、熔断与可观测性一起上。”

高频追问 Q&A

Q1微服务之间的事务怎么处理?

放弃跨服务的分布式强事务(2PC 又慢又复杂),改为最终一致:本地事务 + 可靠消息(Saga / 事务性消息),配合对账与补偿。核心话术:“能用消息异步解的不用同步调用;必须同步的,容忍最终一致并加对账。”

Q2如何定位“到底是谁慢”了?

靠链路追踪:每个请求带全局 traceId,各服务上报调用耗时,拼成完整调用链(Zipkin/Jaeger/云 Trace);再加指标(RED:Rate/Errors/Duration)与日志。没有可观测性就上微服务 = 蒙眼开车。

Q3微服务和 SOA 什么区别?

SOA 是更早的面向服务架构(偏企业总线 ESB 重集成),微服务强调轻量通信(HTTP/RPC 直连)、去中心化治理、按业务能力拆分、独立部署——一句话:微服务是“把 SOA 的理念做轻做彻底”。面试提一嘴即可,别展开。

Q4数据库要跟着服务拆吗?

典型做法是数据自治:每个服务拥有自己的库/表,别家只能通过它的 API 访问(否则改表结构就波及全网)。代价是跨服务查询要拼装或走 CQRS/事件。早期阶段允许共享库,拆服务与拆库最好同步演进。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0):Application layer、Microservices、Service discovery。

组件篇 · §9

数据库 SQL / NoSQL Database · RDBMS vs NoSQL

数据库是系统的“存储之锚”:关系型用表 + 外键 + ACID 事务守住复杂关系与强一致,NoSQL 用键值 / 文档 / 列族 / 图四种模型换灵活与横向扩展。选型先答三问:关系强不强、模式固定吗、要不要横着长。

数据库:关系型与 NoSQL

🔥 面试核心章难度 ★★☆
先背这一句“关系型赢在『关系+事务』,NoSQL 赢在『模型灵活+水平扩展』;选型就问三个问题:数据之间关系强不强、访问模式固定吗、要不要横着长。”

关系型数据库:表、外键与 ACID

关系型数据库(Relational Database,代表:MySQL、PostgreSQL)把数据组织成一张张“表”:表由固定的列结构(严格 schema,模式)与行组成,行与行之间通过外键(Foreign Key)互相引用;需要跨表取数时,用联结(JOIN)在查询里把它们拼起来——这正是关系模型的核心能力。

它真正的护城河是“事务”:ACID——原子性 Atomicity(事务内所有操作要么全部完成、要么全部不完成)、一致性 Consistency(事务使数据库从一个有效状态迁移到另一个有效状态)、隔离性 Isolation(并发执行事务的结果与串行执行相同)、持久性 Durability(事务提交后,对系统的影响永久保留)。账户、订单、库存这类“错了要命”的数据,靠的就是这条承诺。

代价同样清楚:单机写扩展有天花板,所以它的扩展有固定套路——主从复制(读写分离)、主主复制、联合、分片,再配合非规范化与 SQL 调优,每一步都是“清晰的扩展模式”。

订单表 orders 用户表 users PK id FK user_id amount PK id name email 1001 1 ¥299 1002 2 ¥199 1003 1 ¥449 1 张伟 zhang@mail.com 2 李娜 li@mail.com 3 王强 wang@mail.com 外键:user_id → users.id 主键 PK:一行数据的唯一标识 外键 FK:引用另一张表的主键 A C I D 原子性 · Atomicity 一致性 · Consistency 隔离性 · Isolation 持久性 · Durability 全部完成或全部不完成 库在有效状态间迁移 并发执行 = 串行执行 提交后影响永久保留
图 9-1关系模型的三件套:表、行、外键与 ACID 事务。订单表用外键 user_id 引用用户表的主键 id;行数据的每一次写入都由 ACID 事务包裹,保证并发与崩溃下“账目不乱”。

NoSQL:一个名字,四种模型

NoSQL 不是“没有 SQL”,而是四类数据库的统称:键值(Key-Value)、文档(Document)、列族(Column)、图(Graph)。它们的共同点很一致:数据是非规范化的,联结大多在应用程序代码里完成;绝大多数 NoSQL 无法提供真正符合 ACID 的事务,转而支持最终一致,用 BASE 描述自己——基本可用(Basically Available)、软状态(Soft state)、最终一致(Eventually consistent),把可用性放在一致性之前。

键值存储 Key-Value Redis 以哈希表为抽象:按 key 一次定位,读写接近 O(1) 数据常驻内存/SSD,适合简单、高频改动的数据 典型场景 缓存 · 会话状态 · 购物车 · 排行榜 文档存储 Document MongoDB 以“文档”为值的键值库:一个文档装下对象全部信息 字段可各不相同、按内部结构查询,模式天然灵活 典型场景 用户资料 / 帖子 / 商品——结构常变的数据 列族存储 Column Cassandra 嵌套列族:行键 + 列 + 带版本时间戳的值,键按序存放 师承 Bigtable,高可用、高可扩展,面向海量数据 典型场景 埋点 / 日志 / 时序——海量写入型大数据 图数据库 Graph Neo4j 节点 = 记录、弧 = 关系,为外键繁多、多对多而优化 社交类深链查询性能高;较新,工具与生态资源偏少 典型场景 好友 / 关注关系——“朋友的朋友”式深链查询
图 9-2四类 NoSQL:一个名字、四种数据模型。每一类都源于一种抽象模型(哈希表 / 文档 / 列族 / 图),也各自绑定最典型的场景与代表产品;选 NoSQL 之前,先选对“模型”。

顺着这四种模型认产品:键值存储以哈希表为抽象(Redis),读写接近 O(1),操作有限,常作为缓存、会话与元数据,也是文档、图等更复杂存储的“地基”;文档存储把一个对象整体存进一个文档(MongoDB、CouchDB),支持类 SQL 查询;列族存储面向海量写入(Cassandra、HBase,师承 Google 的 Bigtable),键按字典序存放,值带版本时间戳;图数据库把“关系”当一等公民(Neo4j),节点是记录、弧是关系。

SQL 还是 NoSQL:三个问题定方向

源料把“选 SQL 的理由”与“选 NoSQL 的理由”各列了一屏:SQL 侧是结构化数据、严格模式、关系型、需要复杂联结、事务、清晰的扩展模式、索引查询快、生态资源丰富;NoSQL 侧是半结构化、动态模式、非关系型、无需复杂联结、TB 级甚至 PB 级数据、高数据密集负载、IOPS 高吞吐。抽象成一道三问决策题,面试照这个答就立得住:

1 2 3 数据关系强不强? 访问模式固定吗? 要不要横着长? 常做多表 JOIN、外键绕不开? schema 与字段会频繁变动? 数据 TB 级、写吞吐单机难扛? 三问都偏“结构化 · 固定 · 规模可控” → 选 SQL 只要有一问明显偏“动态 · 海量 · 要横着长” → 按模型选 NoSQL 选 SQL · 关系型 结构化数据、严格模式,关系与复杂联结是强项 ACID 事务、强一致——账与订单的默认选择 索引让查询飞快;主从/联合/分片扩展模式清晰 生态成熟:开发者、工具、社区与既有代码库多 选 NoSQL · 四选一 半结构化、动态 schema,非关系数据无需复杂联结 数据量冲 TB~PB 级、高 IOPS 高写入吞吐 键值 / 文档 / 列 / 图——按“最痛的一问”挑模型 多数无真 ACID、最终一致(BASE),联结移到应用层
图 9-3SQL 还是 NoSQL?先答三个问题。三问都偏“结构化 / 固定 / 可控”走 SQL;只要出现“灵活 / 海量 / 要横着长”的强信号,就按最痛的那一问去挑对应的 NoSQL 模型。
演进次序:别把 NoSQL 当 SQL 的“高性能平替”。多数业务的正确顺序是——先加索引、做 SQL 调优,热点数据上缓存,读多写少做主从读写分离(从库扛读),这三步做完还撑不住,才轮到分片或迁 NoSQL。
最常见的坑:把“要事务的数据”硬塞进文档库。NoSQL 大多没有真正符合 ACID 的事务、复杂联结要自己在应用层拼;而反范式(非规范化 Denormalization)是在多张表里冗余数据副本、用写性能换读性能、省掉高成本联结(PostgreSQL / Oracle 的物化视图可帮忙维持副本一致)——它是优化关系型的工具,不是抛弃关系型的理由。

选型速查表:对号入座

面试现场不用背清单,记住“数据长什么样 → 该上什么”这条映射即可:

数据长什么样首选类型一句话理由
账户 / 订单 / 资金——强关系、要事务MySQL / PostgreSQL关系型ACID 与复杂 JOIN 是刚需,账不能算错
会话 / 购物车 / 排行榜Redis键值O(1) 读写、内存级延迟,还能兼任缓存层
用户资料 / 帖子 / 商品——结构常变MongoDB文档一个文档装下对象全部信息,字段可不同
埋点 / 日志 / 时序——海量写入Cassandra / HBase列族师承 Bigtable,高可用高扩展,为大数据而生
社交关系 / 关注链——多对多、外键繁多Neo4j图节点 = 记录、弧 = 关系,深链遍历是强项
读多写少的存量业务SQL + 缓存 + 主从复制关系型“清晰的扩展模式”第一步:先扩读,再谈分片

面试 QA

Q1什么时候才该放弃 SQL、换 NoSQL?

信号来自数据本身:半结构化、字段与访问模式频繁变化;不需要复杂联结;写入吞吐单机扛不住、数据量走向 TB~PB;业务能接受最终一致。反过来,如果天天 JOIN、账务类强一致需求,NoSQL 只会逼你手写联结和补偿——老老实实用 SQL。

Q2关系型数据库怎么水平扩展?

源料给的是组合拳:主从复制(主库读写、从库只读,主库离线可先只读运行再提升从库)、主主复制(两主互相协调)、联合(按业务域拆成多库)、分片(按 key 把数据打散),再配合非规范化与SQL 调优。推荐的落地顺序:先索引 → 再缓存 → 主从读写分离 → 最后才分片。

Q3Redis 到底是缓存还是数据库?

键值存储以哈希表为抽象、读写 O(1),常驻内存/SSD,性能高但操作有限——按 key 存取,更复杂的查询逻辑要上抛到应用层,或换文档型。所以它最适合做缓存、会话、购物车这类临时高频数据:作为“缓存层”写进架构,而不是硬当核心账本。

Q4选了 NoSQL,事务怎么办?

先承认代价:绝大多数 NoSQL 没有真正符合 ACID 的事务,默认最终一致(BASE 把可用性放在一致性前面)。对策要么把强一致部分留在 SQL(账本、库存),要么用应用层模式兜底:幂等 + 补偿(如 Saga),把“最终一致”补成“业务上的一致”。面试答到“我不是白拿一致性,而是明确为它付了什么代价”就够了。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:

组件篇 · §10

缓存 Cache 缓存策略 · 淘汰 · 一致性

缓存是系统设计里性价比最高的第一板斧:本章图解五层缓存金字塔、Cache-aside 旁路读写流程、直写/回写/TTL 失效三种写策略,再聊淘汰、一致性这个必考点与「击穿/穿透/雪崩」三兄弟。约 9 分钟读完,含 4 张图解。

缓存

🔥 面试必考难度 ★★☆
先背这一句“缓存把『贵』的读取结果存进『快』的地方:读多写少才值得缓存;套路是 Cache-aside(先读缓存,未命中再查库并回填),并想清楚失效与淘汰策略。”

为什么需要缓存:读路径上的第一层缓冲

缓存 Cache 的原始动机很朴素:把「已经算过的答案」暂存起来,下次直接复用——它能提高页面加载速度,也能降低服务器与数据库的负载。在一个分发器模型里,请求进来先查“这个请求之前响应过没有”,命中就直接把旧结果吐回去,省掉真正的处理。

数据库分片、副本理论上能让读分布均匀,但真实流量总有热点:热门数据被反复读,形成不均匀的读放大。此时在数据库前面加一层缓存,就能抹平热点与突发流量对数据库的冲击。所以判断“要不要缓存”的标准只有两条:访问模式读多写少、数据允许短暂过期。缓存永远只装得下“最热”的一小部分——因为放缓存的内存(RAM)比磁盘贵得多、也小得多。

缓存金字塔:从客户端到数据库,一层层叠

从用户到数据库之间,缓存可以出现在好几个位置。越靠近用户的一端命中越快、容量越小;越往下越接近权威数据、容量越大。图 10-1 把常见的层级叠成一座金字塔:

缓存金字塔:请求从上到下逐层试,全 miss 才轮到数据库 离用户越近越快越小 · 越往下越大越全 ① 客户端缓存(浏览器 / OS) 静态资源 · 渲染页面 · 会话 ② CDN 缓存(内容分发网络) 图片 / 视频 / JS 等静态内容分发到就近节点 ③ Web 服务器缓存(反向代理 · Varnish) 直接返回静态与动态内容,不必连到应用服务器 ④ DB 查询级缓存(查询语句哈希 → 结果) 存「查询哈希 → 结果」,命中免查库;失效粗:改一行要删一串结果 ⑤ 对象级缓存(以对象为粒度,常见落地是 Redis / Memcached) 会话 / 活动流 / 序列化对象 / 渲染后的整页 —— 删对象即失效,可异步组装 数据库 DB = 磁盘上的权威数据源 —— 上面每一层缓存,都是为了少打它
图 10-1缓存金字塔。从客户端(浏览器/操作系统)到数据库,缓存一层层叠放:客户端、CDN、Web 服务器各管一摊静态与动态内容;紧贴数据库的是应用层内存缓存(Redis / Memcached)的两种粒度——查询级与对象级。请求自上而下逐层尝试命中,全部 miss 才落到数据库。

第 ④⑤ 层在图中紧贴数据库,因为它们是应用与数据存储之间的键值缓存:数据放在 RAM 里,比磁盘上的典型数据库快几个数量级(内存访问约 100 ns 量级,磁盘寻道约 10 ms 量级)。RAM 又贵又有限,所以要用 LRU 这类淘汰算法把「热门数据」留在内存,冷数据不占地方。在 Memcached 与 Redis 之间选型时记住:Redis 额外提供持久化选项与内置数据结构(有序集合、列表等)。

同一层缓存还能按粒度细分——行级、查询级、完整的可序列化对象、完全渲染的 HTML,从细到粗。查询级把「查询语句哈希 → 结果」存进缓存,实现直白,但复杂查询很难精确失效,一行数据变化可能连带要删掉一堆包含它的缓存结果;对象级把数据当对象处理,底层数据变了就删掉对应对象,还允许 worker 用最新缓存对象异步组装。因此生产上更建议缓存这些对象:用户会话、完全渲染的 Web 页面、活动流、用户图数据。另外尽量别做基于文件的缓存——文件系统在复制与自动伸缩时都很难办。

Cache-aside 旁路缓存:面试默认开局

面试里提到缓存,默认先讲 Cache-aside(旁路缓存,也叫懒加载 lazy loading):缓存不跟数据库直接交互,一切读写都由应用代劳。Memcached 通常就是这种用法。它的完整读写流程见图 10-2——读:先读缓存,未命中才查库,查到后回填缓存再返回;写:直接更新数据库,再把缓存里的旧条目删掉。

Cache-aside 旁路缓存:完整读写流程 蓝=数据库 · 琥珀=缓存 · 红虚线=miss · 灰=客户端 客户端 Client 应用 App 缓存 Cache(Redis) 数据库 DB ① 用户发起读请求 ② 查缓存:GET user:1 ③ miss → null(命中即返回) ④ 查库:SELECT * FROM users WHERE id=1 ⑤ 返回行数据 ⑥ 回填:SET user:1 = 行数据 ⑦ 返回结果给用户 写路径 ⑧ 用户发起写请求(更新 user:1) ⑨ 写库:UPDATE users SET …(先落库) ⑩ 删缓存条目 DEL user:1(防旧值)
图 10-2Cache-aside 旁路缓存读写流程。读:先查缓存,命中直接返回;未命中(③ miss)才去查库、回填缓存再返回,所以只缓存真正被请求过的数据。写:先落数据库,再把缓存条目删除,让旧值尽快失效;下一次读 miss 会自动回填新值。

Cache-aside 有几个必须背的坑:① 未命中要走「查缓存 → 查库 → 回填」三步,首次访问延迟明显;② 数据库更新后缓存会短暂过时,需要靠 TTL 强制刷新或改用直写模式缓解;③ 节点故障被新节点替换时,新节点的缓存是空的(cold cache),延迟升高,直到缓存慢慢热起来。这也是为什么缓存层要预留充足容量、避免频繁上下线。

写入的时候怎么办:直写 / 回写 / TTL 失效与刷新

Cache-aside 只规定了“读”,写入怎么办取决于业务对新鲜度、吞吐与丢失容忍度的要求。图 10-3 对比三种主流的写路径(外加 Refresh-ahead 自动刷新这个“配菜”):

缓存写策略对比:直写 / 回写 / TTL 失效与自动刷新 Write-through 直写:同步写缓存 + 落库 ① 应用写入缓存 缓存当主存储 ② 缓存同步写库 等落库完成再返回 ③ 读刚写的直接命中 数据永不过时 代价:每次写都同步等落库,写路径慢;收益:缓存永不过时、读刚写入的数据立刻命中 —— 适合写量不大又要「写后立读一致」的场景 Write-back 回写:先写缓存,异步落库 ① 应用写入缓存 立即返回(快) ② 缓存异步落库 攒批 / 稍后回写 DB ③ 写吞吐大幅提升 代价:落库前可能丢 收益:写只进内存、立即返回,DB 被异步削峰、吞吐高;风险:内容成功存库前宕机会丢数据,实现也比直写复杂 TTL / 失效 过期兜底 + 自动刷新 ① TTL 内直接命中 数据新鲜度有上限 ② 到期自动失效 热数据到期前自动刷新 ③ miss 后重新回填 与淘汰策略叠加使用 TTL 给旧值设「保质期」,到期强制失效;可配置成在到期前自动刷新最近访问过的内容(Refresh-ahead)——能准确预测会再被请求才划算,预测不准反而比不刷新更糟
图 10-3三种写策略的时序与取舍。直写把缓存当主存储、同步落库,换来“不过时”但写路径慢;回写只写内存、异步批量落库,写吞吐高但要承担落库前丢数据的风险;TTL 失效给旧值设“保质期”,配合 Refresh-ahead 自动刷新可减少过期瞬间的穿透。

选型口诀:读多写少、能容忍短暂旧值 → Cache-aside + TTL 兜底(最简单,默认答案);要求「读己之写」立刻一致且写量不大 → 直写;写入是瓶颈、能容忍丢失窗口 → 回写。另外别忘了两点:直写模式下很多写入的数据可能根本没人读(用 TTL 最小化这种浪费);故障或扩容产生的新节点缓存是空的,需要靠写入或读回填慢慢热起来。

内存放不下:淘汰与过期策略

缓存容量永远有限,必须回答两个问题:空间不够时踢谁?旧到什么程度必须重取?前者是淘汰策略 eviction,后者是过期策略 expiry(TTL)——两个维度正交,真实系统(如 Redis)通常同时支持、叠加使用:

策略怎么工作适合注意
LRU
Least Recently Used
淘汰「最久没被访问」的条目,把热门数据留在 RAM通用热点访问(源料点名的缓存无效算法代表)一次顺序扫描/冷数据突发可能把热点挤掉(缓存污染)
LFU
Least Frequently Used
按「访问频率」淘汰最冷门的条目访问模式稳定的稳定热点要维护计数与历史,新数据难上位,历史高频数据可能一直占着
TTL
Time To Live
条目带过期时间,到期自动失效(过期策略,与 LRU/LFU 可叠加)强制数据“保鲜”,DB 更新后的旧值最多活一个 TTL大量 key 同时到期会引发雪崩——过期时间要加随机抖动
面试一句话:LRU/LFU 管“空间不够踢谁”,TTL 管“旧到什么程度必须重取”;一致性难题靠失效时机解决,淘汰与过期只是兜底手段。

缓存三兄弟:击穿 / 穿透 / 雪崩

中文面试的高频“加餐”:三者共同点是缓存没能接住请求、压力直冲数据库,区别在于“为什么没接住”,应对手段也因此不同(图 10-4):

缓存三兄弟:都是「缓存没拦住,压力涌向 DB」 ① 击穿(热点 key 过期) 单个热点 key 恰在高峰过期 热门 key 已过期(miss) 数据库 DB(被压) 解法:互斥锁 / 单飞回源重建 热点 key 逻辑永不过期 ② 穿透(查不存在的 key) 请求的 key 压根不存在 缓存无此 key(miss) 数据库 DB(被压) 解法:缓存空值 + 短 TTL 布隆过滤器挡不存在的 key ③ 雪崩(大量 key 集体过期) 海量 key 同时失效 / 缓存宕机 大量 key 集体 miss 数据库 DB(被压) 解法:TTL 加随机抖动打散 多级缓存 + 高可用 + 限流降级 注:三兄弟属中文面试高频加餐;教材主线仍是 Cache-aside / 直写 / 回写 / 刷新四种模式,先吃透主线再记极端场景
图 10-4缓存三兄弟:击穿、穿透、雪崩。击穿是单个热点 key 恰好过期被瞬间打穿(锁/单飞重建);穿透是请求的数据根本不存在、永远 miss(缓存空值/布隆过滤器);雪崩是大量 key 同时失效或缓存整体宕机(TTL 随机抖动、多级缓存与限流降级兜底)。
别本末倒置:「三兄弟」属于应对极端场景的加餐,面试主线仍然是四种更新模式(Cache-aside / 直写 / 回写 / 刷新)加两种缓存粒度(查询级 / 对象级)——先把主线讲顺,再补这些边界情况,才显得你有体系而不是背名词。

高频追问 Q&A

Q1数据库更新后缓存怎么处理?为什么「删缓存」比「改缓存」常见?(一致性必考)

标准答案是 Cache-aside 的写法:先写数据库,再删除缓存里的对应条目,让下一次读 miss 后回填新值。删优于改有两个原因:一是「改」意味着数据库写 + 缓存写两次写,两步之间任何并发读都可能拿到旧值,把不一致窗口拉大;二是被更新的数据未必马上有人读,改了也是白改。但“先删后写”与“写完再删”都存在小窗口读到旧值(并发读恰好把旧值回填),常见补救是延迟双删、版本号校验,以及 TTL 兜底强制保鲜;要求强一致的场景干脆别走旁路,用直写模式或让数据库当唯一写路径。核心认知一句话:缓存一致性本质是“失效时机”问题,没有银弹,只能把不一致窗口缩到业务可接受——这也正是源料说的“无效缓存是个难题”。

Q2Cache-aside 会读到脏数据吗?有哪些坑?

会,主要有三个(源料原话级别):① 未命中延迟——miss 要经过查缓存、查库、回填三步,首次访问明显变慢;② 过时数据——数据库更新后,缓存里可能还躺着旧值,要靠 TTL 强制刷新或直写模式缓解;③ 冷缓存——节点故障被新节点替换时,新节点缓存是空的,延迟升高直到热起来。另外并发场景还有个经典竞态:读 miss 回填旧值恰好在删除之后发生,会把旧值“种”回缓存,业界用延迟双删(删→等→再删)或对写加版本号来收口。回答时能主动说出这三个坑并给缓解手段,就是满分节奏。

Q3Write-through 和 Write-back 怎么选?各自风险是什么?

用两条业务线划:“读己之写”必须立刻新鲜 + 写量不大 → 直写;写量大、要给数据库削峰、且允许短暂丢失 → 回写。源料的表述值得直接引用:直写“由于存写操作整体是慢操作,但缓存中的数据不会过时,读刚写入的数据很快”;回写“异步写入、提高写性能,但缓存可能在其内容成功存储之前丢失数据”,而且实现比直写更复杂。再补一句工程细节:直写模式下大量写入的数据可能根本没人读,记得配 TTL 避免缓存被无用的新写占满;回写则要关注持久化与故障转移,否则缓存节点一挂就丢一批写。

Q4击穿、穿透、雪崩怎么快速区分与应对?

一句话区分:击穿 = 单个热点 key 过期瞬间被打穿(key 存在但恰好失效);穿透 = 数据根本不存在、缓存里永远没有它,每次请求都穿到库;雪崩 = 大面积 key 同一时间失效,或缓存集群整体宕机,请求成片打到数据库、可能把库打垮。应对口诀:击穿靠互斥锁/单飞(只放一个请求回源重建)、热点 key 逻辑永不过期;穿透靠缓存空值(短 TTL)与布隆过滤器挡在门外;雪崩靠 TTL 加随机抖动打散过期、多级缓存/缓存高可用,数据库侧限流降级兜底。

Q5缓存粒度选查询级还是对象级?什么数据值得缓存?

查询级实现最直白——把「查询语句哈希 → 结果」存起来,但失效很难:复杂查询难以精确删除对应的缓存结果,某一行数据变化可能要连带删掉“所有可能包含它”的结果。对象级把数据封装成对象、由应用掌握生命周期:底层数据变了删掉对象即可,还方便 worker 用最新缓存对象异步组装。所以生产上更常缓存对象,源料给出的清单是:用户会话、完全渲染的 Web 页面、活动流、用户图数据。选择顺序建议:能用对象级就别用查询级;查询级只适合“结果集天然可整体失效”的场合(比如关键词搜索列表整段失效)。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节,可对照精读:

组件篇 · §11

异步与消息队列 Asynchronism · Message Queue

同步调用最怕两件事:下游慢拖垮自己、流量尖峰打爆系统。异步的思路是“先答应下来,稍后慢慢办”——把请求投进队列就返回,让后台消费者按自己的节奏消化。

异步与消息队列

难度 ★★☆ 面试频率:高 关联:例题 T3 爬虫 · t05 百万用户
先背这一句(开场 30 秒)“同步做不到的事交给队列:请求先落队列立即返回,后台慢慢消化——削峰填谷、解耦上下游、失败可重试;代价是链路变成最终一致,排错与监控更难。”

为什么要异步:削峰与解耦

同步调用 · 一步等一步 用户请求 下单服务 短信服务(慢) 短信网关抖动 3s… 用户要等短信发完才收到“下单成功” ⚠ 下游一慢,下单接口跟着慢 ⚠ 短信服务挂了,下单直接失败 异步 · 队列削峰解耦 用户请求 下单服务 消息队列 先落队列,立即返回“成功” 短信消费者 邮件消费者 消费者按自己节奏处理,还能失败重试
图 11-1同步 vs 异步。异步把“用户必须等”的事变成“后台慢慢办”:下单成功立即返回,短信/邮件由消费者异步发——下游慢或挂都不影响主链路(先保证核心,再补通知)。

消息队列 vs 任务队列:都是队列,用途不同

消息队列 Message Queue任务队列 Task Queue
传的是什么事件/消息(“订单已创建”),广播给多个订阅方一个可执行任务(“给订单 123 发短信”),只被一个 worker 消费
典型模型发布-订阅(Pub/Sub),一个消息多份拷贝点对点(P2P)/工作队列,一个消息一份工作
代表产品Kafka、Pulsar、云 MQ 的 topicRabbitMQ work queue、Celery、Sidekiq
面试怎么用解耦服务 + 削峰 + 事件驱动把重活(发短信/生成缩略图/导报表)丢后台跑

实操中很多系统两者混用同一套基础设施(RabbitMQ 两种都支持),面试把“广播事件”和“派发任务”说清即可。

背压:消费者跟不上时会发生什么

消费者太慢 → 队列膨胀 → 背压 生产者(每秒 1000) 队列:堆积越来越多 内存/磁盘占满 → 消息过期/丢失 消费者(每秒 100) 背压的三种解法 ① 加消费者 水平扩容消费者实例 (注意分区数/顺序性约束) ② 让生产者减速 限流/降级:告诉上游“我扛不住” 或丢弃/合并非关键消息 ③ 治理峰值 流量削峰(见 t05)、错峰任务 监控队列积压量并告警
图 11-2背压 = 生产者比消费者快。队列不是无底洞。生产环境必须监控队列积压(lag):积压上涨要么扩容消费者,要么上游限流降级——否则消息延迟大到“异步等于没异步”。

用了异步,要付出什么

  • 一致性变复杂:主流程成功 ≠ 全部完成,通知可能迟到/丢失 → 需要重试 + 对账。
  • 排错变难:没有调用栈了,靠 traceId 串全链路(呼应 §8 可观测性)。
  • 消息语义要想清:至多一次 / 至少一次 / 精确一次;幂等消费(用业务 id 去重)。
  • 顺序问题:同一实体的消息要保序时,按 key 分区 + 单消费者。
面试判断句:“异步适合『用户不敏感+可补偿』的活(通知、报表、图片处理、削峰);不适合『必须立刻拿到结果』的活(如支付回调里同步校验)——先按主流程画清楚,再决定哪里放队列。”

高频追问 Q&A

Q1消息丢了怎么办?

分三段防:生产端确认收到(ack)失败即重发;队列端持久化(写盘 + 副本)避免进程挂了丢消息;消费端处理成功才 ack,配合幂等(消费端用业务键去重)。回答要点:至少一次投递 + 幂等消费,是最现实的组合。

Q2Kafka 和 RabbitMQ 怎么选?

一句话定位:Kafka 面向高吞吐日志/事件流(顺序写盘、分区并行、可回放,适合削峰与数据管道);RabbitMQ 面向灵活路由与任务分发(多种交换机、即时消费、更适合业务任务队列)。面试按场景说“日志/事件量大上 Kafka,业务异步任务/灵活路由用 RabbitMQ 类”即可。

Q3为什么要“先写库再发消息”,不能先发消息吗?

先发消息后写库,可能消息到了而数据没落库(消费者读不到),或消息发了但库写失败产生脏消息。可靠做法:本地事务里写库 + 同时写一张 outbox 表,再由一个投递进程把 outbox 里的记录发到队列(发成功标记)——这就是“事务性发件箱”模式,面试说出来是加分项。

Q4异步后用户感知会变差吗?

关键动作要“看起来同步”:主流程(下单/支付确认)照常同步返回;把可延迟的部分(通知、报表、画像更新)异步化。前端可用“已提交,处理中”状态 + 轮询/推送收尾。别把用户等待 200ms 能完成的事也塞进队列。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0):Asynchronism(Message queues / Task queues / Back pressure)。

组件篇 · §12

通信协议 HTTP / TCP / UDP / RPC / REST

系统里的组件靠什么“说话”?本节把 HTTP、TCP、UDP、RPC、REST 五个高频词按层次拆开讲清楚,并给出“对外 REST、对内 RPC”的选型直觉。

通信协议

难度 ★☆☆面试频率:中
先背这一句“对外用 REST(资源+语义化),对内在 RPC(方法+契约)——性能敏感选 TCP/UDP 直连;一切应用层协议都跑在 TCP 之上,HTTP 只是最流行的一种。”

HTTP:请求-响应、动词与状态码

HTTP 是客户端与服务端之间编码和传输数据的方法,一个典型的请求-响应协议:客户端把「动词 + 资源」发给服务端,服务端回「状态 + 内容」。HTTP 本身并不绑定传输细节,请求与响应可以穿过一串做负载均衡、缓存、加密和压缩的中间节点——这正是它被 Web 世界选中的原因。

一次 HTTP 对话由四个部分组成:请求 = 方法 + URL 资源 + 请求头 +(可选)请求体;响应 = 状态行 + 响应头 + 响应体。例如浏览器请求 GET /users/42 HTTP/1.1,服务端可能回 HTTP/1.1 200 OK 与用户数据。

客户端 Client 浏览器 / App 服务端 Server API / Web 服务 ① 请求 Request ② 响应 Response 请求消息 Request message GET /users/42 HTTP/1.1 Host: api.example.com Accept: application/json 请求体 Body(可选)携带业务数据 响应消息 Response message HTTP/1.1 200 OK Content-Type: application/json Cache-Control: max-age=60 响应体 Body 返回资源内容 状态码速览:看首位数字定家族,别背字典 1xx 信息 100 Continue 101 Switching Protocols 102 Processing (中间状态,少见) 2xx 成功 200 OK 201 Created 204 No Content 206 Partial Content 3xx 重定向 301 Moved Permanently 302 Found 304 Not Modified 307 Temporary Redirect 4xx 客户端错误 400 Bad Request 401 Unauthorized 403 Forbidden 404 Not Found 5xx 服务端错误 500 Internal Server Error 502 Bad Gateway 503 Service Unavailable 504 Gateway Timeout
图 12-1HTTP 一次请求-响应与状态码家族请求由方法 + URL + 头部(+ 可选请求体)组成,响应以状态行开头;状态码按首位数字分成 5 个家族,记家族比背码值更实用。

HTTP 的动词远不止 GET。源料把常用动词与「幂等 / 安全 / 可缓存」三个属性列在一起,这是面试常挖的点:幂等指多次执行结果一致,安全指不会改变服务端状态。

动词语义幂等安全可缓存
GET读取资源是是是
POST创建资源 / 触发处理否否视响应头而定
PUT创建或整体替换资源是否否
PATCH部分更新资源否否视响应头而定
DELETE删除资源是否否
状态码背几个就够:200、201、301/304、400/401/403/404、429、500/502/503——剩下的按家族含义现场推理即可。

传输层:TCP vs UDP(先认清它俩的位置)

HTTP 只是应用层协议。传输层还有一对经典选择题:TCP 面向连接、可靠有序;UDP 无连接、尽力而为。先看它们在网络栈中的位置:应用层(HTTP 等)是“说什么”,传输层决定“怎么运”,网络层(IP)负责寻址路由,链路层负责物理帧——上层依赖下层,这层抽象正是我们可以只换传输层、不动应用层语义的原因。

应用层 Application HTTP · HTTPS · DNS · SSH · FTP(语言与语义) 传输层 Transport TCP · 面向连接 / 可靠 UDP · 无连接 / 尽力而为 网络层 Internet IP:寻址与路由;更下层是链路层 Ethernet / Wi-Fi 分层要点:应用层负责“语义”,传输层负责“可靠还是快”。HTTP/2 仍跑在 TCP 上, HTTP/3 改用基于 UDP 的 QUIC——换了运输方式,应用层语义基本不变。 客户端 Client 发起连接方 服务端 Server 监听方 ① SYN 同步请求 ② SYN-ACK 同意并回执 ③ ACK 确认,开始传数据 UDP:无需握手,发送方直接向外发出数据报(Datagram)即可 选 TCP:数据必须完整、有序 Web/API · FTP 文件 · 邮件 · SSH · 数据库 选 UDP:低延迟优先,可容忍丢包 语音/视频 · 直播 · 实时游戏 · DNS · DHCP 广播
图 12-2TCP vs UDP:可靠性与延迟的取舍顶部小图先给传输层定位(HTTP 等应用跑在其上);TCP 三次握手建立连接、保证有序不丢,UDP 免握手直接发数据报,底部按场景划界。

TCP 靠什么换可靠性?发送的每个数据包带序列号和校验码,接收方回 ACK,发方没收到正确确认就自动重传,多次超时才断开连接;再叠加流量控制与拥塞控制,让 TCP 自动适应网络吞吐。代价是这些保证机制带来额外延迟,通常比 UDP 效率低;Web 服务器为高吞吐保持大量长连接还会吃内存,连接池(Connection Pool)是常见缓解。

UDP 则把决定权交还给你:数据报可能乱序、可能丢失、没有拥塞控制——换来低延迟与高效率,还支持广播(DHCP 正因设备还没有 IP、无法建 TCP 连接,才走 UDP 广播)。选型一句话:要数据完好无损、想白拿吞吐自动调节,用 TCP;要低延迟、丢包比延迟更糟、或想自己实现纠错,用 UDP。

应用接口风格:RPC vs REST(内部契约 vs 外部资源)

如果说 HTTP 是“语言”、TCP/UDP 是“运输方式”,那 RPC(远程过程调用 Remote Procedure Call)与 REST(表述性状态转移 Representational State Transfer)是服务“开口说话”的两种风格:RPC 专注于暴露方法,REST 专注于暴露资源。

RPC 的理念是“像调本地方法一样调远程方法”:客户端调用 stub,把方法标识与参数打包发出,服务端收到后解包、按标识执行并把结果传回——网络细节被完全抽象。热门的 RPC 框架都走契约先行:用 Protobuf、Thrift、Avro 这类 IDL 定义接口,自动生成客户端与服务端代码。因为契约强、序列化紧凑,RPC 常被选作内部服务间高性能通信的手段。

REST 则把服务端的一切建模为资源,用 URI 标识、用 HTTP 动词操作,且约束所有通信无状态、可缓存。RESTful 接口有四条规则:用 URI 标识资源(同一资源同一 URI)、用动作/头部/请求体表达表示的改变、用自描述状态码表达错误(别自造)、做到 HATEOAS(可被浏览器直接访问)。因为无状态,REST 天然容易横向扩展与故障隔离,是对外公共 API 的首选。

RPC · 远程过程调用 像调用本地函数一样,调用远程服务上的方法 契约先行:Protobuf / Thrift / Avro 生成存根 REST · 表述性状态转移 把数据与能力建模为资源,用 HTTP 动词操作 无状态 + 可缓存,URI 定位资源、动词表达操作 同一个操作,两种接口设计 RPC 风格:每个动作一个专属端点 ① 读取用户信息 GET /readPerson?personid=1234 ② 给用户添加一件物品 POST /addItemToUsersItemsList ③ 删除一件物品 POST /removeItem REST 风格:资源 URI + 标准动词 ① 读取用户信息 GET /persons/1234 ② 给用户添加一件物品 POST /persons/1234/items ③ 删除一件物品 DELETE /items/456 对内选 RPC:低延迟 · 强类型契约 · 适合内部服务 对外选 REST:语义化资源 · 可缓存 · 易横向扩展
图 12-3RPC vs REST:同一操作、两种接口哲学RPC 把动作做成“专属方法端点”(常配 IDL 契约、参数进 body),REST 复用同一资源 URI 加标准 HTTP 动词——对外边界用 REST,内部服务间用 RPC。

对比着看更清楚。同样是增删改查,RPC 每次都要发明一个新“方法名”,REST 则用固定的资源集合 + 有限动词组合出无限操作:

维度RPCREST
关注点方法 / 操作资源 / 状态
端点长相GET /readPerson?personid=1234GET /persons/1234
动词使用动作写进 URL,基本靠 GET/POSTURL 是名词,DELETE/PUT/PATCH 表达意图
契约IDL(.proto / .thrift)强类型,自动生成存根URI + Header + 状态码自描述,无需单独契约
耦合度客户端与服务实现捆绑紧密松耦合,利于独立演进与多端复用
缓存友好难以被通用 HTTP 缓存层命中无状态 + 语义化动词,天然可缓存
典型场景内部服务间、性能与类型安全优先对外公共 API、跨团队边界

两边的缺点也很对称:RPC 每新增一种操作都要定义新接口,客户端与实现绑得紧、难调试,还想被 Squid 这类缓存代理正确缓存要额外费劲;REST 则不适合“动作难以用动词表达”的场景(如归档过期文档),复杂查询往往要拼 URL + 查询参数 + 请求体,嵌套的层级资源渲染一页要多次往返,且响应字段会随时间膨胀、拖慢老客户端。

别把 REST 与 HTTP 划等号:REST 是一种架构风格,HTTP 只是最常用的承载协议;很多“HTTP API”其实是 RPC 风格(动词藏在 URL 里)。面试时先问清讨论的是哪一层,再谈“谁好谁坏”。

小结与延伸阅读

把这五个词放进一张决策图:层次上,HTTP 是应用层协议、跑在 TCP(或 UDP)之上,TCP/UDP 是传输层;风格上,对外公共边界用 REST(资源 + 语义化动词、可缓存、易扩展),内部服务间用 RPC(方法 + IDL 契约、紧凑高效);当两者都需要时,常见的做法是 API 网关在边界把外部 REST 翻译成内部 RPC。

Q1 RPC 和 REST 一句话怎么区分?

看它面向方法还是面向资源:RPC 的端点像函数名(/removeItem),动作和参数常塞进 URL 与请求体,靠 IDL 契约保证类型;REST 的 URL 是名词资源(/items/456),意图交给标准 HTTP 动词。选型一句话:内部、性能敏感、要强类型契约 → RPC(gRPC/Thrift);对外公共 API、要解耦可缓存 → REST。

Q2 什么时候该选 UDP 而不是 TCP?

源料给三条判据:① 你需要低延迟;② 数据迟到比丢失更糟(语音、视频、实时游戏);③ 想自己实现纠错方法(游戏协议常自建重传)。反过来,只要数据必须完好无损(Web/API、文件、邮件、数据库),或者想白拿自动拥塞控制,就用 TCP。注意 UDP 还能广播,DHCP 正是靠它工作——设备还没有 IP,建不了 TCP 连接。

Q3 REST“无状态、可缓存”对系统设计意味着什么?什么时候 REST 反而别扭?

无状态意味着服务端不保存会话上下文,任意实例都能服务同一个请求 → 横向扩展、故障隔离、被 CDN/代理缓存都容易。别扭的场景:动作无法用有限动词表达(归档、批量操作),复杂查询要拼 URL + 查询参数 + 请求体,层级嵌套资源渲染一页要多次往返(移动端尤其吃亏),以及响应字段随时间膨胀拖慢老客户端。

Q4 HTTP/2、HTTP/3 跟 TCP/UDP 是什么关系?

分层语义不变,变的只是“运输方式”:HTTP/2 仍跑在 TCP 上,用多路复用 + 二进制帧缓解应用层队头阻塞,但 TCP 层丢包重传仍会卡住整条流;HTTP/3 改用基于 UDP 的 QUIC——自带加密握手(0/1-RTT)、独立流与连接迁移,绕开 TCP 的队头阻塞。记一句:HTTP/3 = 应用层请求-响应语义 + UDP 之上的 QUIC。所以“HTTP 一定走 TCP”这句话到 HTTP/3 就不再成立了。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:

组件篇 · §13

安全 Security TLS · 攻击面 · 纵深防御

安全不是某个组件的补丁,而是一条贯穿传输、网关、应用与数据的防线链:每一层只挡自己该挡的攻击,单层失守不致命——这就是纵深防御(Defense in Depth)。

安全

难度 ★★☆低频但一票否决
先背这一句“安全的底色是纵深防御:传输层 TLS 加密、网关层限流与 WAF、应用层输入校验与参数化查询、数据层最小权限与加密;每一层都别裸奔。”

传输加密:看得见,读不懂

很多系统把 HTTPS 当成“配一下就有”的事,但它其实是安全的第一道分水岭。没有它,请求从浏览器到服务器的整条链路上都是明文:登录口令、会话 Cookie、业务内容,任何能插到路径上的中间人(公共 Wi-Fi、被黑的网络设备、运营商侧抓包)都能整包读取。TLS(Transport Layer Security,HTTPS 的底层协议)解决的就是“传输加密 Encryption in Transit”——内容离开浏览器那一刻起就变成密文,只有真正完成握手的服务器能解开。

建立连接时,客户端先校验服务器证书,确认对面不是冒名顶替者;随后用非对称加密协商出一把临时的会话密钥,之后所有业务数据都用这把密钥做对称加密传输,并带完整性校验防止被篡改。源料还要求“等待过程中也加密”,即数据落库落盘时的静态加密(Encryption at Rest),这条我们放进 13-3 图的数据层防线。先看同一段登录请求在 HTTP 与 HTTPS 下的区别:

① HTTP 明文:中间人直接读取内容 请求内容:账号 + 密码 + Cookie 谁都能读 —— 抓包即可还原 浏览器 HTTP 客户端 中间人 窃听 + 转发 服务器 Web 后端 ◆ 结果:中间人拿到完整明文 —— 密码与 Cookie 直接可读 ② HTTPS + TLS:加密隧道,中间人读不懂 ① 握手:校验服务器证书(防冒充),协商会话密钥 ② 业务数据用会话密钥做对称加密传输 ③ 线路上只有密文块:a8f3 · 9d2c · 71be · c4e0 … ④ 解密只发生在两端:你的浏览器与真正的服务器 浏览器 TLS 客户端 服务器 TLS 服务端 · 证书 ◆ 中间人视角:只抓到密文乱码 —— 账号、密码、内容一概读不出
图 13-1HTTPS/TLS 让传输加密。同一段登录请求:明文 HTTP 里中间人能直接读出账号密码;换成 TLS 后先握手换钥,之后线路上只剩密文。
记三个动词:证书防“冒充”、加密防“偷听”、完整性防“篡改”。工程细节(本站补充):别忘了 HSTS(HTTP Strict Transport Security)——它强制浏览器只走 HTTPS,否则中间人可把连接降级回明文再偷看。

常见攻击面:五种打法与它们的盾

TLS 只保护“路上”,保护不了服务器内部:数据一旦解密进入应用,若处理不当照样能被利用。源料给的安全底线只有四条——传输与静态加密、防 XSS 与 SQL 注入、用参数化查询、遵循最小权限原则(Principle of Least Privilege);下面每种攻击“怎么打、为什么这么挡”的机制是本站在此基础上的展开(OWASP Top 10 常识,文中以“本站补充”标出)。五个最高频的攻击面见 图 13-2:

常见攻击面:每种攻击都标注了它的标准防御 XSS 跨站脚本 Cross-site Scripting 把 <script> 混进输入、被页面当作代码执行: 偷 Cookie、伪造操作、盗走会话 ✓ 输出编码 + CSP 白名单 ✓ Cookie 加 HttpOnly SQL 注入 SQL Injection 参数被拼进 SQL,从“数据”变成可执行语句: 拖库、绕过登录、删表(如 1=1 永真) ✓ 参数化查询 Prepared Statement ✓ 禁止字符串拼 SQL CSRF 伪造请求 Cross-site Request Forgery 诱导已登录用户浏览器自动发出请求: Cookie 自动带上 → 改密 / 下单 / 转账 ✓ CSRF Token 随机数校验 ✓ SameSite + 校验 Origin 越权访问 IDOR · 水平 / 垂直越权 改 id=1001 就看别人的订单(水平越权), 或越过角色调用管理接口(垂直越权) ✓ 服务端对象级授权 ✓ 最小权限 · 默认拒绝 滥用 / 打瘫 爬虫 · 撞库 · 薅羊毛 · DoS 脚本高频打接口:爬数据、撞库、薅羊毛, 或放大流量把服务打瘫(DoS 拒绝服务) ✓ 网关限流 + 熔断降级 ✓ 验证码 / 风控 + 审计告警
图 13-2常见攻击面与对应防御。五种高频攻击——XSS / SQL 注入 / CSRF / 越权 / 滥用,每种都配上标准盾牌(绿色 ✓ 为对策)。
  • XSS 跨站脚本(Cross-site Scripting):把 <script> 混进输入、被页面当作代码执行。盾:输出编码(Output Encoding)+ CSP + Cookie 加 HttpOnly。
  • SQL 注入(SQL Injection):输入被拼进 SQL,从“数据”变成“可执行语句”。盾:参数化查询(Prepared Statement),数据永远只当字面量。
  • CSRF 跨站请求伪造(Cross-site Request Forgery):诱导已登录用户的浏览器替你发请求,Cookie 被“白用”。盾:CSRF Token + SameSite + 校验来源(本站补充)。
  • 越权访问(IDOR / 水平·垂直越权):改个资源 id 就看别人的数据,或越级调管理接口。盾:服务端对象级授权,默认拒绝(本站补充)。
  • 滥用与打瘫(爬虫 / 撞库 / DoS):高频刷接口、放大流量。盾:网关限流、WAF、风控与验证码(本站补充)。

纵深防御:四层防线,逐层兜底

单一防线总有被绕过的可能:校验可能漏、WAF 规则会过时、密钥会泄露。所以工程上把安全默认设计成“纵深防御(Defense in Depth)”——沿着请求路径铺开四层防线,攻击者必须逐层击穿才能碰到核心数据;而每一层被突破时,都有下一层继续兜底,并由日志留下痕迹。剖面见图 13-3:

纵深防御:从攻击者到数据,要一层一层穿透 ① 传输层 TLS / HTTPS 传输加密:全站 HTTPS,证书防冒充 HSTS 防降级到明文 · 完整性校验防篡改 ② 网关层 WAF / 反代 / 限流 坏流量挡在门口:WAF 识别注入 / XSS 攻击特征 限流防滥用与 DoS · DDoS 清洗 · 反代隐藏后端 ③ 应用层 校验 · 会话 · 鉴权 输入校验 → 参数化查询 → CSRF / 越权检查 认证(你是谁)· 授权(你能干啥)· 审计(留痕) ④ 数据层 DB · 存储 · 备份 最小权限:DB 账号只给必要库表权限 静态加密 + 备份 · 敏感字段脱敏 / 加密 任何单层都可能被绕过 —— 所以每层只做自己擅长的那道拦截,并为下一层留下日志证据
图 13-3纵深防御的四层剖面。传输 → 网关 → 应用 → 数据,逐层内缩;攻击必须层层穿透,单层失守由下一层兜底。

传输层管“路上”、网关层管“门口”、应用层管“业务逻辑”、数据层管“落库之后”。层与层的职责不重叠才叫纵深:例如即使 WAF 漏掉一条注入特征,参数化查询仍在应用层把它拦成字面量;即使应用层校验全被绕过,最小权限的 DB 账号也做不了删表清库。

为什么标“低频但一票否决”:注入、越权这类漏洞平时不犯事,一旦被利用就是拖库、撞库、数据泄露——比任何性能问题都致命。两条通用纪律:①错误信息别把 SQL、堆栈、内部路径直接抛给用户(信息泄露也是漏洞,本站补充);②审计日志要能回答“谁、在什么时候、对哪条数据、做了什么”。

攻击 → 防御:对照速查表

面试或自查时把下表当 checklist 过一遍,比背单个漏洞细节更管用:

攻击面典型手法标准防御防线层
XSS 跨站脚本脚本经输入混进页面执行输出编码 · CSP · Cookie HttpOnly应用层
SQL 注入参数拼进 SQL 成为语句参数化查询 / ORM · 最小 DB 权限应用层 → 数据层
CSRF 伪造请求诱导已登录浏览器发跨站请求CSRF Token · SameSite · 校验来源应用层(会话)
越权 IDOR改资源 id · 越级调接口服务端对象级授权 · 默认拒绝应用层(鉴权)
滥用 / DoS刷接口 · 撞库 · 流量放大网关限流 · WAF · DDoS 清洗网关层
通用底子依赖漏洞 · 密钥泄露 · 无日志补丁更新 · 密钥轮换 · 审计日志全层

常见追问

Q1有了 HTTPS,系统就安全了吗?

不是。TLS 只解决“传输途中被偷看、被冒充、被篡改”:数据一到服务器就解密了,XSS、SQL 注入、越权这些应用层漏洞照样可以利用;HTTPS 也管不了密钥泄露与内部人员。所以 HTTPS 只是纵深的第一层,不是全部——面试能主动补这一句,比背协议细节更加分。

Q2XSS 和 SQL 注入都要“处理输入”,区别在哪?

一个在输出侧、一个在输入侧。XSS 是数据被拼进 HTML 后被当“脚本”执行,所以盾是输出编码 + CSP——编码必须发生在输出点(本站补充:存储型 XSS 数据会先落库,但清洗永远不能替代输出编码);SQL 注入是数据被拼进 SQL 后被当“语句”执行,参数化查询把语句结构与数据彻底分开,占位符里的内容在数据库眼里永远只是字面量,再“聪明”的 payload 也只是一段字符串。两者共同点:永远不要把用户输入当代码。

Q3CSRF 为什么“防不胜防”?Token 为什么有效?

因为请求“看起来”完全合法:Cookie 自动带上、IP 正常、参数齐全——它确实是受害者自己的浏览器发出的,只是被诱导了。CSRF Token 是服务端下发、存在会话里、攻击者站点拿不到的随机值,服务端校验“请求携带的 Token 与会话一致”才放行;SameSite 则从根上限制跨站请求自动携带 Cookie。(本站补充)

Q4越权为什么难防?前端把按钮藏起来够吗?

不够:藏按钮只是体验,接口照样能直接调。越权难防在于“认证 Authentication(你是谁)”与“授权 Authorization(你能动哪条数据)”是两件事——系统常只检查了“已登录”,没检查“这条数据属于你 / 你的角色够不够”。对策是每个对象级操作都在服务端做属主与角色校验,默认拒绝而非默认放行,再配合审计日志定位。面试口诀:认证是“你是谁”,授权是“你能干啥”,审计是“你干过啥”。(本站补充)

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节。源料 Security 一节本身很短,只列四条底线(传输与静态加密、防 XSS/SQL 注入、参数化查询、最小权限);本文中攻击机制、CSRF / 越权 / 滥用细节与纵深防御分层,属本站按 OWASP Top 10 与工程常识的补充:

例题篇 · T1

设计短链服务 Design a URL Shortener · Pastebin / Bitly

系统设计面试的“入门大题”:麻雀虽小,五脏俱全——它把估算、存储选型、编码方案、缓存、过期清理全串了一遍。跟着四步法走一遍,你就掌握了所有“读多写少”题型的套路。

例题:设计一个短链服务

🔥 经典开场题 难度 ★★☆ 四步法完整演示
30 秒主线答案“读多写少的典型系统:写入时给长链分配一个全局唯一 ID 并编码成短码(Base62),存 KV;读取时先查缓存,未命中再回源并回填;配合 TTL 过期与容量分片即可支撑亿级规模。”

Step 1 · 厘清需求:先问,别急着画

  • 功能:长链 → 短链;访问短链 → 302 跳回长链。要自定义短链吗?要统计点击吗?(第一版不做,可提一句“预留”)
  • 读多写少确认:一次写入、无数次读取,读:写常见 100:1。
  • 规模假设(大声说出来):每天新增 1 亿条?还是 100 万条?——面试按量级给方案,这里我按“年新增 5 亿、总存量 500 亿级”太激进,先按每天 1 亿条新增、读:写 100:1 估算(约等于 YouTube/微博量级的链接场景)。
估算项假设结果
写入 QPS1 亿条/天 ÷ 86,400s,峰值 × 3≈ 1,160 均值 / ~3,500 峰值
读取 QPS写 × 100≈ 11.6 万均值 / ~35 万峰值
存储(1 年)365 亿条 × 约 500B(映射记录)≈ 18 TB 级(可分片)
短码容量Base62 × 7 位 = 62^7≈ 3.5 万亿,足够
带宽35 万 QPS × ~200B 响应≈ 70 MB/s 峰值(几台机器即可)
估算纪律:数字本身不重要,假设 + 量级才重要。背下“86,400 秒/天、峰值系数取 3、读:写 100:1”三个默认值,任何题都能开场。

Step 2 · 高层设计

写入路径(POST /shorten)与读取路径(GET /:code)共用一套后端 客户端 API 服务 写入:生成短码 读取:查映射 + 302 缓存 Redis 短码 → 长链(读 99% 命中) 读命中直接返回 主存储 KV / 分片表:code→url 未命中回源 写入:DB 取号 → 编码 → 落库 + 写缓存 302 重定向 浏览器拿到 302 Location: 长链 跳到目标站 (响应可带 1 年 Cache-Control) 返回长链
图 T1-1高层架构。第一版两件事就够:写(POST 生成短码、落库、写缓存)与读(GET 查缓存 → 未命中回源 → 302 跳转)。复杂度全在“短码怎么生成不重复”与“读怎么扛住 30 万 QPS”。

Step 3 · 核心组件:短码生成(Base62)

两条路线都能做,面试讲清取舍即可 路线 A · 全局发号 + Base62 数据库取号 id = 10001 Base62 编码 10001 → 短码 优点:短码可反解、无碰撞 缺点:发号器是单点/瓶颈 路线 B · 哈希 + 碰撞处理 对长链哈希 取前 7 位做码 冲突?加盐/追加字符重哈希 或检查短码已占用则换一个 优点:无需发号器,可离线生成 缺点:要考虑碰撞与短码复用
图 T1-2短码生成两条路线。Base62 字符表 0-9a-zA-Z,7 位容量 62^7 ≈ 3.5 万亿。路线 A(发号)实现简单、短码有序;路线 B(哈希)无单点。若需求含“点击统计/可自定义”,发号路线更好扩展。

Step 4 · 扩展:读路径是主角

写路径(低频) 取号/编码 写主库 写缓存 返回短码 3500 QPS:单库发号就够,不够就分段发号 读路径(高频 · 主角) GET /abc123 主存储查码 Redis 缓存 302 跳长链 命中(99%) 未命中回源 → 回填 30 万 QPS:Redis 集群 + 本地缓存即可; 存储分片(按短码哈希)防单库热点
图 T1-3读写路径分离:缓存扛读。读:写 100:1 的系统,99% 的优化都发生在读路径——Redis 扛 99% 命中,主存储只接“缓存没兜住”的尾巴;浏览器端再缓存 302 结果(1 年 TTL),热点短链根本到不了你。
  • TTL 与过期:短链一般不需永久保存,定期清理(或懒删除:读时发现已过期即删)控制存储增长。
  • 缓存击穿防护:热点短链(大促链接)可能被刷爆——缓存失效瞬间回源放大,可对热点加“短暂空值缓存”或单飞保护。
  • 安全:只允许 http/https 目标;对短链做风控(防止跳转到钓鱼站被滥用);接口限流防刷。

复盘:怎么答更出彩

面试加分三步:① 开场先确认“读多写少 + 量级”,用 86,400 秒公式给出 QPS 与存储量级;② 主动对比 Base62 发号与哈希两条路线并给选择理由;③ 收尾补一句“缓存 + 302 缓存头 + 分片”的扩展路径——三步下来,10 分钟就能把这个题讲得完整又从容。

高频追问 Q&A

Q1短码重复了怎么办?

发号路线不会重(ID 唯一);哈希路线在写入时检测短码已存在:重哈希加盐(追加随机字符)直到不冲突,或直接查库发现占用就换。检测与重试要在写入路径闭环,别等到跳转时才暴露。

Q2同一个长链接多次提交,生成多个短码行不行?

可以接受,多数产品(含 Bitly)就这么干——每个短码独立计点击。若要求“同链同码”(去重),则要按长链哈希建索引,但会引入“查重”读放大,一般不值得。

Q3删除/过期怎么做?

两种:定期批扫(游标扫过期行批量删,错峰避免压库)或懒删除(读时发现 TTL 过期即删并返回 404)。生产常用 TTL 兜底 + 懒删除补漏,别为“永久链接”买单——先问产品要不要永久。

Q4为什么用 302 而不是 301?

301 永久重定向会被浏览器缓存,后续访问不再请求你的服务——统计点击会失真,想改目标链也难。302 每次都会到服务端走一次(顺带计数),代价是多一次请求。要永久稳定的场景可以 301,一般产品用 302 + 自己的计数与缓存策略。

延伸阅读与来源

本节为本站原创图解;题目与方案结构参考 System Design Primer(CC BY 4.0)题解:Design Pastebin.com (or Bit.ly);容量数字为本站按假设演示(已标注),正式面试以你的口头假设为准。

例题篇 · T2

设计时间线与搜索 Twitter / Facebook Feed

把「发一条推文怎么送到所有粉丝手里」拆开讲清楚:扇出(Fan-out)选推还是拉、主页时间线缓存存什么、以及搜索子系统如何独立演化——这是「读多写少 + 写放大」类问题的最佳入门题。

例题:设计一个时间线系统

🔥 高频大题难度 ★★★
30 秒主线答案“关键在『写时扇出还是读时拉取』:对大多数用户用推(发帖即写入粉丝时间线缓存),对百万粉大 V 用拉(读取时现取)——混合方案平衡写放大与读延迟。”

第一步 · 需求与假设:先把「时间线」这个词说清楚

这类题(Twitter、Facebook Feed 同型)先要区分两种时间线:用户时间线(自己发的推文)和主页时间线 Home timeline(自己关注的所有人最近的活动)。核心动作是扇出 Fan-out——把一条推文「分发」给所有关注它的人。需求范围通常锁定为:用户发推(并推送给粉丝、发通知/邮件)、浏览用户时间线、浏览主页时间线、关键词搜索、高可用。可以明确排除的:Firehose 流式推送、按「是否可见」设置过滤、数据分析。

面这类题要主动问:读多还是写多?这里显然是读多写少——刷主页远多于发帖,所以要为快速读取优化。常用假设口径(与官方题解一致):

  • 1 亿活跃用户;每天新发布 5 亿条推文(每月约 150 亿条);
  • 扇出递送量是发布的约 10 倍量级:每天约 50 亿次、每月约 1500 亿次推送写入(平均每条推文要写进约 10 个粉丝的 feed,即写放大 ≈ 10×);
  • 每月约 2500 亿次读取、100 亿次搜索——这两项就是「读多写少」的证据。
指标心算算式(按“每秒 400 请求 ≈ 每月 10 亿请求”)结果
发推(写)QPS150 亿条/月 ÷ 10 亿 × 400≈ 6000 条/秒
扇出推送 QPS1500 亿次/月 ÷ 10 亿 × 400≈ 6 万次/秒
主页读取 QPS2500 亿次/月 ÷ 10 亿 × 400≈ 10 万次/秒
搜索 QPS100 亿次/月 ÷ 10 亿 × 400≈ 4000 次/秒
推文存储每条 ≈ 10 KB(含媒体均值)× 5 亿条/天 × 30 天≈ 150 TB/月,3 年 ≈ 5.4 PB

分摊到 1 亿用户,人均每月约读 2500 次主页——换算成每人每天约 80 多次(网络流量并不均匀,热门用户要热得多)。存储上媒体是大头,照片视频放对象存储 Object storage;关系型数据只放「自己发的推文」这类结构化小数据。真正的难点不在存,而在扇出把每秒 6 万次写入砸在什么介质上:官方附录给出内存顺序读 1 MB 约 250 μs,SSD 约 4 倍、磁盘约 80 倍以上——所以热路径数据要进内存。

第二步 · 高层设计:读写分家,扇出独立成服务

整体按读写拆成两条 API 链路:客户端 → 反向代理/Web 服务器 → 读 API / 写 API。写侧的核心不是「存」而是「分发」:扇出服务 Fanout Service 发帖时把推文推进所有粉丝的主页 feed 缓存;配套的还有用户图服务(查粉丝/关注关系,结果放内存缓存)、搜索服务、通知服务(走队列异步推送,避免阻塞写路径)。存储分三层:SQL(存用户自己发的推文与权威数据)、内存缓存(存每个用户的主页时间线)、对象存储(媒体)。

第三步 · 核心权衡:推模型 vs 拉模型

推模型(写时扇出 Fan-out on write):用户发帖后,扇出服务立刻把这条推文写进每一个粉丝的主页 feed 缓存——用 Redis 原生 list,头部插入一条「推文 id + 用户 id」,新推文在最前。好处是粉丝读主页时 O(1) 直接读自己的缓存列表,不用去关注对象那里现凑;代价是写放大 write amplification:发 1 条推文 = 平均写约 10 条 feed 条目,普通用户还好,粉丝一多就是灾难——官方题解直言,粉丝百万级的用户若逐条扇出,可能要好几分钟才能推完,还会和 @回复 产生竞争条件。

拉模型(读时拉取 Fan-out on read):发帖只写自己的时间线(一次写),粉丝刷新主页时才去自己关注的每个对象那里现取、再按时间归并。写入成本 O(1),但读放大:每次刷新要拉 N 个关注对象,热点作者还会被成千上万粉丝同时读。两种极端都不够好。

推模型 · 写时扇出 Fan-out on write 发帖时把推文写进每个粉丝的主页 feed 拉模型 · 读时拉取 Fan-out on read 读取时才去被关注者那里现取,不主动推送 发帖者 A 发布 1 条推文 发帖者 A 发布 1 条推文 发 1 条推文 → 复制 N 份 (写放大 O(N)) 粉丝 B 的 feed 粉丝 C 的 feed 粉丝 D 的 feed 头部插入新推文 ★ 头部插入新推文 ★ 头部插入新推文 ★ 写 O(N) 放大 · 读 O(1) 直接返回自己的 feed A 的推文(只存 1 份) 发帖只写 1 次 不复制给任何人 粉丝 B 粉丝 C 粉丝 D 刷新时拉 A 的推文 刷新时拉 A 的推文 刷新时拉 A 的推文 读时拉取 写 O(1) · 每次刷新要拉 N 个关注对象
图 T2-1推模型 vs 拉模型左:写时扇出把 1 条推文复制进 N 个粉丝的 feed(写放大 O(N),读 O(1));右:读时拉取只写自己一份(读放大 O(N),写 O(1))。
方案发帖时读取时优点代价
推(写时扇出)写进每个粉丝的 feed 缓存(N 次写)读自己的缓存列表,O(1)读延迟最低写放大,大 V 扇出要几分钟
拉(读时拉取)只写自己的时间线,1 次写拉取 N 个关注对象再归并无写放大,冷用户零成本读放大,热点作者被反复读
混合(推荐)普通用户走推,大 V 只写自己缓存 + 现取大 V 推文,归并排序两边都压得住读路径多一步归并

混合方案:普通人用推,大 V 用拉

官方题解给出的做法:不要从高关注量用户那里做扇出。普通用户粉丝量小,照常写时扇出;而对粉丝数百万的大 V,发帖时只写自己的时间线(SQL)并进入搜索索引,一份也不复制给粉丝。粉丝刷新主页时,把缓存里的 feed 和实时拉取的大 V 最新推文(来自时间线 SQL / 搜索索引)归并、按时间排序后返回——顺带解决了扇出太慢导致的 @回复 乱序问题(服务端按时间重排即可缓解竞争条件)。判断标准一句话:用「读的 N」(关注的大 V 数量)换掉「写的 N」(大 V 的粉丝数量),通常划算得多。

混合方案:普通用户「推」+ 百万粉大 V「拉」 发帖扇出交给内存缓存扛;大 V 不扇出,粉丝读时拉取并归并排序 ① 普通用户发推 → Fan-out on write 普通用户 @Alice 粉丝数小(占绝大多数) 发帖 = 1 条推文 Fan-out 服务 把推文写进粉丝主页 feed Redis list 头部插入,写入异步化 每个粉丝的主页 feed 缓存 自己的列表里多 1 条 读取时直接取,O(1) 写放大 1 条 → N 次写 N = 粉丝数 多数用户粉丝量不大,写放大可控;写入都在内存缓存中完成,速度足够快 ② 大 V(百万粉)发推 → 不扇出,Fan-out on read 大 V(粉丝数百万+) 逐条扇出要跑好几分钟 官方题解指出的瓶颈 不扇出 只写自己的时间线(1 次) 推文进 SQL + 搜索索引 粉丝刷新主页时 去大 V 处拉最新推文 (时间线 SQL / 搜索索引) 与主页 feed 缓存归并 按时间重排后返回 消除 @回复 乱序 粉丝读时的拉取次数 ≈ 关注的大 V 数量,比大 V 的粉丝数小得多——用「读的 N」换「写的 N」 业界常见做法:按粉丝数设阈值分流,粉丝少走推、粉丝多走拉
图 T2-2混合方案 Hybrid普通用户发帖照常写时扇出;粉丝数百万的大 V 不扇出,粉丝读主页时实时拉取其最新推文,与缓存中的 feed 归并、按时间排序。

主页时间线读取路径:缓存 + 归并

读路径(每秒 10 万次,必须最便宜):客户端 → LB/Web → 读 API → 时间线服务。官方题解的三步取数法:

  • ① 读自己的 feed 缓存 O(1):时间线服务先从内存缓存取「推文 id + 用户 id」列表——每个用户一条 Redis list,条目只有 tweet_id 8B + user_id 8B + 少量元数据,不存正文;
  • ② multiget 补推文信息 O(n):拿列表里的 id 去推文信息服务批量取正文(正文只在推文侧存一份,不随 feed 复制);
  • ③ multiget 补用户信息 O(n):再取发推者的昵称、头像等展示信息;最后按时间归并排序返回。用户时间线(自己发的)更简单,直接从 SQL 按作者取即可。
主页时间线读取路径:缓存 O(1) + 归并组装 读缓存里的 id 列表 → multiget 补齐正文与作者 → 按时间排序返回 返回:组装好的时间线 JSON(已按时间排序) 客户端 刷新主页时间线 LB / Web 服务器 反向代理 + 负载均衡 读 API 服务 只读接口 时间线服务 取 id 列表 → multiget 补齐后按时间归并 ① ② ③ ① 主页 feed 缓存(Redis list) 每用户 1 条 list:tweet_id + user_id 只缓存几百条、只缓存活跃用户 读列表 O(1) ② 推文信息服务(SQL + 缓存) multiget 按 id 批量取正文 正文单份存储,不随 feed 复制 媒体文件在对象存储 ③ 用户信息服务 multiget 取昵称、头像等 (用户图服务提供关注关系) 缓存未命中 / 30 天未活跃的用户:从 SQL 读副本重建时间线并回填缓存——冷用户不占内存
图 T2-3主页时间线读取路径① 从内存缓存读 id 列表(O(1));②③ 两次 multiget 批量补齐推文正文与作者信息;归并排序后返回。缓存只放 id,正文单份存储。

搜索子系统:倒排索引,独立演进

搜索(每秒约 4000 次)与时间线解耦成独立子系统:发帖时写 API 异步把推文送进搜索索引服务;查询时搜索 API 先做输入处理(去标点、分词、拼写纠正、统一大小写、转布尔查询),再交给搜索集群(如 Lucene)。集群内用倒排索引 inverted index——每个词维护一个「包含它的推文 id 列表」,检索时对集群内所有分片并行查询(发散 Scatter)、合并结果并评分排序(聚集 Gather)。为降低延迟,搜索集群会把推文常驻内存;数据量大后可进一步按时间或用户分片。

搜索子系统:倒排索引 + 发散聚合 Scatter-Gather 与时间线解耦、可独立演进:发帖时异步写索引,查询时并行检索再合并 用户输入关键词 “hello world” 搜索 API 服务 鉴权 / 限流 搜索服务:查询解析 去标点 · 分词 · 拼写纠正 统一大小写 → 布尔查询 搜索集群(Lucene) 各分片并行查(Scatter) 合并、评分、排序(Gather) 返回结果 按相关度排序 倒排索引 inverted index:词 → 含该词的推文 id 列表 hello t9 t42 t87 … 索引(含推文)常驻内存降延迟;写入走异步队列,不影响发帖路径 规模参考:每月约 100 亿次搜索 ≈ 每秒 4000 次
图 T2-4搜索子系统倒排索引把「词」映射到「推文 id 列表」;检索时对各分片并行查询(Scatter)后合并评分排序(Gather)。搜索与时间线解耦,可独立分片演进。

第四步 · 扩展与权衡 + 复盘:这道题怎么答

面试官在第四步通常按这条线追问:扇出服务是最大的瓶颈(所以有混合方案);SQL 写主库被高写入淹没怎么办——加读副本扛缓存未命中、再做分片 Sharding,部分数据可迁 NoSQL;缓存层按上面说的「只缓存活跃用户的数百条」控制内存;其余就是通用套路:Web 层加负载均衡横向扩展、CDN、异步队列解耦通知、持续基准测试与监控。复盘时把这套话术背成自己的:

复盘怎么答(四步法版):

  • 开口给主线:读多写少 ⇒ 目标让读 O(1),热数据放内存缓存,扇出策略是关键权衡。
  • 澄清假设再动笔:DAU、读/写比例、搜索要不要一起做、媒体大小——每题的数字自己现场算并写清算式。
  • 先高层后核心:画出「客户端 → LB → 读/写 API → 存储 + 缓存 + 搜索」,再展开扇出。
  • 主动暴露权衡:讲推模型的写放大(6 千条发布 vs 6 万次推送),主动抛出混合方案——这正是加分点。
  • 收尾聊扩展:搜索独立子系统、SQL 读副本 + 分片、冷用户重建、监控与负载测试。

高频追问

Q1为什么「推」会有写放大?

因为 1 条推文要复制进 N 个粉丝的 feed:发帖 QPS 约 6000 条/秒,而扇出写入约 6 万次/秒——平均每条约 10 次递送,写放大大约一个数量级。粉丝越多越夸张:官方题解说粉丝数百万的用户若逐条扇出,可能要好几分钟才能完成,还会引发与 @回复 的竞争条件。所以扇出写入必须落在内存缓存这种高速介质上,并靠混合方案限制单条推文的 N。

Q2百万粉的「热点用户」怎么办?

不要对这类用户做扇出(这是官方题解的核心结论):发帖时只写自己的时间线并进入搜索索引;粉丝读取主页时实时拉取大 V 的最新推文,与缓存中的 feed 归并、按时间排序后返回。相当于把写放大的 N(粉丝数)换成读时的 N(关注的大 V 数)。还可以补充:定时任务批量扇出、队列异步化、按粉丝数阈值分流,都是可聊的变体。

Q3主页 feed 缓存到底缓存什么?

缓存 「推文 id + 用户 id」列表而不是正文:每个用户一条 Redis list,条目约 tweet_id 8B + user_id 8B + 少量元数据,读取 O(1);正文只在推文服务存一份,展示时用 multiget 批量取。为控制内存:每个主页只缓存几百条、只给活跃用户缓存;30 天未活跃的用户读时从 SQL 重建并回填,读未命中则走 SQL 读副本。

Q4为什么不用 SQL 直接存 feed?介质怎么选?

扇出写入约 6 万次/秒,会把传统关系型数据库压垮;而内存读 1 MB 约 250 μs,SSD 约 4 倍、磁盘约 80 倍以上——feed 这种「高频小写、头插、整条读」的形态,用 Redis list 这类内存结构正合适。SQL 仍然重要:存「用户自己发的推文」作为权威数据与冷用户重建源,再配读副本 + 分片扛缓存未命中和高写入。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:

例题篇 · T3

设计网络爬虫 Web Crawler

爬虫不是一台暴力下载机器,而是一条「生产者-消费者」流水线:把吞吐、去重与礼貌性同时管好,才是这道题真正想考的工程素养。

例题:设计一个网络爬虫

难度 ★★★考队列与去重
30 秒主线答案“爬虫是『生产者-消费者』流水线:URL 队列出队→下载→解析出新 URL→去重后入队;核心是去重(布隆过滤器省内存)与礼貌性限速(别把人家站点打挂)。”

第一步 · 澄清需求与估算

先界定边界:爬虫为搜索引擎生产离线数据——抓取网页后,一是生成包含搜索词的倒排索引,二是为每页生成静态的标题与摘要(不随搜索词变化);用户的搜索读路径只是消费这些产物,不是本题重点。明确不做:搜索分析、个性化结果、页面排名。更重要的隐含需求是:爬虫不能陷入死循环,也不能把站点打挂——前者引出去重,后者引出礼貌性限速。

随后与面试官对齐规模假设(都写清楚、再动手算):

  • 基线:抓取 10 亿 个链接;为保证新鲜度定期重抓,平均每周一遍,热门站点更频繁 → 折算每月抓取约 40 亿 个页面;
  • 每页平均 500 KB(重抓的页面简单起见视作新页面);每月搜索量 1000 亿 次;服务要求高可用。
指标算式(每月按 250 万秒计)结果
抓取写入速率40 亿页 ÷ 250 万秒/月≈ 1600 页/秒(写侧)
页面存储40 亿 × 500 KB = 2×10¹⁵ B≈ 2 PB/月;3 年 ≈ 72 PB
搜索请求(读侧参考)1000 亿次 ÷ 250 万秒/月≈ 40000 次/秒
面试提示:心算前先声明假设——“按每月约 250 万秒、每页 500 KB 来粗估”,数字单位级对了就行;1600 页/秒 与 4 万次/秒 是全程反复引用的锚点。

第二步 · 高层架构:一条环形流水线

抓取是典型的生产者-消费者结构:links_to_crawl(待抓集合,按网站知名度/热度排优先级,可用 Redis 有序集合维护)出队 → 调度器按域名礼貌限速 → 下载器抓页面 → 解析器抽子链接与正文 → 子链接先去重、新的才回队列;正文则入库存档并派发两类任务:倒排索引任务与文档任务(生成标题+摘要)。已抓链接连同页面签名记入 crawled_links,用于查重与防环。整个流水线:

URL 队列 按热度优先出队 调度器 每域限速 + 延时 下载器 HTTP 拉取页面 解析器 抽子链接 / 正文标题 1 2 3 4 URL 去重 · 布隆过滤器 已见过的 URL → 丢弃(防环) 新 URL → 重新入队 位数组判重 · 亿级省内存 存储 · NoSQL + 任务 crawled_links:已抓 + 签名 links_to_crawl:待抓(有序) 文档 / 倒排任务发往下游 下游产出 文档服务:标题 + 摘要 倒排索引 → 搜索服务 查询 API → 客户端 5 6 7 子链接 页面内容 新 URL 回队列
图 T3-1爬虫全景架构:URL 队列 → 调度(礼貌限速)→ 下载 → 解析;子链接经布隆去重后回队形成闭环,页面内容入库并驱动文档与倒排索引产出。

两点取舍先说清:其一,读侧(搜索)不是本例题主角——客户端经反向代理到达查询 API,解析/分词/纠错后命中倒排索引返回标题与摘要即可,一笔带过;其二,高可用来自状态外置:队列与已抓集合都放键值型 NoSQL,抓取实例无状态可水平扩展,任意一台挂掉不丢进度。

第三步 · 核心组件

URL 去重:布隆过滤器是防死循环与省内存的关键

爬虫最常见的死法是死循环:页面之间存在环(A 链 B、B 又链 A)时,同一个 URL 会被反复入队、反复抓取。最简单也最该先说清的去重手段按规模递进:小数据量直接 sort | unique;十亿级链接则用 MapReduce 批量剔除重复记录;而在线判重——每解析出一个子链接就要立刻回答“抓过没有”——最适合用布隆过滤器 Bloom Filter:

待判 URL X 解析器刚吐出的子链接 它抓过没有? k = 3 个哈希 h₁(X) → 位 7 h₂(X) → 位 16 h₃(X) → 位 25 已见过的 URL 插入时:k 个位置全部置 1 1 1 1 三个位全为 1 → 可能已见 丢弃 / 降优先级 · 允许少量误报 任一为 0 → 一定没见过 作为新 URL 入队,并把这些位置置 1
图 T3-2URL 去重(布隆过滤器):一个 URL 经 k 个哈希落到位数组的 k 个位置;全部为 1 才判“可能已见”,任一为 0 则“一定未见”。

为什么必须上布隆过滤器?算一笔账:假设 URL 平均 100 字节,10 亿个 URL 的哈希集合光原始数据就要约 100 GB;而布隆过滤器只需 m ≈ −n·ln(p) / (ln2)² 位——误报率 p 取 1% 时每个元素约 9.6 bit,10 亿元素约 1.2 GB,配 k ≈ 7 个哈希函数即可。它没有漏报(判“未见”必为真),只有误报(把新 URL 误判成“已见”的代价不过是少抓一页),与爬虫场景完美匹配。更进一步的内容级近似去重则基于页面内容生成签名再比较相似度(Jaccard 相似度、余弦相似度等),用来对付内容农场式的重复页。

方案内存 / 成本精确性适用
全量哈希集合10 亿 URL ≈ 100 GB 起100% 精确小规模 / 冷启动种子集
布隆过滤器误报 1% 时 ≈ 1.2 GB / 10 亿零漏报 + 少量误报在线判重主路径
MapReduce 批量去重随作业量走(磁盘为主)精确十亿级离线清洗候选 URL
常见误区:一上来就堆布隆过滤器而不讲“为什么”。正确顺序是:先讲环与重复 URL 的问题 → 全量集合在亿级规模的内存困境 → 布隆用可容忍的误报换内存量级下降 → 再补一句“判已见后我们用签名相似度做内容级去重兜底”。

礼貌性限速:同一站点绝不并发轰炸

全局 1600 页/秒很漂亮,但如果几十个线程同时打同一个站点,站长会直接封你。礼貌性的工程化做法是每个域名一个独立队列与独立消费线程:同一域名串行发送、两次请求之间强制冷却间隔 Δ(Δ 可依据站点 robots.txt 的 Crawl-delay 与历史响应调整);并发能力加在“域名数量”上,而不是同一站点上。别忘了域名归一化:www.example.com 与 example.com 必须哈希到同一队列,否则等于绕过限速开双通道。

调度器 ① host 归一化 www.example.com → example.com ② 每域一个队列 ③ 域内串行消费 ④ 间隔冷却 Δ 队列 · news.example.com 同域 URL 排队 不与其它域互相阻塞 限速器 距上次请求 ≥ Δ Δ 可依 robots.txt 队列 · blog.example.com 同域 URL 排队 不与其它域互相阻塞 限速器 距上次请求 ≥ Δ Δ 可依 robots.txt 礼貌性 = 每域独立队列 + 串行发送 + 请求间隔 Δ:把并发加在“域名数量”上,而不是同一站点上 Δ 参考站点 robots.txt 与负载;失败按域退避重试;热门站点可提高重抓频率,但间隔不变
图 T3-3礼貌性限速(每域名队列 + 延时):URL 按归一化域名分入独立队列,每域一个线程串行消费并强制冷却间隔,域与域之间互不阻塞。

配合限速还要有两件事:失败处理按域做指数退避重试并记录连续失败次数(连续失败说明该域可能已屏蔽爬虫);调度前先读该域 robots.txt 与站点负载反馈动态调 Δ。热门站点可以重抓得更勤(下面会讲如何用数据决定频率),但单次礼貌间隔不打折。

主循环中的状态机

把上面串成一个循环,每次处理“优先级最高的待抓链接”时依次执行:查 crawled_links 中是否存在相似签名——存在则说明内容重复,降低该链接优先级而非删除(防环又保新鲜);否则真正抓取,然后:入倒排索引任务队列、入文档任务队列、生成页面签名、从 links_to_crawl 删除、把链接与签名写入 crawled_links。这张表是整套设计的记账中心:

存储 / 队列记什么为什么需要
links_to_crawl待抓 URL + 优先级(有序集合)出队调度;降优先级防环
crawled_links已抓 URL + 页面签名 + 时间戳去重、内容相似检测、断点续爬、新鲜度
倒排索引任务队列页面 → 词项映射任务喂给搜索侧(异步解耦)
文档任务队列生成静态标题 / 摘要任务喂给搜索结果展示

第四步 · 扩展、容错与权衡

断点续爬:因为队列与已抓集合全部持久化在 NoSQL 里,任何抓取实例崩溃重启后都能从 links_to_crawl 接着干,不丢进度;重复抓到的页面靠签名比对降优先级处理,天然幂等。这也正是“不用内存队列、坚持把 frontier 外置”的理由。

内容变化检测与重抓频率:每条已抓记录带 last_crawled 时间戳,默认按周期(如一周)全量重抓;同时做数据挖掘估算各站点的平均内容更新时间,变化快的站提高重抓频率、稳定站拉长。重抓后比对新旧签名:变了才重新派发文档/索引任务并更新时间戳;没变只更新时间戳,省掉大量无效下游写。

扩展与容量:下载与解析实例无状态、按域分片水平扩展(同一域始终落在同一消费者上,避免并发违约);三年 72 PB 的页面库要走向对象存储 + 压缩 + 按时间/域名分区,倒排与文档存储另行独立伸缩;frontier 的 Redis 有序集合用一致性哈希分片。权衡主线一句话:新鲜度(重抓更勤)与礼貌负载(Δ 更大)是同一根旋钮的两端,调度参数化即可,不必推倒架构。

复盘:这题怎么答

按四步法走满 30 分钟时间盒:前 5 分钟澄清范围并抛出规模假设,把 1600 页/秒、2 PB/月 两个数字算给面试官看;5-15 分钟画流水线架构(图 T3-1),点明生产者-消费者与状态外置;15-25 分钟深挖两个主角——布隆过滤器的内存账(100 GB → 1.2 GB,零漏报只误报)与礼貌性限速(每域队列 + Δ);最后 5 分钟讲断点续爬、签名比对做内容变化检测与新鲜度调度,收在权衡上。三个可反复使用的口头禅:

  • “队列是骨架”:URL 队列管进度,任务队列解耦下游,队列状态落库才有断点续爬;
  • “布隆是滤网”:判重必须算内存账,误报代价要说清楚(少抓一页 vs 内存量级);
  • “礼貌是限速”:单域串行 + 冷却间隔,把并发加在域名数量上。

QA

Q1URL 去重怎么做到不炸内存?

全量哈希集合存 10 亿个 URL 需约 100 GB 起步,单机扛不动。改用布隆过滤器:误报率 1% 时每个元素约 9.6 bit,10 亿元素约 1.2 GB,配 k ≈ 7 个哈希。它没有漏报(判“未见”必为真),误报只是把新 URL 当成“已见”少抓一页,代价可接受。若误报率仍嫌高,可加一层精确集合兜底校验(命中布隆后再查精确表),或用 Redis 布隆扩展把位数组分布式化;离线超大列表用 MapReduce 批量 sort | unique。内容级近似重复则靠签名相似度(Jaccard / 余弦)检测。

Q2robots.txt 和“礼貌”具体指什么?

三层含义:一是尊重站点的 robots.txt(Disallow 的路径不抓,按 Crawl-delay 建议间隔);二是工程上的每域队列——同一域名单线程串行、两次请求间冷却 Δ,且 www 前缀与裸域归一化到同一队列,防止多路并发绕过限制;三是对站点信号敏感——超时/4xx/5xx 增多就降速,连续失败按域指数退避。面试答出“单域并发=1,全局并发靠域名数量撑”这句就稳了。

Q3抓了一半宕机,怎么断点续爬?

关键在于状态外置:待抓队列 links_to_crawl 与已抓集合 crawled_links(含签名、时间戳)都存键值型 NoSQL,抓取实例本身无状态。任何实例崩溃后,重启从库里按优先级继续出队即可,队列不丢、进度不丢。万一挂在“下载后、入库前”,重抓一次也安全:内容按签名/版本覆盖更新,重复派发的文档与索引任务在下游按时间戳幂等处理。这也是为什么 frontier 不能图省事放在进程内存里。

Q4怎么检测页面内容变化、决定何时重抓?

每个已抓页面保存 签名 + last_crawled 时间戳。重抓周期先按默认值(如每周一遍),再通过数据挖掘估计各站点的平均内容更新时间:变化频繁的站(新闻、论坛)提高重抓频率,稳定站拉长。重抓后比较新旧签名——变了才重新派发文档与倒排索引任务并更新时间戳;没变只更新时间戳,避免无意义的下游写入。热门度与新鲜度两个信号共同驱动调度参数,热门站点可以更勤但依然遵守每域礼貌间隔。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:

例题篇 · T4

设计 KV 存储 Key-Value Store / Query Cache

把搜索引擎最热的查询结果缓存进一个分布式 KV:一致性哈希分片、主从复制、最终一致——完整推演一遍"从单机 LRU 到分片集群"的演进。读完约 10 分钟,含 4 张图解。

例题:设计一个分布式 KV 存储

难度 ★★★考分片与一致性
30 秒主线答案"把 key 空间用一致性哈希分片到多节点,每片主从复制保证可用性;写入先落日志(WAL)再落内存表,用版本号/读修复换取最终一致——节点增删只影响相邻分片。"

第 1 步 · 先把题问清楚:这是 KV,还是搜索?

这道题的原始英文标题写得很直白:Design a key-value cache to store the results of the most recent web server queries(设计一个 KV 缓存,存放最近 Web 查询的结果)。也就是说,我们不是要造搜索引擎,而是给搜索链路加一层查询缓存 Query Cache:key 是规范化之后的查询词,value 是命中的结果页(标题 + 摘要)。开场先自己对齐这些假设:

  • 读多写少:写只发生在缓存未命中回填、以及内容变更后的刷新;
  • 热查询高度集中:经常被查询的内容应当一直留在缓存里;
  • 规模:1000 万用户,每月 100 亿次查询(约 4000 QPS 平均,峰值更高);
  • 内存有限:目标不是"全存",而是让百万级最热查询长期驻留,并想清楚淘汰与过期规则;
  • 高可用:缓存挂了,不能让整个搜索链路跟着挂。

接着做一次快速估算。查 1 MB 内存数据的连续读约 250 微秒——从 SSD 读同样数据约 4 倍时间,机械盘要 80 倍以上(延迟数字以官方附录为准)。所以"命中缓存"和"回源搜索"之间,隔着几个数量级:

指标估算说明
月查询量100 亿次 / 月源题假设:1000 万用户、每月 100 亿次查询
平均 QPS≈ 4000 / 秒每月约 250 万秒;1 QPS ≈ 每月 250 万次请求
单条缓存≈ 270 Bquery 50 B + title 20 B + snippet 200 B
若全量缓存2.7 TB / 月100 亿条互不相同的查询都存 → 内存放不下,必须靠淘汰
命中读延迟~250 µs / MBSSD 约 4×、机械盘 80× 以上 → 命中越快越好
话术提醒:先报"每月 100 亿次 ≈ 4000 QPS、全量要 2.7 TB 所以只留热点"这套数,比上来就画架构更能体现估算习惯。面试官通常会在"吞吐"和"内存"两条线上继续追问。

第 2 步 · 高层设计:把 KV 挡在昂贵查询前面

请求链路一句话:客户端 → 反向代理 / Web 服务器 → 查询 API → 内存缓存(KV)→ 未命中才回源。回源方是反向索引服务(召回 + 排序)与文档服务(取标题与摘要)。查询 API 是唯一入口,职责两半:先解析查询(去掉杂质、切词、纠错、大小写归一、转布尔运算),再查缓存:

  • 命中:get(query) 有值 → 把该条目移到 LRU 链表头部 → 直接返回,走的是内存的微秒级读;
  • 未命中:调反向索引服务、文档服务组装结果 → set(query, results) 回填 → 同样移到链表头部;
  • 淘汰与失效:容量到顶时从链表尾部淘汰最久未用的条目;页面内容变化 / 增删页面 / 权重变动时主动失效,最省事的兜底是给每条设存活时间 TTL(Time-to-Live)。

单机上的经典实现是 LRU 缓存:用哈希表做 O(1) 定位,用双向链表维护访问顺序——新元素加在头、过期元素从尾删。到这一步还只回答了"一台机器怎么做",面试先停在这里把读写路径讲干净,下一步再回答"一台不够了怎么办"。

画图顺序:高层阶段先画"缓存挡在索引 / 文档服务前面"的骨架,不要急着铺分片与副本——先有全貌,再谈扩张。

第 3 步 · 核心组件:从单机缓存到分片集群

请求负载与内存需求都涨上来后,把内存缓存横向扩到多台机器,有三种放法(这也是常见的讨论顺序):

  • 每台机器一份自己的缓存:实现最简单,但同一热查询在不同实例上会各自 miss,命中率被稀释;
  • 每台机器都复制全量缓存:命中率最高,但内存按实例数成倍浪费;
  • 对缓存分片(Sharding):把 key 空间切成多段、每段归一台机器,machine = hash(query) 决定去哪台——更复杂,却是正解:全集群共享一份逻辑缓存,命中率不被实例数稀释,内存与吞吐还能随节点线性扩展。
① 单机:一个 KV 实例全装 查询 API(单实例) 单机 KV:哈希表 + LRU 链表 k1 k2 k3 k4 k5 → → → → 读:哈希表命中 → 条目移到表头 写:未命中回填;满则淘汰表尾最旧 缓存条目 ≈ 270 B(query+title+snippet) 瓶颈:单机内存有限 · 单点故障 · 吞吐触顶 数据与流量 持续增长 ② 分布式:key 分片到多节点 查询 API 分片路由:machine = hash(key) 分片 1 分片 2 分片 3 k1 · k4 · k7 k2 · k5 · k8 k3 · k6 · k9 每片约 1/3 数据 每片约 1/3 数据 每片约 1/3 数据 每片只装 key 空间的一小段 → 容量与吞吐随节点数增长(可用性交给副本)
图 T4-1单机 KV → 分布式 KV(分片)。单机瓶颈在内存与吞吐,三种横扩方案里"分片"最复杂但命中率与内存效率最好:全集群共享一份逻辑缓存,加机器即可线性扩容。

分片用 machine = hash(query) 就好吗?直接取模(hash mod N)有两个毛病:节点数一变(扩到 N+1 台、某台宕机),几乎每个 key 的归属都跳变——对缓存来说等于一次集体失效,所有未命中请求同一瞬间回源,反手把反向索引、文档服务打爆(这正是缓存雪崩最常见的引信)。于是要上一致性哈希 Consistent Hashing:把节点和 key 都哈希到同一个环上,key 顺时针遇到的第一个节点就是它的归属——增删节点时,只有相邻区间里的 key 需要换主,波及面从"全部"降到"约 1/N"。

一致性哈希环:key 顺时针找归属节点 0 / 2³²(hash 空间起点) N1 N2 N3 N4 k1 k5 k2 k3 k4 顺时针遇到的第一个节点即归属;节点增删只影响相邻区间 虚拟节点 Virtual Node 物理节点少:哈希偏斜,容易出热点 每个物理节点在环上放 k 个虚拟点 k 越大分布越均匀,迁移粒度越细 N1 N2 N3 小点 = 虚拟节点:同色同属一台物理机(示意 k = 2~3)
图 T4-2一致性哈希环:节点与 key 的分布、虚拟节点。key 落到环上后顺时针找归属节点;加入/移出节点只影响其两侧相邻区间。物理机少时用"一台放 k 个虚拟点"把负载摊匀,增删的冲击也随之细化。

第 3 步(续)· 复制与一致性:可用性靠副本,不靠侥幸

分片解决了容量,但每片仍是单点:一片只有一台机器,宕机就等于该片数据全没。给每片配上主从复制:写请求只走主节点,主节点先把这次操作追加进写前日志 WAL(Write-Ahead Log),确认日志落好后再更新内存表、向客户端回包;随后把日志异步复制到从副本。主节点挂了,从副本晋升为新主,读请求依旧有处可去——副本数(常见 2~3 份)本质是"读吞吐与可用性"对"写放大"的权衡。

写入路径 · 先日志后应用(主从) 客户端 set 主节点 Primary ① 先写 WAL ② 再更新内存表 → 回包 ① WAL:先追加日志 顺序追加 · 故障后重放不丢已确认写 ② 更新内存表 + LRU 索引 淘汰策略在每片内各自维护 ③ 异步复制 从副本 R1 异步追日志 从副本 R2 异步追日志 读路径 · 版本对比与读修复 客户端 GET(k) 1 从副本 R1 返回旧值 v5(复制滞后) 2 主节点 Primary 持有最新 v9 ③ 读修复:拉 v9 回填 R1 异步复制 → 从副本短暂落后,读到旧值是可以接受的窗口 读修复:发现版本旧 → 拉最新值回填,读路径顺带自愈 要求刚写即可读?把该 key 的读路由到主节点即可
图 T4-3复制与一致性:主从 / WAL / 读修复。写路径先落 WAL 再更新内存表并异步复制;从副本滞后时可能返回旧值,读路径发现版本落后即拉新值回填,把系统收敛到最终一致。

注意一致性在这里的取舍:异步复制意味着"同一条 key 可能读到旧值",而查询缓存对这种短暂不一致尤其宽容——数据本身来自回源,可以重建,旧一两秒的结果远好于把请求阻塞住。所以主从之间用版本号 / 读修复 Read Repair收敛,走最终一致而不是强一致(为什么选 AP 而不是 CP,见文末 Q4)。

第 4 步 · 扩展与故障处理:把"加节点"变成日常

架构演进是循环:基准测试 → 定位瓶颈 → 换方案再测,不要从初版一步跳到终版。当某片吞吐触顶或内存逼近水位,就往环上加机器:新节点先在环上占位,只把相邻区间的一小段 key 迁过去,其余分片不动;反过来,节点故障时,路由层把主身份切换给从副本,客户端无感知。迁移与切换做对之后,扩容就从"停机大搬家"变成"无声的日常"。

故障转移:从副本晋升为新主 客户端 / 路由层 主节点 P1(宕机) 故障:心跳超时 → 被标记不可用 从副本 R1 → 晋升为新主 接管读写;旧主恢复后先当从节点 故障转移对客户端无感:请求始终发给"当前主" 前提:副本已同步到足够新(复制因子 ≥ 2) 新节点加入:只迁走相邻一小段 key N1 N2 N3 N4 N5(新) 琥珀弧 = 从 N1 迁到 N5 的一小段 key(原属 N1 的相邻区间) 其余节点与数据不动 → 扩容只动相邻区间,平滑完成
图 T4-4故障处理:故障转移与节点加入。左:主节点宕机后从副本晋升、路由层切换,客户端无感知;右:新节点只搬走相邻区间的一小段 key,扩容对存量数据的影响极小。

到这里还能点到的扩展项(点到为止,别展开):监控命中率与 p99 延迟、逐 key 的 TTL 分层、针对单个热点 key 的副本、多机房时把"机房"也编进哈希实现同机房优先——它们都能挂到同一套"分片 + 复制"的语言上。

复盘怎么答:30 秒版

  • 先定量:100 亿次/月 ≈ 4000 QPS;全量缓存要 2.7 TB/月 → 结论是"只留热点,靠 LRU + TTL 淘汰";
  • 一句话主线:一致性哈希把 key 分片到多节点,每片主从 + WAL 保可用,读修复 + 最终一致换低延迟;
  • 两个必画图:分片演进(图 T4-1)与哈希环(图 T4-2);复制 / 故障用因果链讲:挂一台 → 读旧值 → 读修复;
  • 主动说权衡:命中率 vs 内存效率、一致性 vs 可用性(本场景选 AP)、副本数 vs 写放大;
  • 收尾留钩子:命中率与 p99 监控 → 基准测试 → 迭代,正好呼应四步法的第 4 步。

高频追问 QA

Q1为什么用一致性哈希,而不是 hash(query) mod N?

差别在"成员变动时的波及面"。取模方案里,节点数从 N 变 N+1、或某台机器离线,几乎全部 key 的归属都变——对缓存等于集体失效,所有请求同一瞬间回源,反向索引与文档服务会被打满(缓存雪崩最常见的引信)。一致性哈希把节点与 key 都映射到同一个环上、key 顺时针找归属,节点增删时只有两侧相邻区间里的 key 需要搬家,波及 ≈ 1/N。对"命中率即生命"的查询缓存,少一次集体回源就少一次雪崩风险;代价是映射查找从 O(1) 取模变成环上的有序查找,以及需要额外管理成员与虚拟节点,工程上完全可接受。

Q2虚拟节点到底解决了什么?

物理机少时,环上只有很少几个位置,节点间的弧长可能悬殊——热 key 密集的一段会压垮单台,别的机器却很闲。虚拟节点的做法是:每台物理机在环上取 k 个哈希位、放 k 个点(图 T4-2 为示意只画 2~3 个,工程上通常取数百量级)。好处有两点:① 分布更均匀——k 足够大时每台负责的弧长都接近 1/N,不易"一台 90%、其余 10%";② 故障与扩容的影响被摊薄——某台退出时,它的 key 由 k 个相邻虚拟节点各自承担一小块,而不是整个区间砸给一个邻居。查归属时先定位到虚拟节点,再映射回物理机即可。

Q3缓存场景的"写放大"指什么?

查询缓存读多写少,写只发生在三类时刻:未命中回填、TTL 过期后刷新、内容 / 权重变更后的主动失效。所以这里的写放大要分三处看:① 复制放大——一次 set 要写到主节点和每个从副本,副本数为 3 时逻辑 1 次写 = 物理 3 次(还没算 WAL 的一次顺序日志写);② 再分布放大——节点增删触发的 key 搬迁会在一段时间内产生大量迁移写;③ 惊群回填——缓存集体失效让"回填"同一瞬间成百上千倍爆发,本质也是写放大。控制手段对应:副本用异步复制(不必等从节点确认)、一致性哈希 + 虚拟节点把搬迁摊薄、用 TTL + LRU 让失效是"渐进"的而不是"瞬间全体"的。对可重建的缓存,甚至可以放弃持久化(省掉 WAL)换取更低的写成本。

Q4这个系统该选 CP 还是 AP?

选 AP。查询缓存的数据来自反向索引 / 文档服务,丢了可以重建,属于可再生状态:写入只等主节点确认即回包,从副本异步追;节点故障不阻塞读写;短暂的旧值通过读修复收敛即可,最终一致足够。反过来若选 CP(同步复制 + quorum 读),每次写都要等多数派确认,延迟与写放大同时上升,对"以延迟换命中率"的缓存很不划算。唯一需要加强的场景是"用户刚提交的变更立刻可读"——那属于内容更新后的主动失效 + 写后读一致性(把该 key 的读路由到主节点),仍然不必全局 CP。

延伸阅读与来源

本节为本站原创图解;知识结构参考 System Design Primer(CC BY 4.0)对应章节:

例题篇 · T5

支撑百万用户 Scaling to Millions of Users

全站收尾的“综合大题”:不设计某个具体功能,而是把一台服务器一步一步演进出能支撑百万用户(并继续往上走)的架构。它把前面所有章节串成一条主线——每一步只解决当下最疼的瓶颈,并为下一步留好口子。

例题:从单机到支撑百万用户

🔥 综合大题 · 全站考点总复习 难度 ★★★ 关联:§1 §3 §6 §10 §11 §12 · 所有例题
30 秒主线答案“把扩展当演进而不是一步到位:单机 → 加 LB 与多副本 → 数据库读写分离 → 缓存 → CDN → 消息队列削峰 → 数据分片 → 多机房容灾——每一步讲清『解决什么瓶颈、引入什么新问题、下一步在哪』。”

演进总览:八步,每步一个瓶颈

每一步的“金句”:解决什么 → 引入什么 ① 单机起步 一个进程 + 一台 DB —— 别过度设计 ② LB + 应用多副本 无状态化(见 §1/§6),流量分给 N 台 ③ 数据库读写分离 主库写 + 只读副本扛读(见 §9) ④ 缓存上阵 热数据进 Redis,DB 只接冷尾巴(§10) ⑤ CDN 挡静态 图片/前端资源就近分发(§5) ⑥ 异步削峰 重活/洪峰进消息队列(§11) ⑦ 数据分片 单库写不动 → 按用户/ID 分片(T4) ⑧ 多机房容灾 主备机房 + DNS/全局调度(§3/§4) 新问题:单点 + 扛不住 新问题:DB 变瓶颈 新问题:读多写少的热点 新问题:一致性/淘汰(§10 QA) 新问题:动态内容仍回源 新问题:最终一致 + 排错难 新问题:跨片事务/热点 key 新问题:成本与复杂度爆炸 ← 到这儿就够百万级 ← 千万级才需要 ⑦⑧
图 T5-1八步演进总览。面试答这题的最高级形态:不是背顺序,而是每一步都在“解一个瓶颈、造一个新问题”——说到哪一步,就主动指出它的下一个瓶颈。

前三步:从单机到“能用”

Step 1 · 单机 Web + 应用 一台机器全干 数据库(同机) 用户 < 1 万时足够 Step 2 · LB + 多副本 Web A Web B 负载均衡器 数据库(独立机器) 应用无状态 → 故障只损 1/N Step 3 · 读写分离 主库(写) 只读副本 只读副本 读流量横向扩,写仍是单点(§9)
图 T5-2前三步细节。第一步别急着上集群;第二步引入 LB 时同步做“无状态化”;第三步读写分离后,读已经是横向的——此时你的系统约能扛住几十万 DAU 的读流量。

中段:缓存、CDN、异步加入后的完整形态

用户(百万级) CDN(静态资源) 图片 / JS / CSS 就近返回 负载均衡 按策略分发(§6) 消息队列 削峰/通知/报表(§11) Web A Web B Web C Redis 缓存集群 读多写少的热数据(§10) 主库(写) 副本扛读(§9) 后台消费者们 发短信/导报表/建索引 动态内容仍走 Web 未命中回源 重活异步化
图 T5-3中段完整架构(百万 DAU 级)。读路径:CDN 兜静态 → LB 进 Web → Redis 兜热点 → DB 兜底;写路径:直接写主库,重活丢队列后台消化。到这一步,面试“百万级系统”的主架构就完整了。

终态:分片 + 多机房(千万级以上才谈)

  • 数据分片:单库写不下了 → 按 user_id / 订单号哈希分片(一致性哈希,见 T4)。代价:跨片查询、事务、热点 key——所以分片是“最后的手段”而不是默认。
  • 多机房:主备机房(同构部署,故障切流)或两地三中心;DNS/全局负载均衡把用户导向最近/健康的机房(呼应 §4)。代价:数据同步延迟与一致性策略(呼应 §3)。
  • 别再往上涨怎么办:接受“用钱换可用性”,或从业务侧削峰(预约制、错峰推送)——架构不是万能的。
面试红线:不要一上来就画微服务 + 分片 + 多机房的“豪华大图”——那是终态,不是答案。从单机一步一步演进,才是这题要考的结构化思维;每一步问“现在的瓶颈是什么”。

复盘:怎么答更出彩

全场最佳话术:“我先用一句话给路线图:LB → 读写分离 → 缓存 → CDN → 队列 → 分片 → 多机房。然后我从单机开始画,每画一步就说『这步解决 X,代价是 Y,下个瓶颈在 Z』——如果时间不够,我就停在缓存 + CDN 这步,因为对百万用户已经足够。”

高频追问 Q&A

Q1为什么顺序必须是“读写分离 → 缓存 → CDN”?

按“收益/成本”排序:读写分离解决数据库这个最硬瓶颈(架构简单,收益立竿见影);缓存进一步削掉 DB 读压力(但要处理一致性);CDN 只对静态资源有效,所以排后面。别把 CDN 当第一步——动态接口它帮不上。

Q2百万 DAU 大概对应多少 QPS?

用 A1 的公式粗算:100 万 DAU × 每人每天 20 次核心动作 ≈ 2000 万次/天 ÷ 86,400 s × 峰值系数 ≈ 峰值 5000-1 万 QPS 量级,读:写常见 10:1-100:1。这个量级:Web 层几台到十几台机器 + Redis + 读写分离即可,根本不需要分片——面试里用数字证明“现在还不用分片”非常加分。

Q3加了缓存和 CDN 之后,下一个瓶颈是什么?

通常轮到写路径:主库单点(读写分离只扩了读)、以及消息队列的积压与消费者吞吐。再往后是单表数据量(索引变慢)→ 分片;然后是多机房之间的数据同步。能按“读瓶颈 → 写瓶颈 → 存储瓶颈 → 地理瓶颈”的顺序说,架构师的味道就出来了。

Q4这题和“设计 XXX 系统”有什么区别?

它是方法题:没有具体业务,考的是你脑子里有没有“演进路线”和“瓶颈判断”。答具体业务题(如 T1-T4)时,可以把这条演进线当作骨架,往里填业务细节——所以这题放最后复习,等于把前面 19 章串成一条线。

延伸阅读与来源

本节为本站原创图解;题目与演进结构参考 System Design Primer(CC BY 4.0)题解:Design a system that scales to millions of users on AWS;对应概念详见站内 §1/§3/§5/§6/§9/§10/§11 各章。

附录 · A1

数字速查表 Latency & Capacity Cheat Sheet

系统设计面试的心算“一页纸”:2 的次方、延迟数字、估算套路。数据出自 System Design Primer 附录,考前 5 分钟过一遍即可。

2 的次方表

容量估算的地基:记住 2^10 = 1,024 ≈ 1K,之后每 +10 就乘 1,024,跳一个数量级单位。

幂精确值约值字节单位
2^7128——
2^8256——
2^101,0241 thousand(千)1 KB
2^1665,536—64 KB
2^201,048,5761 million(百万)1 MB
2^301,073,741,8241 billion(十亿)1 GB
2^324,294,967,296—4 GB
2^401,099,511,627,7761 trillion(万亿)1 TB
快速换算:1 亿(10^8)≈ 2^27;10 亿(10^9)≈ 2^30。把“每个对象几 KB × 数量级”套进 2 的幂,心算就不容易错。

每个程序员都应该知道的延迟数字

这组数字是心算的“参照物”,背到数量级即可:内存 ≈ 100ns,SSD ≈ 0.1–1ms,磁盘 ≈ 10ms,同机房往返 ≈ 0.5ms,跨洲 ≈ 150ms。

操作耗时备注(相对倍数)
L1 缓存引用0.5 ns—
分支预测失败5 ns—
L2 缓存引用7 ns14× L1
互斥锁加锁/解锁25 ns—
主存引用100 ns20× L2,200× L1
Zippy 压缩 1KB10,000 ns = 10 us—
1 Gbps 网络发送 1KB10,000 ns = 10 us—
SSD 随机读 4KB150,000 ns = 150 us~1GB/s 的 SSD
内存顺序读 1MB250,000 ns = 250 us—
同数据中心往返500,000 ns = 500 us—
SSD 顺序读 1MB1,000,000 ns = 1 ms4× 内存读
磁盘寻道10,000,000 ns = 10 ms20× 机房往返
1 Gbps 网络顺序读 1MB10 ms40× 内存读,10× SSD
磁盘顺序读 1MB30,000,000 ns = 30 ms120× 内存读,30× SSD
跨洲发送包(CA→NL→CA)150,000,000 ns = 150 ms—
记法:把 1 个数据中心内每秒约有 2,000 次往返、光每秒绕地球 6–7 圈这类“感觉”数字记住,心算时用来校准量级。

心算三件套:QPS、存储、带宽

  • QPS 估算:日均请求量 ÷ 86,400(秒)再乘峰值系数(保守取 5–10)。例如 100 万 DAU、每人每天 20 次核心动作 → 峰值 QPS ≈ 2,000 万 ÷ 86,400 × 10 ≈ 2,300——量级“几千”,够画架构了。
  • 存储估算:每对象字节数 × 对象总数,再用 2 的幂表换单位。100 亿条 × 1KB = 1TB(写进 Redis 就要问自己:放得下吗?热数据占几成?)。
  • 带宽估算:QPS × 单响应字节数。2,300 QPS × 10KB ≈ 23MB/s ≈ 184 Mbps——一张 1Gbps 网卡的承受力立刻可算。
面试心算纪律:先大声说出假设(“我按 DAU 100 万、峰值是均值 10 倍估”),再给算式与量级结论;数字算错不要慌,量级和口径对了就能继续。

来源

数值出自 System Design Primer 附录(Copyright 2017 Donne Martin,CC BY 4.0)「Powers of two table / Latency numbers every programmer should know」;排版与记忆法为本站整理。

附录 · A2

面试应答清单 Checklist & Scripts

把方法论章(四步法)落成面试现场能直接照做的动作清单:问什么、怎么分配 30 分钟、用什么话术开场与收尾。考前一天过一遍,比临时背题有用。

开场 60 秒:提问清单(按顺序过)

  • 功能:核心动作是什么?读多还是写多?要不要搜索 / 推荐 / 报表这类“次级系统”?
  • 规模:DAU / 总用户量级?每天多少次核心请求?数据量级(百万条 / TB / PB)?峰值与均值差多少?
  • 质量:P99 延迟要多少(百毫秒 / 秒级)?可用性要几个 9?单机房还是全球?
  • 约束:允许用什么技术栈?要不要考虑成本与运维复杂度?有什么已知的“红线”?(容量估算默认自己来)
话术模板:“我先把它当成一个读多写少、DAU 在 百万级 的典型 Web 服务来设计,重点讲时间线的读取路径;如果你说的规模是十亿级,我们再讨论分片与异步。”——主动立假设,比等面试官喂需求加分。

30 分钟时间盒

时间阶段该产出什么雷区
0–5 分钟澄清需求问题清单 + 明确读多写多、量级、延迟目标不问就开始画图
5–15 分钟高层架构一张全貌图:客户端 → LB → 服务 → 存储/缓存,标出读写两条路径一上来钻细节(如分库分表)
15–25 分钟深挖核心组件挑 1–2 个最“有戏”的点展开(存储选型 / 缓存一致性 / 扇出)全程平均用力、没有重点
25–30 分钟扩展与权衡指出下一步瓶颈 + 给出 2 个 tradeoff,问面试官想听哪个方向突然收尾、没有结论

四步法记忆卡

  1. Step 1 梳理用例、约束与假设:把模糊题目翻译成可计算的数字(见 A1 心算)。
  2. Step 2 高层设计:方框 + 箭头画清“谁调用谁、数据放哪”,写与读两条路径分开画。
  3. Step 3 设计核心组件:每个方框落到具体技术并说明“为什么是它”,如“这里用 Cache-aside 的 Redis,因为读多写少”。
  4. Step 4 扩展设计:从单机出发一步步加 负载均衡 → 缓存 → 读写分离 → 消息队列 → 分片,每一步说清解决什么、代价是什么。

话术模板

开场(30 秒版):“我按『入口分流 → 后端多副本 → 存储选型 → 扩展路径』来讲这个系统。先确认两个前提:读多写多?数据量级?”
选型时:“这里我倾向 X,因为访问模式是……;代价是……;如果场景变成……,我会换 Y。”
收尾(30 秒版):“如果要落地,我会先做最小可用版本 + 压测,重点盯三个指标:QPS、P99、错误率;下一步的瓶颈大概率在数据库写路径,所以预留了分片方案。”

高频易错点清单

  • 忘记问读写比例就定了缓存方案——读多写少才能先上 Cache-aside。
  • 只说“加机器”不说后端无状态化——有状态时加机器无效(见 §6 负载均衡)。
  • 缓存一致性含糊其辞——主动讲 Cache-aside + TTL + 兜底,别等追问。
  • 容量估算不给口径——先声明假设,再给算式,再给量级结论。
  • 全程没提监控与故障预案——结尾补一句“上线前补全健康检查、限流与告警”即可救场。

来源

清单为本站按 System Design Primer(CC BY 4.0)「How to approach a system design interview question」整理成可执行动作;时间盒与话术为本站原创补充。

ROADMAP · 全量完成

章节地图

全部 20 章图解完成(方法论 → 地基 → 组件 → 例题 → 附录;图文均为本站原创重写,知识底座来自 System Design Primer,CC BY 4.0)。点击卡片直达对应章节。

方法论 M

系统设计面试方法论 · 四步法 · 心算估算

四步法 + 心算:拿到任何题都知道怎么开场、怎么画图、怎么收尾

已图解
地基篇 §1

性能 vs 扩展性 · Performance vs Scalability

纵向不够就横向:三层各自怎么扩

已图解
地基篇 §2

延迟 vs 吞吐 · Latency vs Throughput

水管隐喻 + 必须背的延迟数字

已图解
地基篇 §3

可用性与一致性(CAP) · Availability vs Consistency

CAP 三角 · 一致性模式 · 可用性 9s

已图解
组件篇 §4

DNS 域名系统 · Domain Name System

一次域名解析的完整旅程

已图解
组件篇 §5

CDN 内容分发 · Content Delivery Network

就近分发 · 推 vs 拉 · 回源

已图解
组件篇 §6

负载均衡 Load Balancer · L4/L7 · 算法 · 健康检查

试读样板章:6 张图解完整拆解

已图解
组件篇 §7

反向代理 / Web 服务器 · Reverse Proxy

替服务器挡在前面:正反代理与 LB 边界

已图解
组件篇 §8

应用层 · 微服务 · Application Layer / Microservices

单体拆微服务的动机、代价与服务发现

已图解
组件篇 §9

数据库 SQL / NoSQL · Database · RDBMS vs NoSQL

关系型与四类 NoSQL 的选型决策

已图解
组件篇 §10

缓存 Cache · 缓存策略 · 淘汰 · 一致性

金字塔 · Cache-aside · 穿透击穿雪崩

已图解
组件篇 §11

异步与消息队列 · Asynchronism · MQ · 背压

削峰填谷的代价与队列模型

已图解
组件篇 §12

通信协议 · HTTP / TCP / UDP / RPC / REST

内部 RPC vs 外部 REST · TCP vs UDP

已图解
组件篇 §13

安全 Security · TLS · 攻击面 · 纵深防御

常见攻击的图与防御对照

已图解
例题篇 T1

设计短链服务 · Pastebin / Bitly

四步法完整演示:编码 · 读写路径 · 容量

已图解
例题篇 T2

设计时间线与搜索 · Twitter / Facebook Feed

推 vs 拉:扇出的艺术

已图解
例题篇 T3

设计网络爬虫 · Web Crawler

URL 队列 · 去重 · 礼貌性限速

已图解
例题篇 T4

设计 KV 存储 · Key-Value Store

一致性哈希 · 复制 · 故障处理

已图解
例题篇 T5

支撑百万用户 · Scaling to Millions

从单机到全球的八步演进总复习

已图解
附录 A1

数字速查表 · 延迟数字 · 2 的幂 · 心算

考前一页纸:必须背的数字

已图解
附录 A2

面试应答清单 · Checklist · 话术模板

开场问题 · 时间盒 · 四步法记忆卡

已图解