专业书籍精读 · System Design Interview 卷一 · 第 1 章
System Design Interview: An Insider's Guide · Vol 1 · Ch 1 · Alex Xu · 2020
你现在打开的任何一个网站或 App,最早很可能只是某个人租的一台机器。这一章讲的就是:这一台机器怎么一步步长成一整个机房。它是《System Design Interview》第一卷的开场,也是全书唯一一章不设计任何具体产品——它给的是一条路线:用户从 0 涨到几百万的路上,每一次该改造哪里。
把网站想成一家面馆。开张时老板一个人:接单、下面、收钱、记账本全在一个铺子里。生意火了怎么办?直觉是「换个更能干的老板」,真正的解法却是分工:门口站个领位的、后厨多加几口锅、常点的几样先做好放保温柜、账本抄一份给会计、外卖单贴墙上让骑手自取、最后城东城西各开一家分店。这一章的每一步扩展,都对得上其中一件。
一台机器扛不住时,最省事的办法是换更强的机器。这条路能走得比大多数人想的远——Stack Overflow 在 2016 年一天扛住 两亿多次请求,用的不过是九台网页服务器加四台数据库。但它有个躲不掉的问题:那台机器一坏,网站就整个没了,没有替补。所以真正的转折点不是「更强」而是「更多」——一堆普通机器,坏一台不影响别人。
① 门口站个领位的。客人不再直接找某个服务员,而是由领位的分派给当下空着的那个。某个服务员请假了,客人照样有人接。
② 但服务员不能「记住」客人。你在 3 号服务员那点的单只记在他脑子里,他一下班你就得重新点一遍。改成把「谁点了什么」统一写到前台一个共享的本子上,任何一个服务员就都能接手你——这是能自由加人减人的总开关。
③ 账本抄几份。看账的人远比记账的人多。让「记」只写进正本、「看」都去翻抄本,正本立刻轻松下来。
④ 常点的先做好放保温柜。热门那几样不必每次从头炒,直接从柜里取——这就是缓存。图片、视频这类更干脆在各个城市都存一份,谁近取谁的。
⑤ 慢活写成单子排队。修图、发通知这种要花时间的事别让客人干等:写张单子扔进队列,后厨慢慢消化,忙不过来就多叫两个。
⑥ 实在装不下,才把账本切开。一本记不下就按客人姓氏拆成四本。这是最后一步,也是唯一一步几乎回不了头的:拆开之后,很多原本一句话就能查的东西(比如「把所有客人按第一次来的时间排序」)会突然变得很麻烦。
这六步是按性价比从高到低排的:前面几步便宜、可逆、见效快,最后一步又贵又难回头。所以真正该问的从来不是「我们该上什么技术」,而是「我现在是哪一处先撑不住?」
把系统做大不是「买更强的机器」,而是一次次把「记住的东西」从会挡路的地方搬走:搬出服务员的脑子(无状态)、搬到抄本上(读写分离)、搬到离客人更近的地方(缓存与就近分发)、搬进队列里(异步),最后才切开(分片)。
想进到具体机制、量级和示意图? → 切到精读版
你以为这一章在教「怎么把系统做大」,其实它在教一个顺序。负载均衡、缓存、CDN、分片这些零件哪里都查得到,这一章真正给出的,是把它们排成一条性价比递减、不可逆性递增的施工路线——每一步只解决一个特定瓶颈。顺序排错,你会为一个还不存在的问题,付出永久的复杂度。
本章是卷一 Part I「方法与基础」三章的第一章,也是全书唯一不设计具体产品的一章:Ch2 教怎么把 QPS、存储、带宽在白板上算到量级正确,Ch3 教四步面试框架,而 Ch1 先给出这两者要作用其上的公共骨架。后面十二个案例——限流器、短网址、信息流、YouTube、Google Drive——几乎都是在这张骨架的某一层上做加法。
「你的系统能扛多少用户」,工程师最常见的答法是报菜名:我们上了 Redis、Kafka、分库分表。但名词回答不了为什么是现在、为什么是这个。这一章要解决的是另一个问题:下一个会先撑不住的是哪个东西,以及为它花的钱和复杂度值不值。
不解决会怎样?做早了:用户五千人就上微服务加分库分表加多活,团队八成时间在维护基础设施而不是做产品,而分片几乎收不回来。做晚了:等唯一那台数据库 CPU 打到 90% 才想起读写分离,此时任何改造都得带着生产流量做,风险最高。
拿个场景当锚:一个内容社区,日活 100 万,人均每天打开 10 次、每次拉 20 条。读请求约 1000 万次/天,摊平后约 116 QPS,按峰值是均值的 3–5 倍算,峰值 350–600 QPS;写约是读的百分之一。这个量级下一台像样的服务器加一个数据库其实还撑得住——先出问题的不是「总量」,而是某一类请求(比如首页那条要连接三张表的查询)和没有替补。这就解释了为什么第一步往往是负载均衡加从库,而不是分片。
最初的形态:一台服务器同时跑 web 应用、数据库和缓存。用户输入域名 → 问 DNS 拿到 IP → 浏览器直连 → 服务器返回 HTML 或 JSON。先看清这条链路,是因为后面每一步扩展本质上都是往里插一个新的跳——插负载均衡、插缓存、插 CDN、插队列。看不清它,后面就只能死记零件名。
第一次真正的分家,是把数据库从 web 服务器上搬走:两者要的资源根本不同——web 吃 CPU 和并发连接,数据库吃内存和磁盘 IO,挤在一起互相抢,也没法各自独立扩。选型上原书的判据很朴素:默认选关系型数据库(RDBMS),只有超低延迟、数据非结构化、无需复杂查询、数据量极大时才考虑 NoSQL——别因为它听起来更现代就选。
纵向扩展就是换更强的单机。它的好处被严重低估:简单——应用几乎不用改,不用面对分布式的任何麻烦。问题有两条:有硬天花板,以及没有故障转移、没有冗余。第二条才是决定性的:很多团队以为自己是因为「不够快」才转横向,真正逼他们转的其实是「不敢只有一台」。
横向扩展 = 一堆普通机器 + 前面站一个负载均衡器。LB 持有公网 IP,客户端只认得它;后面的 web 服务器只有私网 IP,走内网通信、不直接暴露给公网。收益有二:一台挂了流量分给其余的;流量涨了把新机器加进池子即可。
web 层加机器容易,因为它们不存东西;数据库不行。第一招是复制:一个主库接收所有写,若干从库持有副本、只提供读。
它划算,是因为绝大多数应用读远多于写——前面那个社区的读写比约 100:1。把读摊到 N 个从库上,读能力近乎线性增长,应用侧的改动只有一件事:读走从库、写走主库。附带白拿可靠性与可用性(从库挂了读切别处,主库挂了提升一个从库顶上)。
诚实的代价:复制通常是异步的,从库会滞后。用户刚发的帖,若下一个请求恰好落到还没同步上的从库,他会「看不到自己刚写的东西」。常见对策是写后一小段时间内把该用户的读钉在主库上。
缓存层是一个独立的内存存储(如 Redis、Memcached),插在 web 层和数据库之间。最常见的用法叫 cache-aside(旁路缓存):先查缓存,命中就返回;未命中才查数据库,把结果写回缓存再返回。
给个量级就知道为什么有效:从内存读一条数据是微秒级,走一次网络加数据库磁盘查询通常是毫秒级——差三个数量级。命中率做到 90%,库上读压力直接掉到十分之一:前面那个峰值 600 QPS 的社区,只剩 60 QPS 落到库上。
原书列的五个考量,每个背后都是一类线上事故:何时该用——只有读频繁、改动很少的数据才值得缓存,且缓存不是持久存储、重启即空;过期策略(TTL)——太短等于白设,太长则长期陈旧;一致性——缓存与库的更新不在同一事务里,必然有窗口期不一致;单点故障——只有一台缓存服务器的话,它一挂所有读会在同一瞬间全砸到库上、通常直接把库打死,对策是多台分担且容量留冗余;淘汰策略——满了扔谁,LRU(扔最久没用到的)最常用,另有 LFU 和 FIFO。
CDN 是同一思想换个维度:把静态内容推到全球各地的边缘节点,用户就近取。缓存省的是后端计算,CDN 省的是跨洲的网络往返——一次美国到亚洲的往返常在 150–200 毫秒量级,边缘节点能压到十几毫秒。坑同样具体:按流量计费,冷门内容放上去只是白花钱;TTL 要按更新频率调;内容更新得靠失效 API 或改 URL 版本号(?v=2)才推得下去。
前面说「web 层加机器容易」,有个前提一直没兑现:如果 web 服务器上存了用户的会话数据,它就不能被随便替换。有状态的做法是:用户 A 的 session 存在服务器 1 的内存里,于是 A 的每一个请求都必须路由回服务器 1——这叫粘性会话(sticky session)。后果一环扣一环:LB 调度被绑死、服务器 1 一挂 A 就掉线重登、每次加减机器都踢掉一批用户,自动伸缩根本无从谈起。
解法一句话说得完,意义却极大:把 session 搬到共享的持久存储里,web 服务器自己什么都不记。于是任何一台都能处理任何一个请求,机器从「有身份的资产」变成可随意增删的一次性资源。后面所有弹性能力——自动伸缩、多活、灰度发布——全以此为前提。这一章若只带走一条,带走它。
多数据中心:用 geoDNS(按地理位置把同一域名解析到不同 IP)把用户导到最近的机房,延迟更低,且一个机房整体故障时能把流量全导走。原书点到即止的三个难点每个都硬:流量重定向(受 DNS 缓存存活时长影响,不是按下开关就生效)、数据同步(跨机房怎么保持一致,最难的一条)、测试与部署。
消息队列:生产者把任务发进队列,Worker 异步取走处理,价值在解耦。原书的例子是图片处理:用户上传照片,web 服务器把「裁剪、锐化、模糊」扔进队列就立刻返回成功,Worker 在后面慢慢消化。真正的好处是两侧各自独立扩:队列积压了就加 Worker,空了就减,web 层完全不受影响。
只有当数据库大到单机装不下了,才轮到分片:把一张大表按分片键水平切成多个库,原书的例子就是 user_id % 4。分片键决定了负载是否均匀,是整件事里最关键、也最难回头改的决定。随之而来三个新问题:
1/N。所以这一节的结论不是「怎么分片」,而是「把前面所有便宜的事都做完,再考虑分片」。
系统一旦上了规模,「看不见」就等于「修不了」。原书给的是三层指标:主机级(CPU、内存、磁盘 IO)、聚合级(数据库层整体性能、缓存命中率)、业务级(日活、留存、收入),再加日志集中化与自动化。它是把前面所有改造从「能跑」变成「能持续跑」的那一步。
表 1 · 扩展阶梯:每一步解决哪个瓶颈、代价多大、能不能回头(本章的主线)
| 步骤 | 解决的瓶颈 | 典型收益量级 | 代价 | 可逆性 |
|---|---|---|---|---|
| ① 拆开 Web 与 DB | 两类资源互抢 | 两层各自独立伸缩 | 多一跳网络(亚毫秒级) | 易回退 |
| ② 负载均衡 + 多 Web | 单点故障;Web 算力不足 | Web 层近乎线性可扩 | 多一个组件要运维 | 易回退 |
| ③ Web 层无状态化 | 粘性会话锁死调度与伸缩 | 解锁自动伸缩 / 多活 / 灰度发布 | 会话读写多一跳 | 易回退 |
| ④ 主从复制 | 库读压力;数据只有一份 | 读写比 100:1 时读能力近乎线性增长 | 复制滞后——可能读到旧数据 | 易回退 |
| ⑤ 缓存 + CDN | 重复的库查询;跨洲往返 | 命中率 90% → 库读压力降到 1/10;跨洲 RTT 从 150ms+ 降到十几毫秒 | 一致性窗口;缓存挂掉会瞬间打死库 | 易回退 |
| ⑥ 消息队列 + Worker | 慢任务占着主链路 | 响应立刻返回;两侧各自独立扩 | 最终一致;要处理重试与重复消费 | 中等 |
| ⑦ 数据库分片 | 单机装不下了 | 存储与写入能力可持续扩 | 热点、重分片、跨片 join 全部变难 | 近乎不可逆 |
表 2 · 缓存 / CDN / 从库:三者都是「多存一份」,区别在存什么、省什么
| 缓存 | CDN | 从库 | |
|---|---|---|---|
| 存的是 | 查询结果、热点对象 | 静态文件(图片 / JS / 视频) | 数据库的完整副本 |
| 省的是 | 后端计算与库查询 | 跨地域网络往返 | 主库的读负载 |
| 新鲜度 | 受 TTL 与失效策略影响 | 最差——TTL 常以小时 / 天计 | 最好——滞后通常毫秒到秒级 |
| 它挂了 | 最危险:读压力瞬间全砸向数据库 | 可回源,慢但不致命 | 读切回主库或别的从库 |
这一章成了系统设计面试的通行开场,是因为它给了一条可复述的路线。面试官问「用户涨了十倍怎么办」,糟糕的答案是报菜名,好的答案是:「先看哪一层先撑不住——Web 层 CPU 满了就加机器;数据库读满了先上缓存和从库;只有单机装不下了才谈分片。」能对每一步说清「我卡在哪、所以我加这个」,面试和架构评审就赢了一大半。这条路线正反两面都有公开记录:
209,420,973 次 HTTP 请求、其中 66,294,789 次页面加载,跑在 9 台主力 Web 服务器 + 4 台 SQL Server 上;他还写道「我们已经降到只需要 1 台 Web 服务器」,且「无意中成功验证过几次」——反证:纵向扩展加认真优化能走得比大多数人以为的远,很多产品这辈子用不上分片。N. Craver「Stack Overflow: The Architecture — 2016 Edition」↗① 这一章不是零件清单,是施工顺序:每一步只解决一个瓶颈,且性价比递减、不可逆性递增。
② 第一次分家是把数据库和 Web 拆开;数据库默认选关系型,只有超低延迟、非结构化、无需复杂查询、数据量极大同时成立才考虑 NoSQL。
③ 从纵向转横向的真正理由不是「不够快」,而是「不敢只有一台」——单点故障才是转折点。
④ 无状态是总开关:会话搬进共享存储,机器才从「有身份的资产」变成一次性资源,自动伸缩才成立。
⑤ 读多写少下最便宜的杠杆是主从复制,代价是复制滞后——用户可能看不到自己刚写的东西。
⑥ 缓存省后端计算、CDN 省跨洲往返;缓存层是最危险的单点,它一挂读压力会瞬间全砸向数据库。
⑦ 消息队列的价值是解耦与各自独立扩:慢活扔进队列立刻返回,积压了就加 Worker。
⑧ 分片留到最后:随之而来重分片、名人热点、跨片 join 三个新问题,且几乎回不了头。
⑨ 最后配上日志、三层指标与自动化——没有它们,前面所有改造只是「能跑」而非「能持续跑」。