针对 SAAS 私有化部署,如何优雅合并微服务

需求背景

在 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);
    }
}

数据库

在私有云场景下,我们采用了相对务实的方案:统一数据源,所有业务表放在同一个数据库中。原因在于:

私有云客户的数据规模和并发模型通常是可预期的

客户更关心系统是否稳定、是否易维护

多数据源反而会增加部署和排查成本

在这种背景下,统一数据源往往是 性价比最高 的选择。

私有云不需要考虑那么多,什么性能问题、数据量等等,很少会成为私有化部署的瓶颈。私有云场景下,数据规模和并发模型通常是可预期的,因此我们选择了更简单、可维护性更高的方案。

相关推荐
zandy10112 小时前
衡石科技实践:如何基于统一指标平台,实现从传统BI到Agentic BI的架构演进
科技·架构
GJGCY2 小时前
技术拆解:从Manus的通用推理到金智维K-APA的受控执行,企业级AI架构如何选择?
人工智能·架构
黎雁·泠崖2 小时前
Java字符串API:String/StringBuffer/StringBuilder详解
java·开发语言
山枕檀痕2 小时前
JPA Projection 详解(接口投影 / 类投影 / 动态投影 / 原生SQL映射)
java·hibernate·jpa
Jack_abu2 小时前
stream().toList()与.collect(Collectors.toList())
java·stream·jdk8
黎雁·泠崖2 小时前
Java核心API之Object类:所有类的根父类
java·开发语言
Huanlis2 小时前
Spring Data Redis Stream:全景架构、交互流转与线程池陷阱深度解析
redis·spring·架构
Remember_9932 小时前
【LeetCode精选算法】位运算专题
java·开发语言·jvm·后端·算法·leetcode