需求背景
在 SAAS 系统中,一个非常经典、同时也非常"现实"的部署场景是:
客户要求提供私有云部署方案
但是对于微服务架构场景,私有云部署的成本是非常大的,例如:对于有些大型SAAS系统来说,一个系统会有几百个、上千个 Docker 容器需要部署启动到客户的私有云环境中,即便是做好了通用的部署方案,部署的成本也是非常高的,无论是人力,还是服务器资源。
所以很多 SAAS 厂商针对于这种私有云部署场景,会考虑:将微服务进行合并。假设你的系统中微服务容器有300个,通过合并可能只需要部署60个服务,差别非常大。
在不推翻现有微服务架构的前提下,对微服务进行"部署维度"的合并。
为什么不能"简单粗暴"地合并
最直观、也最容易被想到的方式是:直接把多个微服务的代码复制(CV)到一个工程里,做成一个大单体。但在真实项目中,这种方式几乎不可行,尤其是大型系统:
❌ 改造成本极高
❌ 风险不可控
❌ 回退成本巨大
❌ 公有云 SAAS 架构会被直接破坏
一旦合并完成,系统将:
❌ 很难再拆回微服务
❌ 形成私有云与公有云两套代码分支
❌ 后续维护成本指数级上升
因此,我们在实际项目中采用了一种 改造成本极低、可随时回退、对原架构几乎零侵入 的方案。这里就介绍一个我们真实落地的微服务合并方案:聚合启动
聚合启动(All‑in‑One)
Maven import 多个微服务 → 上层 All-in-One 聚合服务 → 一个启动类统一启动
原本的微服务项目不做任何改动,新创建一个All-In-One服务作为聚合工程,然后将想要合并的微服务放到该工程下,这样 All In One 服务有一个启动类,作为该工程下所有微服务统一启动的入口,原本的微服务代码不需要做任何的调整,依旧提供原本的能力(公有云),项目结构大概如下:
java
all-in-one
├── all-in-one-starter (SpringBootApplication 启动类)
├── user-service
├── order-service
├── auth-service
├── pay-service
不需要推翻原本的微服务架构,公有云 SAAS 依旧按照以前的微服务方式进行部署,不会对之前的架构造成任何影响,改造成本极低。
本方案并不是将系统从微服务退化为单体,而是在部署维度对微服务进行收敛,在代码与架构维度保持微服务形态不变。
这种结构的最大优势在于:
✅ 公有云 SAAS 架构 完全不受影响
✅ 私有云方案 仅新增一个工程
✅ 微服务依然可以独立开发、测试、发布
注意点
Maven 依赖
所有微服务都会通过依赖绑定到父工程中,例如:
xml
<dependencies>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>user-service</artifactId>
</dependency>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>order-service</artifactId>
</dependency>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>auth-service</artifactId>
</dependency>
</dependencies>
聚合启动类
All‑in‑One 工程提供唯一的启动类,通过 @ComponentScan 扫描所有微服务包:
java
// 关键:通过 @ComponentScan 扫描所有微服务包,确保 Bean 被加载
@SpringBootApplication(
scanBasePackages = {
"com.xxx.user",
"com.xxx.order",
"com.xxx.hr",
"com.xxx.auth"
}
)
public class AllInOneApplication {
public static void main(String[] args) {
SpringApplication.run(AllInOneApplication.class, args);
}
}
服务调用
原本的微服务之间通讯用的是 Feign,我们的 Feign URL 都在配置文件中维护好了,那么合并之后,直接就可以重写 ALL IN ONE 聚合工程中的配置,将 Feign URL 调整为本地,这样Feign注解就可以将请求路由到当前服务中了,例如:
java
@FeignClient(name = "user-service", url = "http://127.0.0.1:8080")
还有一个办法,在 All‑in‑One 工程中,对原有 Feign Client 进行本地实现覆盖:
java
@Primary
@Component
public class LocalUserClient implements UserClient {
@Autowired
private UserService userService;
@Override
public UserDTO getById(Long id) {
return userService.getById(id);
}
}
数据库
在私有云场景下,我们采用了相对务实的方案:统一数据源,所有业务表放在同一个数据库中。原因在于:
私有云客户的数据规模和并发模型通常是可预期的
客户更关心系统是否稳定、是否易维护
多数据源反而会增加部署和排查成本
在这种背景下,统一数据源往往是 性价比最高 的选择。
私有云不需要考虑那么多,什么性能问题、数据量等等,很少会成为私有化部署的瓶颈。私有云场景下,数据规模和并发模型通常是可预期的,因此我们选择了更简单、可维护性更高的方案。