拆开之后,什么变好了,什么变坏了
一句话总结
微服务不是为性能作出的选择,而是为了让团队无需互相等待即可发布。代价是:原本的函数调用会变成网络调用。
为什么需要了解这些
人们经常说单体应用不好。但观察系统真正崩溃的顺序,会发现先出问题的不是代码,而是发布。八个团队向同一个仓库提交代码,创建发布分支要花两天,一个团队的缺陷会阻塞整个发布——到了这种时候,人们才开始说“拆分吧”。
这种诊断通常是正确的。问题在于下一步:团队往往没有准备好拆分后会失去什么。单体应用中的 inventory.reserve(sku, 3) 是纳秒级的函数调用,几乎不会失败。一旦拆成服务,它就会变成毫秒级 HTTP 调用,可能超时、可能只成功一半、对方也可能正在重启。而且,包裹这次调用的事务不复存在。
工作原理
判断标准有三项。
第一,是否真的需要独立发布。如果两个功能始终一起发布,拆分几乎没有收益。拆开后仍然总是共同发布,就是分布式单体:同时拥有单体应用与分布式系统的缺点。
第二,数据是否真正分离。如果两个服务仍然持续写入同一张表,说明边界划错了。服务边界不是代码边界,而是数据所有权边界。
第三,是否有相应团队。每个服务都需要有人承担值班。三人团队维护九个服务,发布可能变快,凌晨告警却会增加三倍。
迁移方式不是大爆炸式重写,而是绞杀者模式(Strangler Fig)。先在前方放置门面,逐项把功能迁移到新服务,按比例转移流量;稳定后,再从单体中删除对应代码。持续重复“转换 → 共存 → 删除”。这种方式的核心是可以随时回退,只需撤销门面中的一条路由配置。
生产现场中的常见情况
门面通常由 API 网关(Kong、Envoy、NGINX)承担,并配合防腐层(Anti-Corruption Layer)。该层负责转换旧系统的数据模型,避免其渗透到新服务中。省略后,新服务只需六个月就会变得与旧模式一模一样,拆分也就失去了意义。
最常见的失败是:“库存服务已经拆分,库存表却仍然共享。”这样,部署虽然分开,模式变更却仍然需要两个团队共同同意。因为耦合并不在代码中,而在数据中。
拆分前必须承担的成本清单
增加一个服务,增加的不只是代码。以下内容都会随之而来。
| 项目 | 增加的内容 |
|---|---|
| 仓库与 CI | 一条流水线、构建时间、秘密管理 |
| 部署 | 清单、发布策略、回滚流程 |
| 可观测性 | 仪表板、告警、日志标签、追踪传播 |
| 值班 | 一份运行手册、指定负责人 |
| 通信 | 网络往返、序列化、重试与超时设计 |
| 数据 | 事务边界断裂——需要 Saga 或最终一致性 |
最后一项成本最高。原本在一个事务中结束的工作,会变成补偿事务和状态机。 代码量增加到三倍,缺陷也会藏在其中。
即使如此,也应拆分的信号
尽管成本很高,仍有必须拆分的情况。满足以下任一项,就可能值得。
发布周期差异很大。 如果每天发布十次的部分与每季度发布一次的部分位于同一仓库,慢的一方会拖住快的一方。
资源特性不同。 如果把使用 GPU 的推理与普通 CRUD 放在同一个 Pod,CRUD 也会被调度到 GPU 节点。扩展维度不同,就应拆分。
需要故障隔离。 如果报表生成耗尽内存,连支付业务也一起崩溃,这两者就必须分离。
仅仅团队不同还不够。团队会改变;如果按照组织结构拆分系统,每次组织调整都必须重新拆分系统。
合并回去也是一种设计
把已经拆分的服务重新合并,往往比想象中更正确。但多数团队把它视为失败,因此不断拖延。
以下是应当合并的信号。
- 两个服务始终一起发布
- 开发一个功能时两边都必须修改
- 两者之间的调用同步且不可缺少(一方故障,另一方也失去意义)
- 数据本应处于一个事务,却用 Saga 勉强模拟
如果符合三项以上,合并通常更好。回到模块化单体不是倒退,而是纠正。
拆分顺序——先拆什么
决定拆分后,下一个问题是顺序。随意选择一处先拆,很容易让团队一开始就挑战最难的部分。
**优先候选应满足三个条件:**几乎不共享数据、发布周期与主体不同、即使失败主体仍能继续运行。通知发送、文件转换、报表生成、搜索索引等都属于这一类。
交易核心应最后拆分。 订单、支付、库存等原本位于同一事务中的功能,一旦拆开,就必须承担分布式事务问题。团队需要补偿事务或 Saga 模式,而这些应在能力成熟后再处理。
先拆分数据。 只拆服务却继续共享数据库,是最糟糕的状态。部署分别进行,模式却必须共同变更,最终变成必须同时协调两边发布。与其如此,不如保留单体。
拆分前,先在代码内部建立边界。 先划分模块,把模块间调用收敛到一个接口,并等待该接口稳定。达到这个状态后,拆分进程只是机械性工作。在不知道边界的情况下先拆进程,就只能隔着网络修改边界。
以可回退方式拆分。 逐步把少量流量发送到新服务,出现问题就回到主体代码。只有在数周内都没有问题后,才从主体删除旧代码。
提前决定测量指标。 包括发布频率、变更失败率,以及修复一个功能时需要修改的仓库数量。如果最后一项不断增长,说明边界错误;应先停止继续拆分,重新审视设计。
下一次实验要做什么
接收一个把订单和库存放在同一进程中的单体应用,先用文档固定契约,再把它拆成两个服务。拆分后编写脚本,自动比较两种实现的响应是否一致;最后还要写下一条证据,说明“这部分本不该拆分”。