Module 8: 扩展性与高可用
📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。
来源:Acing the System Design Interview + DDIA相关章节
Module 7 讨论了分布式系统中”事情会怎么出错”——网络不可靠、时钟不准、节点会挂。这个模块的主题是”出了错怎么办”以及”流量暴增怎么办”。扩展性解决的是容量问题,高可用解决的是故障问题。两者相辅相成:一个无法扩展的系统在流量洪峰面前就是故障,一个不能容错的系统扩展再多节点也是脆弱的。
第一部分:扩展策略
Section titled “第一部分:扩展策略”8.1 水平扩展 vs 垂直扩展
Section titled “8.1 水平扩展 vs 垂直扩展”定义:系统面对流量增长时,有两种扩展方向:(1) 垂直扩展(Scale Up)——给单台机器加更多资源(更多CPU核心、更大内存、更快磁盘)。把一台4核8GB的机器换成64核512GB的机器。(2) 水平扩展(Scale Out)——增加更多机器,让多台机器分担负载。从1台变成10台、100台。垂直扩展有天花板(单机硬件有物理极限,且高端硬件价格呈指数增长),水平扩展理论上无上限,但需要处理分布式带来的复杂性。
为什么重要:系统设计面试中一个核心命题就是”当流量增长10倍、100倍时,你的方案还能工作吗?“垂直扩展是最简单的做法——不用改架构,只换更大的机器——但迟早会遇到天花板。一台最强的物理服务器也就几TB内存、几百个CPU核心,而且价格极其昂贵(性能翻倍价格可能翻4倍)。水平扩展是大规模系统的必经之路,但它要求系统在架构层面支持多节点协作——这涉及到负载均衡、数据分片(Module 3)、缓存(Module 4)、一致性(Module 6)等一系列问题。
案例:URL Shortener 是一个典型的适合水平扩展的系统。每次短链接跳转是一个独立的读请求,请求之间没有依赖关系,天然适合分散到多台服务器处理。如果当前1台服务器能处理1万QPS,需要10万QPS就加10台服务器。而 Gaming Leaderboard 则更复杂——排行榜需要全局排序,简单地加机器并不能直接解决问题,因为每台机器只有部分数据,无法直接算出全局排名。这时需要先垂直扩展(用一台大内存Redis装下整个排行榜),在数据量超过单机极限后才考虑水平分片。
先想一想 🤔 一个数据库目前跑在单台服务器上,CPU使用率已经到了90%。你会建议先垂直扩展还是水平扩展?什么时候该切换策略?
点击查看解析
先垂直扩展。原因:(1) 成本最低——换一台更大的机器比改架构快得多,可能几小时就完成迁移;(2) 不引入分布式复杂性——不需要处理数据分片、跨节点查询、分布式事务等问题。
什么时候切换到水平扩展:(1) 成本拐点——当单机价格已经很贵(如每月几万元),而同样的钱可以买10台中等配置的机器来分担负载;(2) 物理极限——单机最大内存也装不下所有数据了(参考Module 3的分片策略);(3) 可用性需求——单机意味着单点故障,业务要求99.99%可用性时必须有多节点冗余。
实际中很多公司的演进路径是:先垂直扩展撑过早期 → 流量增长到单机极限 → 引入读写分离(Module 3)→ 进一步增长 → 引入分片 → 最终全面水平扩展。不要过早优化,但也不要在架构上堵死水平扩展的路。
8.2 无状态设计原则
Section titled “8.2 无状态设计原则”定义:无状态(Stateless)设计是指服务器不保存任何与特定客户端绑定的状态信息。每个请求都携带处理该请求所需的全部信息(如JWT token、请求参数),服务器不依赖”上一次请求的记忆”来处理当前请求。与之对立的是有状态(Stateful)设计——服务器在内存中保存了用户的会话数据(如购物车、登录状态),客户端必须被路由到同一台服务器才能正常工作。
为什么重要:无状态是水平扩展的前提条件。如果服务器是无状态的,任何请求可以被路由到任何一台服务器——新增服务器只需要加到负载均衡器后面就能立即开始工作,移除服务器也不会丢失任何数据。如果服务器是有状态的,负载均衡器必须用”粘性会话”(Sticky Session)把同一用户的请求总是发到同一台服务器,这严重限制了扩展灵活性——那台服务器挂了,用户的会话就丢了。
案例:Hotel Reservation 的搜索服务应该是无状态的。用户搜索”北京、3月15日、2人”时,搜索请求携带了所有必要参数,任何一台搜索服务器都能处理。即使用户上一秒在服务器A搜了”上海”,这一秒被路由到服务器B搜”北京”,也完全没问题——服务器不需要知道用户之前搜过什么。
但 Chat System 的 WebSocket 连接天然是有状态的——服务器需要维护每个用户的连接。这时不能简单地做无状态处理,而是把”连接状态”和”业务数据”分开:连接状态由WebSocket服务器维护(有状态),但消息存储、用户信息等由独立的无状态服务处理。同时在一个单独的”连接注册表”(如Redis)中记录”用户A连接在服务器3上”,这样任何服务都能查到该信息。
先想一想 🤔 你的Web应用把用户登录状态存在服务器的内存Session中。现在要从1台服务器扩展到5台。有哪些方案?各自的利弊?
点击查看解析
方案1:粘性会话(Sticky Session) 负载均衡器通过cookie或IP哈希保证同一用户始终路由到同一台服务器。利:改动最小。弊:某台服务器挂了,该服务器上所有用户的Session丢失,需要重新登录;负载不均衡(某些服务器可能承载更多活跃用户)。
方案2:Session共享存储 把Session存到Redis或数据库中,所有服务器从Redis读取Session。利:任何服务器都能处理任何用户的请求,服务器可以自由增减。弊:引入Redis依赖,Redis成为潜在瓶颈和单点故障。
方案3:无状态化——JWT Token 把登录状态编码到JWT token中,客户端每次请求携带token。服务器不存储任何Session,只需验证token签名。利:真正的无状态,最易扩展。弊:token无法主动失效(除非额外维护黑名单);token携带的数据量有限。
现代系统普遍选择方案3(JWT),需要主动失效时配合Redis黑名单——本质上是把大量Session数据缩减为少量黑名单数据。
第二部分:流量管理
Section titled “第二部分:流量管理”8.3 负载均衡
Section titled “8.3 负载均衡”定义:负载均衡(Load Balancing)将客户端请求分发到多台后端服务器上,实现流量的均匀分配。按工作层级分为两类:(1) L4负载均衡(传输层)——基于TCP/UDP的IP地址和端口做转发决策,不解析应用层内容,速度极快,适合高吞吐场景。(2) L7负载均衡(应用层)——解析HTTP/HTTPS内容(URL路径、Header、Cookie等),可以做更智能的路由决策,如按URL路径转发到不同的后端服务。常见负载均衡算法:轮询(Round Robin)、加权轮询(Weighted Round Robin)、最少连接(Least Connections)、IP哈希(IP Hash)。
为什么重要:负载均衡是水平扩展的基础设施。Module 4中我们讨论过负载均衡的基本概念,这里深入到选型和算法层面。不同的业务场景需要不同的负载均衡策略——选错策略可能导致负载严重不均,甚至让部分服务器过载宕机而另一些空闲。
案例:
- URL Shortener:纯粹的HTTP短连接请求,每个请求几毫秒就处理完。简单的轮询就足够了——每台服务器处理能力相当,请求时间也相当,轮询天然均衡。
- Chat System:WebSocket长连接,连接建立后可能持续数小时。最少连接算法更合适——新用户连接到当前活跃连接数最少的服务器。如果用轮询,可能出现某台服务器堆积了大量不活跃但未断开的连接,实际活跃连接却集中在另外几台上。
- Search Engine:如果使用分布式缓存(每台服务器缓存不同的热门查询结果),可以用IP哈希或一致性哈希——相同用户的搜索请求总是路由到同一台服务器,利于命中本地缓存。
- YouTube:视频转码服务中,不同视频的处理时间差异巨大(10秒短视频 vs 2小时电影)。加权轮询让高配服务器处理更多任务,最少连接保证不会把新任务分给正在处理长视频的服务器。
先想一想 🤔 L4和L7负载均衡,在什么场景下你会选L4?什么场景选L7?能否两者组合使用?
点击查看解析
选L4的场景:(1) 极高吞吐量——L4不解析HTTP内容,转发速度比L7快一个数量级,适合每秒百万级请求的入口;(2) 非HTTP协议——如数据库连接、自定义TCP协议,L7无法识别;(3) 简单均衡就够了——不需要按URL或Header做路由。
选L7的场景:(1) 需要按URL路由——
/api/users到用户服务,/api/orders到订单服务;(2) 需要基于内容做决策——如根据Cookie中的用户ID做粘性路由;(3) SSL终止——在L7层解密HTTPS,后端用HTTP明文通信;(4) 需要修改请求/响应——如添加Header、压缩响应。组合使用:生产环境常用两层架构。第一层L4(如AWS NLB)做高速流量分发到多个L7实例;第二层L7(如Nginx/Envoy)做智能路由和SSL终止。这样既获得了L4的高吞吐,又获得了L7的灵活路由能力。
8.4 反向代理与API网关
Section titled “8.4 反向代理与API网关”定义:反向代理(Reverse Proxy) 位于后端服务之前,代替后端接收客户端请求,提供SSL终止、压缩、缓存、安全防护等功能(Module 4已介绍)。API网关(API Gateway) 是反向代理的升级版,专门为微服务架构设计,在反向代理的基础上增加了:认证鉴权(统一验证JWT/OAuth token)、请求限流(Rate Limiting)、请求/响应转换(如将多个微服务的响应聚合为一个)、API版本管理、监控与日志收集。常见的API网关:Kong、Envoy + Istio、AWS API Gateway、Nginx Plus。
为什么重要:在微服务架构中,如果每个服务都自己实现认证、限流、日志、协议转换,会导致大量重复代码和不一致的行为。API网关将这些”横切关注点”统一到入口层处理。客户端只需要和API网关交互,不需要知道后面有多少个微服务、每个服务的地址是什么。API网关同时也是系统安全的第一道防线——所有请求必须经过网关的认证和限流检查。
案例:Google Drive 的架构中,客户端一个”上传文件”操作可能涉及多个微服务:认证服务验证用户身份、权限服务检查文件夹写权限、元数据服务创建文件记录、存储服务写入文件内容、通知服务告知协作者。没有API网关,客户端需要逐个调用这些服务。有API网关后,客户端只发一个 POST /api/files/upload 请求到网关,网关内部编排这些微服务调用。
News Feed 的API网关还承担”BFF(Backend for Frontend)“角色——移动端和Web端看到的信息流格式不同。API网关根据请求来源(通过User-Agent或自定义Header判断)调用相同的底层服务,但返回不同格式的响应:移动端返回精简版(节省流量),Web端返回完整版。
先想一想 🤔 API网关本身是不是也会成为单点故障和性能瓶颈?如何解决?
点击查看解析
是的,API网关是所有请求的入口,如果挂了整个系统就不可用了,如果慢了整个系统都慢。解决方案:
多实例部署 + 负载均衡:网关前面再加一层L4负载均衡(如DNS轮询 + NLB),多个网关实例水平扩展。
无状态设计:网关自身不保存任何会话数据,所有状态存在外部存储(如Redis中的限流计数器)。这样网关实例可以自由增减。
降级策略:网关可以配置”如果认证服务不可达,临时允许请求通过”(适合内部系统,不适合金融系统)。
避免在网关做重逻辑:网关只做路由、认证、限流等轻量操作。如果在网关中做复杂的请求聚合或数据转换,会让网关成为性能瓶颈。复杂的编排逻辑应该下沉到专门的编排服务中。
多级网关:超大规模系统可能使用多级网关——全局入口网关(DNS + CDN + L4 LB)→ 区域网关 → 服务级网关,分散压力。
8.5 降级与限流
Section titled “8.5 降级与限流”定义:(1) 限流(Rate Limiting)——对请求速率设置上限,超过阈值的请求直接拒绝或排队等待。常见算法:令牌桶(Token Bucket)——以固定速率往桶中放令牌,每个请求消耗一个令牌,桶空则拒绝,允许一定程度的突发流量;漏桶(Leaky Bucket)——请求进入固定容量的队列,以固定速率处理,超出队列容量的请求被丢弃,输出速率严格固定;滑动窗口(Sliding Window)——统计最近N秒内的请求数,超过阈值则拒绝。(2) 降级(Degradation)——系统过载时主动关闭非核心功能,保证核心功能可用。比如电商大促时关闭推荐系统、关闭商品评论加载,只保证”搜索→下单→支付”的核心链路。
为什么重要:任何系统都有容量上限。没有限流,一次流量洪峰(恶意攻击或突发热点)就能把系统打垮——不只是自己挂,还会拖垮依赖的数据库和下游服务(级联故障)。没有降级策略,系统在过载时所有功能一起变慢,用户体验全面崩溃。限流和降级的核心思想是”壮士断腕”——牺牲部分请求或部分功能,保全整体。
案例:Hotel Reservation 在”双十一”或节假日促销时,搜索流量可能是平时的50倍。限流策略:
层级限流: - 全局限流:整个系统最多处理 10万 QPS(超出直接返回 "系统繁忙" 页面) - 用户级限流:每个用户每秒最多 10 次搜索请求(防止脚本刷单) - IP级限流:每个IP每分钟最多 200 次请求(防止爬虫和DDoS)降级策略:
流量 < 5万QPS:全功能运行流量 5-8万QPS:关闭个性化推荐,搜索结果返回缓存数据(可能延迟几分钟)流量 8-10万QPS:关闭评论加载、关闭历史订单查询流量 > 10万QPS:只保留 搜索→选房→下单→支付 核心链路Web Crawler 需要对外限流——避免爬取频率太高把目标网站打垮。这是一种”对外的限流礼仪”:每个域名每秒最多请求N次(通常遵守robots.txt中的Crawl-delay指令)。
先想一想 🤔 令牌桶和漏桶的区别在哪里?Gaming Leaderboard的积分更新API应该用哪种?
点击查看解析
核心区别:令牌桶允许突发流量(桶里攒了令牌可以一次性用掉),漏桶不允许(输出速率严格固定)。
令牌桶:如果桶中有100个令牌,瞬间来了100个请求可以全部通过。适合允许短时间突发的场景。 漏桶:即使瞬间来了100个请求,也只能以固定速率(如每秒10个)依次处理,其余排队或丢弃。适合需要严格平滑流量的场景。
Gaming Leaderboard的积分更新适合令牌桶。原因:游戏结束时会有一批玩家同时提交积分(如一局比赛结束),这是合理的突发流量,不应该拒绝。令牌桶允许这种突发——只要不超过桶容量就全部处理。但如果有人恶意刷积分(持续高频请求),令牌会很快耗尽,后续请求被限流。
如果用漏桶,游戏结束时的正常突发也会被排队延迟,导致积分更新不及时,排行榜刷新变慢,影响玩家体验。
第三部分:服务发现与容错
Section titled “第三部分:服务发现与容错”8.6 服务发现
Section titled “8.6 服务发现”定义:服务发现(Service Discovery)是指在微服务架构中,服务实例启动时自动注册自己的网络地址,其他服务通过查询发现可用的实例地址。Module 7(7.9)已经介绍了基本概念,这里聚焦两种模式的架构权衡:(1) 客户端发现(Client-side Discovery)——客户端从注册中心(etcd、Consul、Nacos)获取服务实例列表,自己做负载均衡决策。(2) 服务端发现(Server-side Discovery)——客户端将请求发给负载均衡器/网关,由它从注册中心获取实例列表并转发。Kubernetes的Service + kube-proxy本质上是服务端发现。
为什么重要:服务发现的选择直接影响系统的运维复杂度和弹性扩缩容的速度。客户端发现给了客户端更多控制权(可以实现智能路由、就近访问),但增加了客户端复杂性(每个服务都要内嵌发现逻辑);服务端发现对客户端透明(客户端只需知道一个固定地址),但负载均衡器成为了额外的跳数和潜在瓶颈。
案例:Search Engine 的查询链路中,Query Service需要调用Index Service,Index Service需要调用Ranking Service。
客户端发现的优势在于:Query Service可以感知Index Service每个实例的延迟,自动把请求发给响应最快的实例。如果某个Index Service实例变慢了(可能正在做垃圾回收),Query Service可以临时减少发给它的请求——这种”客户端自适应”在服务端发现模式下很难做到。
服务端发现的优势在于:如果Query Service是用多种语言写的(Go写的主服务 + Python写的实验版),每种语言都要实现一套服务发现客户端。而服务端发现只需要在Envoy/Nginx层统一处理,业务代码完全不感知。Kubernetes环境下几乎都用服务端发现——用Service名调用就行,K8s自动处理一切。
先想一想 🤔 如果一个服务实例已经宕机但还没来得及从注册中心注销(比如进程被直接kill,没有触发优雅退出),会发生什么?如何缓解?
点击查看解析
请求会被路由到已经死掉的实例,导致调用失败。这就是Module 7中讨论的”故障检测延迟”问题。
缓解方案:
健康检查(Health Check):注册中心定期向每个实例发送心跳探测。如果连续N次没有响应,自动将该实例标记为不健康并从列表中移除。但检测有延迟(通常10-30秒),这段时间内请求仍会发往死掉的实例。
客户端重试 + 快速失败:客户端调用失败时立即重试到另一个实例(配合超时设置,如500ms无响应就切换)。这比等注册中心摘除实例快得多。
客户端本地黑名单:客户端记住最近调用失败的实例,短时间内(如30秒)不再往该实例发请求。
Kubernetes的做法:Pod有Readiness Probe,探测失败后K8s立即将Pod从Service Endpoint中移除(通常几秒内)。比传统注册中心的心跳机制更快。
8.7 熔断器模式
Section titled “8.7 熔断器模式”定义:熔断器(Circuit Breaker)是一种保护调用方的容错模式,灵感来自电路中的断路器。它有三个状态:(1) 闭合(Closed)——正常放行所有请求,同时监控失败率。(2) 打开(Open)——当失败率超过阈值(如最近100次请求失败了50次),熔断器”跳闸”,直接拒绝后续请求,不再调用下游服务。返回预设的降级响应或错误码。(3) 半开(Half-Open)——经过一段冷却时间后,放行少量探测请求。如果这些请求成功,回到闭合状态;如果仍然失败,回到打开状态。
为什么重要:没有熔断器,当下游服务故障时,上游服务会持续向它发送请求。这些请求都会超时(等待几秒后失败),上游服务的线程/连接被这些超时请求占满,导致上游服务自己也变慢甚至崩溃——这就是级联故障(Module 7中讨论的场景)。熔断器通过”快速失败”切断了故障传播链:下游挂了,上游在毫秒内返回降级结果,不浪费资源等待超时。
案例:News Feed 的信息流生成流程涉及多个服务调用:
生成用户信息流: 1. 调用 关注关系服务 → 获取用户关注的人列表 2. 调用 帖子服务 → 获取这些人的最新帖子 3. 调用 广告服务 → 获取要插入的广告 4. 调用 推荐服务 → 获取推荐内容 5. 调用 用户画像服务 → 获取个性化排序权重如果”推荐服务”挂了但没有熔断器:
- 每次调用推荐服务都要等3秒超时
- 用户刷信息流从200ms变成3200ms
- 大量请求堆积,News Feed服务的线程池耗尽
- 整个信息流功能完全不可用
有熔断器时:
- 推荐服务失败率超过50% → 熔断器打开
- 后续请求直接跳过推荐服务(返回空列表)
- 用户看到的信息流没有推荐内容,但关注的人的帖子和广告正常显示
- 整体延迟仍然是200ms,用户几乎无感知
先想一想 🤔 熔断器的”半开”状态为什么很关键?如果没有半开状态,只有开和关,会怎样?
点击查看解析
如果没有半开状态,熔断器打开后就永远打开了——下游服务恢复后,上游永远不知道,需要人工介入重置。
半开状态提供了自动恢复的能力:
- 熔断器打开后等待一段冷却时间(如30秒)
- 进入半开状态,放行1-5个探测请求
- 如果探测请求成功 → 下游已恢复 → 关闭熔断器,恢复正常流量
- 如果探测请求失败 → 下游仍未恢复 → 重新打开熔断器,再等30秒
这形成了一个自动的”探测-恢复”循环。半开状态还有一个设计细节:放行的探测请求数量要少(比如每次只放1-2个),避免下游刚恢复就被大量请求再次压垮。
在实际工程中(如Netflix的Hystrix、阿里的Sentinel),熔断器的参数可以精细配置:失败率阈值(如50%)、统计时间窗口(如最近10秒)、冷却时间(如30秒)、半开时探测请求数(如3个)。
8.8 故障转移
Section titled “8.8 故障转移”定义:故障转移(Failover)是指当系统中的某个组件发生故障时,自动将其职责切换到备用组件的机制。两种主要模式:(1) 主备模式(Active-Passive)——主节点处理所有请求,备节点处于待命状态。主节点故障后,备节点接管。切换期间有短暂的服务中断(通常几秒到几十秒)。(2) 双活模式(Active-Active)——多个节点同时处理请求(各处理一部分流量或处理相同流量的不同区域)。任一节点故障,其流量自动分配到其他节点。没有切换延迟,但系统设计更复杂(需要处理数据同步和冲突)。
为什么重要:任何单节点都可能故障(硬件损坏、网络中断、软件bug),故障转移是实现高可用(如99.99%——每年停机不超过52分钟)的基础机制。Module 7讨论了故障检测(怎么发现节点挂了),故障转移解决的是”发现了之后怎么办”。注意Module 7中提到的脑裂风险:故障转移时如果判断不准确,可能导致两个节点都认为自己是主节点。
案例:
主备模式 — Hotel Reservation 的数据库层:
正常状态: 主库(写+读) ← 所有写请求 备库(实时复制) ← 只同步数据,不处理请求
主库故障: 1. 故障检测:备库连续3次心跳(每2秒一次)没收到主库响应 2. 确认故障:备库通过第三方(如etcd)确认主库确实不可达 3. 切换:备库提升为新主库,开始接受写请求 4. DNS/负载均衡器更新:将写流量指向新主库 5. 恢复:原主库恢复后作为新的备库加入切换期间(通常5-30秒),写入不可用。对Hotel Reservation来说,这意味着”暂时无法下新订单”——可以接受,但不理想。
双活模式 — Google Maps 的多数据中心部署:
美洲数据中心 ← 处理美洲用户请求(地图瓦片、路径规划)亚洲数据中心 ← 处理亚洲用户请求欧洲数据中心 ← 处理欧洲用户请求
每个数据中心都有完整的数据副本。亚洲数据中心故障 → 亚洲用户的请求自动路由到最近的可用数据中心(如美洲)双活模式下没有切换延迟——DNS或全局负载均衡器将请求路由到健康的数据中心。但地图数据需要在所有数据中心之间同步,地图瓦片的更新需要多数据中心一致性策略。
先想一想 🤔 主备切换时,如果主库最后几秒的写入还没同步到备库就挂了(Module 3中讨论的复制延迟问题),会怎样?如何处理?
点击查看解析
那几秒内的写入数据会丢失。在Hotel Reservation中,这可能意味着几个已确认的订单”消失了”——用户收到了订单确认,但数据库中没有记录。
处理方案:
同步复制:主库等备库确认收到后才返回成功。数据不会丢,但写入延迟增加,且备库不可用时主库也写不了(Module 3的trade-off)。
半同步复制:至少一个备库确认收到即可。在”不丢数据”和”可用性”之间取了平衡。
数据修复:原主库恢复后,对比新旧主库的数据,找出”丢失”的写入,人工或自动补回。
业务层兜底:Hotel Reservation可以在支付成功后同时写入一条消息到消息队列(Module 5)。即使数据库主备切换丢了订单数据,消息队列中的记录可以用来恢复。
这是CAP定理的经典体现:主备切换时你要么接受短暂的不可用(等同步完成),要么接受可能丢数据(异步复制切换)。
第四部分:弹性与自愈
Section titled “第四部分:弹性与自愈”8.9 自动伸缩
Section titled “8.9 自动伸缩”定义:自动伸缩(Auto-scaling)是指系统根据实时负载自动增减计算资源的能力。两种主要方式:(1) 基于指标的伸缩(Metrics-based)——监控CPU使用率、内存使用率、请求队列长度、响应延迟等指标,超过阈值时扩容,低于阈值时缩容。如”当CPU使用率>70%持续5分钟,增加2个实例”。(2) 预测性伸缩(Predictive)——基于历史流量模式预测未来负载,提前扩容。如”每天晚上8点流量高峰,7:50提前扩容”。两者通常结合使用:预测性伸缩处理可预期的流量模式,指标伸缩处理突发流量。
为什么重要:固定的资源配置要么浪费(按峰值配置,低谷时大量资源空闲),要么不够(按均值配置,高峰时系统过载)。自动伸缩让资源跟着流量走——高峰时自动扩容,低谷时自动缩容。在云计算的按需付费模式下,这直接节省成本。但自动伸缩不是万能的:扩容需要时间(启动新实例通常需要30秒到几分钟),如果流量在几秒内暴增10倍,自动伸缩来不及反应——这就需要配合限流(8.5)来兜底。
案例:YouTube 的视频转码服务是自动伸缩的典型场景。视频上传量有明显的时间规律(白天多、凌晨少),也有突发事件(某个热点事件导致大量用户同时上传)。
预测性伸缩: - 历史数据显示周日下午 2-6 点上传量是平时的 3 倍 - 系统在周日 1:50 提前将转码集群从 100 台扩到 300 台
指标伸缩: - 监控转码任务队列长度 - 队列中待处理任务 > 1000 且持续 2 分钟 → 扩容 50 台 - 队列中待处理任务 < 100 且持续 10 分钟 → 缩容到基线数量
突发兜底: - 某明星突然发布重大视频,引发百万粉丝同时上传回应视频 - 预测性伸缩没有预料到 → 任务队列迅速增长 - 指标伸缩触发 → 开始扩容(但需要 2 分钟启动新实例) - 这 2 分钟内依靠限流:每用户最多同时提交 3 个转码任务Proximity Service 的自动伸缩更简单:附近商户搜索是无状态的计算(接收经纬度,查询地理索引),直接按CPU使用率或QPS指标伸缩即可。当大型活动(如演唱会)导致某个区域查询激增时,自动扩容处理。
先想一想 🤔 自动缩容(减少实例)需要注意什么?能直接杀掉实例吗?
点击查看解析
不能直接杀掉实例,需要优雅关闭(Graceful Shutdown):
停止接收新请求:通知负载均衡器将该实例从可用列表中移除(或将健康检查标记为不健康)。
处理完在途请求:等待当前正在处理的请求全部完成。设置一个最大等待时间(如30秒),超时则强制关闭。
释放资源:关闭数据库连接、清理临时文件、从服务发现中注销。
缩容速度限制:不要一次缩掉太多实例。如果当前有10台实例,指标显示只需要3台,也不要一次性关掉7台——可能指标有延迟,实际负载比显示的高。应该分批缩容(先缩到7台,观察5分钟,再缩到5台…)。
保留缓冲:缩容后保留一定余量(如实际需要3台但保留5台),应对指标延迟和突发流量。
YouTube的转码服务还有额外考量:某个实例正在转码一个2小时的视频,此时不能关掉它——需要等转码完成或将任务转移到其他实例。
练习一:11个系统的扩展策略总览
Section titled “练习一:11个系统的扩展策略总览”系统设计总览 📋 为11个系统案例标注:主要的扩展瓶颈是什么?优先水平扩展还是垂直扩展?需要哪些高可用机制?
点击查看参考答案
系统 主要瓶颈 扩展策略 关键高可用机制 URL Shortener 读QPS(重定向请求量巨大) 水平扩展(无状态服务+分片数据库) 多层缓存(Module 4)+ 数据库主备 Web Crawler 网络IO + 爬取速度 水平扩展(增加爬虫节点) 域名级熔断 + 任务队列持久化 News Feed 写放大(大V发帖扇出到百万粉丝) 混合:推模式水平扩展 + 拉模式垂直缓存 多级降级 + 异步写入 Chat System WebSocket连接数(有状态) 水平扩展连接层 + 无状态业务层 连接注册表 + 消息持久化 + 故障转移 Search Engine 索引大小 + 查询计算量 索引分片水平扩展 索引副本 + 熔断器(查询链路) YouTube 视频存储 + 转码计算 + CDN带宽 全面水平扩展 + CDN分发 自动伸缩(转码)+ 多区域双活 Google Drive 存储容量 + 同步并发 存储层水平分片 + 元数据分离 多副本存储 + 冲突解决 Proximity Service 地理索引查询QPS 水平扩展(无状态查询服务) 缓存 + 多区域部署 Google Maps 地图瓦片存储 + 路径计算 CDN(瓦片)+ 水平扩展(路径计算) 多数据中心双活 Hotel Reservation 库存一致性(写冲突) 读水平扩展 + 写慎重分片 数据库主备 + 支付幂等(Module 7) Gaming Leaderboard 全局排序计算 优先垂直扩展(单Redis大实例)→ 后期分片 Redis主备 + 定期持久化 核心规律:
- 无状态服务(URL Shortener、Proximity Service)最容易水平扩展
- 有状态服务(Chat System、Gaming Leaderboard)需要先将状态外置,再水平扩展
- 需要全局一致性的服务(Hotel Reservation库存)扩展最困难,常用垂直扩展或谨慎分片
- 高可用不是单一技术,而是多个机制的组合:缓存 + 限流 + 熔断 + 降级 + 故障转移
练习二:级联故障推演
Section titled “练习二:级联故障推演”故障场景 🔧 News Feed 系统在晚高峰时段,“推荐服务”因为一次错误的配置更新开始返回500错误。系统没有熔断器。请推演接下来5分钟内会发生什么?然后描述有熔断器时的不同结果。
点击查看解析
没有熔断器的情况(级联故障):
0-30秒:推荐服务开始返回500。News Feed服务每次调用推荐服务都超时(假设超时设置3秒)。用户刷信息流从200ms变成3200ms。
30秒-1分钟:News Feed服务的线程池开始堆积——每个请求都被推荐服务的超时阻塞3秒。正常情况线程池100个线程能处理500QPS(每请求200ms),现在只能处理约30QPS(每请求3200ms)。大量请求开始排队。
1-2分钟:排队的请求越来越多,News Feed服务自身开始超时。上游的API网关发现News Feed服务响应慢,也开始堆积请求。用户看到”加载中”转圈。
2-3分钟:News Feed服务的内存被大量排队请求占满,开始OOM或GC频繁。API网关的连接池也耗尽。此时不仅信息流不可用,连”发帖”、“点赞”等走API网关的功能也受影响。
3-5分钟:运维收到告警,手动介入。此时整个News Feed系统几乎瘫痪——一个推荐服务的故障拖垮了所有功能。
有熔断器的情况:
0-10秒:推荐服务返回500,熔断器统计失败率迅速上升。
10秒:失败率超过50%阈值,熔断器打开。后续对推荐服务的调用直接返回空结果(不等超时)。
10秒之后:News Feed服务正常运行,只是信息流中没有推荐内容。用户看到的帖子都是关注的人发的——体验稍差但完全可用。响应时间仍然是200ms。
30秒后:熔断器进入半开状态,探测到推荐服务仍然返回500,重新打开。
运维修复配置后:下一次半开探测成功 → 熔断器关闭 → 推荐内容恢复。全程用户几乎无感知。
对比:没有熔断器——一个非核心服务的故障导致整个系统瘫痪5分钟。有熔断器——核心功能全程可用,只是推荐内容暂时缺失。这就是熔断器的价值。
练习三:设计自动伸缩策略
Section titled “练习三:设计自动伸缩策略”场景设计 📋 Hotel Reservation 即将迎来国庆黄金周(10月1日-7日),预计流量是平时的20倍。请设计一个完整的扩展与高可用方案,涵盖:
- 提前准备(预测性伸缩)
- 活动期间的指标伸缩和限流策略
- 降级方案(哪些功能可以关、哪些必须保)
- 故障应急预案
点击查看解析
1. 提前准备(9月28日-30日)
基线容量:平时 5千QPS,国庆预期 10万QPS预扩容:- 搜索服务:从 10 实例扩到 200 实例- API网关:从 5 实例扩到 50 实例- 数据库读副本:从 3 个扩到 15 个- Redis缓存:内存从 64GB 扩到 256GB- CDN预热:将热门城市的酒店图片和信息推送到CDN压测验证:- 在预扩容后进行全链路压测(模拟 10万QPS)- 确认每个环节的瓶颈已消除- 验证自动伸缩策略在压测中正常触发2. 活动期间的指标伸缩和限流
指标伸缩:- 搜索服务:CPU > 60% 持续 2分钟 → 扩容 20%- 下单服务:队列积压 > 500 → 扩容 50%- 数据库连接数 > 80% → 切换更多流量到读副本限流:- 全局:15万QPS 硬限制- 用户级:每用户 20次/秒 搜索,5次/秒 下单- IP级:每IP 300次/分钟- 对搜索API用令牌桶(允许突发),对下单API用漏桶(平滑处理)3. 降级方案
三级降级:L1(流量到预期1.5倍):- 关闭个性化推荐(返回热门酒店)- 搜索结果缓存时间从 30秒 延长到 5分钟L2(流量到预期2倍):- 关闭评论、图片评论- 关闭历史订单查询- 搜索结果只返回前20条(平时50条)L3(系统临近崩溃):- 只保留"搜索→选房→下单→支付"核心链路- 启用排队页面:"当前排队人数xxx,预计等待x分钟"- 非核心API直接返回"系统繁忙"必须保的功能:搜索、下单、支付、订单查询可以关的功能:推荐、评论、收藏、积分、个人主页4. 故障应急预案
数据库主库故障:→ 自动切换到备库(预计30秒内完成)→ 期间写入不可用,搜索走缓存+读副本仍可用→ 值班DBA立即介入确认数据一致性支付服务故障:→ 熔断器打开,返回"支付系统繁忙,请稍后重试"→ 已提交的支付请求通过消息队列异步重试→ 禁止自动取消未支付订单(给用户更多时间)缓存集群故障:→ 数据库限流器立即生效(防止被打垮)→ 启用L2降级(减少数据库查询量)→ 运维快速恢复或启用备用缓存集群单机房故障:→ DNS自动切换到备用机房→ 备用机房有完整的服务和数据副本→ 切换后检查数据一致性
本模块核心收获:扩展性和高可用不是事后补救,而是架构设计中需要提前考虑的维度。无状态设计让水平扩展成为可能,负载均衡和服务发现让多节点协作成为现实,熔断器和降级让系统在部分故障时仍能提供核心服务,自动伸缩让资源跟着流量走。最重要的认知是:没有银弹——每种高可用机制都有trade-off(Module 7的脑裂、Module 3的复制延迟、Module 4的缓存一致性),好的架构是在具体业务场景下找到这些trade-off的最佳平衡点。