Oleksii Dushenin 2021-10-22
单体架构是一种构建应用程序的传统方式。这种软件架构原则既有优点,也有缺点。一方面,它可能带来便利;另一方面,也可能引发问题。下面我们来回顾它在软件架构中的地位。
上图展示了一个典型的单体架构。其关键特征是:所有组件(如用户服务、航班服务和计费服务)都打包在同一个可交付单元中(例如 Spring Boot 项目中的一个 JAR 文件)。这些服务虽然功能完全不同,但彼此紧密耦合,并且共用同一个数据库。
目前,这种架构是最常见的做法。实际上,在单体架构中,前端 UI 部分也可以与后端服务一起打包。因此,UI、用户服务、航班服务和计费服务都被整合到同一个可交付单元中。
为了实现横向扩展,通常会部署多个相同的可交付单元。负载均衡器(Load Balancer)负责将请求路由到各个已部署的单体服务实例。
单体架构的优点
- 开发简单:单体架构是构建应用程序的标准方式,无需额外知识。所有源代码集中在一个地方,便于快速理解。
- 调试简单:由于所有代码都在一处,调试过程非常直接。你可以轻松跟踪请求流程并定位问题。
- 测试简单:你只需测试一个服务,没有外部依赖,整体逻辑清晰。
- 部署简单:只需部署一个部署单元(如 JAR 文件),不存在依赖问题。如果 UI 与后端代码打包在一起,则不会出现接口不兼容的问题——所有内容都在同一处变更。
- 应用演进简单:从业务逻辑角度看,应用几乎没有限制。如果新功能需要某些数据,这些数据通常已经存在。
- 横切关注点只需处理一次:安全、日志、异常处理、监控、Tomcat 参数配置、数据源连接池设置等横切关注点只需统一处理一次。
- 新人上手容易:源代码集中存放,新成员可以轻松调试某个功能流程,快速熟悉整个应用。
- 初期成本低:所有源代码集中、打包为单一部署单元并部署,既无基础设施开销,也无额外开发成本。
正因上述优势,单体架构通常用于应用开发的早期阶段,原因如下:
- 应用的核心目标是实现盈利。因此,快速推出概念验证(POC)方案以在真实环境中验证想法至关重要,同时也要尽快吸引用户。后续再逐步优化。
- 在早期阶段,需求往往不明确。在需求模糊的情况下,很难设计出有意义的架构。只有当部分功能上线后,真实用户才能帮助明确业务需求。
单体架构的缺点
然而,当应用取得成功后,单体架构的问题便开始显现。根本原因很简单:应用规模的增长。随着时间推移,单体应用往往会因以下原因而变得难以维护:
- 开发速度变慢:最明显的缺点体现在 CI/CD 流水线上。假设你的单体应用包含大量服务,每个 Pull Request 都要运行所有服务的测试。即使只做微小修改,也可能需要等待很长时间(例如 1 小时)才能完成流水线。若流水线失败,你还得重新等待。此外,团队规模扩大后,同事合并代码会导致你频繁 rebase/merge,并再次等待。
- 代码高度耦合:尽管你可以在仓库内部保持清晰的服务结构,但实践中,最终总会出现几处“意大利面条式代码”(spaghetti code),使系统更难理解,尤其对新成员而言。
- 无法实现代码所有权:随着系统增长,自然需要将职责划分给多个团队(例如一个团队负责航班服务,另一个负责计费服务)。但由于服务之间没有明确边界,一个团队的改动可能会影响另一个团队。
- 测试变得更困难:即使是微小改动,也可能对整个系统产生负面影响,因此每次变更都需要进行完整的回归测试。
- 性能问题:虽然可以通过横向扩展整个单体服务来应对性能瓶颈,但数据库呢?所有服务共享同一个数据库。你可以优化查询或使用只读副本,但这类优化终有极限。
- 基础设施成本高:遇到性能问题时,你只能整体扩展整个单体服务,这会带来额外的运维成本。
- 技术栈陈旧:假设你的应用基于 Java 8 开发,那么将其整体(包含多个内部服务)迁移到 Java 11 需要多少时间?与此同时,新功能开发任务仍在继续。结果可能是:应用永远无法完成技术升级。
- 缺乏灵活性:采用单体架构意味着你被锁定在现有技术栈中。即使有更适合特定问题的新工具或框架,也无法单独引入使用。
- 部署困难:哪怕只是微小的代码变更,也需要重新部署整个单体应用。
总结
单体架构因其开发迅速、测试与调试简单、成本低廉等优势,非常适合小型应用。然而,当系统不断增长时,它反而可能成为业务发展的障碍,此时应考虑向其他架构形式(如微服务架构)演进。