LabHub
学习 学习路径 课程

微服务架构

拆开之后,什么变好了,什么变坏了

在 LabHub 中继续学习

一句话总结

微服务不是为性能作出的选择,而是为了让团队无需互相等待即可发布。代价是:原本的函数调用会变成网络调用。

概念图: 补偿事务和状态机 · 发布周期差异很大。 · 资源特性不同。 · 需要故障隔离。

为什么需要了解这些

人们经常说单体应用不好。但观察系统真正崩溃的顺序,会发现先出问题的不是代码,而是发布。八个团队向同一个仓库提交代码,创建发布分支要花两天,一个团队的缺陷会阻塞整个发布——到了这种时候,人们才开始说“拆分吧”。

这种诊断通常是正确的。问题在于下一步:团队往往没有准备好拆分后会失去什么。单体应用中的 inventory.reserve(sku, 3) 是纳秒级的函数调用,几乎不会失败。一旦拆成服务,它就会变成毫秒级 HTTP 调用,可能超时、可能只成功一半、对方也可能正在重启。而且,包裹这次调用的事务不复存在。

工作原理

判断标准有三项。

第一,是否真的需要独立发布。如果两个功能始终一起发布,拆分几乎没有收益。拆开后仍然总是共同发布,就是分布式单体:同时拥有单体应用与分布式系统的缺点。

第二,数据是否真正分离。如果两个服务仍然持续写入同一张表,说明边界划错了。服务边界不是代码边界,而是数据所有权边界。

第三,是否有相应团队。每个服务都需要有人承担值班。三人团队维护九个服务,发布可能变快,凌晨告警却会增加三倍。

迁移方式不是大爆炸式重写,而是绞杀者模式(Strangler Fig)。先在前方放置门面,逐项把功能迁移到新服务,按比例转移流量;稳定后,再从单体中删除对应代码。持续重复“转换 → 共存 → 删除”。这种方式的核心是可以随时回退,只需撤销门面中的一条路由配置。

生产现场中的常见情况

门面通常由 API 网关(Kong、Envoy、NGINX)承担,并配合防腐层(Anti-Corruption Layer)。该层负责转换旧系统的数据模型,避免其渗透到新服务中。省略后,新服务只需六个月就会变得与旧模式一模一样,拆分也就失去了意义。

最常见的失败是:“库存服务已经拆分,库存表却仍然共享。”这样,部署虽然分开,模式变更却仍然需要两个团队共同同意。因为耦合并不在代码中,而在数据中。

拆分前必须承担的成本清单

增加一个服务,增加的不只是代码。以下内容都会随之而来。

项目 增加的内容
仓库与 CI 一条流水线、构建时间、秘密管理
部署 清单、发布策略、回滚流程
可观测性 仪表板、告警、日志标签、追踪传播
值班 一份运行手册、指定负责人
通信 网络往返、序列化、重试与超时设计
数据 事务边界断裂——需要 Saga 或最终一致性

最后一项成本最高。原本在一个事务中结束的工作,会变成补偿事务和状态机。 代码量增加到三倍,缺陷也会藏在其中。

即使如此,也应拆分的信号

尽管成本很高,仍有必须拆分的情况。满足以下任一项,就可能值得。

发布周期差异很大。 如果每天发布十次的部分与每季度发布一次的部分位于同一仓库,慢的一方会拖住快的一方。

资源特性不同。 如果把使用 GPU 的推理与普通 CRUD 放在同一个 Pod,CRUD 也会被调度到 GPU 节点。扩展维度不同,就应拆分。

需要故障隔离。 如果报表生成耗尽内存,连支付业务也一起崩溃,这两者就必须分离。

仅仅团队不同还不够。团队会改变;如果按照组织结构拆分系统,每次组织调整都必须重新拆分系统。

合并回去也是一种设计

把已经拆分的服务重新合并,往往比想象中更正确。但多数团队把它视为失败,因此不断拖延。

以下是应当合并的信号。

如果符合三项以上,合并通常更好。回到模块化单体不是倒退,而是纠正。

拆分顺序——先拆什么

决定拆分后,下一个问题是顺序。随意选择一处先拆,很容易让团队一开始就挑战最难的部分

**优先候选应满足三个条件:**几乎不共享数据、发布周期与主体不同、即使失败主体仍能继续运行。通知发送、文件转换、报表生成、搜索索引等都属于这一类。

交易核心应最后拆分。 订单、支付、库存等原本位于同一事务中的功能,一旦拆开,就必须承担分布式事务问题。团队需要补偿事务或 Saga 模式,而这些应在能力成熟后再处理。

先拆分数据。 只拆服务却继续共享数据库,是最糟糕的状态。部署分别进行,模式却必须共同变更,最终变成必须同时协调两边发布。与其如此,不如保留单体。

拆分前,先在代码内部建立边界。 先划分模块,把模块间调用收敛到一个接口,并等待该接口稳定。达到这个状态后,拆分进程只是机械性工作。在不知道边界的情况下先拆进程,就只能隔着网络修改边界。

以可回退方式拆分。 逐步把少量流量发送到新服务,出现问题就回到主体代码。只有在数周内都没有问题后,才从主体删除旧代码。

提前决定测量指标。 包括发布频率、变更失败率,以及修复一个功能时需要修改的仓库数量。如果最后一项不断增长,说明边界错误;应先停止继续拆分,重新审视设计。

下一次实验要做什么

接收一个把订单和库存放在同一进程中的单体应用,先用文档固定契约,再把它拆成两个服务。拆分后编写脚本,自动比较两种实现的响应是否一致;最后还要写下一条证据,说明“这部分本不该拆分”。