P15

扩展性模式

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

1 扩展性模式要回答什么

零售订单 API 在平日运行稳定,促销开始后却逐渐变慢。团队准备增加应用副本,但评审者先问:订单究竟慢在哪里?如果请求都在等待数据库连接,新增应用可能只是增加等待者。扩展性模式讨论的是需求增长时,系统如何增加有效处理能力,以及能力增长会受什么限制。它既涉及资源配置,也涉及请求路径、状态归属与故障处理。

本课沿用零售订单 API:顾客提交订单,服务校验商品与价格,调用库存预留接口,将订单写入数据库,再返回订单号;支付通知随后更新订单状态。商品查询与订单查询是读请求,创建订单与支付状态变更是写请求。库存预留成功并不等于订单已经提交,因此异常时还需要释放预留或通过核对任务补偿。扩展过程中,这些业务事实始终不能改变。

先把需求写成可评审的假设。假设平日峰值为每秒 200 个请求,促销目标为每秒 800 个请求,其中创建订单占 20%;在约定的测试数据和依赖条件下,端到端延迟 p95 不超过 500 毫秒,失败比例低于 0.5%。p95 表示 95% 的请求在该时间内完成。还要单独要求:同次购买意图因重试产生的重复请求,只形成 1 个有效订单。性能目标与正确性目标需要同时验收。

这里的数字是方案输入,不是普遍适用的承诺。促销持续多久、购物车包含多少商品、顾客是否集中访问热门商品,都会影响结果。每秒 800 次查询和每秒 800 次下单不是相同负载。容量方案应保存请求组成、数据规模、并发用户与依赖响应时间,否则只记录“支持 800 请求每秒”,下次业务变化时便难以判断结论是否仍然有效。

扩展性与高可用有关,但不能互相替代。增加副本可能提高总吞吐,也可能让服务在损失部分实例后继续接单;如果所有副本仍依赖同个数据库写入口,数据库故障依然影响全部订单。反过来,部署了备用数据库也不代表写入吞吐翻倍。前者讨论增长时如何增加能力,后者讨论故障时如何维持服务,两者要在同份方案中分别给出证据。

订单 API 的需求、扩展选择与验证闭环
订单 API 的需求、扩展选择与验证闭环

判断扩展是否有效,要比较相同负载下的吞吐、延迟、错误与成本。吞吐是单位时间完成的请求数,不能只统计进入系统的请求数。积压增加时,入口看起来很热闹,实际成功订单却没有增加。还应观察正常完成订单数、重复请求命中数和待补偿订单数,避免性能曲线掩盖业务异常。扩展方案最终要回答:哪段能力增加了,谁成为新的限制,以及下一次行动由什么指标触发。

2 垂直扩展、水平扩展与分片

垂直扩展是给同个处理单元增加资源,例如为订单服务增加 CPU 与内存,或者提高数据库实例的处理规格。它通常不要求立即改变请求分配方式,适合资源不足且应用能利用新增资源的阶段。但更大的机器仍有上限,调整过程也可能需要重启。若程序因共享锁串行处理订单,即使增加 CPU,实际吞吐也可能变化很小。

水平扩展是增加处理单元,例如把 2 个订单服务副本增加到 4 个,并由负载均衡器分配请求。它适合可以独立处理请求的工作负载。理想情况下,新增副本分担计算与连接压力;实际效果受共享依赖、负载均衡、请求差异和资源争用影响。因此“副本翻倍,能力翻倍”只能作为待验证的假设,不能直接写入承诺。

可以用简单的上限帮助思考。假设单个应用副本在当前请求组成下可稳定完成每秒 120 个请求,数据库在相同负载下可支持相当于每秒 360 个入口请求的工作量。配置 2 个应用副本时,应用侧上限约为 240;配置 4 个时,应用侧理论上限为 480,但整体仍可能被数据库限制在 360 附近。这里没有计入安全余量,也没有推导延迟,所以不能将上限直接当成可用容量。

分片则把数据与相应工作拆到不同分区,由不同处理单元承担。例如按顾客标识将订单划入不同数据分片,每个分片保存自己的订单记录。与单纯增加无状态副本相比,分片改变了数据分布。请求需要知道应访问哪个分片,跨顾客汇总、全局查单和数据迁移也会变复杂。它通常适用于确认单个数据单元已成为长期限制,并且较简单的优化不足以满足需求之后。

分片键决定压力怎样分布。按门店分片容易解释,但大型门店可能成为热点;按顾客标识分片可能分布更均匀,却让门店汇总需要访问多个分片。不能仅凭记录数量相等就断言负载均匀,还要看访问频率、订单大小与热门时段。分片算法、在线迁移和跨分片事务的内部实现超出本课范围,这里先掌握它带来的责任:定位数据、识别热点、处理跨边界操作。

垂直扩展、水平扩展与分片改变的位置
垂直扩展、水平扩展与分片改变的位置

选择顺序不必僵化,但需要可解释。资源不足且升级成本可接受时,可以先垂直扩展争取时间;请求能够独立处理时,可增加应用副本;共享数据库成为瓶颈时,先检查查询、索引、事务范围与连接使用,再评估数据层扩展。分片不是应用扩容的必经终点。对规模有限的零售业务,清楚的容量边界和可执行的升级路径,可能比提前引入分片更有价值。

还要区分读扩展与写扩展。将允许短暂延迟的历史订单查询交给只读副本,可以分担部分读负载,但不会自动增加主写入能力。顾客刚下单就查订单,若读取滞后的副本,可能暂时查不到记录。方案应说明哪些读取必须看到刚提交的结果,哪些可以接受延迟。扩展模式首先服务于业务语义,然后才是资源数量。

3 无状态化与瓶颈外移

无状态化的实用含义是:处理下个请求时,服务副本不依赖只有上个副本才保存的必要业务状态。订单 API 可以在内存中保存临时计算结果,但订单事实、购买幂等记录和必要会话信息不能只留在某个进程里。这样,负载均衡器才有条件把请求分配给任意健康副本,扩容、重启与故障切换也更容易进行。

假设应用 A 保存顾客购物会话,应用 B 没有这份内容。为了让结账继续,入口把该顾客后续请求固定发送到 A,这叫会话粘滞。它能暂时维持功能,却把顾客负载绑定到特定实例。若促销顾客主要落在 A,即使增加 B、C、D,A 仍然很忙。无状态化需要先识别这些隐藏绑定,再决定由客户端携带可验证信息,还是由服务读取共享状态。

状态外移不等于成本消失。把会话放进共享存储后,应用内存压力可能下降,却新增了网络访问、共享存储容量与故障依赖。每个请求都读取多次共享会话,也可能把新存储推成瓶颈。因此要记录访问次数、超时预算和不可用时的处理方式。身份校验所需信息不能因为共享存储异常就被跳过;业务状态不能仅为减少延迟而失去可靠保存。

数据库连接池也是典型的瓶颈外移位置。假设每个应用副本最多建立 40 个连接,2 个副本的配置上限是 80,扩到 8 个后便是 320。如果数据库只允许为此服务分配 160 个连接,应用扩容会挤占连接预算。需要按全部副本计算连接总量,并给运维、后台任务与恢复过程留出空间。连接数更多,也不意味着数据库能同时高效执行更多事务。

扩展后的观察可以分为连续的几个问题:请求是否分配均匀,应用是否还在忙于计算,依赖等待是否增加,成功订单是否随资源增长。如果应用 CPU 从 80% 降到 45%,而端到端延迟继续升高,不能立即判定扩容无效;应用压力可能已经缓解,数据库等待却开始占主导。应沿着请求路径重新定位限制,而不是重复执行上次有效的动作。

消息队列也可能成为扩展路径的一部分。例如订单提交后发送通知,可以在业务允许时异步执行,让下单返回不必等待通知完成。但把库存预留或订单持久化移到后台,会改变“下单成功”的含义,需要额外设计,不能当作普通性能优化。即使适合异步处理,队列仍需容量、重试和积压告警;它可以缓冲短期波动,却不能长期弥补消费者处理能力不足。

理解瓶颈外移,意味着对每次优化保留新的问题清单。应用扩容后检查数据库,查询优化后检查库存接口,异步化后检查积压和完成时间。系统不存在永远固定的最慢环节。好的扩展设计让限制可以被测量,让下一步行动有准备,并让正确性责任在组件移动时仍然清楚。

4 动手:定位一次扩展后的新瓶颈

下面开展受控演练,使用测试环境、测试顾客与模拟库存接口。准备固定的数据集,包含足够多的顾客与商品,使请求不会全部集中在相同记录;同时保留少量热门商品,用于观察热点影响。测试请求组成固定为 80% 查询、20% 创建订单,每次新的购买意图生成新的幂等键,网络重试则复用原键。压测结果仅对这组条件负责。

第 1 轮部署 2 个应用副本,每个副本连接池上限为 40。在每秒 100、200、300 个请求的台阶上分别保持 5 分钟,记录成功吞吐、p95、错误比例、应用 CPU、连接等待时间与数据库事务耗时。每轮结束等待积压恢复,再进入下轮。若业务错误或待补偿量持续增加,应停止提高负载,先确认是否触及正确性边界。

第 2 轮仅把应用副本增加到 4 个,保留相同的数据集、依赖延迟和流量台阶。这里的“仅”很关键:若同时修改索引、线程数和超时,就很难解释结果。下面是一组教学观察值,表示测量方法,不代表任何具体产品能力。每个延迟指标都是在对应观测窗口内得到的结果。

条件输入请求每秒成功请求每秒端到端 p95应用平均 CPU连接等待 p95
2 个副本200199310 毫秒76%8 毫秒
2 个副本300238920 毫秒91%24 毫秒
4 个副本300298420 毫秒58%65 毫秒
4 个副本4003321100 毫秒62%270 毫秒

这组结果支持有限的结论:在每秒 300 个输入请求时,增加应用副本改善了完成量与延迟;继续提高到 400 后,应用 CPU 没有耗尽,连接等待却显著增加。它提示下一步应调查数据库路径,但尚不能证明数据库 CPU 就是根因。连接被长事务占用、锁等待、查询变慢和数据库连接预算不足,都可能表现为应用等待连接。

以相同负载对比扩容前后的应用与依赖指标
以相同负载对比扩容前后的应用与依赖指标

接着抽取慢请求的分段耗时:入口等待、应用处理、连接获取、SQL 执行、库存调用与响应返回。分段记录应能关联到同次请求,同时避免写入顾客敏感信息。不要把各段的 p95 直接相加当成端到端 p95,因为不同分段的慢请求可能不是同批。优先查看具体慢请求的完整路径,再用总体指标判断问题是否普遍。

假设调查发现,订单列表查询缺少合适索引,扫描大量历史记录并长时间占用连接。可以单独优化该查询,再在同组负载下比较;也可以先减少非必要查询,为促销保留写入资源。若发现主要是库存接口超时,则应调整依赖预算和并发保护。修复动作必须对应证据,不能看到连接等待就直接扩大所有连接池。

最后进行恢复观察:将流量降回每秒 100 个请求,检查延迟、连接占用和待补偿量是否回落。若压力下降后仍无法恢复,可能存在长时间排队、资源泄漏或重试放大。容量边界需要包含恢复能力。记录本次结论为“当前条件下,每秒 300 个请求满足目标,400 不满足,新增限制待数据库路径优化后复测”,比宣称“4 个副本支持 400”更有依据。

5 成本与复杂度取舍

扩展方案中的成本不仅是实例账单,还包括共享存储、网络、监控、备份以及人员维护时间。新增副本比较容易计算,分片后的迁移演练、跨分片排查和恢复流程却可能持续消耗团队精力。入门阶段可以先按资源费用与运维复杂度分别比较,避免把难以量化的工作假装成精确金额,也避免因为暂时没有账单就忽略它。

假设某种测试实例每小时费用为 1 个成本单位,共享依赖每小时为 3 个成本单位。2 个副本的基础费用是 5,4 个是 7,8 个是 11。如果满足服务目标的容量分别为每秒 200、300、320 个请求,最后 4 个副本增加的收益便很有限。这里应使用达标的成功容量,而不是超时与错误大量出现时的入口峰值。

比较成本时还要问“买到了什么”。保留空闲副本可能是为了应对实例故障和扩容等待,而不是追求平时最高利用率。若每个副本的安全容量为每秒 100 个请求,3 个副本承担每秒 200 个请求时,在失去 1 个副本后仍有理论上的承接空间;但切换期间的连接重建、负载偏斜和依赖压力仍需演练。用平均利用率解释冗余价值,容易漏掉故障瞬间。

垂直扩展可能在短期内成本更低,也可能推高单点故障影响;水平扩展便于逐步增减,但需要状态与流量分配配合;分片能扩大数据处理边界,却提高路由、迁移和恢复复杂度。选型应列出当前限制、预计增长周期、团队维护能力与退出条件。不能只写“为了未来扩展选择分片”,却没有说明未来何时到来,以及现在为此承担什么。

架构决策记录 ADR 可以很短。针对本阶段订单 API,可以记录:由于应用计算首先触及限制,采用无状态应用副本水平扩展,同时限制总连接预算;暂不分片,因为数据库在目标负载下尚未证明成为不可优化的限制;接受共享会话存储带来的依赖,并补充监控与故障策略。当数据规模或写入需求越过已验证边界时,重新评审数据层方案。

自动扩缩容也有成本取舍。它需要检测时间、创建时间和预热时间,无法保证在突发流量到达的瞬间提供容量。对有明确开始时刻的促销,可以提前扩容,再用自动策略处理后续变化。缩容则要停止接收新请求并等待在途请求完成;过早终止实例可能让客户端重试。自动化应执行已经验证的策略,而不是代替容量推理。

6 常见坑:先扩副本再补幂等

创建订单最危险的扩展误区,是把重复请求当成只会在故障时出现的小概率事件。顾客重复点击、客户端超时重试、入口重试和实例退出,都可能让相同购买意图到达不同副本。单副本内存里保存“刚处理过”的标记,在多副本条件下无法提供统一判断。即使只有 1 个副本,重启也可能丢失这份记录。

假设客户端发送购买请求,应用 A 已经提交订单,但响应在网络中丢失。客户端看不到订单号,使用相同幂等键重试,请求落到应用 B。如果 B 只检查自己的内存,就会再创建订单。错误发生在“操作已完成,但调用者不知道”的窗口,不能靠提高机器速度消除。扩容只是让原本隐藏的假设更容易失效。

可行的基础设计是为每次购买意图生成幂等键,并在服务端以顾客或租户范围约束其唯一性。服务保存请求关键字段的摘要与处理结果;相同键、相同请求可以返回已有结果,相同键却携带不同购买内容应被拒绝。幂等键要在请求首次发出前确定,重试时保留;用户主动发起新的购买则使用新键,不能把合法重复购买误判成重试。

“先查有没有,再插入”本身不足以防止并发重复,因为 2 个副本可能同时查到不存在。需要利用数据库唯一约束等可靠机制,让竞争只有 1 个请求能够取得处理资格,并让订单写入与幂等结果的关系可恢复。遇到正在处理的相同请求,可以返回明确的处理中状态,或按接口约定稍后查询;不应无界等待,也不能在状态不明时重新执行全部副作用。

库存预留涉及另个系统,仅靠订单数据库事务不能保证跨系统动作只执行 1 次。订单服务应把稳定的业务操作标识传给库存接口,并约定重复预留的处理方式。如果对方不支持,则需要明确核对与补偿流程,避免因为订单写入失败而永久占住库存。支付通知同样需要按事件或业务标识去重,并校验允许的状态变化;收到重复通知不能重复扣减或重复发货。

扩容后重复请求的证据与处理路径
扩容后重复请求的证据与处理路径

幂等记录也有生命周期。保留时间应覆盖接口约定的重试窗口,并与客户端行为一致;清理后再次收到旧请求时如何处理,需要提前定义。日志中应能关联幂等键、订单号与结果,但不要直接记录敏感请求内容。验收时主动模拟响应丢失、同时重试和实例重启,检查有效订单、库存预留与支付状态是否一致。正确性通过后,再扩大流量和副本数。

7 案例:会话粘滞抵消水平扩展

某次零售促销演练中,订单 API 从 2 个应用副本扩到 4 个,总 CPU 看起来只有 50%,顾客却仍频繁超时。团队最初怀疑副本启动不完整,检查后发现所有实例都健康。进一步按实例查看,A 的 CPU 达到 94%,B 为 70%,新加入的 C、D 均低于 20%。平均值掩盖了请求分布不均。

原因是顾客的结账会话保存在应用内存,入口使用粘滞策略将已有顾客固定发送给原实例。促销前大量用户已经进入购物流程,新增副本主要接收少量新会话,无法分走原实例上的活跃顾客。此时服务具备多个副本,却没有充分具备跨副本处理请求的条件。盲目继续扩容,会增加费用而难以降低最忙实例的等待。

团队先验证关联:按实例比较活跃会话数、请求量、p95 与 CPU,同时区分已有会话和新会话。随后在测试环境让同个会话访问不同副本,确认结账信息会丢失。这些证据说明会话位置是扩展障碍,但也揭示不能直接关闭粘滞;那样可能让正在结账的顾客失败。性能修复需要兼顾迁移过程中的业务连续性。

改造方案将必要会话存入共享存储,应用通过会话标识读取,并把订单事实继续保存到订单数据库。对旧会话设置有限过渡期,在原副本处理时迁移;过渡期结束后再取消对粘滞的依赖。上线策略先覆盖少量流量,比较跨副本访问成功率、共享存储延迟、错误比例和请求分布,再逐步扩大。迁移失败的会话应有明确恢复方式。

复测时,4 个副本请求量趋于接近,最忙实例的 CPU 降低,但共享会话存储的访问量明显增加。团队因此补充连接预算、超时和容量监控,并演练该存储不可用时结账如何返回可理解的错误。会话外移解决了分配约束,同时带来了新依赖。案例的验收条件不是“已关闭粘滞”,而是顾客流程连续、请求可跨副本、目标负载达标且依赖风险有处置。

8 把扩展路径写进容量方案

容量方案应从业务预测走到可执行动作。先写活动时段、请求组成、目标成功率与延迟,再列当前拓扑和压测边界。每个容量结论都带上环境、数据规模和依赖假设。若生产中的购物车平均商品数、热门商品集中度或库存接口延迟超出测试范围,原结论就需要重新评估,而不是继续引用旧报告里的峰值数字。

扩展路径可以分阶段描述。当前采用 4 个无状态应用副本,先处理慢查询和连接预算;业务增长触发应用扩容时,验证请求分布与依赖等待;读负载成为主要压力时,评估允许延迟的读分流;单个数据单元经优化仍无法满足目标时,再讨论分片。每阶段都需要进入条件、预期收益、验证方式与撤回办法,避免方案只剩组件清单。

触发指标应结合持续时间与业务结果。例如成功吞吐接近已验证安全容量,且 p95 连续 10 分钟上升,同时应用 CPU 较高而依赖等待稳定,可以触发应用扩容。若 CPU 较低、数据库等待显著增加,则进入依赖排查。这里的时间窗和阈值需要根据业务波动校准;同时设置最大副本数、数据库连接总量与费用边界,防止自动策略放大共享资源压力。

风险记录要把现象、影响和动作连起来。会话仍绑定实例,意味着新增副本难以分担旧用户;缺少幂等约束,意味着重试可能生成重复订单;依赖容量不足,意味着应用扩容后错误可能增加。每项风险应有责任人、验证时间和残余风险说明。无法立即解决的限制也要写清楚,例如促销期间限制非关键查询,以保证创建订单的资源预算。

高可用部分需单独核验故障后的容量。假设 4 个副本分布在 2 个可用区,失去 1 个可用区可能同时损失 2 个副本,不能只按损失 1 个实例计算余量。还要确认数据库、会话存储和库存服务在该故障范围下是否可用。演练时观察切换时间、在途请求、幂等命中与恢复后的积压;容量够用只是故障恢复成功的条件之一。

最后给方案附上简短 ADR 与复测计划:为何采用当前模式、比较过哪些选项、接受哪些代价、何时重审。扩容前检查正确性和依赖预算,扩容中观察业务与系统指标,扩容后确认稳定与成本。撤回应停止继续放量,排空在途请求后逐步减少副本;涉及数据分布变化时,则必须另行设计数据恢复与回切,不能照搬应用缩容步骤。

9 从扩展性模式走向缓存架构(P16)

沿着本课的路径,应用副本增加后,商品信息、价格与订单查询可能成为反复访问数据库的主要来源。此时自然会出现问题:相同内容为什么每次都重新读取?缓存通过保存可复用结果,减少部分重复工作,可能降低延迟与后端负载。它是扩展路径中的候选手段,但收益取决于哪些数据会被重复读取,以及这些结果可以复用多久。

零售订单 API 中,商品描述通常比库存余量更容易复用;顾客刚创建的订单状态,也与历史商品介绍有不同的新鲜度要求。若缓存中的价格过期,结算时应依据明确的价格规则重新确认;若库存展示允许短暂延迟,真正下单仍须执行可靠的预留判断。不能为了提高命中率,把业务必须检查的事实直接替换成旧结果。

缓存还会带来自己的容量与故障问题。大量请求同时读取某个热门商品,缓存可以减少数据库访问;但缓存失效或不可用时,这些请求可能突然回到数据库。若容量方案把数据库预算建立在稳定的高命中率上,就必须说明低命中率时如何保护订单链路。缓存不是消除后端容量需求,而是改变通常情况下的访问分布,也改变异常时的压力形态。

进入 P16 前,先整理候选清单:数据来源、读取频率、更新频率、允许陈旧时间、错误影响以及回源成本。对本课案例,可以先选择商品描述这类边界清楚的数据研究缓存,再逐步讨论更敏感的价格和订单状态。下一课将围绕缓存位置、命中与失效建立基本模型,并把缓存故障重新接回本课的容量、成本与高可用方案。

放进方案里

在零售订单 API 方案中,将扩展目标写成带条件的承诺:促销目标为每秒 800 个请求,创建订单占 20%,端到端 p95 不超过 500 毫秒,失败比例低于 0.5%;该目标需要在约定数据规模与依赖延迟下验收,不能从课堂演示数据直接推定已达标。重复购买请求只生成 1 个有效订单,作为独立的正确性要求。

当前选项采用无状态应用水平扩展,订单事实与幂等结果可靠保存,会话通过共享存储跨副本读取。ADR 记录选择理由、总连接预算与暂不分片的原因。垂直扩展作为短期备选,数据分片保留到单个数据单元经优化仍不能满足目标时再评审;允许延迟的读分流另行说明一致性边界。

容量表列出副本数、达标吞吐、延迟、依赖等待与每小时费用,并预留实例或可用区故障后的容量。促销前完成预热与分阶段压测,活动中按持续指标扩容,同时限制最大副本数。若新增副本后数据库等待上升而完成量不再增加,停止继续扩副本,转入查询、事务和依赖检查。

扩容验收保留同组负载下的前后对照,逐实例记录请求量,确认新增副本确实承接流量;同时核对全部副本的连接上限之和。证据附压测脚本版本、热门商品占比及慢请求链路。若商品集中度或库存接口延迟越过已测范围,先重测再调整容量承诺。

风险清单覆盖会话粘滞、连接总量、重复下单与共享存储故障。验收包括响应丢失后的重试、跨副本会话、实例退出、可用区故障和压力回落后的恢复。上线先放少量流量,观察成功订单、延迟和待补偿量;异常时停止放量,排空请求后撤回应用变更。最后标注责任人、证据位置与复测日期,并将商品查询缓存列为 P16 的候选改进。

10 小结:下一步缓存架构(P16)

扩展性模式把增长需求转换为资源、状态和数据边界的选择。垂直扩展增加单个单元的能力,水平扩展增加处理单元,分片改变数据与工作分布;每种方式都有适用条件,也会带来新的限制。订单 API 需要先保证跨副本正确处理请求,再用测量决定下一步动作。

自测时可以回答:应用 CPU 降低但订单延迟升高,下一步观察哪里?4 个副本各自配置 40 个连接,总连接预算是多少?响应丢失后重试,为何必须复用幂等键?失去 1 个可用区后,剩余容量是否仍满足业务要求?这些问题能把扩容操作重新连接到可评审的方案。

下一课 P16 进入缓存架构:从可复用的数据开始,分析命中收益、陈旧边界与回源压力,并继续使用同个零售订单 API 验证成本与风险。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务