概述
作为软件开发者,我们拥有多种工具来构建软件。以Java构建的软件为例,我们有方法,一旦我们有一组相关的方法,我们可以将它们分组到类中,这些类可以分组到包中,这些包可以外部化为模块。
软件架构就是关于所有这些工具如何相互链接和建立关系的。
在本文中,我们将介绍最常用的软件架构风格,包括单体和微服务,然后我们将介绍另一种被认为是这两种风格之间中间地带的风格,称为模块化单体(modulith)。
什么是单体应用程序
单体架构
在单体风格中,有一个单一的代码库,所有内容都打包并部署为单个单元。
单体应用程序使用的数据存储在单个数据库中。
对于处理中等数据量的小型到中型应用程序,这种架构风格仍然非常实用和高效,以下是一些优势:
- 开发简单性:开发者可以轻松理解应用程序流程
- 重构和调试简单:使用现代IDE,这两项任务变得简单直接
- 低延迟:调用是在属于同一进程的函数之间进行的,没有网络开销
单体的问题开始于应用程序在职责、库和应该处理的数据量方面增长时,在这些情况下,我们需要处理:
- 低效扩展:如果应用程序的单个部分获得大量流量,处理此问题的唯一选择是扩展整个应用程序及其所有部分
- 复杂的团队并行化:使用这种架构风格,很容易使业务领域相互耦合,在同一代码库上工作的团队可能会遇到合并冲突
- 低弹性:如果应用程序的一个部分崩溃,整个应用程序将停止运行
什么是微服务应用程序
微服务架构
这是关于在职责方面独立部署的小型软件,在单独的进程中运行,并通过网络调用相互通信。
每个软件都应该有自己的数据库。
这种架构风格是为了克服单体应用程序在变大时面临的挑战而引入的。以下是一些优势:
- 选择性扩展:可以只扩展负载较重的服务,而不是整个系统
- 清晰的上下文边界:每个服务应该专注于特定的业务领域并具有明确的职责,这种分离使维护变得更容易
- 容错和增强的弹性:如果一个服务宕机,其他服务仍然可以继续处理接收到的请求
微服务并不是免费提供这些好处的,它们也有缺点:
- 增加延迟:服务之间的通信涉及网络调用,而网络意味着潜在的不可靠性、延迟、超时
- 额外的开发复杂性:使用这种风格,开发者必须付出额外的努力来处理服务发现、数据一致性和分布式日志记录/跟踪/监控
微服务风格比单体风格更好吗?
大多数时候,我们似乎对单体有负面看法,另一方面,我们对微服务有很好的印象,这通常是因为微服务被科技行业中非常占主导地位的公司推动。然而,这种观点是相当主观的。
单体应该被视为在低层次上非常有用,在那里我们将有一个小团队生产简单的应用程序,并且在这方面非常高效,没有任何微服务的开销。
模块化单体(Modulith):中间地带
模块化单体架构
除了单体和微服务之外,还有另一种架构风格,称为模块化单体(modulith),它专注于将应用程序分解为业务模块,这些模块都在同一个代码库中,所以它仍然是一个单体,但是它们的源代码不会相互纠缠,模块相互隔离,它们之间的通信通过API或事件进行。
当谈论模块化单体中的模块时,我们指的是业务模块,而不是Java模块或构建工具模块,这些是完全不同的概念。
这种风格的有趣之处在于,您可以受益于单体和微服务的优势:
- 应用程序的结构清晰,使开发者更容易理解流程
- 源代码在一个地方,这一点可以用来受益于IDE的重构和调试功能
- 没有网络开销,意味着低延迟
- 模块之间有清晰的上下文边界
您无法从微服务中获得的好处是:
- 容错(弹性)
- 选择性扩展
如何在Java中实现模块化单体
在Java中,有三种主要方法来实现模块化单体:
- 每个业务模块一个包
- 每个业务模块一个构建工具模块(使用Maven或Gradle)
- 每个业务模块一个Java模块(JPMS)
使用包进行模块化
这是最简单和最常用的方法。应用程序仍然是单个传统的Java模块,每个业务模块都放在专用的Java包中(例如,com.myapp.orders、com.myapp.billing等)。
这种方法实现简单,但是它在编译时或运行时都不强制执行业务模块之间的严格边界,为了确保模块之间的低耦合,应该在代码审查中做出努力,没有纪律,项目很容易变成纠缠的单体。
使用Maven/Gradle多模块项目进行模块化
在这种方法中,每个业务模块都分割到自己的Maven或Gradle项目中。每个模块编译为单独的JAR,模块依赖关系在构建配置中明确声明。
这种方法的好处是,编译时隔离由构建工具强制执行,模块只能访问它作为依赖关系的模块,这减少了最终变成纠缠单体的可能性,但是在运行时,所有项目的所有类都可以通过反射在所有类中使用。
使用JPMS进行模块化
使用JPMS(在Java 9中引入),每个业务模块都放在自己的Java模块中,用module-info.java文件定义。
JPMS在编译时和运行时都强制执行上下文之间的隔离,但是与现代框架如Spring Boot集成有点复杂。
在下一篇博客文章中,我将介绍Spring Modulith,这是一个为了帮助我们使用包模块化以简单和健壮的方式构建模块化单体而引入的项目。
架构风格对比总结
特性单体模块化单体微服务开发复杂度低中等高部署复杂度低低高扩展性整体扩展整体扩展选择性扩展团队协作困难中等容易故障隔离无部分完全网络延迟无无有数据一致性简单简单复杂技术栈统一统一多样化
选择指南
何时选择单体架构
- 小型团队(1-5人)
- 简单的业务逻辑
- 快速原型开发
- 资源有限的初创公司
- 不需要高可用性的应用
何时选择模块化单体
- 中型团队(5-15人)
- 复杂的业务逻辑但仍在可控范围内
- 需要清晰的模块边界
- 希望为未来迁移到微服务做准备
- 需要低延迟的应用
何时选择微服务架构
- 大型团队(15+人)
- 高度复杂的业务逻辑
- 需要独立扩展不同功能
- 需要高可用性和容错能力
- 多技术栈团队
实际应用示例
单体架构示例
// 传统的Spring Boot单体应用结构
src/
├── main/
│ ├── java/
│ │ └── com/myapp/
│ │ ├── controller/
│ │ │ ├── UserController.java
│ │ │ ├── OrderController.java
│ │ │ └── ProductController.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ ├── OrderService.java
│ │ │ └── ProductService.java
│ │ ├── repository/
│ │ │ ├── UserRepository.java
│ │ │ ├── OrderRepository.java
│ │ │ └── ProductRepository.java
│ │ └── model/
│ │ ├── User.java
│ │ ├── Order.java
│ │ └── Product.java
│ └── resources/
│ └── application.properties
模块化单体示例
// 模块化单体应用结构
src/
├── main/
│ ├── java/
│ │ └── com/myapp/
│ │ ├── user/
│ │ │ ├── UserController.java
│ │ │ ├── UserService.java
│ │ │ ├── UserRepository.java
│ │ │ └── User.java
│ │ ├── order/
│ │ │ ├── OrderController.java
│ │ │ ├── OrderService.java
│ │ │ ├── OrderRepository.java
│ │ │ └── Order.java
│ │ └── product/
│ │ ├── ProductController.java
│ │ ├── ProductService.java
│ │ ├── ProductRepository.java
│ │ └── Product.java
│ └── resources/
│ └── application.properties
微服务示例
# Docker Compose微服务配置
version: '3.8'
services:
user-service:
build: ./user-service
ports:
- "8081:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
depends_on:
- user-db
order-service:
build: ./order-service
ports:
- "8082:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
depends_on:
- order-db
product-service:
build: ./product-service
ports:
- "8083:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
depends_on:
- product-db
user-db:
image: postgres:13
environment:
POSTGRES_DB: userdb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
order-db:
image: postgres:13
environment:
POSTGRES_DB: orderdb
POSTGRES_USER: order
POSTGRES_PASSWORD: password
product-db:
image: postgres:13
environment:
POSTGRES_DB: productdb
POSTGRES_USER: product
POSTGRES_PASSWORD: password
迁移策略
从单体到模块化单体
- 识别业务边界
- 重构包结构
- 引入模块间API
- 逐步解耦模块
从模块化单体到微服务
- 识别独立部署的候选模块
- 提取共享数据
- 实现服务间通信
- 独立部署和测试
总结
选择合适的架构风格应该基于您的具体需求、团队规模和业务复杂性。没有一种架构风格适合所有情况,关键是要理解每种风格的优缺点,并根据您的具体情况做出明智的决定。
记住,架构是一个演进的过程,您可以从简单的单体开始,随着需求的增长逐步演进到更复杂的架构。