图解系统设计
给进阶工程师的“图文版系统设计课”:先把面试到底怎么考讲清楚,再把 DNS、CDN、负载均衡、缓存、数据库、异步… 一个个拆成看得懂的图;最后回到经典大题,带你用四步法现场解题。
全站学习路径
五站式结构:先方法论,再打地基,然后把“积木”一个个看懂,最后用真题限时演练。第 3 站的 负载均衡 是本期完整图解试点。
怎么用本站
系统设计面试没有“标准答案”,考的是结构化的工程判断。本站的拆法有三步:
- 先学套路:拿到任何题,都先走四步——① 厘清需求与约束 → ② 画高层架构 → ③ 拆核心组件 → ④ 估算并扩展(见 方法论章)。
- 组件当“词典”:DNS、CDN、负载均衡、缓存、数据库…每个都是面试里的“单词”,遇到不会的随时回来看图。
- 例题限时演练:经典大题单独成章,先自己写 20 分钟,再看本站的逐步拆解对照。
负载均衡 Load Balancer
面试出现频率最高的一课:一台机器不够用了,你第一个要认识的角色。读完约 8 分钟,含 6 张图解。
负载均衡 Load Balancer
为什么需要它:单机走不远的三个理由
先想一个残酷的现实:一台服务器的处理能力是有限的。用户变多时,单机方案会在三件事上先后崩溃:
- 流量上限:CPU、内存、带宽总有一个先被打满,之后请求开始排队、超时、报错。
- 单点故障:机器宕机、机房断电、磁盘损坏——任何一件事发生,整个服务就“团灭”。
- 没法平滑扩容:换更强的机器(纵向扩展)有物理天花板且贵;买新机器(横向扩展)又需要有人把流量“分”过去。
负载均衡(Load Balancer,简称 LB)就是为第三件事而生的“分发器”:它站在用户和后端机器之间,把请求按策略转发给多台机器中的一台。
它站在架构的哪里
“负载均衡”不是一个固定位置的角色,而是一种会反复出现的思想:只要有一群“能互相替代的实例”在前面共享流量,前面就可能蹲着一个 LB。下图把一张典型 Web 架构里出现 LB 的位置都标了出来:
只认包,还是看懂请求:L4 vs L7
负载均衡器收到一个网络请求后,能“看多深”决定了它是哪一型。想象请求是一个多层信封:最外面是 TCP/IP 的“收发地址”,里面才是 HTTP 的“信件内容”:
| 维度 | L4(传输层) | L7(应用层) |
|---|---|---|
| 看什么 | IP、端口、协议号 | URL、Host、Cookie、Header、报文内容 |
| 性能 | 更快(内核/DPDK 转发) | 较慢(要解析并加工报文) |
| 能力 | 转发;支持任意 TCP/UDP 协议 | 按业务路由、重写、压缩、缓存、限流、灰度、TLS 卸载 |
| 适用 | 数据库连接、游戏、高吞吐网关 | Web/API/微服务入口 |
| 典型产品 | LVS、DPVS、HAProxy L4、AWS NLB | Nginx、HAProxy L7、AWS ALB、云 SLB |
分发算法:怎么决定“给谁”
LB 拿到一个请求后,选哪台后端?有几种典型算法,面试至少要把前三种说清楚:
| 算法 | 一句话 | 典型场景 |
|---|---|---|
| 轮询 RR | 轮流派发,数量公平 | 各机器能力接近、请求耗时均匀 |
| 加权轮询 | 按权重(机器配置高低)分配比例 | 新旧机器混跑、不同规格实例 |
| 最少连接 | 派给连接数最少的机器 | 长短请求混合(如 Web/API) |
| 最短响应时间 | 统计每台近期响应耗时,选最快 | 对延迟敏感的场景 |
| IP / 一致性哈希 | 按来源 IP 或 key 固定映射到某台 | 需要“同一用户/同一 key 总去同一台”(会话、缓存亲和) |
挂了怎么发现:健康检查
LB 的“健康检查”决定了一个谎言是否成立——它说自己只把请求分给“健康的机器”。做法是周期性探测每台后端:主动发 TCP 握手 或 GET /healthz,连续失败 N 次就把机器摘除,恢复后再自动加回:
别忘了让后端“无状态”
LB 有一个“天敌”:有状态的后端。如果登录态、购物车存在某台服务器的内存里,那么“同一个用户两次请求被分到不同机器”就会出 bug——于是人们发明了会话保持(Sticky Session):让 LB 记住“这个用户永远去 A”。这能解决一时,却把 LB 变成了新的单点与调度枷锁。
更优雅的做法是把状态拿出来:存进所有机器共享的 Redis / 数据库。后端从此“无状态”,谁都能接任何请求,LB 也彻底自由:
面试应答要点
- 先给结构:“我按『入口分流 → 后端多副本 → 健康检查 → 无状态化』来讲这个系统的负载均衡设计。”
- 画图从粗到细:第一张图只画 客户端 → 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)的对应章节,可对照精读:
- Load balancer · Load balancer vs reverse proxy · Horizontal scaling · Availability patterns(故障转移 / 冗余)
- 进阶延伸:HAProxy / Nginx 官方文档的算法说明;《数据密集型应用系统设计》第 1 章关于“分区与复制”的讨论。
系统设计面试方法论 Four-Step Method & Estimation
系统设计面试没有标准答案,但有标准打法——用四步法把一场开放式对话拆出节奏:先问清边界,再画骨架、钻细节,最后用数字和取舍收口。
系统设计面试方法论
系统设计面试到底在考什么
它不是知识问答,也没有唯一正确答案:面试官丢给你一个模糊需求,看你能不能自己把边界问出来、把骨架画出来、把关键组件想清楚,并且让每个结论都经得起一句“为什么”。原仓库把这类面试定性为一场开放式的对话——而且期望的是你来主导这场对话,不是等面试官一个个提问。
准备节奏可以参考它的学习指引(Study guide):时间线短,就以主题广度为目标、挑几道题练手;时间中等,追求广度加初级深度、刷很多题;时间充裕,再加高级深度、把大部分题做透。多数岗位只需要“广度 + 一套熟练的四步打法”,这也是本章的全部目的。
四步法:把开放题变成有节奏的对话
四步不是瀑布式的死流程,而是你主导对话的脚手架:每一步都有明确产出,方便你随时向面试官汇报进度、拿到反馈再往前走,而不是闷头画一张没人跟得上的大图。
第 1 步回答“系统是什么、边界在哪”,产出是澄清问题清单;第 2 步产出一页纸的高层架构草图(主组件与连线,边画边证明每个组件为什么存在);第 3 步对最关键的 1~2 个组件深入下去——原仓库的例子是 URL 缩写服务,要谈哈希与 Base62、碰撞怎么办、SQL 还是 NoSQL、数据库模型;第 4 步回头确认瓶颈与限制,逐个尝试负载均衡、水平扩展、缓存、数据库分片。
注意第 4 步的原话语气:论述可能的解决办法与代价——每一件事都需要取舍。面试加分点从来不是“方案多完美”,而是你能主动把权衡讲出来。
开场先问什么:澄清问题清单
四步法的第 1 步决定后面所有数字和选型,所以提问是重头戏。下面 5 个维度是提问的主骨架——对应原仓库“第一步”的官方问题清单(谁使用、怎样使用、多少用户、系统作用、输入输出、每秒请求、数据量、读写比率),整理成更好记的“5 问”:
| 维度 | 要问清的问题 | 影响哪个决策 |
|---|---|---|
| 读写模式 | 读多还是写多?比例大致多少? | 缓存 / 消息队列 / 读写分离的取舍方向 |
| 规模量级 | 多少用户?每秒、每天多少请求?存量数据多大? | 单机够不够,要不要分布式与分片 |
| 延迟要求 | 可接受的读 / 写延迟?有没有 P99 等硬指标? | 缓存、CDN、异步处理是否必须 |
| 可用性要求 | 允许停机多久?SLA 是几个 9? | 副本数、故障切换、一致性代价 |
| 数据生命周期 | 数据保留多久?能不能丢?要不要过期清理? | 存储选型、TTL 与清理任务、备份策略 |
别只问一轮。把答案整理成“候选假设”报给面试官确认(“我按读:写 = 100:1、日增 1 亿条来估,可以吗?”)——既展示结构化思考,也防止方向错了整段白画。
心算 Back-of-the-envelope:让每个结论都有数字
第 2~4 步里你会反复遇到“要不要缓存、要不要分片、扛不扛得住”,答案要靠数量级说话。原仓库在第四步之后单列了“预估计算量”(Back-of-the-envelope calculations),并指向 2 的次方表与延迟数两张附录。心算的纪律是:先立假设 → 快速算 → 结论写成“带假设、可修正”的一句话。下面用“设计短链服务”的每秒写请求做一次完整示范(数字均为示范假设,重在看方法):
随手可查的心算常数(延迟数字)
下表数字全部取自原仓库附录《每个程序员都应该知道的延迟数》,背熟几个关键锚点,估算就有了参照物:
| 操作 | 延迟 | 一句话感觉 |
|---|---|---|
| L1 缓存引用 | 0.5 ns | 纳秒级,最快 |
| L2 缓存引用 | 7 ns | 约 14× L1 |
| 主存引用 | 100 ns | 约 20× L2 |
| 同一数据中心往返 | 500 μs | 亚毫秒,本地调用无感 |
| SSD 随机读 4 KB | 150 μs | 毫秒内 |
| SSD 顺序读 1 MB | 1 ms | 吞吐约 1 GB/s |
| 磁盘寻道 | 10 ms | 约 20× 数据中心往返 |
| 磁盘顺序读 1 MB | 30 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 分钟。把它切成四个阶段,每阶段对应四步法的一步,就能保证“开头有时间问清、结尾有时间收口”:
五个高频问答
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。
- Study guide 学习指引——按时间线安排复习广度与深度
- Latency numbers every programmer should know——本章心算常数表出处(附录)
- Powers of two table——2 的次方表,估容量量级时配合使用(附录)
性能 vs 扩展性 Performance vs Scalability
面试里“系统怎么扛住更大流量”的第一问。先把两个词分开:性能是单台机器快不快,扩展性是加机器以后系统能不能真的变强。
性能 vs 扩展性
两个词,两条路
性能(Performance):单台服务器在单位时间内能处理多少请求、单个请求多快返回。指标:QPS、P99 延迟、吞吐。扩展性(Scalability):系统能否通过“加资源”持续变强——加机器(横向)或换大机器(纵向)后,吞吐是否近似线性增长。
一个常见的面试陷阱是:把“系统慢”直接归因于“机器不够”。慢可能是代码问题、慢查询、锁竞争——先诊断再扩容,否则加多少机器都白搭。
纵向 vs 横向
| 维度 | 纵向扩展 | 横向扩展 |
|---|---|---|
| 做法 | 换更大的机器(CPU/内存/磁盘) | 增加同构机器实例 |
| 代码改动 | 基本不用 | 需无状态化、可水平伸缩的设计 |
| 上限 | 单机物理天花板 | 理论上无限(钱与复杂度说了算) |
| 成本曲线 | 越往上越陡(大机器贵得离谱) | 近似线性,还能按量付费 |
| 停机影响 | 换机/升级通常要停机 | 滚动发布、随时增减实例 |
| 故障半径 | 单点,挂了全挂 | 坏一台只是容量少 1/N |
三层各自怎么扩展
一个典型 Web 系统分三层,每层“能不能横着长”难度完全不同,面试要分层答:
什么时候“先纵后横”
- 起步阶段:用户少,直接上集群是过度设计——一台高配机器最省钱省事。
- 遭遇单机瓶颈后:先纵向(顺手的事),同时做无状态化与健康检查,为横向铺路。
- 规模与成本拐点:当“加一台机器”比“换更大机器”更划算、或需要容灾时,切横向。
高频追问 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。
延迟 vs 吞吐 Latency vs Throughput
两个总被混用的词:延迟看单个请求有多快,吞吐看整体每秒能处理多少。心算、容量估算、压测报告都建立在把它们分开之上。
延迟 vs 吞吐
水管比喻:把系统想成管道
延迟数字金字塔:心里要有数量级
系统设计的判断力,很大程度来自“对数量级有感觉”。下面这组数字(完整版见 附录 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」。
可用性与一致性 Availability vs Consistency · CAP
分布式系统最绕不开的一对矛盾:机器会挂、网络会断,这时候你是继续服务但给旧数据,还是宁可报错也不给错数据?这一章把 CAP、一致性模式与可用性数字一次讲清。
可用性与一致性(CAP)
先理解为什么绕不开
在单台机器上,可用性和一致性根本不是问题:数据就在本地,写进去立刻能读到。问题出在分布式:数据要复制到多台机器(为了容灾与扩展),机器之间靠网络通信,而网络是会断的。
一旦某两台机器“失联”(分区发生),它们各自都还活着、都能服务请求,但无法同步数据。这时你被迫回答一个问题:用户读到的那份数据,允不允许不是最新的?
- 选一致性 C:我宁可拒绝这次读/写(或等分区恢复),也绝不返回旧数据——适合钱、库存、投票这类“错了就要命”的场景。
- 选可用性 A:我继续响应,先给“可能旧一点”的数据,等网络恢复再把账算平——适合动态、feed、评论数这类“旧几秒无所谓”的场景。
CAP 三角:三个愿望只能实现两个
一致性模式:强、弱、最终
可用性数字:几个 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 负载均衡)。
- 多机房 / 多区域:单机房整体不可用时的最后防线(成本与复杂度最高的那一档)。
高频追问 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。
DNS 域名系统 Domain Name System
把 "www.example.com" 翻译成机器能连的 IP 地址——DNS 是互联网的全球电话簿,靠「分层委托 + 逐级缓存」扛住全网的解析流量,还兼职做粗粒度的流量调度。
DNS 域名系统
1. 名字 vs 地址:DNS 在解决什么问题
网络层只认 IP 地址:想访问一个网站,TCP 连接必须落到类似 203.0.113.10 的四段数字上。但让用户去记数字既不现实也不稳定——服务器迁移、扩容都会换 IP。DNS(域名系统)就是夹在中间的翻译层:它维护一张分布式的「大表」,key 是 example.com 这样的域名,value 是对应的 IP 以及一批附加元数据。
关键在设计约束:这张表太大了、查询量也太大了,任何一台服务器都装不下也扛不住。所以 DNS 选择了两个朴素手段:按层级分工(每个层级只负责自己辖区内的「指路」)与逐级缓存(就近记住结果,避免每次都问到底层)。这正是下面图 4-1 里那趟旅程能这么快完成的原因。
2. 一次解析的完整旅程
先纠正一个常见误解:浏览器并不会「自己满世界问」。它只做两件事——先查自己的缓存(浏览器缓存、操作系统缓存),没命中就把域名交给系统里配置好的递归解析器(通常由运营商或公共 DNS 提供),然后坐等答案。承担逐级询问的是递归解析器,它走的是「迭代」路线:
- 根服务器 Root:不存储任何具体域名,只负责回答「.com 这类顶级域归谁管」;逻辑上全球共 13 组,通过 Anycast 分布在世界各地的大量镜像上。
- 顶级域服务器 TLD(如
.com、.cn):回答「example.com的权威服务器是谁」,通常以NS记录形式给出。 - 权威服务器 Authoritative:真正持有该域名记录、给出最终答案的一方——通常由域名注册商或托管 DNS 服务(Cloudflare、AWS Route 53 等)代为配置。
3. 记录类型:DNS 里装的远不止 IP
DNS 记录可以理解为「域名的各种用途条目」。面试最常问的是 A 与 CNAME,但整套常用类型必须能脱口而出:
几个面试加分点:
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 服务正是以这种方式集中地路由流量:
常用策略及适用场景对比如下:
| 策略 | 原理 | 典型用途 |
|---|---|---|
| 轮询 Round-robin | 多条 A 记录按顺序轮流返回 | 多台同质服务器均摊流量 |
| 加权轮询 Weighted RR | 按权重比例决定返回哪条 | 不同容量集群配比;A/B 测试;把权重调 0 让维护中的机器退出流量 |
| 基于地理位置 | 按用户出口位置就近返回 | 多机房就近接入(CDN 常用) |
| 基于延迟 | 返回实测延迟最低的节点 | 多可用区 / 多云择优 |
注意定位:DNS 负载均衡是入口级、粗粒度的调度。它不感知后端进程健康状态(除非运维手动摘除记录),也不保证会话保持;更关键的是解析结果会被各级缓存按 TTL 保留,权重调整不会「实时」生效。生产架构里的分工通常是:DNS 负责把流量引到正确的机房 / 集群入口(含容灾切换),真正的负载均衡器在 IP 之后做细粒度分发与健康检查——两者配合而非互替(详见后文 CDN 与负载均衡章节)。
5. TTL 与缓存:为什么改记录「不生效」
图 4-1 里反复出现的 TTL(存活时间)决定了每条记录能在各级缓存里活多久——短的只有几十秒,长的可达数天。它直接影响两个体验:
- 解析快不快:TTL 越长,缓存命中率越高,递归解析器越少打扰根与权威服务器;
- 变更生效快不快:TTL 越长,改完记录后旧值「赖在缓存里」的时间也越长,这就是常说的 DNS 传播延迟(propagation delay)——不是网络慢,而是旧缓存没到期。
dig example.com +trace 逐级观察每层缓存。6. 缺陷与坑
- 解析本身有延迟:虽然各级缓存极大缓解,但冷查询仍要发起网络往返;冷启动页面常因此慢一拍。
- 管理复杂、责任分散:域名体系通常由注册局、托管服务商、权威服务器等多方共同维护,出问题时的排查链条很长。
- 是集中攻击的靶子:DNS 是公共咽喉,2016 年 10 月针对 Dyn 的大规模 DDoS 曾让大量用户因「解析不到 Twitter 的 IP」而无法访问 Twitter——服务本身没挂,入口却瘫痪了。
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)对应章节:
- DNS · Domain Name System(记录类型、托管 DNS 路由策略与缺陷清单)
- Load balancer——与 DNS 轮询对照,理解两层调度的分工
- CDN——地理 / 延迟路由的进阶应用(由 DNS 解析决定连哪台节点)
- Wikipedia · Domain Name System(TTL、根服务器与协议细节)
CDN 内容分发 Content Delivery Network
图片、CSS、JS 这些静态资源占了站点流量的大头。CDN 的思路很朴素:把内容搬到离用户近的地方,让“最后一公里”变快。
CDN 内容分发网络
没有 CDN 时,跨半个地球的请求有多痛
推模式 vs 拉模式:内容怎么进 CDN
缓存失效与回源放大的坑
- TTL 过期:节点缓存带 TTL,过期后回源取新——更新频率决定 TTL 长短。
- 主动失效/刷新:内容下线或改错时,调 CDN 的 purge 接口强制清掉指定 URL。
- 版本化文件名:
app.a1b2c3.js这类带哈希的文件名,让“新版本”天然是“新 URL”,不用清缓存——前端资源的标准做法。 - 回源风暴:热点内容 TTL 同时过期 → 所有节点同时回源 → 源站被打穿。解法:错峰过期(TTL 加随机抖动)、源站套缓存/限流。
高频追问 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)。
反向代理 / Web 服务器 Reverse Proxy & Web Server
所有请求进系统的“第一道门”。反向代理本身不写业务,却决定了系统的入口体验:TLS、压缩、静态资源、限流都在这一层。与 §6 负载均衡常被混为一谈,这里把边界讲清。
反向代理 / Web 服务器
正向代理 vs 反向代理:一个替用户跑,一个替服务器挡
反向代理能干的事(一图流)
反向代理 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。
应用层 · 微服务 Application Layer & Microservices
从“一台服务器跑整个应用”到“几百个服务各自独立部署”,中间隔着的不是炫技,而是一整套关于故障隔离、伸缩、团队组织的权衡。这一章讲清应用层为什么最好无状态、微服务拆了什么又付出了什么。
应用层与微服务
单体不是原罪:先理解它为什么撑不住
- 部署耦合:改一行代码也要整包发布,任何模块的缺陷都可能拖垮全体。
- 伸缩浪费:某个功能(如报表)吃 CPU,却要为整个单体多开机器。
- 团队踩踏:几十人改同一个代码库,合并冲突与发布窗口成为日常痛苦。
- 技术栈锁死:想给某个模块换语言/框架,几乎不可能。
但注意:单体的部署与事务最简单、最不易错。所以行业共识是——先用单体把业务跑通,等痛点真实出现(部署太频繁/团队太大/局部需要独立伸缩)再拆。
单体 vs 微服务
服务间调用:失败是会传染的
单体里函数调用失败=异常;微服务里服务调用失败=网络错误,而网络错误会连锁放大:A 调 B 超时,A 的线程被占住,流量继续涌来,A 的线程池被打满,A 自己也开始超时——这就是“雪崩”。面试里聊微服务必带三件套:
- 超时与重试上限:给每次调用设超时;重试要带退避与上限,否则故障时全员重试等于自轰。
- 熔断(Circuit Breaker):下游连续失败达到阈值,上游直接快速失败(不再等超时),让下游有时间恢复。
- 舱壁/隔离(Bulkhead):不同下游用独立线程池/连接池——报表服务挂了不该拖垮支付链路。
服务发现:新实例上线,谁来告诉调用方?
什么时候“别”上微服务
- 团队 < 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。
数据库 SQL / NoSQL Database · RDBMS vs NoSQL
数据库是系统的“存储之锚”:关系型用表 + 外键 + ACID 事务守住复杂关系与强一致,NoSQL 用键值 / 文档 / 列族 / 图四种模型换灵活与横向扩展。选型先答三问:关系强不强、模式固定吗、要不要横着长。
数据库:关系型与 NoSQL
关系型数据库:表、外键与 ACID
关系型数据库(Relational Database,代表:MySQL、PostgreSQL)把数据组织成一张张“表”:表由固定的列结构(严格 schema,模式)与行组成,行与行之间通过外键(Foreign Key)互相引用;需要跨表取数时,用联结(JOIN)在查询里把它们拼起来——这正是关系模型的核心能力。
它真正的护城河是“事务”:ACID——原子性 Atomicity(事务内所有操作要么全部完成、要么全部不完成)、一致性 Consistency(事务使数据库从一个有效状态迁移到另一个有效状态)、隔离性 Isolation(并发执行事务的结果与串行执行相同)、持久性 Durability(事务提交后,对系统的影响永久保留)。账户、订单、库存这类“错了要命”的数据,靠的就是这条承诺。
代价同样清楚:单机写扩展有天花板,所以它的扩展有固定套路——主从复制(读写分离)、主主复制、联合、分片,再配合非规范化与 SQL 调优,每一步都是“清晰的扩展模式”。
NoSQL:一个名字,四种模型
NoSQL 不是“没有 SQL”,而是四类数据库的统称:键值(Key-Value)、文档(Document)、列族(Column)、图(Graph)。它们的共同点很一致:数据是非规范化的,联结大多在应用程序代码里完成;绝大多数 NoSQL 无法提供真正符合 ACID 的事务,转而支持最终一致,用 BASE 描述自己——基本可用(Basically Available)、软状态(Soft state)、最终一致(Eventually consistent),把可用性放在一致性之前。
顺着这四种模型认产品:键值存储以哈希表为抽象(Redis),读写接近 O(1),操作有限,常作为缓存、会话与元数据,也是文档、图等更复杂存储的“地基”;文档存储把一个对象整体存进一个文档(MongoDB、CouchDB),支持类 SQL 查询;列族存储面向海量写入(Cassandra、HBase,师承 Google 的 Bigtable),键按字典序存放,值带版本时间戳;图数据库把“关系”当一等公民(Neo4j),节点是记录、弧是关系。
SQL 还是 NoSQL:三个问题定方向
源料把“选 SQL 的理由”与“选 NoSQL 的理由”各列了一屏:SQL 侧是结构化数据、严格模式、关系型、需要复杂联结、事务、清晰的扩展模式、索引查询快、生态资源丰富;NoSQL 侧是半结构化、动态模式、非关系型、无需复杂联结、TB 级甚至 PB 级数据、高数据密集负载、IOPS 高吞吐。抽象成一道三问决策题,面试照这个答就立得住:
选型速查表:对号入座
面试现场不用背清单,记住“数据长什么样 → 该上什么”这条映射即可:
| 数据长什么样 | 首选 | 类型 | 一句话理由 |
|---|---|---|---|
| 账户 / 订单 / 资金——强关系、要事务 | 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)对应章节:
缓存 Cache 缓存策略 · 淘汰 · 一致性
缓存是系统设计里性价比最高的第一板斧:本章图解五层缓存金字塔、Cache-aside 旁路读写流程、直写/回写/TTL 失效三种写策略,再聊淘汰、一致性这个必考点与「击穿/穿透/雪崩」三兄弟。约 9 分钟读完,含 4 张图解。
缓存
为什么需要缓存:读路径上的第一层缓冲
缓存 Cache 的原始动机很朴素:把「已经算过的答案」暂存起来,下次直接复用——它能提高页面加载速度,也能降低服务器与数据库的负载。在一个分发器模型里,请求进来先查“这个请求之前响应过没有”,命中就直接把旧结果吐回去,省掉真正的处理。
数据库分片、副本理论上能让读分布均匀,但真实流量总有热点:热门数据被反复读,形成不均匀的读放大。此时在数据库前面加一层缓存,就能抹平热点与突发流量对数据库的冲击。所以判断“要不要缓存”的标准只有两条:访问模式读多写少、数据允许短暂过期。缓存永远只装得下“最热”的一小部分——因为放缓存的内存(RAM)比磁盘贵得多、也小得多。
缓存金字塔:从客户端到数据库,一层层叠
从用户到数据库之间,缓存可以出现在好几个位置。越靠近用户的一端命中越快、容量越小;越往下越接近权威数据、容量越大。图 10-1 把常见的层级叠成一座金字塔:
第 ④⑤ 层在图中紧贴数据库,因为它们是应用与数据存储之间的键值缓存:数据放在 RAM 里,比磁盘上的典型数据库快几个数量级(内存访问约 100 ns 量级,磁盘寻道约 10 ms 量级)。RAM 又贵又有限,所以要用 LRU 这类淘汰算法把「热门数据」留在内存,冷数据不占地方。在 Memcached 与 Redis 之间选型时记住:Redis 额外提供持久化选项与内置数据结构(有序集合、列表等)。
同一层缓存还能按粒度细分——行级、查询级、完整的可序列化对象、完全渲染的 HTML,从细到粗。查询级把「查询语句哈希 → 结果」存进缓存,实现直白,但复杂查询很难精确失效,一行数据变化可能连带要删掉一堆包含它的缓存结果;对象级把数据当对象处理,底层数据变了就删掉对应对象,还允许 worker 用最新缓存对象异步组装。因此生产上更建议缓存这些对象:用户会话、完全渲染的 Web 页面、活动流、用户图数据。另外尽量别做基于文件的缓存——文件系统在复制与自动伸缩时都很难办。
Cache-aside 旁路缓存:面试默认开局
面试里提到缓存,默认先讲 Cache-aside(旁路缓存,也叫懒加载 lazy loading):缓存不跟数据库直接交互,一切读写都由应用代劳。Memcached 通常就是这种用法。它的完整读写流程见图 10-2——读:先读缓存,未命中才查库,查到后回填缓存再返回;写:直接更新数据库,再把缓存里的旧条目删掉。
Cache-aside 有几个必须背的坑:① 未命中要走「查缓存 → 查库 → 回填」三步,首次访问延迟明显;② 数据库更新后缓存会短暂过时,需要靠 TTL 强制刷新或改用直写模式缓解;③ 节点故障被新节点替换时,新节点的缓存是空的(cold cache),延迟升高,直到缓存慢慢热起来。这也是为什么缓存层要预留充足容量、避免频繁上下线。
写入的时候怎么办:直写 / 回写 / TTL 失效与刷新
Cache-aside 只规定了“读”,写入怎么办取决于业务对新鲜度、吞吐与丢失容忍度的要求。图 10-3 对比三种主流的写路径(外加 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 同时到期会引发雪崩——过期时间要加随机抖动 |
缓存三兄弟:击穿 / 穿透 / 雪崩
中文面试的高频“加餐”:三者共同点是缓存没能接住请求、压力直冲数据库,区别在于“为什么没接住”,应对手段也因此不同(图 10-4):
高频追问 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)对应章节,可对照精读:
异步与消息队列 Asynchronism · Message Queue
同步调用最怕两件事:下游慢拖垮自己、流量尖峰打爆系统。异步的思路是“先答应下来,稍后慢慢办”——把请求投进队列就返回,让后台消费者按自己的节奏消化。
异步与消息队列
为什么要异步:削峰与解耦
消息队列 vs 任务队列:都是队列,用途不同
| 消息队列 Message Queue | 任务队列 Task Queue | |
|---|---|---|
| 传的是什么 | 事件/消息(“订单已创建”),广播给多个订阅方 | 一个可执行任务(“给订单 123 发短信”),只被一个 worker 消费 |
| 典型模型 | 发布-订阅(Pub/Sub),一个消息多份拷贝 | 点对点(P2P)/工作队列,一个消息一份工作 |
| 代表产品 | Kafka、Pulsar、云 MQ 的 topic | RabbitMQ work queue、Celery、Sidekiq |
| 面试怎么用 | 解耦服务 + 削峰 + 事件驱动 | 把重活(发短信/生成缩略图/导报表)丢后台跑 |
实操中很多系统两者混用同一套基础设施(RabbitMQ 两种都支持),面试把“广播事件”和“派发任务”说清即可。
背压:消费者跟不上时会发生什么
用了异步,要付出什么
- 一致性变复杂:主流程成功 ≠ 全部完成,通知可能迟到/丢失 → 需要重试 + 对账。
- 排错变难:没有调用栈了,靠 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)。
通信协议 HTTP / TCP / UDP / RPC / REST
系统里的组件靠什么“说话”?本节把 HTTP、TCP、UDP、RPC、REST 五个高频词按层次拆开讲清楚,并给出“对外 REST、对内 RPC”的选型直觉。
通信协议
HTTP:请求-响应、动词与状态码
HTTP 是客户端与服务端之间编码和传输数据的方法,一个典型的请求-响应协议:客户端把「动词 + 资源」发给服务端,服务端回「状态 + 内容」。HTTP 本身并不绑定传输细节,请求与响应可以穿过一串做负载均衡、缓存、加密和压缩的中间节点——这正是它被 Web 世界选中的原因。
一次 HTTP 对话由四个部分组成:请求 = 方法 + URL 资源 + 请求头 +(可选)请求体;响应 = 状态行 + 响应头 + 响应体。例如浏览器请求 GET /users/42 HTTP/1.1,服务端可能回 HTTP/1.1 200 OK 与用户数据。
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)负责寻址路由,链路层负责物理帧——上层依赖下层,这层抽象正是我们可以只换传输层、不动应用层语义的原因。
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 每次都要发明一个新“方法名”,REST 则用固定的资源集合 + 有限动词组合出无限操作:
| 维度 | RPC | REST |
|---|---|---|
| 关注点 | 方法 / 操作 | 资源 / 状态 |
| 端点长相 | GET /readPerson?personid=1234 | GET /persons/1234 |
| 动词使用 | 动作写进 URL,基本靠 GET/POST | URL 是名词,DELETE/PUT/PATCH 表达意图 |
| 契约 | IDL(.proto / .thrift)强类型,自动生成存根 | URI + Header + 状态码自描述,无需单独契约 |
| 耦合度 | 客户端与服务实现捆绑紧密 | 松耦合,利于独立演进与多端复用 |
| 缓存友好 | 难以被通用 HTTP 缓存层命中 | 无状态 + 语义化动词,天然可缓存 |
| 典型场景 | 内部服务间、性能与类型安全优先 | 对外公共 API、跨团队边界 |
两边的缺点也很对称:RPC 每新增一种操作都要定义新接口,客户端与实现绑得紧、难调试,还想被 Squid 这类缓存代理正确缓存要额外费劲;REST 则不适合“动作难以用动词表达”的场景(如归档过期文档),复杂查询往往要拼 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)对应章节:
安全 Security TLS · 攻击面 · 纵深防御
安全不是某个组件的补丁,而是一条贯穿传输、网关、应用与数据的防线链:每一层只挡自己该挡的攻击,单层失守不致命——这就是纵深防御(Defense in Depth)。
安全
传输加密:看得见,读不懂
很多系统把 HTTPS 当成“配一下就有”的事,但它其实是安全的第一道分水岭。没有它,请求从浏览器到服务器的整条链路上都是明文:登录口令、会话 Cookie、业务内容,任何能插到路径上的中间人(公共 Wi-Fi、被黑的网络设备、运营商侧抓包)都能整包读取。TLS(Transport Layer Security,HTTPS 的底层协议)解决的就是“传输加密 Encryption in Transit”——内容离开浏览器那一刻起就变成密文,只有真正完成握手的服务器能解开。
建立连接时,客户端先校验服务器证书,确认对面不是冒名顶替者;随后用非对称加密协商出一把临时的会话密钥,之后所有业务数据都用这把密钥做对称加密传输,并带完整性校验防止被篡改。源料还要求“等待过程中也加密”,即数据落库落盘时的静态加密(Encryption at Rest),这条我们放进 13-3 图的数据层防线。先看同一段登录请求在 HTTP 与 HTTPS 下的区别:
常见攻击面:五种打法与它们的盾
TLS 只保护“路上”,保护不了服务器内部:数据一旦解密进入应用,若处理不当照样能被利用。源料给的安全底线只有四条——传输与静态加密、防 XSS 与 SQL 注入、用参数化查询、遵循最小权限原则(Principle of Least Privilege);下面每种攻击“怎么打、为什么这么挡”的机制是本站在此基础上的展开(OWASP Top 10 常识,文中以“本站补充”标出)。五个最高频的攻击面见 图 13-2:
- 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:
传输层管“路上”、网关层管“门口”、应用层管“业务逻辑”、数据层管“落库之后”。层与层的职责不重叠才叫纵深:例如即使 WAF 漏掉一条注入特征,参数化查询仍在应用层把它拦成字面量;即使应用层校验全被绕过,最小权限的 DB 账号也做不了删表清库。
攻击 → 防御:对照速查表
面试或自查时把下表当 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 与工程常识的补充:
设计短链服务 Design a URL Shortener · Pastebin / Bitly
系统设计面试的“入门大题”:麻雀虽小,五脏俱全——它把估算、存储选型、编码方案、缓存、过期清理全串了一遍。跟着四步法走一遍,你就掌握了所有“读多写少”题型的套路。
例题:设计一个短链服务
Step 1 · 厘清需求:先问,别急着画
- 功能:长链 → 短链;访问短链 → 302 跳回长链。要自定义短链吗?要统计点击吗?(第一版不做,可提一句“预留”)
- 读多写少确认:一次写入、无数次读取,读:写常见 100:1。
- 规模假设(大声说出来):每天新增 1 亿条?还是 100 万条?——面试按量级给方案,这里我按“年新增 5 亿、总存量 500 亿级”太激进,先按每天 1 亿条新增、读:写 100:1 估算(约等于 YouTube/微博量级的链接场景)。
| 估算项 | 假设 | 结果 |
|---|---|---|
| 写入 QPS | 1 亿条/天 ÷ 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 峰值(几台机器即可) |
Step 2 · 高层设计
Step 3 · 核心组件:短码生成(Base62)
0-9a-zA-Z,7 位容量 62^7 ≈ 3.5 万亿。路线 A(发号)实现简单、短码有序;路线 B(哈希)无单点。若需求含“点击统计/可自定义”,发号路线更好扩展。Step 4 · 扩展:读路径是主角
- TTL 与过期:短链一般不需永久保存,定期清理(或懒删除:读时发现已过期即删)控制存储增长。
- 缓存击穿防护:热点短链(大促链接)可能被刷爆——缓存失效瞬间回源放大,可对热点加“短暂空值缓存”或单飞保护。
- 安全:只允许 http/https 目标;对短链做风控(防止跳转到钓鱼站被滥用);接口限流防刷。
复盘:怎么答更出彩
高频追问 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);容量数字为本站按假设演示(已标注),正式面试以你的口头假设为准。
设计时间线与搜索 Twitter / Facebook Feed
把「发一条推文怎么送到所有粉丝手里」拆开讲清楚:扇出(Fan-out)选推还是拉、主页时间线缓存存什么、以及搜索子系统如何独立演化——这是「读多写少 + 写放大」类问题的最佳入门题。
例题:设计一个时间线系统
第一步 · 需求与假设:先把「时间线」这个词说清楚
这类题(Twitter、Facebook Feed 同型)先要区分两种时间线:用户时间线(自己发的推文)和主页时间线 Home timeline(自己关注的所有人最近的活动)。核心动作是扇出 Fan-out——把一条推文「分发」给所有关注它的人。需求范围通常锁定为:用户发推(并推送给粉丝、发通知/邮件)、浏览用户时间线、浏览主页时间线、关键词搜索、高可用。可以明确排除的:Firehose 流式推送、按「是否可见」设置过滤、数据分析。
面这类题要主动问:读多还是写多?这里显然是读多写少——刷主页远多于发帖,所以要为快速读取优化。常用假设口径(与官方题解一致):
- 1 亿活跃用户;每天新发布 5 亿条推文(每月约 150 亿条);
- 扇出递送量是发布的约 10 倍量级:每天约 50 亿次、每月约 1500 亿次推送写入(平均每条推文要写进约 10 个粉丝的 feed,即写放大 ≈ 10×);
- 每月约 2500 亿次读取、100 亿次搜索——这两项就是「读多写少」的证据。
| 指标 | 心算算式(按“每秒 400 请求 ≈ 每月 10 亿请求”) | 结果 |
|---|---|---|
| 发推(写)QPS | 150 亿条/月 ÷ 10 亿 × 400 | ≈ 6000 条/秒 |
| 扇出推送 QPS | 1500 亿次/月 ÷ 10 亿 × 400 | ≈ 6 万次/秒 |
| 主页读取 QPS | 2500 亿次/月 ÷ 10 亿 × 400 | ≈ 10 万次/秒 |
| 搜索 QPS | 100 亿次/月 ÷ 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 个关注对象,热点作者还会被成千上万粉丝同时读。两种极端都不够好。
| 方案 | 发帖时 | 读取时 | 优点 | 代价 |
|---|---|---|---|---|
| 推(写时扇出) | 写进每个粉丝的 feed 缓存(N 次写) | 读自己的缓存列表,O(1) | 读延迟最低 | 写放大,大 V 扇出要几分钟 |
| 拉(读时拉取) | 只写自己的时间线,1 次写 | 拉取 N 个关注对象再归并 | 无写放大,冷用户零成本 | 读放大,热点作者被反复读 |
| 混合(推荐) | 普通用户走推,大 V 只写自己 | 缓存 + 现取大 V 推文,归并排序 | 两边都压得住 | 读路径多一步归并 |
混合方案:普通人用推,大 V 用拉
官方题解给出的做法:不要从高关注量用户那里做扇出。普通用户粉丝量小,照常写时扇出;而对粉丝数百万的大 V,发帖时只写自己的时间线(SQL)并进入搜索索引,一份也不复制给粉丝。粉丝刷新主页时,把缓存里的 feed 和实时拉取的大 V 最新推文(来自时间线 SQL / 搜索索引)归并、按时间排序后返回——顺带解决了扇出太慢导致的 @回复 乱序问题(服务端按时间重排即可缓解竞争条件)。判断标准一句话:用「读的 N」(关注的大 V 数量)换掉「写的 N」(大 V 的粉丝数量),通常划算得多。
主页时间线读取路径:缓存 + 归并
读路径(每秒 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 按作者取即可。
搜索子系统:倒排索引,独立演进
搜索(每秒约 4000 次)与时间线解耦成独立子系统:发帖时写 API 异步把推文送进搜索索引服务;查询时搜索 API 先做输入处理(去标点、分词、拼写纠正、统一大小写、转布尔查询),再交给搜索集群(如 Lucene)。集群内用倒排索引 inverted index——每个词维护一个「包含它的推文 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)对应章节:
设计网络爬虫 Web Crawler
爬虫不是一台暴力下载机器,而是一条「生产者-消费者」流水线:把吞吐、去重与礼貌性同时管好,才是这道题真正想考的工程素养。
例题:设计一个网络爬虫
第一步 · 澄清需求与估算
先界定边界:爬虫为搜索引擎生产离线数据——抓取网页后,一是生成包含搜索词的倒排索引,二是为每页生成静态的标题与摘要(不随搜索词变化);用户的搜索读路径只是消费这些产物,不是本题重点。明确不做:搜索分析、个性化结果、页面排名。更重要的隐含需求是:爬虫不能陷入死循环,也不能把站点打挂——前者引出去重,后者引出礼貌性限速。
随后与面试官对齐规模假设(都写清楚、再动手算):
- 基线:抓取
10 亿个链接;为保证新鲜度定期重抓,平均每周一遍,热门站点更频繁 → 折算每月抓取约40 亿个页面; - 每页平均
500 KB(重抓的页面简单起见视作新页面);每月搜索量1000 亿次;服务要求高可用。
| 指标 | 算式(每月按 250 万秒计) | 结果 |
|---|---|---|
| 抓取写入速率 | 40 亿页 ÷ 250 万秒/月 | ≈ 1600 页/秒(写侧) |
| 页面存储 | 40 亿 × 500 KB = 2×10¹⁵ B | ≈ 2 PB/月;3 年 ≈ 72 PB |
| 搜索请求(读侧参考) | 1000 亿次 ÷ 250 万秒/月 | ≈ 40000 次/秒 |
第二步 · 高层架构:一条环形流水线
抓取是典型的生产者-消费者结构:links_to_crawl(待抓集合,按网站知名度/热度排优先级,可用 Redis 有序集合维护)出队 → 调度器按域名礼貌限速 → 下载器抓页面 → 解析器抽子链接与正文 → 子链接先去重、新的才回队列;正文则入库存档并派发两类任务:倒排索引任务与文档任务(生成标题+摘要)。已抓链接连同页面签名记入 crawled_links,用于查重与防环。整个流水线:
两点取舍先说清:其一,读侧(搜索)不是本例题主角——客户端经反向代理到达查询 API,解析/分词/纠错后命中倒排索引返回标题与摘要即可,一笔带过;其二,高可用来自状态外置:队列与已抓集合都放键值型 NoSQL,抓取实例无状态可水平扩展,任意一台挂掉不丢进度。
第三步 · 核心组件
URL 去重:布隆过滤器是防死循环与省内存的关键
爬虫最常见的死法是死循环:页面之间存在环(A 链 B、B 又链 A)时,同一个 URL 会被反复入队、反复抓取。最简单也最该先说清的去重手段按规模递进:小数据量直接 sort | unique;十亿级链接则用 MapReduce 批量剔除重复记录;而在线判重——每解析出一个子链接就要立刻回答“抓过没有”——最适合用布隆过滤器 Bloom Filter:
为什么必须上布隆过滤器?算一笔账:假设 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 |
礼貌性限速:同一站点绝不并发轰炸
全局 1600 页/秒很漂亮,但如果几十个线程同时打同一个站点,站长会直接封你。礼貌性的工程化做法是每个域名一个独立队列与独立消费线程:同一域名串行发送、两次请求之间强制冷却间隔 Δ(Δ 可依据站点 robots.txt 的 Crawl-delay 与历史响应调整);并发能力加在“域名数量”上,而不是同一站点上。别忘了域名归一化:www.example.com 与 example.com 必须哈希到同一队列,否则等于绕过限速开双通道。
配合限速还要有两件事:失败处理按域做指数退避重试并记录连续失败次数(连续失败说明该域可能已屏蔽爬虫);调度前先读该域 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)对应章节:
设计 KV 存储 Key-Value Store / Query Cache
把搜索引擎最热的查询结果缓存进一个分布式 KV:一致性哈希分片、主从复制、最终一致——完整推演一遍"从单机 LRU 到分片集群"的演进。读完约 10 分钟,含 4 张图解。
例题:设计一个分布式 KV 存储
第 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 B | query 50 B + title 20 B + snippet 200 B |
| 若全量缓存 | 2.7 TB / 月 | 100 亿条互不相同的查询都存 → 内存放不下,必须靠淘汰 |
| 命中读延迟 | ~250 µs / MB | SSD 约 4×、机械盘 80× 以上 → 命中越快越好 |
第 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)决定去哪台——更复杂,却是正解:全集群共享一份逻辑缓存,命中率不被实例数稀释,内存与吞吐还能随节点线性扩展。
分片用 machine = hash(query) 就好吗?直接取模(hash mod N)有两个毛病:节点数一变(扩到 N+1 台、某台宕机),几乎每个 key 的归属都跳变——对缓存来说等于一次集体失效,所有未命中请求同一瞬间回源,反手把反向索引、文档服务打爆(这正是缓存雪崩最常见的引信)。于是要上一致性哈希 Consistent Hashing:把节点和 key 都哈希到同一个环上,key 顺时针遇到的第一个节点就是它的归属——增删节点时,只有相邻区间里的 key 需要换主,波及面从"全部"降到"约 1/N"。
第 3 步(续)· 复制与一致性:可用性靠副本,不靠侥幸
分片解决了容量,但每片仍是单点:一片只有一台机器,宕机就等于该片数据全没。给每片配上主从复制:写请求只走主节点,主节点先把这次操作追加进写前日志 WAL(Write-Ahead Log),确认日志落好后再更新内存表、向客户端回包;随后把日志异步复制到从副本。主节点挂了,从副本晋升为新主,读请求依旧有处可去——副本数(常见 2~3 份)本质是"读吞吐与可用性"对"写放大"的权衡。
注意一致性在这里的取舍:异步复制意味着"同一条 key 可能读到旧值",而查询缓存对这种短暂不一致尤其宽容——数据本身来自回源,可以重建,旧一两秒的结果远好于把请求阻塞住。所以主从之间用版本号 / 读修复 Read Repair收敛,走最终一致而不是强一致(为什么选 AP 而不是 CP,见文末 Q4)。
第 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)对应章节:
- 例题原文(英文题解):Design a key-value cache to store the results of the most recent web server queries
- 题解索引中的定位:Design a key-value store for a search engine
- 缓存 Cache:在哪缓存 / 缓存什么 / 何时更新
- 分片 Sharding:哈希与一致性哈希的取舍
- 主从复制 Master-Slave Replication
- 一致性模式:弱一致 / 最终一致 / 强一致
- 可用性模式:故障转移 / 复制(选 AP 的依据)
- 附录:延迟数字(内存 ~250 µs / MB 等)
支撑百万用户 Scaling to Millions of Users
全站收尾的“综合大题”:不设计某个具体功能,而是把一台服务器一步一步演进出能支撑百万用户(并继续往上走)的架构。它把前面所有章节串成一条主线——每一步只解决当下最疼的瓶颈,并为下一步留好口子。
例题:从单机到支撑百万用户
演进总览:八步,每步一个瓶颈
前三步:从单机到“能用”
中段:缓存、CDN、异步加入后的完整形态
终态:分片 + 多机房(千万级以上才谈)
- 数据分片:单库写不下了 → 按 user_id / 订单号哈希分片(一致性哈希,见 T4)。代价:跨片查询、事务、热点 key——所以分片是“最后的手段”而不是默认。
- 多机房:主备机房(同构部署,故障切流)或两地三中心;DNS/全局负载均衡把用户导向最近/健康的机房(呼应 §4)。代价:数据同步延迟与一致性策略(呼应 §3)。
- 别再往上涨怎么办:接受“用钱换可用性”,或从业务侧削峰(预约制、错峰推送)——架构不是万能的。
复盘:怎么答更出彩
高频追问 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 各章。
数字速查表 Latency & Capacity Cheat Sheet
系统设计面试的心算“一页纸”:2 的次方、延迟数字、估算套路。数据出自 System Design Primer 附录,考前 5 分钟过一遍即可。
2 的次方表
容量估算的地基:记住 2^10 = 1,024 ≈ 1K,之后每 +10 就乘 1,024,跳一个数量级单位。
| 幂 | 精确值 | 约值 | 字节单位 |
|---|---|---|---|
| 2^7 | 128 | — | — |
| 2^8 | 256 | — | — |
| 2^10 | 1,024 | 1 thousand(千) | 1 KB |
| 2^16 | 65,536 | — | 64 KB |
| 2^20 | 1,048,576 | 1 million(百万) | 1 MB |
| 2^30 | 1,073,741,824 | 1 billion(十亿) | 1 GB |
| 2^32 | 4,294,967,296 | — | 4 GB |
| 2^40 | 1,099,511,627,776 | 1 trillion(万亿) | 1 TB |
每个程序员都应该知道的延迟数字
这组数字是心算的“参照物”,背到数量级即可:内存 ≈ 100ns,SSD ≈ 0.1–1ms,磁盘 ≈ 10ms,同机房往返 ≈ 0.5ms,跨洲 ≈ 150ms。
| 操作 | 耗时 | 备注(相对倍数) |
|---|---|---|
| L1 缓存引用 | 0.5 ns | — |
| 分支预测失败 | 5 ns | — |
| L2 缓存引用 | 7 ns | 14× L1 |
| 互斥锁加锁/解锁 | 25 ns | — |
| 主存引用 | 100 ns | 20× L2,200× L1 |
| Zippy 压缩 1KB | 10,000 ns = 10 us | — |
| 1 Gbps 网络发送 1KB | 10,000 ns = 10 us | — |
| SSD 随机读 4KB | 150,000 ns = 150 us | ~1GB/s 的 SSD |
| 内存顺序读 1MB | 250,000 ns = 250 us | — |
| 同数据中心往返 | 500,000 ns = 500 us | — |
| SSD 顺序读 1MB | 1,000,000 ns = 1 ms | 4× 内存读 |
| 磁盘寻道 | 10,000,000 ns = 10 ms | 20× 机房往返 |
| 1 Gbps 网络顺序读 1MB | 10 ms | 40× 内存读,10× SSD |
| 磁盘顺序读 1MB | 30,000,000 ns = 30 ms | 120× 内存读,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 网卡的承受力立刻可算。
来源
数值出自 System Design Primer 附录(Copyright 2017 Donne Martin,CC BY 4.0)「Powers of two table / Latency numbers every programmer should know」;排版与记忆法为本站整理。
面试应答清单 Checklist & Scripts
把方法论章(四步法)落成面试现场能直接照做的动作清单:问什么、怎么分配 30 分钟、用什么话术开场与收尾。考前一天过一遍,比临时背题有用。
开场 60 秒:提问清单(按顺序过)
- 功能:核心动作是什么?读多还是写多?要不要搜索 / 推荐 / 报表这类“次级系统”?
- 规模:DAU / 总用户量级?每天多少次核心请求?数据量级(百万条 / TB / PB)?峰值与均值差多少?
- 质量:P99 延迟要多少(百毫秒 / 秒级)?可用性要几个 9?单机房还是全球?
- 约束:允许用什么技术栈?要不要考虑成本与运维复杂度?有什么已知的“红线”?(容量估算默认自己来)
30 分钟时间盒
| 时间 | 阶段 | 该产出什么 | 雷区 |
|---|---|---|---|
| 0–5 分钟 | 澄清需求 | 问题清单 + 明确读多写多、量级、延迟目标 | 不问就开始画图 |
| 5–15 分钟 | 高层架构 | 一张全貌图:客户端 → LB → 服务 → 存储/缓存,标出读写两条路径 | 一上来钻细节(如分库分表) |
| 15–25 分钟 | 深挖核心组件 | 挑 1–2 个最“有戏”的点展开(存储选型 / 缓存一致性 / 扇出) | 全程平均用力、没有重点 |
| 25–30 分钟 | 扩展与权衡 | 指出下一步瓶颈 + 给出 2 个 tradeoff,问面试官想听哪个方向 | 突然收尾、没有结论 |
四步法记忆卡
- Step 1 梳理用例、约束与假设:把模糊题目翻译成可计算的数字(见 A1 心算)。
- Step 2 高层设计:方框 + 箭头画清“谁调用谁、数据放哪”,写与读两条路径分开画。
- Step 3 设计核心组件:每个方框落到具体技术并说明“为什么是它”,如“这里用 Cache-aside 的 Redis,因为读多写少”。
- Step 4 扩展设计:从单机出发一步步加 负载均衡 → 缓存 → 读写分离 → 消息队列 → 分片,每一步说清解决什么、代价是什么。
话术模板
高频易错点清单
- 忘记问读写比例就定了缓存方案——读多写少才能先上 Cache-aside。
- 只说“加机器”不说后端无状态化——有状态时加机器无效(见 §6 负载均衡)。
- 缓存一致性含糊其辞——主动讲 Cache-aside + TTL + 兜底,别等追问。
- 容量估算不给口径——先声明假设,再给算式,再给量级结论。
- 全程没提监控与故障预案——结尾补一句“上线前补全健康检查、限流与告警”即可救场。
来源
清单为本站按 System Design Primer(CC BY 4.0)「How to approach a system design interview question」整理成可执行动作;时间盒与话术为本站原创补充。
章节地图
全部 20 章图解完成(方法论 → 地基 → 组件 → 例题 → 附录;图文均为本站原创重写,知识底座来自 System Design Primer,CC BY 4.0)。点击卡片直达对应章节。
系统设计面试方法论 · 四步法 · 心算估算
四步法 + 心算:拿到任何题都知道怎么开场、怎么画图、怎么收尾
已图解 地基篇 §1性能 vs 扩展性 · Performance vs Scalability
纵向不够就横向:三层各自怎么扩
已图解 地基篇 §2延迟 vs 吞吐 · Latency vs Throughput
水管隐喻 + 必须背的延迟数字
已图解 地基篇 §3可用性与一致性(CAP) · Availability vs Consistency
CAP 三角 · 一致性模式 · 可用性 9s
已图解 组件篇 §4DNS 域名系统 · Domain Name System
一次域名解析的完整旅程
已图解 组件篇 §5CDN 内容分发 · 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 · 话术模板
开场问题 · 时间盒 · 四步法记忆卡
已图解