C36

微服务架构

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

课内目录

1 微服务架构要回答什么

用户在商店点击“提交订单”,看到的是订单能否成立;系统内部却可能同时涉及订单记录、库存预留、支付尝试和通知。把这些代码放在同一个应用里,可以完成业务;把它们拆成多个独立运行的服务,也可以完成业务。微服务架构首先要回答的不是拆成多少个进程,而是哪些业务能力值得独立负责,以及独立之后如何继续可靠协作。

这里的服务是围绕明确业务能力组织、通过契约交互、能够独立部署的软件单元。独立部署意味着修改库存预留规则时,可以在兼容已有调用方的前提下发布库存服务,不必要求订单服务同时更新。它并不意味着服务完全没有依赖,也不意味着每个服务只能有很少的代码。判断边界的依据是规则是否内聚、数据由谁解释、变更是否经常一起发生。

想象商店正在促销。浏览商品的流量很大,下单流量较小,支付结果又依赖外部处理。如果整个应用只能整体扩容,增加支付处理能力时也会复制许多无关组件。独立服务可能让扩容和发布更有针对性。但新增的网络通信、部署配置、权限管理和故障排查都需要人负责。只有独立带来的收益超过协作成本,拆分才有意义。

订单业务中的服务与云资源责任
订单业务中的服务与云资源责任

云平台提供运行计算、私有网络、身份权限、数据库和监控等基础能力,但不会自动决定服务边界。订单服务可能运行在容器或其他计算环境中,通过服务身份访问获准的数据资源,通过网络规则接受指定调用方的请求。运行实例可以增加或重建,因此关键业务状态应持久保存;某个实例消失,不能让订单的唯一记录也一起消失。

还要区分业务边界与故障边界。库存服务独立部署后,库存进程崩溃不必直接终止订单进程;但如果订单的每个请求都无限等待库存,故障仍会沿调用关系传播。多个服务共用同一个数据库实例,也可能受相同的数据库故障影响。图上的方框只表示结构,真正的隔离还依赖等待上限、资源限制、数据访问和恢复流程。

对于规模较小、由同一个团队维护的业务,先在单个应用中建立清晰模块通常更容易。模块之间通过接口访问,禁止任意修改彼此的数据,已经能解决不少协作问题。等到某项能力确实需要独立发布、独立扩容或明确责任时,再把进程边界移出去。微服务是支持组织和业务演进的选择,不是应用成熟度的统一等级。

2 服务边界、契约与数据所有权

划分边界可以从业务问题开始:谁有权回答“这件商品还能预留多少”,谁决定“订单现在能否取消”,谁解释“这次支付是否已经成功”。库存服务负责可售数量、预留及释放规则;订单服务负责订单生命周期;支付服务记录支付尝试与支付结果。商品名称等展示信息可能被多处使用,但某个数据出现在哪里,并不能直接决定谁有权修改它。

数据所有权是指某个服务负责数据含义、校验规则和写入入口。其他服务需要改变库存时,应请求库存服务执行预留,而不是直接执行库存表更新。否则库存服务即使写出了正确规则,也可能被旁路写入破坏。独立所有权不强制每个服务租用单独的数据库实例;入门阶段可以共用基础设施,同时分离逻辑空间、访问账号和写入权限,并明确共享设施的故障影响。

契约跨越边界,写入权留在服务内部
契约跨越边界,写入权留在服务内部

契约不只是接口路径和字段类型。预留接口至少要说明商品标识、数量、业务关联号、幂等键,以及成功后返回的预留标识。还应解释库存不足、请求格式错误和暂时不可用分别代表什么,预留是否会过期,超时后如何查询。调用方需要依据这些语义决定下一步,不能看到所有非成功响应都机械重试。

幂等键用来识别同一个业务操作的重复请求。例如订单为某次预留生成固定的操作标识,网络超时后再次提交时继续使用它。库存服务记录该标识对应的结果,避免同一次预留重复扣减。相同键携带不同商品或数量,应当被识别为冲突,而不是静默复用结果。键的作用范围、保留期限和结果查询方式也属于契约。

服务可能需要其他领域的数据来展示页面。例如订单详情需要展示商品名称,可以保存下单时的名称快照,也可以维护供查询使用的本地视图。快照表达成交时的信息,本地视图则要说明更新时间与允许的陈旧程度。复制数据并不等于转移修改权;订单服务不能因为持有库存视图,就据此自行承诺预留成功。

版本变化同样会检验边界。增加可选字段通常比立刻移除旧字段更容易兼容;但字段形式兼容,也可能发生含义变化,例如把金额从含税改成不含税。发布前应检查现有调用方能否继续正确解释结果,为迁移保留过渡期。服务如果每次发布都要求所有调用方同时上线,其独立部署能力就很有限。

3 同步调用与异步协作边界

同步调用是调用方发出请求后等待结果,再决定当前操作如何继续。订单需要确认库存是否已预留,通常适合使用能明确返回业务结果的交互。它的优点是流程直观,代价是调用方的响应时间和成功机会受被调用方影响。必须把网络失败、远端处理失败和业务拒绝分别理解,它们对应的处理动作不同。

异步协作把发出消息和完成业务处理分开。订单确认后,通知服务可以稍后发送提醒,用户不必等待短信或邮件通道完成。生产者把消息交给消息通道,并不代表消费者已经处理成功;接口若返回“已受理”,也不能向用户展示“业务已完成”。系统需要让用户查询状态,并为迟迟没有推进的任务提供处理办法。

订单关键路径与异步通知的完成时点
订单关键路径与异步通知的完成时点

消息也可以表达不同含义。“请释放这份预留”是请求某项动作的命令;“订单已经确认”是说明事实的事件。事件接收者可以据此采取自己的动作,但不能把已经发生的事实当作任意修改的共享记录。命名应清楚表达完成时点,避免把“准备确认订单”与“订单已确认”混用,否则订阅者会对业务状态产生错误判断。

异步并没有消除失败,只是改变了等待和恢复的位置。消费者可能重复收到消息,也可能暂时停机,因此需要按业务操作去重,成功处理后记录结果,并能重新处理失败任务。对同一订单有关联的事件,还应检查状态或版本,避免较旧的消息覆盖较新的状态。不要默认所有消息都会严格按期望顺序到达。

订单写入成功而事件没有发出,是常见的间隙。入门阶段应先掌握原则:把业务状态和待发送事件意图在同一个本地事务里持久记录,再由后台过程发布;发布过程允许重试,消费者允许重复。这样即使发布进程重启,也能找到尚未完成的发送工作。具体消息中间件内部实现与更复杂的投递保证留到后续学习。

选择同步还是异步,应先问当前动作需要什么承诺。库存确认若是下单成立的前提,就不能仅因为异步更快而把排队成功当作预留成功;通知通常允许延后,适合离开下单关键路径。如果业务接受“订单处理中”,也可以异步完成库存确认,但页面、客服、取消规则和超时处理都必须理解这个中间状态。交互方式要服从业务语义。

4 动手:画一条跨服务请求的失败域

现在做纸面实验,不需要先搭建集群。画出入口、订单服务、库存服务和库存数据库,标出调用方向与各自持久保存的状态。入口给下单请求分配 1200 ms 总等待预算;假设校验和写入等步骤也要消耗时间,订单对库存的本次尝试最多等待 300 ms。这是练习设定,不是通用配置值。每条箭头旁写清楚“等待谁”和“到期后怎么办”。

先走正常路径:订单记录待确认状态,库存完成本地预留并返回结果,订单推进为已确认。然后只改动返回路径:库存已经提交预留,但响应在返回途中丢失。订单看到超时,只能确定自己没有及时得到结果,不能据此断言库存没有执行。把“结果未知”画成独立状态,是本实验最重要的动作。

为未知状态补上恢复路径。订单用原来的幂等键查询库存结果;查询暂时不可用时,可以保持待确认并由后台继续核对。用户已取消而库存最终预留成功时,需要显式发起释放。释放可能再次失败,所以要记录其进度并重试或转人工处理。跨服务协作的可靠性,依赖这些不顺利时仍有出口的路径。

继续标出失败影响范围:库存实例退出会影响哪些请求,数据库慢查询会占用哪些连接,订单等待时是否占用工作线程或其他并发资源。即使服务进程相互独立,共享连接池、热点数据和共同基础设施也可能让影响扩散。给等待设置上限,还要给并发数量和排队长度设置合理限制,避免所有资源被等待中的请求耗尽。

重试应同时受剩余预算、尝试次数和操作语义约束。参数错误和库存不足通常不因立即重试而改善;暂时的连接失败才可能值得有限重试。避免入口、订单客户端和库存客户端各自多次重试,否则同一次用户操作可能产生许多下游请求。退避和随机错开可以减轻同时重试造成的压力,但不能代替幂等与容量控制。

用日志、指标和追踪关联请求结果
用日志、指标和追踪关联请求结果

最后为图上的结论指定证据。用追踪标识连接跨服务调用的时间,用业务订单号和尝试号查询具体状态,用指标观察超时率、等待时长和资源占用。追踪采样可能遗漏某次请求,因此业务结果仍要能从持久记录核实;日志也不应包含支付凭据等敏感内容。实验合格的标准是:响应丢失后,你能解释实际状态、下一步动作及验证它的记录。

5 拆分粒度与事务取舍

把每个数据库表都变成服务,通常会让原本简单的操作变成长调用链。订单头、订单明细和订单金额校验经常一起变化,还需要保持紧密规则,未必值得分别部署。反过来,库存预留与通知发送有不同的扩容需求、失败容忍度和业务责任,把它们长期绑在同一个发布单元里,也可能妨碍演进。粒度应由变化与规则共同决定。

可以观察真实变更:修改某项业务规则时通常涉及哪些代码,发布记录中哪些组件总是一起上线,故障时哪些状态必须同时修复。如果多数变更都跨越同样的服务边界,说明边界可能划错,或者契约过于暴露内部细节。代码行数和接口数量可以作为描述,不能单独充当拆分阈值。

独立服务越多,并不必然让每个用户请求更复杂;决定请求风险的是它实际依赖的路径。若请求必须串行经过 3 个关键依赖,每个依赖在时限内成功的概率都按 99.5% 估算,并假设彼此独立,那么全部成功约为 98.51%。现实中的公共网络、数据库或部署故障还可能让失败相关,因此这个算式只是帮助识别关键依赖。

数据拆开后,原本由数据库本地事务保证的原子性可能跨越了服务边界。在单个数据库事务中,相关修改可以一起提交或回滚;订单和库存分别提交后,订单回滚不会自动撤销库存预留。如果业务规则要求某组状态必须立即一起变化,又无法接受中间状态,应先考虑把这组规则保留在同一个服务的本地事务内。

若业务允许分阶段完成,就可以设计明确的状态推进与补偿。先创建待确认订单,再预留库存,成功后确认订单;失败则取消订单并释放已经持有的资源。补偿是新的业务操作,不是把时间倒回去:通知已经被用户看到就无法“撤回未读”,退款也不等于支付从未发生。每个补偿动作都需要说明能否重试、如何判断完成。

“最终一致”还需要限定范围与时间。应写明哪些数据暂时不同、通常何时追平、超过期限谁处理。例如订单已取消但库存释放处理中,需要暴露待释放记录,并对超出目标时长的任务告警。不能用最终一致解释无限期差异。涉及严格一致性或复杂跨库提交的高级机制,本课不展开;先把本地事务、中间状态和恢复责任说明白。

6 常见坑:共享库伪装成解耦

共享库可以减少重复代码,例如统一日志字段、提供协议客户端或复用通用编码工具。问题出在把共同业务规则塞进共享库,再由多个服务各自执行。假设订单和库存服务都引入同一个库存规则库,分别直接修改库存表,那么规则虽然看似复用,数据修改权却散布在多个进程,升级时也可能出现不同版本同时运行。

更隐蔽的情况是共享领域对象。库里某个字段变化,订单、支付、通知都必须重新编译并一起发布;每个服务都“独立部署”,发布日历却完全绑定。这时要检查共享内容究竟是稳定的数据交换格式,还是其他服务的内部模型。契约可以表达必要信息,但不应要求调用方理解对方所有内部状态与数据库结构。

生成的接口客户端也需要版本治理。新服务端上线前,应验证已有客户端还能调用;客户端升级也要能面对尚未升级的服务端。共享库版本相同不能证明运行时契约兼容,版本不同也不必然不兼容。评估重点是请求与响应的语义、必填约束、错误行为和迁移安排,而不是仓库中是否统一了依赖号。

另一个常见误区是共享数据库写权限。团队可能为了少写接口,允许订单直接修库存表,允许通知直接改订单状态。短期调用少了,长期却难以解释谁负责校验、谁能变更表结构、审计记录由谁生成。应把跨边界修改收回到所有者提供的操作契约,并通过账号权限限制旁路访问,避免规则只写在文档中。

运营查询确实可能需要跨领域数据。可以构建供报表使用的汇总视图,标明数据来源与刷新延迟;报表视图不应成为修改生产业务状态的后门。紧急修复也要经过可审计的流程,记录原因、操作人和结果。否则日常接口保持的规则,可能在运维脚本中被破坏,服务独立性只剩下部署图上的外观。

还应警惕只有技术名称的拆分,例如所有校验归“校验服务”,所有数据库操作归“数据服务”。这样任何业务动作都要穿越共同瓶颈,规则被拆散后更难修改。较好的检验方法是让团队描述服务负责的业务承诺,并拿最近的需求检查:能否主要在该边界内完成、测试、发布和恢复。共享技术能力值得治理,业务责任仍须清楚归属。

7 案例:分布式事务超时放大锁定

某商店把库存独立部署后,下单延迟突然升高。开发者为了保持“全过程一起成功”的直觉,在订单数据库开启事务、更新订单行,然后持有事务等待库存预留。库存正常时几乎看不出问题;促销期间库存数据库出现热点争用,预留变慢,订单本地事务也跟着持续占用锁和连接。

用户看到请求超时后再次提交,入口的自动重试也在工作。同一订单相关请求开始争用已有锁,更多数据库连接被等待中的事务占住。连接池可用数量下降后,原本不访问库存的查询也可能排队。表面现象是“订单数据库全面变慢”,触发点却来自远程调用延迟,以及本地事务把远程等待带进了资源占用周期。

远程等待、事务持锁与重试形成放大回路
远程等待、事务持锁与重试形成放大回路

排查时不要仅凭错误最多的组件定位原因。先对齐故障时间线:库存调用延迟何时上升,订单事务持续时间是否同步增长,锁等待和连接占用是否随后增加,重试量有没有进一步上升。再抽取具体订单,查看库存预留提交时间与订单超时时间。如果库存已成功而订单本地回滚,就证明本地事务没有覆盖远端结果。

这类写法常被误称为“加了事务就有分布式事务”。实际上,本地数据库事务只约束自己的提交范围,无法因为调用方回滚就自动撤销另一个服务的提交。即使请求携带某种事务标识,也要检查参与系统是否真正支持相应协议与失败恢复,不能从标识名称推导出全局原子性。本案例无需引入高级协议,也能识别错误的保证。

止血首先要阻止压力继续放大:对下单入口限流,限制等待和重试,隔离库存相关并发,检查并妥善处理长时间未结束的事务。不能只把超时调得更长,因为更长的等待可能进一步延长资源占用。也不能直接把所有未知订单标记为失败后丢弃,否则远端已经成功的预留会长期占住库存。

长期改造是把远程调用移出持锁阶段。订单先用短本地事务记录待确认状态与业务操作标识,事务结束后请求库存,再用新的短事务推进订单状态。后台核对处理未知结果,取消流程显式释放预留。这样并不让整个业务瞬间完成,但让等待期间的状态与责任变得可追踪,也减少本地数据库持续持锁的时间。

验证要覆盖故障路径:让库存延迟,模拟响应丢失,重复提交同一操作,观察是否重复预留、是否出现无法解释的终态。还要确认等待中的订单最终被推进或进入明确处理队列,锁等待和连接占用没有随远程延迟持续累积。成功率恢复只是部分证据,订单记录与库存记录的业务核对同样必要。

8 把服务边界写进演进方案

演进方案应从可观察的问题起笔,例如库存规则每周调整,却总要和订单应用一起发布,促销期间库存扩容也必须复制整个应用。接着说明希望获得的独立性:库存团队拥有预留规则与数据写入口,能够在兼容契约下独立发布和扩容。若无法指出具体收益,可以先改善模块边界,不必立即增加运行服务。

从模块边界到逐步切换的演进路径
从模块边界到逐步切换的演进路径

为候选服务列清职责、输入输出、数据所有者和不变量。不变量是业务必须持续满足的规则,例如同一个预留操作不能因为重试多次扣减库存。同步接口写出完成含义与错误分类,异步事件写出发生时点与重复处理办法。对于结果未知的情况,要规定查询入口、最长处理目标、人工接管条件与责任团队。

部署方案则回答服务如何运行:实例如何启动与检查健康,发布如何逐步替换,依赖通过什么地址发现,调用使用什么服务身份。网络可达不等于有权操作,应分别配置网络访问和应用授权;库存账号只获得所需数据权限。机密凭据应由合适的机密管理机制提供,不随意写进代码或镜像。入口用户身份也不应被当成服务自身身份混用。

数据迁移不能只写“切换数据库”。要说明初始数据如何复制、迁移期间新增修改如何衔接、如何核对数量与业务含义,以及何时停止旧写入口。临时双写会出现部分成功和顺序问题,不能把它当成免费的过渡手段。尽量明确每个阶段的唯一权威写入者,通过受控同步与核对推进切换。

发布前建立基线:下单成功率、关键分位延迟、库存预留正确性、未知状态积压和恢复耗时。切换后先用有限流量观察,再逐步扩大。回退条件要具体,并区分流量回切与数据回退:新服务已产生的预留记录不会随着路由改变自动消失。方案必须解释旧路径如何识别这些记录,避免回切后再次处理同一个业务操作。

独立服务还意味着明确值班与成本责任。谁收到告警,谁决定暂缓发布,谁负责核对差异,谁处理契约变更,都应落到团队。计算实例之外,还会增加日志存储、消息处理、网络传输与运维投入。以一次独立发布和一次故障恢复作为评审演练,比仅统计拆出了多少服务更能检验方案是否完整。

9 从微服务架构走向服务网格与 API 网关(C37)

当服务数量增长,许多通信能力会重复出现:外部请求需要统一入口,服务之间需要身份验证、连接保护、流量观察和路由控制。若每个团队独立实现,行为可能不一致,也会增加升级负担。下一课的 API 网关和服务网格,就是理解这些共性通信能力如何被集中管理或统一执行的入口。

API 网关通常位于客户端进入后端系统的边界,承担请求路由、身份校验、流量限制等入口职责。服务网格通常关注服务之间的通信治理。具体产品可能覆盖相邻能力,不能只按名字判断。先画出请求来自哪里、流向哪里、需要执行什么策略,再决定哪些能力适合放在入口,哪些需要服务间一致执行。

本课已有的业务责任不会因为加入通信设施而消失。网关可以限制提交频率,却不知道某次库存预留是否应当撤销;统一重试可以再次发送请求,却不能替业务定义幂等键;链路追踪能够展示等待位置,却不能决定订单是否已成立。基础设施帮助实施通信策略,业务状态与契约仍由服务负责。

还应记住通信策略会改变失败表现。某层缩短超时、某层开启重试,都可能影响调用方看到的结果和下游承受的压力。如果入口与服务间代理都能配置这些策略,必须明确总预算与策略所有者,避免不同层各自认为自己只增加了少量重试。下一课会继续沿着这条跨服务请求,讨论治理能力放置的位置与边界。

带着当前架构图进入 C37:保留服务边界、同步关键路径、异步完成条件和数据归属,再在图上标出外部入口与内部通信。你应该能说明哪些规则是所有调用都需要的技术策略,哪些规则依赖订单语义。先分清责任,再选择治理设施,才能让新增组件服务于已经明确的问题。

放进方案里

本项目先保持订单主流程的模块边界,将库存预留作为候选独立服务。拆分原因是库存规则与扩容需求已形成独立变化节奏,目标是在兼容订单调用的前提下独立发布。库存服务拥有可售数量、预留和释放记录的写入权;订单服务保存订单生命周期与关联标识,通过契约请求库存操作,不直接修改库存表。

下单先记录待确认状态,再在本地事务之外请求预留。预留接口接受稳定幂等键,区分库存不足、暂时不可用和结果未知,并提供结果查询。响应超时不能直接解释为未预留;后台任务负责核对未知状态,取消后仍已预留的记录进入显式释放流程。订单确认事件采用持久化待发记录,通知允许延后,消费者负责重复处理保护。

服务以受控实例部署,配置健康检查、最小数据权限和限定网络访问;对调用设置等待预算、并发上限及有限重试。请求追踪关联订单与库存操作,业务记录保留可核对的状态推进历史。告警覆盖未知订单积压、预留延迟、释放失败和数据库连接占用,由订单与库存责任团队共同确认恢复结果。

迁移按契约确认、数据复制核对、有限流量切换、逐步扩大执行,每个阶段明确唯一权威写入口。验收比较拆分前后的成功率、延迟、独立发布能力和故障恢复耗时,并注入响应丢失验证幂等与核对路径。出现业务核对差异或指标越界时按约定回切流量,同时处理新服务已产生的数据,避免回切造成重复预留。评审同时计入计算、通信、观测与值班成本;证据不足时继续保留模块化实现。

10 小结:下一步服务网格与 API 网关(C37)

微服务架构把业务能力、契约、数据所有权和独立运行责任组织到明确边界中。同步协作要管理等待与未知结果,异步协作要说明完成条件、重复处理和状态恢复。拆分后的收益需要通过独立发布、扩容与故障恢复来证明,不能用服务数量代替。

自测可以围绕当前订单图进行:库存响应超时后能否直接判定预留失败?订单回滚能否撤销库存提交?通知消费成功是否属于下单成功的必要条件?如果新旧服务不能分别发布,应回头检查哪些契约或共享规则?尝试用状态、所有权和证据回答,而不是只说“加重试”或“换中间件”。

进入 C37 时,继续保留这些边界与失败路径,进一步判断 API 网关和服务网格分别适合承接哪些通信治理责任。

同阶段相关课

AI 答疑

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

配置自己的服务