# 扩展就是拓宽瓶颈

速成课,讲系统设计与扩展。21 个概念——Scalability、Bottleneck、Horizontal Scaling、Stateless Server、Load Balancer、Cache、CDN、Read Replica、Sharding、队列和 Design for Failure——每个都配有英文名称、一幅图和一张带例子的卡片。最后是扩展阶梯和一份涵盖所有术语的速查表。只讲精髓:先测量再猜。

- **Author:** Fedor Chishchin
- **Level:** intro
- **Reading time:** ~35 分钟阅读
- **Published:** 2026-06-02
- **Source:** https://fedorthinks.com/zh/learn/system-design-express

---

## 1. 扩展意味着什么

扩展不是你一按就灵的某个开关——它是一项持续的活儿:扛住更多负载而不倒下。它从一个想法起步:找出最先崩掉的那一处。

### Scalability — 扛住更多而不崩

> 一家小咖啡馆运转良好,直到一辆旅游大巴停了下来——突然之间,那一个咖啡师、那一台收银机、那几张桌子都跟不上了。扩展就是为这辆大巴做准备。

**Scalability**(可扩展性)是系统应对增长的能力——更多用户、更多请求、更多数据——同时保持快速、保持在线。它不是比谁最聪明;而是在负载成倍增加时,系统不会撑不住。

_Diagram: 负载在增长。可扩展的系统依然很快;不能扩展的系统会在某个时刻突然崩掉。_

#### Scalability (应对增长)

- **是什么:** 在不变慢、不宕机的前提下,承受更多负载的能力。
- **如何运作:** 几乎所有扩展问题都归结为一件事:**某个地方成了跟不上的那一环。**
- **例子:** 一辆旅游大巴刚刚停在门口的那家咖啡馆。
- **关联:** 靠 **Vertical Scaling** 和 **Horizontal Scaling** 增长;会撞上 **Bottleneck**。

### Vertical vs Horizontal — 向上还是向外

> 想多运货,你可以买一辆更大的卡车——但大也有个限度——或者买更多卡车:几乎没有上限,但现在你需要一个调度员。

增加容量恰好有两种办法。**Vertical scaling(scale up,纵向扩展)**是换一台更大的机器——更多 CPU、更多内存。简单,但有一个硬性的天花板,而且仍然是单点故障。**Horizontal scaling(scale out,横向扩展)**是让更多机器一起干活——几乎没有上限,但需要协调(负载均衡、共享状态)。垂直扩展为你争取时间;**真正的规模在水平扩展里。**细节见第 2 章。

_Diagram: 左——一台机器变大,撞上天花板。右——机器变多,前面放着一个 load balancer。_

### Bottleneck — 最窄的那一点

> 一条公路的速度取决于它最窄的那一段——把其他每条车道都拓宽也没用,除非你先解决那个卡口。

**Bottleneck**(瓶颈)是系统里最慢的那一环,它限制着其他一切。系统的速度只和那一环一样快。把资源砸在错的地方,就是白白烧钱;要做的是**找出真正的瓶颈**——数据库、某个慢服务、网络——把它拓宽,然后找下一个。

_Diagram: 一条链子能承受多少,取决于它最弱的那一环。这里就是数据库。_

#### Bottleneck (卡口)

- **是什么:** 那个跟不上、拖慢整个流程的环节。
- **如何运作:** 拓宽一个瓶颈,下一个就会冒出来。**扩展是一连串的瓶颈,不是一次性的修复。**
- **例子:** CDN 和应用服务器每秒能处理几千个请求,数据库只能处理八百个。再加服务器也无济于事。
- **陷阱:** 人们很容易去"修"那些只是感觉慢的地方,而不是真正慢的地方。
- **关联:** 用 **Measure First** 找出来;用 **Scaling Ladder** 的各级台阶来处理。

### Premature Scaling — 为你还没有的负载提前建设

> 你不会为通往林中小屋的路修一条六车道的高速公路。一条好路就能载下所有会来的人。

**Premature scaling**(过早扩展)就是为你并没有的负载去建设。一台像样的服务器加一个数据库,能服务的用户数多得惊人。在还没有负载之前就上 sharding、微服务、复杂的缓存——这是你不需要的成本和复杂度。

_Diagram: 你的真实负载、一台服务器加一个数据库能扛住的量,以及“为了未来”建出来的东西。_

#### Premature Scaling (扩展过早)

- **问题:** 复杂度和金钱花在了并不存在、也许永远不会来的负载上。
- **例子:** 给一个只有一百个用户的应用上分片数据库和微服务。
- **补救:** **先测量;在真正疼的时候,去扩展真正疼的那一处。**
- **关联:** 解药是 **Measure First** 和 **Scaling Ladder**。

### 全景图:七组共 21 个概念

> 一张地铁线路图:先看到整条线路,再一站一站地坐过去。

课程的每一章就是一组概念。它们大致按一个不断成长的系统遇到它们的顺序排列:先是基础,然后是更多服务器、分散流量、缓存、数据库、队列——最后是头脑清醒地运用这一切的规则。

_Diagram: 七组概念——第 1–7 章依次对应。_

### 如何阅读每个概念

> 一本护照:每个人的栏目都一样,所以很容易比较。

从这里开始,每个概念都有相同的几个部分:**英文名称**(记住它——这是你在文档和面试里会遇到的术语)、一幅生活中的画面、一张**图**和一张**卡片**:_问题_、_思路_、_例子_、_代价_、_何时使用_,以及它与其他概念的*关联*。最后有一份**速查表**,把所有术语放在一页上。

> **扩展之前:** "在负载下最先崩的是哪一处——而我真的已经有这么大的负载了吗?"

> 扩展不是让一切都变快。而是找到瓶颈,把它拓宽,然后重复。

## 2. 先垂直,再水平

沿着扩展阶梯迈出的头几步:先把一台大机器用到头,再让应用能跑在任意多台相同的机器上。

### Vertical Scaling — 先换台更大的机器

> 厨房忙不过来时,第一个办法是换个更大的灶台、加更多操作台——而不是开第二家餐厅。

**Vertical scaling(scale up)**是最简单的一招:**一台更大的机器。**不用改代码,很快就能换来实实在在的余量——往往够用很长时间。但它有一个硬性的天花板(你买不到无限大的服务器),而且这一台机器仍然是**单点故障**。

_Diagram: 每一级都是更强的机器。但天花板很近,而且它仍然只是一台机器。_

#### Vertical Scaling (scale up)

- **问题:** 服务器扛不住负载:CPU 或内存不够。
- **思路:** 换一台 CPU 和内存更多的机器。
- **例子:** 数据库从 8 核服务器换成 64 核的。
- **代价:** 有硬性的天花板;一台机器 = 一个单点故障。
- **何时使用:** 最先用。**用它来争取时间,而不是当作终点。**
- **关联:** **Scaling Ladder** 的第一级台阶;下一步是 **Horizontal Scaling**。

### Horizontal Scaling — 许多台机器,干同一件事

> 一个收银台变成十个——现在十位顾客可以同时被服务,而且如果有一台卡住了,其余九台照常运转。

**Horizontal scaling(scale out)**意思是**运行应用的许多个副本**,把工作分摊到它们身上。它几乎没有上限,而且能扛住任何一台机器的损失。难点在协调:得有东西来分发请求(第 3 章),而且各个副本不能各自持有私有状态——这就是下一个关键想法。

_Diagram: 五个相同的副本放在 load balancer 后面。一个挂了——另外四个照常工作。_

#### Horizontal Scaling (scale out)

- **问题:** 一台机器,哪怕是最大的,也会撞上天花板。
- **思路:** 运行 N 个相同的应用副本,把请求分摊给它们。
- **例子:** 黑色星期五,一家商店运行 20 个副本,而不是平时的 4 个。
- **代价:** 需要协调:一个 load balancer,和一个放状态的共享位置。
- **何时使用:** 垂直扩展不够用,或者需要扛住机器宕机的时候。
- **关联:** 只有配合 **Stateless Server** 才行得通;需要 **Load Balancer**。

### Stateless Server — 任何一台机器都能处理任何请求

> 一个呼叫中心,任何一位坐席都能接任何一通电话,因为客户的所有资料都存在共享系统里——而不是在某一位坐席的脑子里。

要让水平扩展奏效,应用服务器必须是**无状态(stateless)**的:在请求之间,它们不在自己的内存里保留任何关于用户的东西。会话数据、上传的文件、进度——全都存在某个共享的地方(数据库、缓存、客户端携带的 token)。这样任何请求都可以落到任何一台服务器上,增加或失去一台机器都不会有什么改变。

_Diagram: 请求可以落到任何一台服务器上:服务器什么都不记,所有状态都放在共享存储里。_

#### Stateless Server (无本地状态)

- **问题:** 服务器把会话存在自己的内存里——用户被"钉"在它身上,一旦崩溃,一切都丢了。
- **思路:** 服务器在请求之间什么都不保留;状态存在数据库、缓存或客户端的 token 里。
- **例子:** 购物车存在 Redis 里,而不是进程内存里;登录就是一个 token。
- **代价:** 每个请求都要从共享存储里取回自己的状态。
- **何时使用:** 只要你打算用不止一台服务器,就始终用。**正是无状态,才让"多加几台服务器"真正行得通。**
- **关联:** 是 **Horizontal Scaling** 和 **Rolling Deploy** 的前提。

> **水平扩展之问:** "一个请求落到我的任意一台服务器上,行为都一样吗——还是每台都偷偷持有状态?"

> 垂直扩展买来时间;水平扩展买来规模。而水平扩展只有在你的服务器自己什么都不记的时候才行得通。

## 3. 把流量分散开

一旦有了许多台服务器,就得有东西站在前面,决定每个请求由谁处理。这就是 load balancer——扩展后系统里的交通警察。

### Load Balancer — 一个前门,许多服务器

> 银行里的引导员,把每位顾客指给任何一个空闲的柜员——这样就不会有哪个柜员被淹没,而其他柜员闲着。

**Load balancer**(负载均衡器)——Nginx、HAProxy、云厂商的均衡器——是唯一的入口,它按简单的规则把进来的请求分给你的服务器池:**round-robin**(轮询),或者发给最闲的那台(**least connections**,最少连接)。客户端找的是它,而不是某一台具体的服务器,所以你可以在它后面悄无声息地增减机器。

_Diagram: 客户端只知道一个地址。Load balancer 把它们的请求分给各台服务器。_

#### Load Balancer (LB)

- **问题:** 有许多台服务器——谁来决定每个请求去哪里?
- **思路:** 一个入口把请求分给一池服务器。
- **例子:** Nginx 放在某个 API 的四个副本前面,按 round-robin 分发请求。
- **代价:** 多了一个组件,而它自己不能变成单点故障。
- **何时使用:** 只要你有不止一台服务器。
- **关联:** **它把一堆服务器变成一个系统。**使用 **Health Checks**。

### Health Check — 别再往死掉的机器上发请求

> 引导员发现某个柜员离开了,就干脆不再把人派到那个窗口——没人会在空桌子前干等。

有了 **health check**(健康检查),load balancer 会不断**检查哪些服务器是健康的**,并绕开不健康的那些。宕机或过载的机器会被自动摘出轮转,流量流向其余机器。扩展后的系统就是这样在一台机器挂掉时,让用户毫无察觉的。

_Diagram: 服务器 B 没有回应检查——load balancer 不再给它发流量。_

#### Health Check (liveness probe)

- **问题:** Load balancer 还在往一台已经死掉的服务器发请求。
- **思路:** 定期探测每台服务器;没有回应——摘出轮转;恢复了——放回去。
- **例子:** 每隔几秒 `GET /health`;没有 `200`——这台服务器被摘掉。
- **代价:** 写得不好的检查会撒谎:数据库已经连不上了,它还说"健康"。
- **关联:** **Load Balancer** 的一部分;是 failover 和 **Design for Failure** 的基础。

### Rolling Deploy 与 Failover — 白捡的好处

> 你可以一次翻修一条收银通道,而其他通道继续营业——商店从不关门。

一旦流量通过均衡器流向可互换的服务器,一些强大的能力几乎是白送的:**零停机(滚动)部署**——每次更新几台服务器;**failover**(故障切换)——一台挂了,流量转移到其余的;还有快速的**回滚**。均衡器加上无状态服务器,是让系统既可扩展又有韧性的骨架。

_Diagram: 服务器一次更新一台,其余的继续为用户服务。_

#### Rolling Deploy (零停机部署 + failover)

- **问题:** 发布新版本就意味着"网站停机"。
- **思路:** 每次更新几台服务器,其余的继续扛流量。
- **例子:** 四台服务器里,两台已经是 v2,一台正在更新,一台还在 v1——用户毫无察觉。
- **代价:** 有一段时间两个版本同时运行——它们得能和平共处。
- **关联:** 需要 **Stateless Server**、**Load Balancer** 和 **Health Checks**。

> **负载均衡之问:** "一台服务器挂了,或者我发布新版本时,用户会有感觉吗?"

> Load balancer 把许多台服务器变成一个地址——也把一台机器的死亡变成一件无关紧要的事。

## 4. 少干点活

最快的活儿,就是你不用干的活儿。在扩展干活的机器之前,先减少活儿本身——用缓存和 CDN。

### Cache — 别把同一个答案算两遍

> 一位厨师早上把热门酱汁一次做好,而不是每来一单就从头做一遍。

**Cache**(缓存)保存昂贵工作的结果——一次数据库查询、一个算好的页面、一次 API 调用——这样下一个请求能立刻拿到,而不用重做。数据库前面放一个快速的内存存储(**Redis**、**Memcached**),就能吸收大部分读请求。最常见的方案是 **cache-aside**:先查缓存;没命中(miss)时,去查数据库,并把答案放进缓存。

_Diagram: Cache-aside:先查缓存 → 没命中就问数据库 → 把答案存下来留给下次。_

#### Cache (cache-aside)

- **问题:** 数据库一遍又一遍地算出相同的答案。
- **思路:** 把结果记在快速内存里,再次直接返回。
- **例子:** 商品页缓存在 Redis 里——数据库不再重新计算它。
- **代价:** 多了一份"真相"的副本,它可能会过期。
- **何时使用:** **你那个不堪重负的数据库,往往真正需要的是缓存,而不是更多服务器。**
- **关联:** 需要谨慎的 **Cache Invalidation**;它在网络边缘的亲戚是 **CDN**。

### CDN — 从就近的地方服务用户

> 一家全球连锁店,在各地的本地仓库里都备着同样的货——顾客从街角就能拿到,而不是从海那边运过来。

**CDN(Content Delivery Network,内容分发网络)**把你的静态内容——图片、脚本、视频,以及越来越多的整页内容——的副本存放在世界各地的数据中心(**edge**,边缘),由离用户最近的那个来服务。它大幅降低延迟,并为你的 **origin**(源站)——也就是你自己的服务器——卸下巨大的负载。

_Diagram: 源站把副本交给不同区域的边缘节点;每个用户由最近的那个来应答。_

#### CDN (Content Delivery Network)

- **问题:** 地球另一端的用户,要等字节横跨一个大洋。
- **思路:** 把静态内容的副本放在离用户近的节点上。
- **例子:** 网站图片从法兰克福的节点发出,而不是美国的服务器。
- **代价:** 同样的缓存问题:内容一变,副本就得刷新。
- **何时使用:** 凡是全球性的、静态的内容——在你的服务器看到请求之前,CDN 就把重活干完了。
- **关联:** 边缘的 **Cache**;同样受 **Cache Invalidation** 困扰。

### Cache Invalidation — 难的部分

> 冰箱上的日历比看手机更快——直到有人在手机上改了计划,于是冰箱上的那份就在说谎了。

缓存是"真相"的第二份副本,而副本可能会过期(**stale data**,陈旧数据)。真正难的问题不是加一个缓存——而是**知道什么时候把它扔掉**;这就是 **cache invalidation**(缓存失效)。缓存那些读得远多于写的东西,设置合理的过期时间(**TTL**),并且始终清楚缓存出错时会发生什么。

_Diagram: 数据库里的价格变了,但缓存不知道,继续返回旧价格。_

#### Cache Invalidation (让过期副本失效)

- **问题:** 用户从缓存里看到过时的数据。
- **思路:** 数据变化时删除缓存条目,并且/或者给它设一个生存期(TTL)。
- **例子:** 价格变化时,从 Redis 里删除 `product:42` 这个键;另外设一个 5 分钟的 TTL 以防万一。
- **陷阱:** 某条被遗忘的写入路径没有清缓存——数据就悄悄地撒了好几个小时的谎。
- **关联:** 每个 **Cache** 和 **CDN** 的另一面。

> **缓存之问:** "我在反复重新计算或重新获取的,有什么是可以直接记住的——而我怎么知道它什么时候过期了?"

> 最便宜的请求,是你根本不用服务的那个。缓存热点内容,让静态内容从边缘发出,并在失效问题上多下功夫。

## 5. 扩展数据

数据库迟早会成为瓶颈——它是唯一真正难以扩展的部分,因为它保存着所有人共享的"真相"。

### Database Bottleneck — 数据库通常就是那堵墙

> 十个收银台全都把手伸进同一个仓库——收银员可以扩展,但唯一的那扇仓库门成了堵点。

你可以运行一百台无状态的应用服务器,但它们通常共用**一个数据库**——这就成了上限。和应用服务器不同,你不能简单地克隆它,因为每个副本都必须在数据上达成一致。所以扩展数据库,是扩展一个系统中最难、最需要谨慎的部分。

_Diagram: 应用服务器很容易成倍增加,但它们全都撞上同一个共享数据库。_

#### Database Bottleneck (那堵墙)

- **是什么:** 每台应用服务器都会撞上的那个共享数据库。
- **为什么:** 数据库的各个副本必须在数据上一致——你不能直接克隆它。
- **怎么办:** 先用缓存尽量往后拖;然后上副本;sharding 放在最后。
- **关联:** 用 **Cache**、**Read Replica**、**Sharding** 来处理。

### Read Replica — 为读请求准备的副本

> 把参考书的复印件分发出去,让很多人能同时阅读,而唯一的原本是唯一有人往里写的那一本。

大多数应用读得远比写得多。**Read replicas**(只读副本)是数据库的副本,负责处理读查询,把读负载分摊到多台机器上,而写入仍然只去一个**主库(primary)**。这是扩展数据库的第一招,也是最容易的一招——但副本会稍微落后一点(**replication lag**,复制延迟),所以刚写入的值可能不会立刻出现在副本上。

_Diagram: 写入只去主库;读请求分散到各个副本上。副本带着一点延迟追上来。_

#### Read Replica (只读副本)

- **问题:** 数据库被读查询淹没了。
- **思路:** 数据库的副本回答读请求;所有写入都去一个主库。
- **例子:** 信息流和搜索从三个副本读取;订单写入主库。
- **代价:** 复制延迟:用户保存了个人资料,刷新页面——看到的却是旧的。
- **何时使用:** 负载主要是读,而缓存已经不够用的时候。
- **关联:** **Cache** 之后的下一级台阶;如果问题出在写入——用 **Sharding**。

### Sharding — 把写入拆开

> 一本写满的账簿变成几本——A–M 在一本里,N–Z 在另一本里——这样两位职员可以同时记账,而不必争抢同一页纸。

当连写入都超出一台机器的能力时,你就要做 **shard**(分片):按某个键——**shard key**(分片键)——把数据拆到几个数据库里,每个负责一部分。它能解锁巨大的规模,但确实很难——跨分片查询会变得痛苦,而分片键几乎是个一旦选定就改不了的决定。

_Diagram: 路由器看着键,把数据送到正确的分片。每个分片保存自己的那一部分。_

#### Sharding (水平分区)

- **问题:** 写入多到一个主库跟不上。
- **思路:** 按键把数据切成几个独立的数据库。
- **例子:** 客户 A–H 在分片 1,I–P 在分片 2,Q–Z 在分片 3。
- **代价:** 跨分片的查询很痛苦;分片键几乎不可能再改。
- **何时使用:** **这是深水区;放到最后才去,在副本和缓存真的不够用的时候。**
- **关联:** **Scaling Ladder** 的最后一级台阶;排在 **Read Replica** 之后。

> **数据之问:** "我的负载主要是读(加副本、多缓存)——还是写入超出了一台机器(分片这条难走的路)?"

> 应用服务器很容易克隆;数据库不行。在分片之前,先长时间地缓存和复制。

## 6. 削峰与解耦

不是所有工作都必须在用户等待时完成。把慢的活儿移出请求,在各部分之间放上缓冲,系统才能保持快速、扛住峰值。

### Background Job — 把慢活儿移出请求路径

> 在餐厅里,他们接了你的单,你就坐下——不会让你一直站在柜台前,等到饭菜做好。

当一个请求触发了某件很慢的事——发邮件、生成报表、处理上传——别让用户等着。**把它交给后台队列**,然后立刻返回;稍后由一个 **worker** 来做慢的那部分。这就是 **background job**(后台任务)。页面保持敏捷,而重活在一旁完成。(这是协议课和架构课里讲过的消息传递思想。)

_Diagram: 应用把任务放进队列,立刻回复。一个 worker 发送邮件——稍后,在一旁完成。_

#### Background Job (异步处理)

- **问题:** 服务器干慢活儿时,用户要等上好几秒。
- **思路:** 把任务放进队列,立即回复,让 worker 去干活。
- **例子:** 注册之后,欢迎邮件在后台发送——页面立刻打开。
- **代价:** 结果不是即时的;你需要一个队列,以及处理 worker 错误的办法。
- **何时使用:** 凡是用户不需要马上看到的事情。
- **关联:** 同一个队列还给你带来 **Load Leveling** 和 **Decoupling**。

### Load Leveling — 队列吸收峰值

> 洪水泛滥的河流和城镇之间的一座水库——洪峰灌满水库而不是淹没街道,然后以稳定的速度排出去。

队列同时也是一个**缓冲**。这个技巧叫做 **queue-based load leveling**(基于队列的负载平滑)。当流量突然飙升——一次发布、一次爆红——工作堆积在队列里,worker 按自己的节奏把它清空,而不是让洪水冲垮系统。没有这个缓冲,一个到来速度快过你处理速度的峰值,会把一切都拖垮。

_Diagram: 左——起伏不定的进入流量;中——队列存住工作;右——worker 以稳定的速度运行。_

#### Load Leveling (queue-based load leveling)

- **问题:** 流量峰值到来的速度,快过系统能处理的速度。
- **思路:** 队列存住工作;worker 以恒定的速度把它清空。
- **例子:** 发布时,涌来平时十倍的订单——它们在队列里等着,而不是把服务打垮。
- **代价:** 峰值期间工作会延迟完成;队列需要监控。
- **关联:** **队列把致命的峰值变成了可以应付的积压。**是 **Background Job** 的亲戚。

### Decoupling — 一个慢的部分不会拖垮其余

> 船上的防水隔舱——一个进水了,隔板让其他隔舱保持干燥,船继续漂着。

有了 **decoupling**(解耦),服务之间通过队列和事件通信,而不是直接的阻塞调用,所以一个变慢或失败的组件**不会冻住所有等着它的人。**即使邮件服务挂了,订单照样能接下来;邮件只是稍后补上。解耦让大系统在压力下能优雅地降级,而不是一下子全部垮掉。

_Diagram: 上——直接调用:邮件服务挂了,所以订单也失败了。下——通过队列:订单被接下,邮件稍后再发。_

#### Decoupling (通过队列和事件通信)

- **问题:** 一个失败的服务,拖垮所有调用它的服务。
- **思路:** 通过队列或事件通信,而不是等待直接的回答。
- **例子:** 订单服务发布"订单已创建";邮件服务恢复后再把它取走。
- **代价:** 更难追踪发生了什么;数据只是最终一致,而不是即时一致。
- **关联:** **Design for Failure** 的基础;建立在和 **Background Job** 相同的队列之上。

> **异步之问:** "用户真的需要等这件事吗——还是它可以放在队列后面,而响应现在就返回?"

> 只让用户等他现在需要的东西。其他一切都放到队列后面——峰值来的时候,这也会救你一命。

## 7. 理智地扩展

扩展做得好,大半靠克制和次序:测量真正的瓶颈,先爬便宜的台阶,并为更多机器带来的故障做好准备。

### Measure First — 扩展之前先测量

> 医生动手术前先做检查——因为猜测而切错了器官,对谁都没好处。

在添加任何东西之前,先用指标和 **profiling**(性能剖析)**找出真正的瓶颈**——是数据库、一条慢查询、网络,还是 CPU?工程师经常去优化错误的东西,只因为它感觉慢。数据会告诉你系统真正疼在哪里;**扩展那里,别的都不要动。**(这正是 **observability** 派上用场的地方。)

_Diagram: 把一个请求拆开来看:几乎所有时间都花在一条数据库查询上。该修的就是它。_

#### Measure First (profiling)

- **问题:** 人们优化"感觉"慢的地方,而瓶颈其实在别处。
- **思路:** 指标和 profiling 显示时间到底花在哪里。
- **例子:** 一个请求耗时 400 毫秒,其中 280 毫秒是一条数据库查询。所以你需要的是索引或缓存,而不是更多服务器。
- **代价:** 你需要 observability:指标、链路追踪、日志。
- **关联:** 找出 **Bottleneck**;防范 **Premature Scaling**。

### Scaling Ladder — 按顺序爬便宜的台阶

> 你会先加保温层、封好窗户,然后才去买第二台锅炉——便宜的办法在先。

有一个顺序,从最便宜、最容易的开始:**一台更大的机器**,然后是**缓存和 CDN**,然后是 **load balancer 后面的无状态服务器**做水平扩展,然后是 **read replicas**,然后是为慢活儿准备的**队列**——最后到最远的一端,才是 **sharding**。每一级台阶都意味着更多工作和更多复杂度。

_Diagram: 六级台阶:越往上,越贵、越复杂。只有负载逼你的时候才往上爬。_

#### Scaling Ladder (先用最便宜的办法)

- **问题:** 人们一上来就抓起最强大、最复杂的工具。
- **思路:** 一级一级来:更大的机器 → 缓存和 CDN → 无状态 + load balancer → 副本 → 队列 → sharding。
- **例子:** 网站慢了——先加内存、加缓存;也许永远轮不到 sharding。
- **何时使用:** 始终。**只爬到你的负载逼你爬到的高度。**
- **关联:** 把第 2–6 章的每个概念串在一起。

### Design for Failure — 机器越多,故障越多

> 一盏灯泡在某个晚上很少会坏;在一座有一万盏灯的体育场里,总有几盏是灭的。规模一大,总有东西是坏的。

随着机器增多,在任何时刻*总有某个东西*在出故障的概率,会趋向于确定。所以扩展和可靠性是同一个项目:**冗余**——没有**单点故障(SPOF)**;**health checks 和 failover**;以及 **graceful degradation**(优雅降级),让一个坏掉的部分只是降低质量,而不是让整个系统死掉。

_Diagram: 每个组件都有备份:第二个 load balancer、多台服务器、一个带 failover 的备用数据库。_

#### Design for Failure (冗余、优雅降级)

- **问题:** 规模一大,总有某台机器是坏的。
- **思路:** 冗余、health checks、failover 和 graceful degradation——一个坏掉的部分不会拖垮整体。
- **例子:** 推荐服务挂了——商店展示的页面少了"你可能还喜欢"那一块,但照常卖货。
- **代价:** 备用机器要花钱;failover 必须定期演练。
- **关联:** 汇集了 **Health Check**、**Rolling Deploy**(failover)和 **Decoupling**。

### 这一切如何串起来

> 旅程结束后的地图:现在你看得出哪些站在同一条线上。

这门课里的概念不是互不相干的小技巧,而是一条链。**测量** → 找到 **bottleneck** → 爬到你需要的那一级 **ladder** 台阶。水平扩展**需要**无状态服务器和 load balancer;load balancer 需要 health checks;缓存需要失效机制;队列给你带来 leveling 和 decoupling。而这一切都服务于同一个目标:**一个部分出故障,不能让整个系统死掉。**

_Diagram: 课程中各概念之间的主要联系。_

#### 扩展任何东西之前

- **我测量过了吗**,真正的瓶颈在哪里——而不是猜的?
- **在这一级之前,有没有更便宜的台阶**——更大的机器、缓存、CDN?
- **我的应用服务器是无状态的吗**,这样我才*能*加更多?
- **数据库的负载是读**(副本、缓存),还是写(难走的路)?
- **慢活儿能不能挪到**队列后面?
- **我真的有这么大的负载吗**——还是在为一个也许不会到来的未来而建设?

#### 过度设计的气味测试

- 给一个一台机器就能轻松装下的数据库做 **sharding**。
- 给只有一百个用户的应用上**微服务和队列**。
- 去扩展**并不是瓶颈的那一部分**,只因为它感觉慢。
- 一个**缓存层**纠缠得没人敢确定它什么时候过期。
- 为你没有的**几百万用户**,和一场还没发生的发布而建设。

#### 你扩展得好的迹象

- 你**扩展的是测量出来的瓶颈**,而下一个现在已经看得见了。
- 服务器是**无状态**的;增加或失去一台都无关紧要。
- 读请求被**缓存并复制**;数据库得以喘息。
- 慢活儿跑在**队列后面**,峰值变成积压,而不是崩溃。
- **没有单点故障**——任何一台机器都能悄无声息地挂掉。
- 你**只在负载要求的范围内**往阶梯上爬。

## 速查表:全部 21 个概念

### 基础

- **Scalability** (应对增长): 在保持快速、保持在线的同时承受更多负载。
- **Bottleneck** (卡口): 最慢的那一环;它限制着整个系统。
- **Premature Scaling** (扩展过早): 为你没有的负载去建设。有成本,没回报。

### 垂直与水平

- **Vertical Scaling** (scale up): 一台更大的机器。简单,但有天花板,也是单点故障。
- **Horizontal Scaling** (scale out): 许多相同的副本。几乎没有上限,需要协调。
- **Stateless Server** (无本地状态): 请求之间什么都不记——任何一台服务器都能处理任何请求。

### 把流量分散开

- **Load Balancer** (LB): 一个前门,把请求分给各台服务器。
- **Health Check** (liveness probe): 死掉的服务器被自动摘出轮转。
- **Rolling Deploy** (零停机部署、failover): 每次更新几台服务器;一台挂了,流量转移到其余的。

### 少干点活

- **Cache** (cache-aside): 记住一个昂贵的答案,再次从内存里返回。
- **CDN** (Content Delivery Network): 在全球各地靠近用户的地方存放静态内容的副本。
- **Cache Invalidation** (让过期副本失效): 知道什么时候该扔掉一份副本:写入时删除,加上 TTL。

### 扩展数据

- **Database Bottleneck** (那堵墙): 一个无法简单克隆的共享数据库。
- **Read Replica** (只读副本): 副本负责读,写入去一个主库。有一点延迟。
- **Sharding** (水平分区): 按键把数据拆到几个数据库里。最后的手段。

### 削峰与解耦

- **Background Job** (异步处理): 慢活儿交给队列;用户不用等。
- **Load Leveling** (队列当缓冲): 队列把峰值变成积压,而不是崩溃。
- **Decoupling** (队列和事件): 通过队列和事件通信——一个坏掉的部分不会拖垮其余。

### 理智地扩展

- **Measure First** (profiling): 指标和 profiling 显示真正的瓶颈。
- **Scaling Ladder** (先用最便宜的办法): 机器 → 缓存/CDN → 无状态 + LB → 副本 → 队列 → sharding。
- **Design for Failure** (冗余): 冗余、failover 和优雅降级:规模一大,总有东西是坏的。

> 扩展你测量出来的那个瓶颈,用能解决它的最便宜的工具,并且假定规模一大总有东西是坏的。其余的一切都是过早的。

速成课结束 · 7 章 · 21 个概念 · 先测量再猜

接下来是深度:再读一遍《Designing Data-Intensive Applications》、《System Design Interview》系列,以及那些真正扛过规模的公司的工程博客。但把上面那个核心想法,举得高过那些套路——一个系统在负载下,只跟它最弱的那一点一样强。找出那一点,把它拓宽,而在你不得不动手之前,别去建造其余的。
