Language:Chinese VersionEnglish Version

微服务赢得了营销战。每场会议演讲、每篇架构博客、每个招聘启事似乎都假设将应用程序拆分为数十个独立部署的服务是软件演化的自然终点。但在行业范围内采用十年后,证据很明确:对于大多数中小型团队来说,微服务的成本大于其带来的价值。模块化单体架构——一个具有清晰内部边界、结构良好的可部署单元——值得作为默认架构认真考虑。

编者按:钟摆终于开始回摆。在多年过早采用微服务导致不需要它的团队复杂度增加之后,行业正在展开更诚实的对话。Michael 用数字而非教条阐述了真正的权衡取舍。

为什么微服务成为默认答案

微服务的宣传很有吸引力:独立部署能力、技术多样性、团队自主性以及单个组件的水平扩展能力。这些是实实在在的好处——在特定规模下。Netflix、Amazon 和 Uber 真的需要微服务,因为他们有数百个工程团队,无法在单个代码库中协调部署。

问题是,大多数团队采用微服务并非因为他们面临这些扩展挑战,而是因为微服务成为了”现代架构”的代名词。一个五人团队构建 SaaS 产品不会有 Netflix 的协调问题。他们有一个可以通过共享代码库、Slack 频道和站会来管理的协调面。

会议演讲中没人提及的成本

运营开销

你添加的每个微服务都是另一个需要部署、监控、记录日志、扩展和调试的东西。单体架构有一个部署管道。十个微服务就有十个部署管道,每个都有自己的 CI 配置、Docker 镜像、健康检查和回滚程序。对于小团队来说,这个运营面不是免费的——它是以每周花在非产品基础设施上的小时数来衡量的。

考虑一个具体例子:一个典型的 SaaS 应用程序被拆分为用户服务、计费服务、通知服务、API 网关和前端 BFF。那是五个需要维护的服务。每个都需要自己的:

  • Dockerfile 和构建管道(5倍的 CI 配置维护)
  • 健康检查端点和存活探针
  • 日志配置和日志聚合
  • 错误跟踪设置
  • 数据库或模式管理(如果使用每个服务一个数据库)
  • 网络策略和服务网格配置

保守估计:每个服务每周增加 2-4 小时的运营开销。对于五个服务来说,就是每周 10-20 小时——一到两个完整的工程日——花在基础设施而不是功能上。

分布式系统的复杂性

当你将单体应用拆分为通过网络通信的服务时,你就继承了分布式系统中的所有问题。网络调用会失败。服务会独立宕机。跨服务的数据一致性需要精心编排。在单体应用中微不足道的数据库事务,在分布式系统中变成了包含补偿事务、死信队列和最终一致性语义的saga模式,而你的用户可能并不欣赏这些。

调试跨越五个服务的请求比调试停留在单个进程中的请求要困难得多。你需要分布式追踪(Jaeger、Zipkin)、关联日志以及能够跟踪请求跨服务边界的思维模型。这些工具确实存在,但它们又增加了需要维护的基础设施层。

数据管理的烦恼

微服务正统观念认为每个服务应该拥有自己的数据。在实践中,这意味着你不能跨服务边界进行连接。需要在计费数据旁边获取用户信息?那是一个API调用,而不是SQL连接。需要运行一个跨越三个领域的报告?你需要构建数据管道或API聚合层。

许多团队通过跨服务共享数据库来妥协。这确实可行,但它将你的服务在数据层耦合起来,失去了大部分架构优势,同时保留了所有运维成本。

模块化单体应用的替代方案

模块化单体应用是一个可部署的单一应用程序,具有明确定义的内部模块,这些模块有清晰的边界、明确的接口和最小化的耦合。可以把它想象成没有网络边界的微服务架构——你获得了分离的组织优势,却没有分布式的运维成本。

这看起来像什么

你的代码库被组织成模块:users/billing/notifications/,每个模块都有公共接口和私有内部实现。模块通过函数调用或内部事件总线进行通信,而不是HTTP请求。数据库可以共享,但采用模式所有权——计费模块拥有自己的表并通过其公共API暴露数据,而不是通过直接表访问。

在具有强大模块系统的语言中(Elixir、Rust、Go),编译器会强制执行边界。在没有这些模块系统的语言中(Node.js、Python),你通过约定、linting规则或像dependency-cruiser这样的工具来强制执行这些边界。

你保留了什么

  • 清晰的边界: 模块有定义好的接口。模块内部的变更不会影响到其他模块。
  • 团队所有权: 不同的人或团队可以拥有不同的模块。
  • 独立开发: 只要接口保持稳定,模块可以按照自己的节奏演进。
  • 可测试性: 每个模块都可以在隔离环境中进行测试,依赖项在接口级别被模拟。

您将获得什么

  • 单一部署: 一个流水线,一个 Docker 镜像,一次回滚。
  • 本地函数调用: 无网络延迟,无序列化开销,无需重试逻辑。
  • 数据库事务: 在需要时,跨模块边界的 ACID 保证。
  • 更简单的调试: 是堆栈跟踪,而非分布式跟踪。是一个日志文件,而非日志聚合系统。
  • 更低的基础设施成本: 一个服务器进程而非五个。一个负载均衡器。一个监控目标。

微服务何时真正有意义

微服务没有错——它们是针对特定问题的解决方案。在以下情况下考虑使用它们:

您的团队超过 30-50 名工程师。 在这个规模下,单一代码库中的协调开销会成为真正的瓶颈。团队成员相互干扰,CI 时间变得痛苦,并且每次部署都包含许多团队的变更,导致部署风险增加。

您有真正不同的扩展需求。 如果您的图像处理服务需要 GPU 实例,而您的 API 服务器需要 CPU,将它们运行在同一进程中是浪费的。不同的扩展模式合理化了不同的服务。

您有正当理由需要技术多样性。 如果您的 ML 流水线使用 Python 而您的 API 使用 Go,那么服务边界是有意义的。但”我们想为这个服务尝试 Rust”不是好理由——这是一种维护负担。

出于监管或可靠性原因需要独立部署。 如果您的支付系统的故障不能影响您的用户界面应用程序,那么具有独立部署和故障隔离的硬性服务边界是合理的。

迁移路径

从模块化单体架构开始的美妙之处在于,向微服务的迁移路径很直接。如果一个模块确实需要成为服务——由于扩展、部署或团队自主性需求——您可以将其提取出来。接口已经定义,数据所有权已经明确。您是在现有的模块边界上添加一个网络边界。

反方向的转变——从微服务回归单体架构——要困难得多。你需要移除网络边界、合并数据库、整合部署流水线,并梳理分布式事务逻辑。几家知名公司已经这样做了(Segment、Istio、Prime Video),他们都形容这个过程痛苦但值得。

决策框架

在将某个组件拆分为服务之前,请先问自己这些问题:

  1. 我们的团队是否大到无法协调部署?如果所有人都能在一个Slack频道里沟通,答案可能是否定的。
  2. 我们是否有不同的扩展需求?不是理论上的,而是实际需求。你是否在某个特定工作负载上已经面临容量不足的问题?
  3. 运维成本是否能被架构收益所证明?计算一下在服务基础设施上花费的时间,与独立部署所节省的时间进行比较。
  4. 我们是否已经尝试过更简单的替代方案?模块边界、后台任务队列和只读副本能以更低的成本解决许多通常使用微服务解决的问题。

如果你对这些问题大部分回答”否”,那么模块化单体架构是更好的选择。不是因为微服务不好,而是因为它们解决的是你尚未遇到的问题——而解决不存在的问题的开销是非常真实的。

我的看法

我两种架构都使用过。我见过的最佳系统是模块化单体架构,只有在不得不拆分为服务时才进行提取。我见过的最糟糕的系统是过早采用微服务架构的案例,一个三人团队花费在基础设施上的时间比在产品上的还多。

从单体架构开始。让它模块化。当你有证据——而非推测——表明需要服务时再进行提取。这不是保守立场,而是高效的做法。

关键要点

  • 微服务解决了大规模(30+工程师)的协调问题,但大多数小团队过早采用它们,用产品开发时间换取基础设施开销。
  • 每个微服务每周增加2-4小时的运维成本——部署流水线、监控、日志记录、跨网络边界调试。五个服务每周可能消耗两个工程师的工作日。
  • 模块化单体架构为你提供了清晰的边界、团队所有权和可测试性,同时保持了单一部署的简单性、数据库事务和直接的调试能力。
  • 如果你的模块有良好的定义接口,单体到微服务的迁移路径是清晰的。相反方向(微服务到单体)则痛苦且昂贵。
  • 在提取服务之前,要求提供证据:实际的扩展瓶颈、团队协调失败或监管要求——而非架构时尚。

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed