这种场景比应该发生的还要常见:一个三人初创公司在第一天就决定构建微服务。六个月后,他们有了十五个服务,一个没人完全理解的 Kafka 集群,以及一个需要追踪跨越八个服务的请求才能复现的 bug。团队精疲力竭,产品完成了一半。而在某个 Slack 消息串里,有人打出了这几个字:我们基本上构建了一个分布式单体。
微服务确实很强大——在合适的规模、合适的团队、合适的原因下。但它们是你为组织规模支付的代价,而不是起点。本文诚实地引导你走过三种架构形态——单体、模块化单体和微服务——并给你识别何时(以及是否)应该在它们之间迁移的信号。
单体:一个可部署单元统治一切
单体是一个单一的可部署单元。一次构建,一个二进制文件(或一套编译产物),一个部署步骤。每个功能、每个模块、每个数据库查询都住在同一个进程里。
这听起来很有局限性,“单体”这个词本身也带上了一点略显尴尬的味道——仿佛承认自己有单体就是承认自己没跟上现代工程的节奏。这种框架是错的,它已经造成了很多不必要的痛苦。
单体是正确的默认选择。原因如下:
- 本地调用是免费的。进程内的函数调用是纳秒级的。服务间的网络调用是毫秒级的——而且它可以失败、超时、返回部分结果,或者完全丢失。在你真正需要之前,不必承担这种复杂性。
- 事务是简单的。关系型数据库在整个数据模型上提供 ACID 事务。在单体中,从账户 A 向账户 B 转账是一次事务。在微服务中,它变成了分布式事务或 saga——这是有真实失败模式的真正困难问题。
- 调试是直接的。一个进程,一个日志流,一个栈跟踪。你找到问题。在分布式系统中,一次用户操作可能在多个服务间派生出几十个异步操作,关联它们需要专门构建的可观测性工具。
- 部署是简单的。一个产物,一个流水线,一个回滚策略。你可以在第一天就自信地发布。
- 大公司也在运营大型单体。Shopify 以单体 Rails 应用运营了多年,规模相当可观。Stack Overflow 用少数几台服务器上运行的单体每天处理数百万请求。单体并不妨碍你扩展——只是扩展方式不同。
单体真正的痛点是真实的,但它们比人们预期的来得更晚。构建时间随着代码库增长——50k 行时两分钟的完整重建,在 500k 行时需要十二分钟。合并摩擦随着二十名工程师都在修改同一个代码库而增加;长期分支和冲突变成日常仪式。部署耦合意味着每个团队都一起部署——一个团队的高风险变更可能阻塞其他所有人的发布。
这些痛点是真实的。但它们是成功的痛点,而且比炒作所暗示的晚得多。大多数团队能遇到这些问题算是幸运了。
模块化单体:大多数团队跳过的甜蜜点
模块化单体仍然是一个可部署单元——但内部它被组织成有强制边界的模块。模块通过清晰的接口通信。一个模块不允许直接进入另一个模块的内部:不能从兄弟模块 import 私有类,不能共享数据库表,不能跨模块行调用内部辅助函数。
这听起来像是一个小小的纪律,但它显著地改变了局面。
把它想象成一座房子的平面图。没有模块的单体是一个开放式公寓:一个大开间,所有东西都到处摆着。模块化单体给了你带墙的房间——你仍然住在一栋房子里,仍然共用管道和前门,但厨房和卧室各有自己明确的用途,你不会在床上做饭。
模块自然地映射到领域驱动设计中的限界上下文:一个 Billing 模块,一个 Inventory 模块,一个 Notifications 模块。每个模块拥有自己的领域逻辑和自己的数据库 schema 切片。它们暴露一个公开接口(一个 service facade 或一组公开函数),并把其他一切保持私有。
实际收益是可观的:
- 独立推理。一个在
Billing上工作的开发者可以理解并修改它,而不需要把整个代码库装在脑子里。 - 安全重构。只要公开接口保留,你可以重写一个模块的内部——应用的其余部分不会察觉。
- 明确的所有权。团队认领模块。代码审查保持聚焦。引导新开发者意味着给他们一个模块,而不是一个 20 万行的代码库。
- 迁移就绪的缝合处。如果你以后确实需要提取一个服务,边界已经画好了。模块变成服务,接口变成 API。工作量很大,但是已知的工作,不是探索性手术。
模块化单体是大多数团队直接跳过的架构,从纠缠的单体直接冲向微服务,而没有在这里停留。这是个错误。对于大多数产品团队——不到 100 名工程师、个位数服务、一两条部署流水线——一个维护良好的模块化单体是最富有生产力的居所。
微服务:你真正买到了什么
微服务将系统分解为独立可部署的服务。每个服务拥有自己的进程、自己的部署流水线,以及——至关重要的——自己的数据存储。服务之间通过网络通信:HTTP、gRPC、消息队列、事件流。
当微服务运转良好时,它们在规模上提供了其他方式无法匹敌的四件事:
- 独立部署。支付团队可以在周二下午发布支付服务的变更,而无需与推荐团队、搜索团队或通知团队协调。对于拥有数百名工程师的公司,这是革命性的——这是一周部署一次和一天部署一百次的差别。
- 独立扩展。如果你的
ImageProcessing服务在峰值时间需要四十个 CPU 核心,而你的UserProfile服务只需要两个,你可以分别扩展它们。在单体中,你扩展整体——你为每个功能购买四十个核心,不管它是否需要。 - 团队自治。每个服务是一个小的、自包含的产品。五名工程师的团队可以完全拥有一个服务:选择自己的语言、数据库、部署节奏。这是微服务的组织超能力——康威定律为你所用,而不是对你作对。
- 故障隔离。
RecommendationEngine的内存泄漏让RecommendationEngine宕机,而不是结账。你可以优雅降级——商店仍然工作,产品只是没有个性化建议。在单体中,任何模块中未捕获的异常都可能让整个进程宕机。
注意这个模式:那个列表里的一切都是关于组织和运营规模的。这些不是适用于一个有八个人还没找到产品市场契合点的团队的问题。
你真正支付的代价:分布式系统税
微服务不是免费的。上面每个好处都有一个对应的成本,而这些成本绝非微不足道。低估它们是团队最终筋疲力竭、交付不足的原因。
最糟糕的结果既不是微服务也不是单体——而是分布式单体:耦合如此紧密以至于必须一起部署、共享同一个数据库或一起失败的微服务。你付出了微服务的每一分成本,却没有得到任何好处。这种情况在团队按技术层而不是按业务领域拆分时会发生,或者当服务在长链中同步调用彼此时。如果 ServiceA 只能在 ServiceB 之后部署,你就构建了一个分布式单体。
以下是全部的税:
- 网络调用会失败。函数调用不会失败(除非有 bug)。网络调用可以失败、超时、返回陈旧的缓存值,或者在第一次尝试已经产生副作用之后重试成功。你必须在每个地方、随时处理部分失败。
- 最终一致性。服务拥有各自的数据。在服务边界之间保持数据一致性需要仔细的设计——事件驱动模式、saga、幂等操作、补偿事务。向产品经理解释为什么用户可以下订单但不能立即在订单历史中看到它,是很尴尬的。
- 分布式事务很难。两阶段提交协议存在且令人痛苦。大多数团队使用 Saga 模式代替,这把复杂性从数据库转移到了应用代码。两者都比
BEGIN; UPDATE; COMMIT;复杂得多。 - 调试是一种不同的技能。栈跟踪不再能告诉你出了什么问题。你需要分布式追踪(Jaeger、Zipkin、Honeycomb),每行日志里的关联 ID,服务网格,以及跨服务关联事件的仪表板。构建和维护这套可观测性基础设施是真实的工程工作。
- 运维开销成倍增加。一个服务需要一个 Dockerfile、一个 Kubernetes 部署、一个 ingress 规则、一个健康检查端点、CPU 和内存限制、日志聚合配置,以及告警规则。乘以三十个服务。这个开销随服务数量线性增长;你的工程师人数不是。
- 测试更难。单元测试仍然容易。但跨越服务边界的集成测试需要同时运行多个服务,管理它们的版本和配置,以及处理跨数据库的测试数据。契约测试(Pact 等)有帮助,但带来了自己需要学习的规范。
这些成本没有一个是无法解决的——业界为所有这些都有成熟的工具。关键在于它们是每天都必须支付的真实成本,在它们解锁的好处真正被需要之前,是不值得支付的。
信号:何时应该拆分(以及何时不应该)
那么你如何知道你已经跨越了微服务值得其成本的阈值?以下是真正表明是时候拆分的信号:
- 独立扩展需求是真实的且昂贵的。你能指出一个特定组件需要系统其他部分十倍资源的情况,而你正在整个单体中为这些资源付费。拆分会节省可观的资金或明显提升性能。
- 部署竞争是慢性的。团队经常被其他团队正在进行中的变更所阻塞,无法发布。共享部署的协调开销正在可测量地拖慢你——不是偶尔,而是作为每周或每天的挫折。
- 团队数量要求如此。你有多个五人以上的团队在不同的业务领域工作,有不同的部署节奏、不同的技术偏好,以及真正独立的路线图。康威定律预测了你最终会得到的架构——你不妨为此做好规划。
- 一个组件有根本不同的可靠性或安全要求。支付、认证和 PII 存储通常值得隔离,不是出于性能原因,而是出于合规、减少爆炸半径,以及独立审计它们的能力。
- 一个模块已经实际上是独立的。它与系统的其余部分没有共享状态,只通过定义明确的事件或 API 通信,由不同的团队拥有。组织上的缝合处已经存在——把它变成服务边界只是在正式化已经存在的事实。
以下是你拆分得太早的信号:
- 你的整个工程团队不到二十人。
- 你在拆分是因为它感觉更具可扩展性,而不是因为你遇到了具体的限制。
- 你计划中的服务需要一起部署才能工作。
- 你还无法画出一个清晰的边界,让每个服务拥有自己的数据,而没有共享表。
- 你的团队还没有调试分布式系统所需的可观测性基础设施(分布式追踪、集中式日志、告警)。
- 主要原因是微服务出现在职位描述或公司的市场宣传中。
如何优雅地拆分:缝合处、绞杀者无花果和数据所有权
如果以上信号指向拆分,如何拆分至关重要。拆分得不好会产生上面描述的分布式单体。拆分得好会产生真正独立的、值回票价的服务。
沿着限界上下文拆分,而不是技术层。不要创建 DatabaseService 或 ValidationService——这些是技术关切,不是业务关切。创建 OrderService、BillingService、InventoryService——在你的领域中有清晰、稳定含义的业务能力单元。限界上下文是特定模型适用且语言一致的领域部分。这就是服务边界应该属于的地方。
使用绞杀者无花果模式。以一种逐渐缠绕宿主树的藤蔓命名,绞杀者无花果模式让你逐步提取服务,而不是一次性全部提取。你在单体旁边建立新服务,将特定的流量切片路由到它,验证它工作正常,然后从单体中删除相应的代码。单体随时间收缩;它从不被重写。这更安全,更可逆,而且远不可能导致一个从未真正完成的六个月大爆炸迁移。
每次提取一个服务。每次提取都教给你一些东西。第二次提取会比第一次做得更好。试图同时提取五个服务会分散学习并成倍增加风险。
数据所有权是不可谈判的。一个与另一个服务共享数据库表的服务不是服务——它是一个带有额外网络开销的模块。每个服务必须拥有自己的数据。如果两个服务需要相同的数据,其中一个是权威的,另一个通过 API 获取或通过事件同步。这个约束建立起来很痛苦,维护起来也很痛苦,但它给了你独立部署性和你期待已久的故障隔离。
在提取之前用 port 定义缝合处。如果你在模块化单体内部遵循了 Ports & Adapters 方式,提取几乎变成机械性的:port 定义 API,其后面的 adapter 变成服务客户端。领域逻辑不变——只有交付机制改变。这是首先用 port 构建最强有力的实际论据之一:它们免费给你提取就绪的缝合处。
并排比较
| 维度 | 单体 | 模块化单体 | 微服务 |
|---|---|---|---|
| 部署单元 | 一个进程,一条流水线 | 一个进程,一条流水线 | 多个进程,多条独立流水线 |
| 数据所有权 | 共享数据库;所有代码可访问所有表 | 共享数据库;模块按约定拥有各自的 schema 区域 | 每个服务拥有自己的数据库;没有共享表 |
| 故障模式 | 一个进程崩溃会让整个应用宕机 | 一个进程崩溃会让整个应用宕机 | 服务故障被隔离;其他服务优雅降级 |
| 团队适配 | 最适合 1–3 个团队;摩擦随团队数增长 | 最适合 2–8 个团队;模块与团队所有权对齐 | 5 个以上团队以独立节奏发布时是必要的 |
| 运维成本 | 低:一次部署,一个日志流,一套告警 | 低:与普通单体相同的运维足迹 | 高:可观测性、容器编排、服务网格、每个服务的 CI/CD |
| 事务模型 | 轻松实现完整 ACID 事务 | 轻松实现完整 ACID 事务 | Saga 或最终一致性;分布式事务很难 |
| 何时使用 | 新产品、小团队、未知领域 | 有清晰领域边界的成长产品,10–100 名工程师 | 多个自治团队、经过验证的领域模型、真实的独立扩展需求 |
按公司规模的诚实视角
架构建议往往来自那些已经跨越了微服务有意义的阈值的公司——因为这些公司有工程博客、会议演讲,以及写详细事后分析的资源。这造成了幸存者偏差。以下是更诚实的地图:
初创公司(1–15 名工程师)。构建单体。你还不知道系统的哪些部分需要扩展,哪些功能会存活,或者哪些领域边界是真实的。过早分解会在你有足够信息做出决策之前锁定决策。Shopify、GitHub 和 Basecamp 都是从 Rails 单体起步的。Twitter 也是,他们在真正的负载下分解之前用了多年。
规模化公司(15–80 名工程师,成长中)。这是模块化单体展现价值的时候。你有足够多的工程师,以至于单个代码库的不受控增长造成了真实摩擦,但你仍然作为一个团队部署,完整微服务的开销会扼杀你的速度。投资于模块边界、团队所有权和清晰接口。保留提取服务的选项,但除非具体的扩展需求迫使你,否则不要行使它。
企业级 / 大型组织(80+ 名工程师,多个自治团队)。选择性的微服务在这里有意义——但“选择性”是关键词。最有效的大规模架构既不是纯单体,也不是一片一样大小的服务:它们是经过深思熟虑的混合。少数核心服务用于最承载负载、最关键领域的能力,模块化单体用于运营中间层,以及少数需要独立扩展或合规真正需要的专业服务。Amazon 没有一次性分解一切;他们在负载下识别出缝合处,一个一个地提取。
当 Netflix 和 Amazon 描述他们的微服务历程时,被引用的部分是那张有数百个服务的架构图。被略去的部分是,他们起步于单体,把它们运行到痛苦不可否认,然后投入了数年时间和巨大的工程努力进行过渡——包括构建了现在让微服务可行的大量工具。他们在告诉你他们到达了哪里,而不是你应该从哪里开始。
核心要点
- 单体是正确的默认选择。构建简单,调试容易,事务轻松。痛苦比炒作所暗示的来得晚,而且只在真正的规模下出现。
- 模块化单体是最被低估的选项。内部模块之间的强制边界给你团队清晰度、安全重构和提取就绪的缝合处——没有任何分布式系统复杂性。
- 微服务是税,不是功能。你在网络失败、最终一致性、分布式追踪和运维开销上支付代价。回报——独立部署、独立扩展、团队自治——是真实的,但只在真正需要的规模下才有意义。
- 分布式单体是最糟糕的结果。必须一起部署的紧耦合服务让你付出微服务的所有成本,却得不到任何好处。沿着限界上下文拆分,而不是技术层。
- 数据所有权是硬约束。与另一个服务共享数据库的服务不是服务。每个服务必须拥有自己的数据;跨边界的一致性需要明确的设计。
- 使用绞杀者无花果模式逐步拆分。每次提取一个限界上下文。每次提取都教给你一些东西,而且是可逆的。永远不要做大爆炸式的分解。
- 让架构与团队规模匹配,而不是与愿景匹配。初创公司:单体。规模化公司:模块化单体。大型自治团队:真正需要时选择性微服务。
架构决策在时间上向前涟漪并复利。很多团队在决定拆分后面临的下一个问题是对前端该怎么办——是提供一个统一的 UI 还是把它也分解。这个兔子洞在 微前端:何时值得,以及它们真正的代价 中探讨。