3.5 可扩展性、性能与韧性
概述与动机
可扩展性、性能和韧性是三种截然不同的特质,人们却常常把它们混为一谈。性能指的是系统响应的速度,以及每单位资源能完成多少工作。可扩展性指的是随着负载增长,系统能在多大程度上保持性能。韧性指的是当出现故障时,系统能在多大程度上继续运作,或者优雅地降级。一个系统可能快但不可扩展(低负载下表现出色,高负载下崩溃)、可扩展但脆弱(能应对大流量,但一个组件故障就会倒下),或者有韧性但缓慢。大型组织三者都需要,而且需要从一开始就设计进去,因为在上线之后再补上其中任何一项都代价高昂、且会造成干扰。
对于企业和政府系统而言,把这些事情搞错的后果是公开且严重的。想想一个在新计划推出第一天就瘫痪的福利门户,一个在截止日期超时的报税系统,或者一个在购物高峰期宕机的支付平台。这些正是会登上新闻头条、引发调查、侵蚀公众信任的失败。这类系统还面临着高度尖峰、往往由法律规定时限的负载(申报截止日期、注册窗口期、发薪日),以及严苛的可用性和恢复义务。你必须为可预测的激增规划容量,在不可预测的激增下优雅降级,并在灾难发生后于规定的时间和数据丢失限度内完成恢复。这是一种带有公共问责维度的工程实践。
本章涵盖水平扩展与垂直扩展、作为规模化推动力的无状态性与分片、负载均衡、自动扩缩容与容量规划、带明确预算的性能工程、韧性模式与混沌工程,以及由 RTO、RPO 和业务连续性所界定的多区域灾难恢复。贯穿全章的信息是:这些特质是有意为之的设计和持续测试的产物,而不是寄望于运气的结果。
另请参阅: 第 3.3 章(分布式系统)、第 9.1 章(站点可靠性工程),以及第 9.2 章(可观测性与监控)。
关键原则
- 为向外扩展而设计,而非向上扩展。 垂直扩展存在上限,也存在一个单点故障;水平扩展才是你实现大规模、高韧性扩展的方式。
- 无状态性是水平扩展的推动力。 如果任何请求都可以发往任何实例,你就能够自由地增加和移除容量。
- 无法度量的东西就无法改进。 性能工作应由针对明确预算的性能剖析和负载测试来驱动,绝不能靠猜测。
- 一切都会故障;要为此设计。 假设组件会故障,并让系统在其故障时依然存活。
- 优雅降级胜过硬性故障。 一个舍弃非核心功能、部分可用的系统,好过一次彻底的宕机。
- 容量要规划,激增要吸收。 预测可预见的负载;对其余部分使用自动扩缩容和余量。
- 恢复目标是业务决策。 RTO 和 RPO 由业务方根据成本权衡后选定,然后据此进行工程实现。
- 有意地测试韧性。 在你刻意让系统故障之前,你并不知道它是否真正具有韧性。
建议
优先采用水平扩展,并设计无状态服务
垂直扩展(更大的机器)简单,有时也是正确的第一步,但它会撞上一个坚硬的上限,在高端会变得代价高得不成比例,并留下一个单点故障。水平扩展(负载均衡器后方更多的机器)能扩展得远得多,也能提升可用性,因为丢失一个实例是可以承受的。前提条件是无状态性。不要在实例上保存客户端会话或请求状态;把它推送到一个共享存储(数据库、缓存、令牌)中。无状态服务可以被自由地增加、移除、替换和负载均衡,这正是自动扩缩容和滚动部署得以实现的原因。在状态必须被分区的地方,按照一个能够均匀分布负载、并让相关数据保持在同一分片上的键进行分片。
负载均衡、自动扩缩容与容量规划
在每一个被扩展的层级前面都放置一个负载均衡器,用于分发流量,并通过健康检查绕开不健康的实例。配置自动扩缩容,当一个先导指标(CPU、请求队列深度、延迟)越过阈值时增加容量,当负载下降时移除容量。调优扩缩容速度和冷却时间,使你既不会滞后于一次流量高峰,也不会来回抖动。自动扩缩容不能替代容量规划。对于可预见的、关系到业务的激增(报税截止日期、注册期、促销活动),要提前预测负载,预先供给或预热容量,并提前针对该目标进行负载测试。自动扩缩容本身无法对一次阶跃式变化做出即时反应,而冷启动恰恰会在你最经不起延迟的时候增加延迟。始终保留余量。以 100% 的利用率运行,就没有任何空间去吸收激增或故障。
针对明确的预算来进行性能工程
设定性能预算(具体的目标,例如 API 延迟 p95 低于 200 毫秒、页面可交互时间低于 2 秒,或每笔交易成本低于某个阈值),并在测试和监控中强制执行,使性能回归导致流水线失败,而不是流向用户。用度量来驱动优化。通过性能剖析找出真正的瓶颈()它很少出现在你猜测的地方()并通过负载测试找出系统在何处失效,以及在接近该极限时的行为方式。要聚焦于关键路径和尾部(p95/p99),因为在大规模场景下,尾部延迟主导着用户体验。先优化最大的瓶颈,再重新度量,当达到预算目标时就停止。过度优化已经足够好的代码是在浪费精力。
内建韧性模式,并通过混沌工程进行验证
应用分布式系统中的韧性模式:超时、带退避的有限重试、熔断器(当某个依赖不健康时快速失败)以及舱壁隔离(bulkhead,隔离资源池,使一处故障无法耗尽其余资源),再加上优雅降级(在压力下舍弃或简化非核心功能:禁用推荐、提供缓存内容、把非紧急工作排队)以及负载削减(拒绝或限流超额请求,以保护核心功能,而不是彻底崩溃)。通过在每一层引入冗余来消除单点故障。然后用混沌工程来验证韧性。在受控实验中有意注入故障(终止实例、增加延迟、切断一个依赖、使一个可用区失效),从测试环境开始,逐步走向生产环境的”游戏日”演练,以证明系统的行为符合设计预期。从未被测试过的韧性只是一个假设。
规划多区域部署、灾难恢复与业务连续性
明确地决定恢复目标:RTO(恢复时间目标,你能够承受多长时间的宕机)和 RPO(恢复点目标,你能够承受损失多少数据)。这些是带有直接成本影响的业务决策,并驱动着架构设计。可选方案在成本和速度上各不相同:备份与恢复(最便宜、最慢)、pilot light(火种)模式、热备用,以及主主多区域(active-active,最昂贵,RTO/RPO 接近于零)。为每个系统选择其关键程度所能证成的层级。并非所有系统都需要主主架构。按照所选定的 RPO 在各区域间复制数据,将故障切换自动化,最重要的是,定期测试故障切换。未经测试的灾难恢复方案,在真正需要用到它的时候,往往会可靠地失败。要用一份涵盖人员、沟通和人工应急手段、而不仅仅是技术的业务连续性计划,把上述所有内容包裹起来。
权衡:利与弊
| 选择 | 优点 | 缺点 |
|---|---|---|
| 垂直扩展 | 简单,无需改代码,初期投入低 | 存在坚硬的上限,高端代价高,存在单点故障 |
| 水平扩展 | 近乎无限的扩展能力,提升可用性 | 需要无状态性、负载均衡,运维工作更多 |
| 自动扩缩容 | 让成本匹配需求,能应对可变负载 | 反应存在滞后;存在冷启动;调优不当会抖动 |
| 主主多区域 | RTO/RPO 接近于零,能承受区域级损失 | 成本和复杂度最高,数据一致性难以保证 |
| 备份与恢复式灾难恢复 | 最便宜、最简单 | RTO 长,数据丢失窗口更大 |
核心的权衡是成本与保障之间的取舍。每一份可扩展性余量、性能和恢复能力的增量都需要花费金钱和复杂度,且回报是非线性的。从 99.9% 的可用性提升到 99.99%,或者把 RTO 从一小时缩短到几秒钟,都可能让成本成倍增长。这里的纪律在于,把每一份投入的规模与系统实际的关键程度、以及业务对宕机和数据丢失的容忍度相匹配,而不是不假思索地把一切都按最高标准来工程化。一个面向公民的支付系统值得拥有主主冗余;一个内部报表工具则不然。
与团队讨论的问题
在你们上一次重大事故中,性能、可扩展性、韧性这三者之中究竟是哪一个出了问题,你们修复的是正确的那一个吗? 本章有意地把三者区分开:一个系统可能快,却在负载下崩溃;可能能扩展,却在一个组件宕机时倒下;也可能能挺过故障,却很慢。团队常常误诊,给一个韧性问题增加容量,或者去加固一个只是在应对一次激增时供给不足的系统。回顾最近两次重大事故,指出到底是哪一种特质出了问题,以及实际的应对措施改善了什么。区分清楚会改变修复方向:无状态性和分片对应可扩展性,冗余和熔断器对应韧性,性能剖析和预算对应性能。分类分对了,就是把钱花在治病上而不是花在治标上的分野。
性能回归会让你们的流水线失败,还是会在没人注意到的情况下流向用户? 一个性能预算(p95 延迟、页面可交互时间、每笔交易成本)只有在被自动强制执行时才能保护用户,也就是说,一次突破预算的变更应当让构建失败,而不是照常上线。在一个有众多贡献者的大型团队中,延迟会通过成千上万个小提交悄悄渗入,如果没有一道关卡,尾部延迟会慢慢腐化,直到一次发布把它暴露出来。带上你们当前的预算,检查它们是否接入了 CI 和监控,以及它们针对的是 p95 和 p99 而不是平均值,因为在大规模场景下,用户感受到的正是尾部。如果还没有预算,设定一个就是第一步。强制执行才是把一个良好意愿变成一项能够经受住团队增长考验的属性的关键。
在压力之下,什么会最先被舍弃,这个次序是你们设计出来的,还是你们要在一次宕机中才会发现? 优雅降级和负载削减意味着系统放弃非核心工作以保护核心功能,但前提是你已经提前决定了什么是核心。对于一个面向公民的服务而言,这种排序往往是一项政策决策:即便状态仪表盘和历史查询都陷入黑暗,提交报税单也必须存活下来。如果没有人做出过选择,系统就会舍弃最先失效的那个功能,而那恰恰可能是用户最需要的东西。把你们的功能按优先级列出来,确认架构能够在不牵连关键路径的情况下舍弃低优先级的部分(缓存响应、禁用推荐、把非紧急工作排队)。然后在真实负载下测试它,因为未经测试的降级只是一个愿望。
对于你们最关键的系统,RTO 和 RPO 分别是多少,这些数字实际上是谁选定的,你们最后一次证明能够达标是什么时候? 恢复时间目标(你能承受多长的宕机)和恢复点目标(你能承受损失多少数据)是带有直接成本影响的业务决策,然而在一个大型团队中,它们往往是由随便哪个编写操作手册的人随手定出来的,而不是由对该服务负责的人所拥有的。这里相互竞争的拉力是成本与保障:把 RTO 从一小时缩短到几秒钟,或把 RPO 从几分钟缩短到零,可能让基础设施账单成倍增长,因此正确的数字是业务方真正愿意为之付费的那个数字,而不是听起来最厉害的那个。带上有文档记录的目标、上一次真实故障切换测试的日期,以及那次测试所测得的时间和数据损失,因为一个未经测试的目标只是一个愿望。在企业和政府场景中,这些数字可能是由法规、合同或 SLA 规定的,因此要指明谁在为它们签字确认,以及上一次演练是达标了,还是悄悄地没能达标。
对于你们最大的可预见激增,你们是在寄望于自动扩缩容能够当场做出反应,还是已经预测了负载、预先供给了容量、并针对那个目标进行了负载测试? 自动扩缩容的反应存在滞后,冷启动恰恰会在你最经不起延迟的时候增加延迟,因此一次已知的阶跃式变化(一个申报截止日期、一个注册窗口期、一次促销活动)正是被动式扩容会失败、而有意为之的容量规划会取胜的那种情形。这里的张力是成本:为一次高峰预热容量,意味着要为一年中大部分时间都闲置的余量付费,而人们的诱惑是寄望自动扩缩容能免费搞定一切。带上去年的峰值数据、今年考虑增长后的预测,以及一次针对该预测数倍目标、而非今天平均流量所进行的负载测试的结果。对于面临法定时限激增的政府或企业服务而言,还要加上做错的后果,因为一个在第一天就瘫痪的福利门户或报税系统,招来的将是一场公开调查,而不仅仅是一个缓慢的下午。
你们是否曾在生产环境中有意让一个组件故障过,每个系统的冗余层级是否真正与其关键程度和成本相匹配? 从未被测试过的韧性只是一个假设,而你能够购买的层级从便宜的备份与恢复,经过热备用,一路到昂贵的主主多区域,因此这里的纪律在于把保障花在值得的地方,而不是给一切都镀金,或者哪里都不保护。相互竞争的考量是爆炸半径和预算:混沌实验必须有护栏和中止开关,为一个内部报表工具配备主主架构是浪费,而只为一个支付平台配备纯备份则是失职。带上你们单点故障的清单、每个关键系统的冗余层级,以及上一次受控故障注入的证据和它揭示出的问题。在企业和政府的资产组合中,要把每个层级映射到一份有文档记录的关键程度评级,这样审计员就能看到钱是跟着风险走的,也没有人需要等到宕机发生时才第一次为这笔支出辩护。
行业视角
初创企业。 你无法预测一次上线会带来五十个注册用户,还是五万个,因此应该购买规模化能力,而不是自己构建:在一个托管负载均衡器后方运行无状态服务,让平台根据请求速率自动扩缩容。设定一个适度的性能预算,选择托管数据存储,这样一次流量高峰就不会逼出一次凌晨两点的架构重构。暂时跳过多区域灾难恢复和混沌工程计划;保留经过测试的备份,把你稀缺的工程注意力花在产品上,而不是花在你的流量还不足以支撑的冗余上。
中小企业。 在没有可靠性专家、预算又紧张的情况下,“买还是造”的选择会强烈地倾向于购买:一个托管平台或无服务器技术栈能把扩缩容和故障切换变成供应商的工作,通常一个运营良好的单一区域就足够了。把韧性设定为你能够兑现的少数几项具体承诺,比如一次你已经真正恢复过一次的夜间备份,以及一个你已经向客户沟通过的现实的恢复窗口。要避免为你既没有相应流量、也没有相应人手来支撑的主主架构或持续负载测试而付费。
大型企业。 问题在于众多团队之间的一致性:把在 CI 中强制执行的性能预算标准化,建立一个共享的韧性模式库(超时、熔断器、舱壁隔离),为每个系统建立一份与其关键程度挂钩、有文档记录的冗余层级。把主主多区域保留给一级服务,运行一个带有护栏和生产环境”游戏日”演练的混沌工程计划,把已知激增的容量规划当作一项按计划进行的纪律,而不是事后补救。要在中心统一治理 RTO 和 RPO,使每个关键系统都有专人负责、经过测试、且审计员能够核实的目标。
政府。 负载往往由法律规定时限,可用性义务是法定的,因此容量规划不能依赖自动扩缩容临场反应:要预测截止日期带来的激增,提前供给容量,并把负载测试的目标定得远高于预测值。采购应当把 RTO、RPO 和一份经过演练的故障切换计划表作为合同要求来明确规定,而不是仅仅作为供应商的口头承诺,并应避免关键服务被锁定在单一区域。要提前决定哪条路径是法律上必不可少的(提交申报单、申领福利),使降级时优先舍弃状态仪表盘和查询功能,并就宕机和恢复情况对公众保持透明,而不是寄望没有人会注意到。
示例
初创企业。 一家在 Product Hunt 上线的小型初创公司无法预测会获得五十个注册用户还是五万个,因此它让自己的服务在一个托管负载均衡器后方保持无状态,并让平台根据请求速率自动扩缩容。它设定了一个适度的性能预算(页面响应在第 95 百分位下低于 300 毫秒),并选择了一个托管数据库,这样一次流量激增就不会逼出一次凌晨两点的架构重构。当上线当天的流量激增真正到来时,网站只是稍微变慢,而不是崩溃,团队得以把这一天花在与新用户交流上,而不是与一场宕机搏斗。
大型企业。 一家流媒体公司在全球负载均衡之后跨多个区域运行无状态服务,根据请求速率自动扩缩容以跟上每日黄金时段的流量波动。性能预算以 p99 启动延迟作为每次发布的门槛。在一次区域故障下,流量会自动转移到健康的区域,非核心功能(个性化封面图、推荐刷新)会最先降级,以保护播放功能。该公司在生产环境中持续运行混沌实验,例行终止实例并注入延迟,使得真实故障与演练毫无区别,也不会造成任何客户可见的宕机。
政府。 一个税务机构知道其报税系统每年都会面临一次由法律规定的巨大截止日期激增。它没有依赖自动扩缩容临场反应,而是根据往年数据预测峰值负载,提前数周供给容量,并按预测值的 150% 进行负载测试。该架构在负载均衡器后方保持无状态,并配有一个热备用的第二区域。RTO 和 RPO 由政策设定(已提交的报税单宕机不超过 15 分钟,数据丢失接近于零),故障切换每季度演练一次。在极端负载下,非关键功能(状态仪表盘、历史查询)会最先被舍弃,从而使报税提交()这条法律上必不可少的路径()保持可用。
商业案例:动机、投资回报率与总拥有成本
可扩展性、性能和韧性是那种失败成本远超预防成本的经典案例。但预防的成本在预算上清晰可见,而失败只是一种可能性,这正是为什么在第一次灾难发生之前,它们长期得不到充分的资金支持。采纳的成本是真实的:冗余基础设施、多区域容量、负载测试和混沌工程工具,以及构建无状态性和韧性模式所需的工程时间。不投入的代价,是在需求高峰期发生一次引人注目的宕机:对商业系统而言,是逐分钟流失的营收;对政府而言,是未能履行法定义务并招致公开调查;此外还有服务水平协议(SLA)罚款和持久的声誉损害。
用业务方本就理解的数字向领导层陈述这个案例。估算高峰期一小时宕机的成本(流失的交易、罚款、补救措施、声誉损失),再将其与预防它所需的冗余和测试的年度成本相比较。对关键系统而言,预防的成本几乎总是一次重大事故成本的一小部分。把 RTO 和 RPO 与具体的金钱挂钩:每小时宕机意味着多少营收或多少笔交易的损失,以及在法律或商业上能够容忍多大的数据损失。把性能陈述为一项营收和满意度的杠杆,因为更快的系统转化率更高、每笔交易成本更低;把韧性陈述为一份保险,其保费相对于所覆盖的损失而言很小。最有力的论点是:这些特质在设计阶段就纳入的成本很低,而在被迫直面问题的那次宕机之后再去补救,则代价惨重。
反模式与陷阱
- 粘性会话和实例内状态。 把会话状态存储在服务器上,阻碍了自由的水平扩展和安全的实例替换。
- 把自动扩缩容当作容量规划。 假设自动扩缩容能够吸收一次已知的阶跃式激增,而它对此的反应实际上太慢了。
- 在没有余量的情况下高负荷运行。 以接近 100% 的利用率运行,没有留下任何空间来吸收激增或故障。
- 在没有性能剖析的情况下进行优化。 调优一段并非瓶颈的代码,而真正的瓶颈却无人问津。
- 忽视尾部。 只报告平均延迟,而 p99 的用户正在受苦;平均值掩盖了大规模场景下的真实痛点。
- 未经测试的灾难恢复。 一份从未被演练过的灾难恢复计划和备份,在真正需要时会失效。
- 单点故障。 一个负载均衡器、一个数据库主节点、一个区域:一个没有冗余的组件把一切都拖垮。
- 没有护栏的混沌工程。 在没有爆炸半径控制和中止开关的情况下注入故障,反而造成了你本想预防的那场宕机。
成熟度模型
- 第 1 级,启动: 临时且被动应对。单实例或垂直扩展,状态保存在服务器上。没有负载测试,没有性能预算,除了偶尔无人恢复验证过的备份之外,没有灾难恢复。任何一个组件故障都会导致完全宕机,扩展性问题是在生产环境中才被发现的。
- 第 2 级,发展: 基本做法开始出现,但因团队而异。一些服务在负载均衡器后方实现了水平扩展和无状态化,其中少数配有基本的自动扩缩容。负载测试在重大上线前进行,但并非常态化,备份是存在的,灾难恢复也有文档记录,却很少被演练。一个团队做得好的事情,另一个团队可能还没开始做。
- 第 3 级,标准化: 实践已被记录并在组织范围内强制执行。已知激增的容量规划留有余量,性能预算在 CI 中强制执行,使回归导致构建失败,韧性模式(超时、有限重试、熔断器、舱壁隔离)加上优雅降级已成为默认做法。每个系统都定义了 RTO 和 RPO,冗余层级按关键程度分配,灾难恢复的故障切换在各团队间按固定计划定期测试。
- 第 4 级,管理: 这些特质被对照基线进行度量和控制。团队跟踪 p95 和 p99 延迟、错误预算,以及利用率和余量与预测值的对比,并在越界时发出告警,而不是等到上线时才发现。经过测试的故障切换时间会与目标 RTO 和 RPO 进行比较,降级和负载削减的阈值通过指标进行验证,发布和容量方面的”上线/不上线”决策由数据驱动。当数字偏离基线时,这种差距是可见且有人负责的,而不是隐藏在平均值背后。
- 第 5 级,编排: 可扩展性、性能和韧性在整个组织范围内被持续改进和集成。凡是关键程度能够证成之处,都使用主主多区域架构;混沌工程持续运行,包括生产环境的”游戏日”演练;容量预测直接反哺规划和采购。韧性得到持续验证,恢复目标被始终如一地达成并加以证明,架构会随着负载模式和风险状况的变化而调整,并与业务连续性和风险规划相衔接。
讨论思路
- 你们的哪些服务仍在实例上保存状态,是什么阻碍着你们让它们变得无状态?
- 对于你们最关键的系统,RTO 和 RPO 分别是多少,是谁设定的,你们最后一次证明能够达标是什么时候?
- 自动扩缩容真的能保护你们抵御最大的已知激增吗,还是你们在指望它做一些它做不到的事情?
- 你们仍然存在的单点故障在哪里,消除它的计划是什么?
- 你们是在度量和为 p99 延迟设定预算,还是在平均值背后藏身?
- 你们是否曾在生产环境中有意让一个组件故障过?如果没有,你们如何知道自己的韧性是有效的?
关键要点
- 区分性能、可扩展性和韧性;一个大型系统三者都需要,而且需要从一开始就设计进去。
- 水平扩展和无状态服务是规模化、可用性和安全部署的基础。
- 把自动扩缩容与真正的容量规划和余量相结合,以应对可预见的、关系到业务的激增。
- 用针对明确预算的性能剖析和负载测试来驱动性能,聚焦于关键路径和尾部。
- 用超时、熔断器、舱壁隔离、优雅降级和冗余来构建韧性,然后用混沌工程来验证它。
- 把 RTO 和 RPO 当作业务决策来设定,让灾难恢复方案与每个系统的关键程度相匹配,并定期测试故障切换。
参考资料与延伸阅读
- Martin Kleppmann, Designing Data-Intensive Applications
- Michael Nygard, Release It!: Design and Deploy Production-Ready Software
- Betsy Beyer et al. (Google), Site Reliability Engineering and The Site Reliability Workbook
- Casey Rosenthal and Nora Jones, Chaos Engineering
- Brendan Gregg, Systems Performance: Enterprise and the Cloud
- John Allspaw, The Art of Capacity Planning
- Ilya Grigorik, High Performance Browser Networking
- Nassim Nicholas Taleb, Antifragile