我花了四年从单体里拆出服务,又花了接下来四年悄悄把它们合并回去。模块化单体不是一次失败的退让——它是一个让选项保持开放、让部署故事保持无聊的设计选择。这篇文章是关于有意选择它的理由,连同那次以惨痛方式教会了我这个教训的迁移经历的实证。

拆分时代

2014年到2018年,我在医院平台(AFAQ、HAKEEM)、招聘平台(Talentera)和其他几个地方工作,对话总是以同样的方式漂移:“我们应该把这个拆成服务。” 服务是你证明代码库是现代的方式。Kubernetes 是你证明基础设施团队是现代的方式。十几个带 pyproject.toml 的 repo 是你证明工程实践是现代的方式。

我们做了。大多数那些拆分都交付了。其中一些甚至成功了。

有一部分没人大声说:让那些服务”成功”的东西,是它们保持与原始数据库的紧密耦合。服务边界是幻象。事务边界不是。当我们不得不跨新服务协调变更时——这大约每周发生一次,因为我们的领域边界和我们的服务边界不匹配——我们用 cron 任务、Slack 协调和祈祷来做。

合并时代

从2020年在 Bytro 开始,这个模式逆转了。我们从一个被拆成看起来合理的服务的事件驱动遗留系统开始,悄悄把它们拉回到一个单一的可部署包,我们根据听众的不同叫它:

  • 设计文档里的”模块化单体”。
  • 工程会议里的”那个大东西”。
  • 私下里的”一个没人在会议演讲里写过的合理架构”。

这不是降级。我们找回的东西:

  • 单一部署。 一条 CI 流水线。一次回滚。一个在事故中需要推理的版本偏斜问题。
  • 可重构的边界。 想把一个函数从模块 A 移到模块 B?这是一个 IDE 操作,不是一个 RFC。
  • 诚实的事务。 如果两个操作需要原子性,它们可以是。如果不需要,我们仍然在模块边界拆分它们——但我们不会为了一个部署故事而撒关于原子性保证的谎。
  • 更快的本地开发。 docker compose up 打开一个进程,不是九个。
  • 更便宜的可观测性。 一个服务的追踪是一棵树。九个服务的追踪是一张图——而且那张图在一半的边上对你撒谎,因为一半的 span 没有正确传播 trace 上下文。

关于微服务,没人告诉你的事

微服务之所以被采用,是组织原因,不是技术原因。它们让团队可以独立部署。这就是全部卖点,而且它是个好卖点——如果你有不止一个团队,其部署节奏真的在冲突。

如果你有三个团队在同样的日子以同样的顺序部署到同一个 staging 环境,你没有部署耦合问题。你有三个团队和一个单体,这是一个可行的安排。

我在三家公司里见过的病症是这样的:

  1. 一个八人团队建了一个单体。
  2. 他们开始感到摩擦。
  3. 有人读了一篇关于 Netflix 如何做微服务的文章。
  4. 他们花了18个月拆出服务。
  5. 最后,他们有八个人和14个服务。
  6. 每个服务几乎没有明确的所有者。
  7. 每次变更仍然需要协调——但现在是跨服务边界,带着最终一致性、分布式追踪,以及一个叫做”服务 X 挂了但服务 Y 还活着,服务 Z 该怎么办?“的全新故障模式。

他们在第2步感受到的摩擦,不是服务边界问题。是内部模块边界问题。那些可以在不引入网络的情况下修复。

模块化真正意味着什么

“模块化单体”里的”模块化”是真有活干的。这个纪律长这样:

  • 由构建系统强制执行的模块边界,而不是约定。如果 orders 在编译时可以从 billing 导入,你没有边界。你有一个愿望。用你的语言支持的任何东西:Go 的 internal 包、Rust 的 crate、TypeScript 的项目引用、Java 的模块系统。选一个,强制执行它,在违规时让构建失败。
  • 模块边界镜像领域边界。 这是困难的部分。第一次你搞不对。你重构两次,然后正确的形状浮现出来。在单体内部重构它们很便宜这一事实,恰恰是它是做这种重构的正确地方的原因。
  • 关于什么跨越边界的约定。 值,不是引用。事件,不是方法调用。发布的类型,不是内部类型。一旦这些到位,以后把一个模块拆出来变成服务是周末的工作量。没有这些,拆分是一个产生分布式单体的18个月项目。

什么时候真的应该拆出一个服务

我不反对服务。我拆出过几十个。我现在在做之前寻找的信号都是组织或运维性的——不是技术性的:

  • 一个特定的团队想要以不同的节奏部署,现在,不是理论上一年后。
  • 一个特定的工作负载有单体无法便宜服务的运行时或扩展配置——比如,一个 CPU 密集型的 ML 推断路径。
  • 一个特定的能力有不同的合规或租户边界——比如,一个需要自己审计线索的 PII 密集型界面。
  • 一个供应商集成天然就是一个服务——比如,一个在单体部署时必须持续运行的 webhook 接收器。

共同因素:这其中的每一个都是一个真实的、有名字的、具体的压力。“为了现代感”不在这个列表上,永远不会在。

实证

这是通常我会引用一个具体的 p99 数字或量化的部署时间改善的地方。我会引用最说服我的东西:

在 Bytro,在我们把一个之前过于碎片化的服务拓扑整合回一个紧密的模块化单体之后,团队的 on-call 轮值变得更安静了。 不是因为我们的事故更少——我们的事故数量大致相同。但一个”单个服务被 paged”的事故是一个工程师、一个终端、一次回滚的问题。一个”六个服务对彼此的状态感到困惑”的事故是一个六工程师电话会议的问题,而且你找不回为这些事故失去的睡眠。

on-call 轮值是你架构的真相告诉者。如果你的轮值是健康的,架构就在工作。如果你的轮值是一种民俗税,架构就出了问题——不管白板上画的是什么形状。

现代的那部分

我承认这个安静的真相:在2025和2026年,单体+无聊技术栈是我会在上面交付最令人兴奋的东西的那个。当整个代码库能放进一个 agent 的上下文窗口时,AI 辅助开发要容易得多。模块化单体放得下。九个服务、四种语言和十六种配置格式放不下。

如果你想让你的 AI 工具有用,给它一个它能读的代码库。2026年支持单体的理由,出乎意料地是,它是 LLM 驱动的工程工作流中最快的架构。这不知为何是我写过的最具2026年气质的一句话。