1. 背景
- 对外交付项目多为微服务架构,全量部署需要占用较多的内存资源。
- 私有化交付时,客户对投入的硬件成本有预算限制,前期太多的硬件投入影响客户体验,不利于成单。
2.目标
- 不改变目前SAAS环境微服务部署架构,避免SAAS环境出现性能问题,进而影响系统的稳定性。
- 私有化交付项目通过部署、打包等技术,将多个微服务部署到一个JVM进程或整合为一个单体项目,进而缩减硬件成本。
- 只针对SpringBoot项目做改造。
3.【Docker】融合部署
3.1 docker单容器多应用
3.2 dockerfile改造
-
端口映射改造
一个docker容器同时映射多个应用服务端口
-
应用jar文件包含改造
一个docker镜像需将多个springboot jar文件打包进去
-
ENTRYPOINT启动脚本改造
ENTRYPOINT启动脚本需包含多个应用的启动过程
4.【Tomcat】融合部署
4.1 Tomcat单实例多应用
4.2 原微服务应用改造
- 修改pom 为war
xml
原:
<artifactId>web-provider</artifactId>
<name>web-provider</name>
<packaging>jar</packaging>
新:
<artifactId>web-provider</artifactId>
<name>web-provider</name>
<packaging>war</packaging>
- 排除SpringBoot内嵌容器tomcat
xml
pom新增坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
- 排除三方组件,减少war包大小
xml
调整spring-boot-maven-plugin插件只把业务代码及依赖的业务组件打包进war中:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includes>
<include> <!-- 根据实际情况修改,<includes>下可以包含多个<include> -->
<groupId>com.lw.micro</groupId> <!-- 业务组件groupId -->
<artifactId>web-provider-api</artifactId><!-- 业务组件artifactId -->
</include>
</includes>
</configuration>
</plugin>
- 改造启动类
scala
原启动类继承SpringBootServletInitializer并实现configure方法:
@ComponentScan
@SpringBootApplication
@EnableFeignClients({"com.web.provider.api"})
public class Application extends SpringBootServletInitializer{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
}
4.3抽取全量三方组件
- 新建项目为pom
- 中需包含各个微服务war包共享的全量三方组件
- 利用maven-dependency-plugin插件将依赖组件抽取到指定目录中
xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory><!-- 依赖包输出目录 -->
<excludeTransitive>false</excludeTransitive>
<stripVersion>false</stripVersion>
<includeScope>runtime</includeScope>
<excludeGroupIds>com.lw.micro</excludeGroupIds><!-- 需要排除的业务组件groupId,多个以逗号分割 -->
</configuration>
</execution>
</executions>
</plugin>
4.4 Tomcat部署
4.4.1 tomcat配置
4.4.1.1server.xml配置
- 根据实际情况修改
xml
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!-- Prevent memory leaks due to use of particular java/javax APIs-->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- Global JNDI resources
Documentation at /docs/jndi-resources-howto.html
-->
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="lw-micro-demo">
<Connector port="8280" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8243"
maxParameterCount="1000"
/>
<Engine name="lw-micro-demo" defaultHost="www.demo.com">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="www.demo.com" appBase="webapps"
unpackWARs="true" autoDeploy="false" deployOnStartup="false">
<Context path="web-provider" docBase="web-provider"/>
<Context path="web-consumer" docBase="web-consumer"/>
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
4.4.1.2 catalina.properties配置
- 根据实际情况修改
ini
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="lw-micro-demo">
<Connector port="8280" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8243"
maxParameterCount="1000"
/>
<Engine name="lw-micro-demo" defaultHost="www.demo.com">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="www.demo.com" appBase="webapps"
unpackWARs="true" autoDeploy="false" deployOnStartup="false">
<Context path="web-provider" docBase="web-provider"/>
<Context path="web-consumer" docBase="web-consumer"/>
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
4.4.2 部署目录
1、war包部署目录为webapps 2、webapps目录为默认自带目录
1、各个war包共享三方组件部署目录为shared
2、shared目录为catalina.properties中shared.loader自定义目录,需自建
5【Maven】融合打包
5.1 打包方案
5.2 原微服务应用改造
- 修改SpringBoot打包类型为普通jar包
xml
注释SpringBoot打包插件:
<!-- <plugin>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-maven-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <fork>false</fork>-->
<!-- </configuration>-->
<!-- </plugin>-->
-
命名冲突改造
- 包名冲突改造
- IOC容器Bean类名冲突改造 禁用如下配置解决Bean类名冲突问题spring.main.allow-bean-definition-overriding: true
-
数据源配置改造
-
单数据源,抽取各个微服务的数据源到公共包中,各个微服务依赖公共包,配置类及配置项key不需要调整
-
多数据源,需要按照各个微服务的命名区分配置类及配置项key,以保障集成为单体时不冲突,如:
-
配置项key
bash当前配置 druid.datasource.url druid.datasource.username druid.datasource.password # 改后配置 xxx.druid.datasource.url xxx.druid.datasource.username xxx.druid.datasource.password
-
配置类
less@Configuration @Slf4j @MapperScan(basePackages = {"com.xxx.dao"}, sqlSessionFactoryRef = "xxxSqlSessionFactory") public class XXXDataSourceConfiguration extends DruidDataSoureConfiguration { @Value("com.alibaba.druid.pool.DruidDataSource") private Class<? extends DataSource> dataSourceType; @Bean(name = "xxxDataSource") @ConfigurationProperties(prefix = "xxx.druid.datasource") public DataSource dataSource() { DataSource dataSource = DataSourceBuilder.create().type(dataSourceType).build(); return buildDataSourcePool(dataSource); } @Bean(name = "xxxSqlSessionFactory") public SqlSessionFactory sqlSessionFactory(@Qualifier("xxxDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(resolveMapperLocations()); return bean.getObject(); } @Bean(name = "xxxTransactionManager") public DataSourceTransactionManager transactionManager(@Qualifier("xxxDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "xxxSqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate(@Qualifier("xxxSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } private Resource[] resolveMapperLocations() { ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); List<String> mapperLocations = new ArrayList<>(); mapperLocations.add("classpath*:com/xxx/dao/**/*.xml"); List<Resource> resources = new ArrayList<>(); if (CollectionUtils.isNotEmpty(mapperLocations)) { for (String mapperLocation : mapperLocations) { try { Resource[] mappers = resourcePatternResolver.getResources(mapperLocation); resources.addAll(Arrays.asList(mappers)); } catch (IOException e) { log.error("resourcePatternResolver getResources error", e); } } } return resources.toArray(new Resource[resources.size()]); } ```
-
-
-
HTTP客户端改造
- 建议HTTP客户端统一改造为OpenFeign
- 建议OpenFeign接口类按单一职责定义
- HTTP客户端情况
5.3 新建单体项目
- 新建一个SpringBoot项目,打包插件为spring-boot-maven-plugin
- 添加原各个微服务普通jar组件依赖
xml
<dependencies>
<dependency>
<groupId>com.lw.micro</groupId>
<artifactId>web-provider</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.lw.micro</groupId>
<artifactId>web-consumer</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
- 添加原各个微服务application.yml配置项(删除重复项)
- 启动类增加@ComponentScan、@MapperScan注解
less
@MapperScan(basePackages = {
"com.web.provider.dao",
"com.web.consumer.dao"
})
@ComponentScan({
"com.web.provider.*",
"com.web.consumer.*"
})
@SpringBootApplication
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 合并mybatis-plus.mapper-locations配置
ruby
mybatis-plus:
mapper-locations: classpath*:com/web/provider/**/mapping/*.xml,classpath*:com/web/consumer/**/mapping/*.xml
- OpenFeign本地调用改造
less
1、新建的单体项目启动类@EnableFeignClients注解只指定依赖的外部服务接口
@MapperScan(basePackages = {
"com.web.provider.dao",
"com.web.consumer.dao"
})
@ComponentScan({
"com.web.provider.*",
"com.web.consumer.*"
})
@EnableFeignClients(basePackages = {
"依赖的外部服务接口"
})
@SpringBootApplication
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2、新建的单体项目对应Controller类实现@FeignClient注解的接口
@FeignClient(name="web-provider", url = "${web.provider.url}")
public interface ProviderTestApi{
@GetMapping("/provider-test/getTest")
Response getTest();
}
@RestController
@RequestMapping("/provider-test")
@Slf4j
public class ProviderTestController implements ProviderTestApi{
@Autowired
private IProviderTestService providerTestService;
@Override
@GetMapping("/getTest")
public Response getTest() {
log.info("remote obj addr={}",this.toString());
Test test = providerTestService.getById(1);
log.info(test.toString());
return Response.ok("调用provider成功");
}
}
6 方案选型
6.1 微服务拆分方式
*备注:较少冲突*
*备注:较多冲突*
6.2 方案选型
- 方案对比
方案 | 优点 | 缺点 | 适用场景 | ffff | 改造Demo |
---|---|---|---|---|---|
【Maven】融合打包 | 1. 全部SpringBoot部署 2. 部署运维方案一致 | 1. 命名冲突较多时改造成本高 2. http客户端需要改造 | 1. 各个微服务包名及类名有少量冲突2. 改造风险可控3. 改造成本可控 | 具体见5.2~5.3 | xxx |
【Tomcat】融合部署 | 1. 不涉及命名冲突改造。2.不涉及http客户端改造 | 1. SaaS SpringBoot部署,OP Tomcat部署 2. 部署运维方案不一致 | 1. 各个微服务包名及类名有大量冲突 2.改造风险高。3.改造成本高。4.缩减JVM内存部署资源 | 具体见4.2~4.3 | xxx |
【Docker】融合部署 | 1. 无需代码变动2. 需要部署的镜像减少,部署运维过程简化 | 1. JVM内存资源未明显缩减 | 1. 各个微服务包名及类名有大量冲突 | 具体见3.2 |
-
改造难度
Maven】融合打包>【Tomcat】融合部署>【Docker】融合部署
-
改造收益
【Maven】融合打包>【Tomcat】融合部署>【Docker】融合部署