1 大规模架构模式要认识哪些
订单服务最初可能只有 1 个应用和 1 个数据库:用户提交订单,应用检查身份、计算金额、保存订单,再返回结果。随着业务增长,麻烦未必首先表现为机器不够快。商品查询挤占下单连接,通知系统故障拖慢支付结果返回,某个大客户批量导入让其他客户超时,部署小改动却需要整个系统停机。架构模式讨论的正是这些责任如何分开、变化如何传播、故障如何限制的问题。
模式是反复出现的问题与组织方式之间的对应关系,不是采购清单,也不是规模认证。分层帮助组织职责,事件驱动帮助分离处理时机,CQRS 帮助分开修改与查询的模型,多租户隔离帮助约束共享资源的影响范围。它们可以组合,但每增加 1 种机制,就要解释它解决哪个已经存在或可预见的问题,以及谁来承担新增的运行责任。
先为服务画出基础云资源:入口把请求送到计算实例;身份系统确定请求来自谁;私有网络和访问规则限制服务能到哪里;数据库保存业务状态;日志、指标与追踪说明系统正在做什么。架构模式建立在这些基础能力上。增加消息队列不会替代身份验证,拆成多个服务也不会自动得到网络隔离。每条新连接都需要明确调用身份、访问范围、超时与可观察证据。
判断“大规模”也不能只看请求数。相同的每秒 1000 次请求,可能是均匀查询,也可能集中修改少量热门商品;相同的 100 个租户,可能规模相近,也可能有 1 个租户占据大部分数据库连接。应同时看流量形状、数据增长、团队协作和故障影响。模式的选择应由瓶颈和约束驱动,而不是由一个抽象规模门槛驱动。
本课采用轻量订单 API 作为例子。下单必须可靠保存,订单列表可以稍晚更新,通知失败不能撤销已经成功保存的订单,租户之间不能互相读到数据。这些要求分别约束同步边界、异步边界和隔离边界。先把业务承诺说清楚,才能决定哪些步骤必须一起成功,哪些允许之后完成,哪些失败应该局部处理。

读图时不要只数方框。入口到应用的连线代表认证与限流责任,应用到数据库的连线代表写入权限与一致性边界,事件到消费者的连线代表重试与积压责任。后续每次引入模式,都要重新检查这些连线。真正能指导部署和运维的架构图,既说明正常请求如何通过,也说明故障在哪里停止传播。
2 分层、事件驱动与 CQRS
分层先解决职责混杂。接口层处理请求格式、身份上下文与响应;业务层判断订单能否创建、金额如何计算;数据访问层负责读写持久化状态。层可以存在于同一个部署包内,不必各自成为网络服务。把层直接等同于服务器,会过早增加部署、鉴权和超时管理成本。初学时更应关注依赖方向:业务规则不应被某个接口格式或存储驱动绑住。
分层也不是让请求机械地经过更多跳数。订单业务层应接收已经验证过的租户和用户上下文,而不是相信客户端传来的租户编号。数据访问层仍须把租户条件带入查询,避免某条新接口绕过过滤。每层有清楚的输入、输出和错误含义,才能在更换入口、扩容计算实例或迁移数据实现时,减少其他部分的变化。
事件驱动把“已经发生的事实”交给其他组件处理。例如,订单保存后产生“订单已创建”事件,通知消费者发送消息,统计消费者更新报表。生产者不必等待所有消费者完成,消费者也能独立扩容。但异步不会让工作消失,只会把等待从请求链路转移到队列。队列里停留的时间成为用户体验的一部分,因此必须说明通知和报表允许延迟多久。
这里必须区分命令和事件。“创建订单”是请求,可能被拒绝;“订单已创建”是事实,说明某个状态变化已经成功提交。若数据库写入失败却发布了成功事件,消费者会根据不存在的订单采取行动。若数据库成功而事件丢失,下游则永远不知道这次变化。入门方案应明确数据库记录和待发送事件如何一起保留,例如同事务写入待发送记录,再由后台投递;具体实现细节留待后续展开。
CQRS 指命令与查询职责分离。命令侧负责修改业务状态和检查规则,查询侧提供适合读取的表示。订单写入表可能强调状态约束,订单列表则需要按用户、时间和状态快速查询。两者可以先共用数据库,仅在代码和接口上分离;当读取压力或模型差异足够明显时,再增加独立查询存储。CQRS 不要求必须使用事件,也不等于把全部历史事件作为唯一事实来源。

设想用户刚创建订单就刷新列表。命令侧已经保存成功,查询视图却可能尚未更新,这称为最终一致性:系统正常推进时,各表示会在之后收敛。接口可以返回订单编号和当前状态,让详情页暂时读取权威状态,或明确提示列表正在更新。不能让列表暂时没有记录被误解为下单失败,否则用户可能重复提交。新增查询模型时,延迟说明和重复请求处理必须同时设计。
3 多租户与隔离边界
租户是共享系统中需要独立管理的组织或客户单位,不一定等于某个登录用户。零售平台可以让每家商户成为 1 个租户,每个商户又有多个员工账号。身份验证回答“是谁”,授权回答“能做什么”,租户边界回答“在哪个组织范围内做”。服务端应根据已验证身份确定租户上下文,检查用户是否属于该租户,再把这个上下文传递到数据和任务处理路径。
常见部署方式包括共享应用与共享数据表、共享应用但按租户分开数据库,以及按租户部署独立资源。共享程度高通常更节省基础资源,也更容易统一升级;独立程度高通常更便于限制故障影响和安排差异化变更,却需要管理更多实例、连接、备份和版本。选择时要同时考虑数据隔离要求、租户规模差异和运维能力,不能仅凭“独立更安全”就忽略长期维护责任。
隔离至少有数据与资源 2 个角度。数据隔离要求任何查询、缓存命中、对象下载和后台任务都不能越出租户授权范围。资源隔离要求某个租户的大量工作不能耗尽全部连接、处理槽位或队列容量。例如数据库查询带租户过滤并不能防止大客户占满连接池;每个租户有独立队列也不能保证数据库不被共同压垮。隔离边界需要沿实际共享资源逐层检查。
缓存键、对象路径和异步消息特别容易遗漏租户信息。若缓存仅以订单编号作为键,而编号只在租户内唯一,就可能返回其他租户的数据。消费者收到消息后同样需要验证可信的租户上下文,不能因为任务来自内部队列就使用无限权限。对象存储的目录名称主要帮助组织内容,是否允许读取仍应由访问策略决定,不能把路径难猜当作授权机制。
资源方面,可以给租户设置请求速率、并发任务数和存储配额,并保留平台总量保护。限制不能只放在入口,因为少量请求也可能触发耗时很长的任务。队列消费者需要公平调度或并发约束,数据库访问需要连接与查询成本控制。对超过配额的情况,应返回明确结果或进入受控等待,避免客户端不断重试,把限制变成更大的负载。

观察隔离效果时,要同时看租户维度和平台维度。某租户失败率上升可能是自身输入问题,也可能是共享数据库被其他工作拖慢。把租户上下文关联到请求追踪和受控日志有助于排查,但不要把数量持续增长的租户编号无差别放进所有指标标签,否则监控系统本身也会产生容量压力。隔离方案应连同观测方式一起评估。
4 动手:为一种模式画失败路径
选择“订单创建后异步发送通知”作为练习。先在纸上或绘图工具中标出客户端、订单 API、订单数据库、待发送记录、队列、通知消费者和通知渠道。用实线画成功路径,再逐段标出故障。练习目的不是证明流程总会成功,而是让每个中断都有可解释的状态、恢复动作和检查证据。这里只使用测试数据,避免把失败演练变成真实用户的重复通知。
首先写出承诺:API 返回成功表示订单和待发送记录已经持久保存,通知可以延后;客户端重试同一次创建操作不能产生重复订单。然后给请求配置稳定的幂等标识。幂等表示同一个业务操作重复执行时,不应反复产生新的业务效果。服务端需要记录标识与结果的关联,并校验重用标识的请求内容,不能只靠客户端“尽量不要重试”。
第 1 个中断点放在数据库提交之后、API 响应之前。此时客户端看到超时,但订单可能已经创建。客户端应携带原标识重试或查询结果;服务端返回已有结果。观察证据包括对应的订单编号、请求标识和提交记录。超时只说明调用方没有及时得到结果,不能直接解释为业务失败。这也是绘制失败路径时最需要改变的直觉。
第 2 个中断点放在事件投递之后、发送确认记录之前。后台投递者恢复后可能再次发送同一个事件,因此消费者要依据事件标识识别重复。处理记录和业务效果如何协调必须写清;如果通知渠道支持幂等键,就使用稳定的业务键。如果外部渠道不提供这种能力,应如实说明重复通知风险与人工处置办法,不能把消息确认当成外部效果绝对只发生 1 次的证明。
第 3 个中断点放在消费者处理失败。暂时性网络故障可以按间隔退避重试,并设置尝试次数或总时长上限;格式错误、缺少必要字段等永久性问题通常不应无限重试。超过上限的消息进入待处理区,保留失败原因、事件标识和租户上下文,供修复后受控重放。队列确认应与实际处理结果匹配,提前确认会让失败任务失去恢复入口。
最后为每个中断点填写“用户看到什么、数据实际是什么、由谁恢复、如何证明恢复完成”。在测试环境暂停消费者,可以观察积压与消息年龄增长;恢复后检查处理速率是否超过新增速率,并确认同一事件重投不会重复改变订单状态。练习通过的标准包括证据和恢复结果,不能只以进程重新启动为准。若无法说清某个消息的最终去向,就还没有完成失败路径设计。
5 模式收益与运维成本取舍
模式收益必须对应具体目标。分层可以降低修改业务规则时牵连接口和存储实现的概率;事件驱动可以缩短同步等待,并吸收短时流量波动;CQRS 可以让查询表示按读取需求优化;租户隔离可以缩小资源争用和数据访问的影响范围。描述收益时应写出可观察的变化,例如通知渠道故障时,下单成功率仍保持目标,而不是只写“更灵活、更高性能”。
成本也要按完整生命周期计算。增加队列后,需要管理容量、消息保留、失败重试、积压告警与重放;增加查询存储后,需要管理数据同步、视图重建和差异核对;增加独立租户资源后,需要管理部署、权限、备份和升级批次。账单只是其中一部分,值班复杂度和排查时间同样影响系统能否持续运行。团队无法操作的机制很难兑现图上的可靠性。
用简单估算观察队列行为。假设平均每秒进入 120 条消息,每条处理平均占用 0.1 秒,理想情况下 1 个消费者每秒约处理 10 条,12 个消费者仅能追平新增消息,没有清理历史积压的余量。若测试证明下游允许 20 个消费者并行,理想总处理能力约每秒 200 条,净清理速度约每秒 80 条,积压 4800 条约需 60 秒。实际还会受重试、长尾延迟和共享数据库限制,估算应再用压测校正。
这个例子说明,队列吸收波峰需要容量和排空时间共同支持。增加消费者只有在下游还有余量时才有效,超过数据库连接或外部渠道限额可能让失败率反而上升。用户允许通知延迟 5 分钟,并不表示任何积压都可以等待 5 分钟后再看;应观察最老消息年龄和增长趋势,给修复预留时间。单看队列长度也不够,因为处理速度不同,相同长度对应的等待时间可能相差很大。
取舍时可以先列出最小方案与增强方案。若单个应用通过职责分层、索引调整和计算扩容已经达到目标,就暂不引入独立查询存储。若查询和写入确实需要不同容量,再记录拆分收益、同步延迟和故障恢复要求。迁移可以分阶段进行,先建立接口边界和指标,再增加运行组件。每个阶段都应有判断是否继续的条件,避免投入越多越难承认模式并不适用。
6 常见坑:为用模式而用模式
第 1 类坑是把部署数量当成架构成熟度。把原本清楚的应用拆成多个服务后,如果每次下单仍同步依赖所有服务,那么任意依赖变慢都可能拉长完整请求。网络调用还增加鉴权、超时和版本兼容责任。边界应围绕业务职责、数据所有权和独立变化需求来划分;只有在独立部署与扩缩容确有收益时,才值得承担分布式运行成本。
第 2 类坑是“全部异步”。用户需要立即知道订单是否被接受时,接口必须准确表达已经承诺的事情。返回“已受理”只能说明任务进入可靠处理流程,不能冒充“订单已创建”。付款、库存和订单之间存在业务约束时,也不能靠多发几条事件自动获得整体一致性。入门阶段应先缩小需要同步保证的范围,明确失败后的状态与补救责任,再考虑更复杂的跨服务协调。
第 3 类坑是把消息重试理解为可靠性全部。没有稳定标识,重试可能重复扣减;没有次数与时间上限,错误消息会持续占用容量;没有失败记录,问题被移走后无人知道;没有重放权限控制,修复动作可能放大影响。可靠性来自持久保存、可识别重复、受控重试和可操作恢复的组合,任何 1 个单点措施都不能独自承担全部保证。
第 4 类坑是为了读写分离复制所有数据。查询视图应该只包含授权查询需要的内容;复制越多,敏感数据暴露面、同步成本和删除责任越大。某个字段更新不频繁,也不表示可以永久保留旧值。需要说明视图的权威来源、刷新时机和过期处理,并确保查询侧继承租户权限。读得快却读到不该读的数据,不是可接受的优化。
第 5 类坑是共享系统只设平台总限额。平台总负载看似正常,某个租户仍可能长期抢不到处理机会;反过来,给每个租户的额度简单相加,也可能超过平台总容量。需要同时设置公平性规则和总量保护,并为批量任务安排可暂停、可续跑的处理方式。管理操作也必须经过身份与审计,不能成为绕过所有隔离的隐藏入口。
这些坑的共同信号是方案里名词很多,承诺却含糊。评审时可以要求作者删除模式名称,只描述输入、状态变化、允许的延迟和失败后的动作。如果删去名称就无法解释系统如何运行,说明理解还停留在组件拼装。反过来,即使方案只用少量组件,只要边界明确、证据充分、恢复可执行,也可能比复杂拓扑更适合当前业务。
7 案例:事件风暴无回溯能力
某订单平台把通知和报表改成异步处理。上线早期,接口响应更快,队列也很短。之后批量导入产生大量订单,某个消费者又因字段不兼容持续失败;失败立即重试,重试继续触发调用,外部渠道开始限流。处理能力下降,更多消息等待,重试流量继续增长。这里的事件风暴来自输入突增、失败反馈与无界重试叠加,不能仅归因于队列容量小。
值班人员首先增加消费者数量,但共享数据库连接很快耗尽,订单 API 也出现超时。随后为降低负载清空了部分队列,积压指标恢复正常,业务却发现大量通知未发送、报表缺少订单。系统只保存了最近的运行日志,没有完整事件记录,消息保留时间也不足。清空操作降低了眼前压力,同时删除了尚未处理的工作,导致无法根据事件可靠补回下游结果。

排查应从业务事实开始:哪些订单已经保存,哪些通知确实成功,哪些事件还在队列或待处理区。用订单编号和事件标识关联发送记录、消费结果与失败原因,分别统计未投递、未处理和处理结果不确定的范围。若缺少这些证据,就必须承认无法直接重放;可以在授权范围内依据订单权威数据生成补偿任务,并处理已经发送却没有记录的重复风险。
恢复时先控制新增压力,暂停非必要批量导入,限制重试和消费者并发,给正常下单保留资源。修复字段兼容问题后,用少量事件验证,再按受控速率补处理。不能把所有历史数据一次性推回队列,否则可能再次触发同样的资源争用。恢复过程需要观察最老消息年龄、处理成功率、下游限流和数据库余量,并安排停止条件。
“有回溯能力”不等于永久保存所有事件。需要根据发现问题所需时间、业务补处理窗口和数据保留要求,决定保留哪些事实、保留多久、谁能读取及重放。重放既需要输入记录,也需要可识别重复的消费者、兼容的事件格式和可核对的结果。只保留一堆消息而没有恢复步骤,同样无法保证可用;事件历史也不能替代数据库备份。
事后整改应覆盖触发链,而不是只扩大队列:为批量任务设置入口配额,为消费者设置退避与上限,为失败事件保留原因和负责人,为关键事实设置足够的保留窗口,并定期演练少量重放。最后用业务核对确认缺失已经补齐。积压归零、进程恢复与业务恢复是不同状态,只有最后一种能说明这次事故对用户的影响已经处理完成。
8 把模式适用条件写进方案
架构方案中的每种模式都应对应一段可核查的适用条件。先写问题证据:哪个接口或流程受影响,当前延迟、失败率或资源争用是什么,为什么现有边界无法满足目标。再写约束:哪些状态必须立即可靠,哪些可以延迟,允许延迟多久,是否存在租户隔离和数据保留要求。这样读者才能判断引入模式是解决问题,还是提前建设暂时用不到的能力。
接着写采用范围。若只把通知异步化,就说明订单创建仍由原事务确认,通知状态独立追踪;若只为列表建立查询视图,就说明它覆盖哪些字段和查询,以及权威状态在哪里。范围越清楚,越容易识别越界扩张。不要用“未来全部事件化”代替设计,也不要把局部查询需求升级为所有存储都必须重新组织的理由。
然后写运行合同,包括组件间身份、网络访问、事件字段、超时、幂等标识和错误分类。对于多租户系统,还要说明租户上下文从哪里产生,如何传递,在哪些位置再次校验。合同不需要把高级协议细节全部展开,但必须足以支持实现者知道何时确认成功、何时拒绝、何时重试,以及需要留下哪些关联证据。
容量和运维条件同样属于适用条件。给出基于测试的处理能力、允许积压、保留时间和下游限额,标注估算假设。指定谁接收告警、谁处理待处理区、谁批准和执行重放。若尚无人员承担这些责任,就应把它记录为采用阻碍,而不是把“之后补监控”藏进上线后的待办。模式收益必须有相应的操作能力支持。
最后写退出和演进条件。例如查询负载持续低且同步维护成本过高时,可以评估合并查询存储;租户资源争用长期超过共享方案能力时,可以把特定租户迁入独立资源。数据迁移、路由切换和回退需要单独准备,不能假设模式选定后永远正确。给出复评条件,有助于团队在业务变化后调整方案,而不把历史选择当作不可更改的规则。
评审可以围绕 4 个问题展开:收益用什么指标证明,故障怎样限制范围,恢复怎样确认完整,新增责任由谁承担。回答时引用压测、演练或业务约束,而不是引用“行业常见做法”作为唯一依据。成熟模式提供了讨论起点,真正的适用性仍需要本系统的证据。方案越能说明边界和假设,后续排查与迁移就越有依据。
9 从大规模架构模式走向多区域容灾(C47)
本课的模式主要帮助系统在职责、处理时机和共享资源之间建立边界,却不能自动抵御整个区域不可用。队列、数据库和应用即使各自有冗余,如果都依赖同一区域的网络或身份入口,区域故障时仍可能共同失效。进入多区域设计前,应先把已有组件及其依赖所在位置标清,避免把多个实例误认为多个独立故障域。
失败路径会成为容灾设计的输入。订单事务提交后事件仍未投递时,区域切换需要考虑待发送记录是否可恢复;查询视图落后时,需要知道能否从权威数据重建;消费者切换到另一区域后,可能重新遇到已经处理的事件,因此幂等标识不能只在短暂进程内保存。局部故障时没说明白的状态,在跨区域切换时通常会更难判断。
多租户也会影响容灾次序。不同租户可能有不同恢复要求,但共享数据库和共享队列未必能独立切换。方案需要清楚描述恢复单位,而不能承诺每个租户都能任意独立恢复。独立资源可能缩小切换范围,却增加复制、部署和配置管理工作。此时仍应沿用本课方法:从业务承诺出发,再判断隔离与共享的取舍。
准备进入 C47 时,可以先整理每类数据的权威来源、允许丢失的时间范围和允许中断的时长。后者通常分别用恢复点目标和恢复时间目标表达;它们是业务目标,需要通过复制、备份、切换和演练落实。不要先指定某种多区域拓扑,再倒推业务必须接受什么结果。下课将把这些目标与区域故障、流量切换及数据恢复联系起来。
带去下一课的最有用材料,是本课画出的失败路径和状态清单:哪些步骤已承诺成功,哪些工作还在等待,哪些结果不确定,以及如何核对。只要这些信息可靠,多区域讨论就有基础;若这些信息缺失,新增区域也可能只复制同样的不确定性。先把单个边界上的恢复责任说清,才有条件扩大故障范围。
放进方案里
本服务以订单数据库为业务状态的权威来源,采用应用内分层组织接口、业务规则与数据访问。订单创建成功表示订单及待发送事件已经持久保存;通知异步处理,不作为下单成功的前置条件。当前查询负载通过现有数据库满足,暂不增加独立查询存储;当列表查询持续影响写入目标且优化无效时,再评估 CQRS,并明确查询延迟与视图重建方式。
租户由已验证身份和成员关系确定,所有订单查询、缓存键、对象访问及异步任务携带可信租户上下文。应用使用受限身份访问私有数据资源,分别设置租户请求与任务并发额度,并保留平台总量保护。共享资源上的连接争用与处理公平性纳入容量测试;超过额度时返回明确结果或进入受控等待,不依赖无上限重试消化压力。
事件带有稳定标识、订单编号、租户上下文和格式版本。消费者按事件标识识别重复,暂时性错误采用退避重试,超过上限进入待处理区并告警。保留窗口覆盖问题发现和补处理所需时间,具体值由业务要求与存储预算确定。重放由指定运维角色执行,先验证少量事件,再限制速率推进;涉及外部通知时核对渠道幂等能力并记录重复风险。
上线前验证数据库提交后响应丢失、重复投递及消费者暂停等故障,确认订单不重复创建、积压可排空、缺失结果可核对。持续观察下单成功率、查询延迟、最老消息年龄、失败事件量及共享数据库余量。值班人员负责告警与止损,服务负责人负责补处理和业务核对。容量超过测试边界或恢复演练不通过时,限制批量任务并暂停扩大异步范围;区域故障恢复目标交由 C47 继续设计。
10 小结:下一步多区域容灾(C47)
分层划分职责,事件驱动分开处理时机,CQRS 分开修改与读取需求,多租户设计明确数据和资源边界。它们的价值取决于适用条件,也取决于团队是否能观察、限制和恢复失败。
自测时试着回答:API 超时后为什么不能直接重建订单?队列长度归零为什么不代表业务完整?增加消费者在什么条件下会让故障加重?若能结合权威状态、稳定标识、共享资源和恢复证据解释这些问题,就可以继续学习 C47,把同样的方法扩展到区域失效与多区域容灾。