1 - 基座接入

1.1 - SpringBoot 或 SOFABoot 升级为基座

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

代码与配置修改

修改 application.properties

# 需要定义应用名
spring.application.name = ${替换为实际基座应用名}

修改主 pom.xml

<properties>
    <sofa.ark.verion>2.2.5</sofa.ark.verion>
    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
</properties>
<dependency>
    <groupId>com.alipay.sofa.serverless</groupId>
    <artifactId>sofa-serverless-base-starter</artifactId>
    <version>${sofa.serverless.runtime.version}</version>
</dependency>

<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
<dependency>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>web-ark-plugin</artifactId>
</dependency>

启动验证

基座应用能正常启动即表示验证成功!



2 - 模块接入

2.1 - SpringBoot 或 SOFABoot 一键升级为模块

本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

步骤 1:修改 application.properties

# 需要定义应用名
spring.application.name = ${替换为实际模块应用名}

步骤 2:添加模块需要的依赖和打包插件

特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;


<plugins>
    <!--这里添加ark 打包插件-->
    <plugin>
        <groupId>com.alipay.sofa</groupId>
        <artifactId>sofa-ark-maven-plugin</artifactId>
        <version>{sofa.ark.version}</version>
        <executions>
            <execution>
                <id>default-cli</id>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <skipArkExecutable>true</skipArkExecutable>
            <outputDirectory>./target</outputDirectory>
            <bizName>${替换为模块名}</bizName>
            <webContextPath>${模块自定义的 web context path}</webContextPath>
            <declaredMode>true</declaredMode>
        </configuration>
    </plugin>
    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    <plugin>
        <!--原来 spring-boot 打包插件 -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
</plugins>

步骤 3:自动化瘦身模块

您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

步骤 4:构建成模块 jar 包

执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

小贴士模块中支持的完整中间件清单

实验:验证模块既能独立启动,也能被合并部署

增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

验证能合并部署到基座上

  1. 启动上一步(验证能独立启动步骤)的基座
  2. 发起模块部署
curl --location --request POST 'localhost:1238/installBiz' \
--header 'Content-Type: application/json' \
--data '{
    "bizName": "${模块名}",
    "bizVersion": "${模块版本}",
    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
}'

返回如下信息表示模块安装成功
image.png

  1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider

image.png

  1. 卸载模块
curl --location --request POST 'localhost:1238/uninstallBiz' \
--header 'Content-Type: application/json' \
--data '{
    "bizName": "dynamic-provider",
    "bizVersion": "0.0.1-SNAPSHOT"
}'

返回如下,表示卸载成功

{
    "code": "SUCCESS",
    "data": {
        "code": "SUCCESS",
        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
    }
}

验证能独立启动

普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
image.png

2.2 - 使用 maven archtype 脚手架自动生成

正在更新中,预计 11 月上线。

3 - 基座与模块并行开发验证

欢迎使用 SOFAServerless 完成多 SpringBoot 应用合并部署与动态更新模块!本文将详细介绍操作流程与方法,希望能够帮助大家节省资源、提高研发效率。 首先,利用 SOFAServerless 完成合并部署与动态更新模块,适用于两种典型场景:

  1. 合并部署
  2. 中台应用(该场景需要先完成合并部署,再完成中台应用 demo)

本文实验工程代码在:开源仓库 samples 目录库里

场景一:合并部署

先介绍第一个场景多应用合并部署,整体流程如下: image.png

可以看到,整体上需要完成的动作是基座/模块接入改造后进行开发与验证,而基座与模块的合并部署动作都是可以并行的。接下来我们将逐步介绍操作细节。

1. 基座接入改造

  1. 为 **application.properties **增加应用名(如果没有的话):

spring.application.name=${基座应用名}

  1. 在 **pom.xml **里增加必要的依赖
<properties>
    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
</properties>
<dependencies>
    <dependency>
        <groupId>com.alipay.sofa.serverless</groupId>
        <artifactId>sofa-serverless-base-starter</artifactId>
        <version>${sofa.serverless.runtime.version}</version>
    </dependency>
</dependencies>

理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里

    <dependency>
        <groupId>com.alipay.sofa</groupId>
        <artifactId>web-ark-plugin</artifactId>
    </dependency>
  1. 点击编译器启动基座。

2. 模块 1 接入改造

  1. 添加模块需要的依赖和打包插件
<plugins>
    <!--这里添加ark 打包插件-->
    <plugin>
        <groupId>com.alipay.sofa</groupId>
        <artifactId>sofa-ark-maven-plugin</artifactId>
        <executions>
            <execution>
                <id>default-cli</id>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <skipArkExecutable>true</skipArkExecutable>
            <outputDirectory>./target</outputDirectory>
            <bizName>${替换为模块名}</bizName>
            <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath>
            <declaredMode>true</declaredMode>
            <!--  配置模块自动排包列表,从 github 下载 rules.txt,并放在模块根目录的 conf/ark/ 目录下,下载地址:https://github.com/sofastack/sofa-serverless/blob/master/samples/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt  -->
            <packExcludesConfig>rules.txt</packExcludesConfig>
        </configuration>
    </plugin>
    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    <plugin>
        <!--原来 spring-boot 打包插件 -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
</plugins>
  1. 参考官网模块瘦身里自动排包部分,下载排包配置文件 rules.txt,放在在 conf/ark/ 目录下

  2. 开发模块,例如增加 Rest Controller,提供 Rest 接口

@RestController
public class SampleController {
    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);

    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {
        String appName = applicationContext.getApplicationName();
        LOGGER.info("{} web test: into sample controller", appName);
        return String.format("hello to %s deploy", appName);
    }
}
  1. 点击这里下载 Arkctl,mac/linux 电脑放入 **/usr/local/bin** 目录中,windows 可以考虑直接放在项目根目录下

  2. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回。显示正常,进入下一步。

hello to ${模块1名} deploy

3. 模块 1 开发与验证

开发与验证需要完成修改代码并发布 V2 版本。具体操作如下:

  1. 修改 Rest 代码
@RestController
public class SampleController {
    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);

    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {
        String appName = applicationContext.getApplicationName();
        LOGGER.info("{} web test v2: into sample controller", appName);
        return String.format("hello to %s deploy v2", appName);
    }
}
  1. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回
hello to ${模块1名} deploy v2

4. 模块 2 接入改造、开发与验证

模块 2 同样采用上述步骤2⃣️3⃣️,即模块 1 接入改造与验证的操作流程。

场景二:中台应用

中台应用的特点是基座有复杂的编排逻辑去定义对外暴露服务和业务所需的 SPI。模块应用来实现这些 SPI 接口,往往会对一个接口在多个模块里定义多个不同的实现。整体流程如下: image.png

可以看到,与场景一合并部署操作不同的是,需要在基座接入改造与开发验证中间新增一步通信类和 SPI 的定义模块接入改造与开发验证中间新增一步引入通信类基座并实现基座 SPI接下来我们将介绍与合并部署不同的(即新增的)操作细节。

1. 基座完成通信类和 SPI 的定义

在合并部署接入改造的基础上,需要完成通信类和 SPI 的定义。 通信类需要以 **独立 bundle **的方式存在,才能被模块引入。可参考以下方式:

  1. 新建 bundle,定义接口类
public class ProductInfo {
    private String  name;
    private String  author;
    private String  src;
    private Integer orderCount;
}
  1. 定义 SPI
public interface StrategyService {
    List<ProductInfo> strategy(List<ProductInfo> products);
    String getAppName();
}

2. 模块 1 引入通信类基座并实现基座 SPI

在上文合并部署模块 1 接入改造 demo 的基础上,引入通信类,然后定义 SPI 实现。

  1. 引入通信类和对应 SPI 定义,只需要在 pom 里引入基座定义的通信 bundle
  2. 定义 SPI 实现
@Service
public class StrategyServiceImpl implements StrategyService {

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public List<ProductInfo> strategy(List<ProductInfo> products) {
        return products;
    }

    @Override
    public String getAppName() {
        return applicationContext.getApplicationName();
    }
}
  1. 执行 arkctl deploy 构建部署,成功后用 curl localhost:8080/${基座服务入口}/biz1/ 验证服务返回

biz1 传入是为了使基座根据不同的参数找到不同的 SPI 实现,执行不同的逻辑。传入的方式可以有很多种,这里用最简单方式——从 **path **里传入。

默认的 products 列表

3. 模块 2 引入通信类基座并实现基座 SPI

与模块 1 操作相同,需要注意执行 arkctl deploy 构建部署时,成功后 curl localhost:8080/${基座服务入口}/biz2/ 验证服务返回。同理,**biz2 **传入是为了基座根据不同的参数,找到不同的 SPI 实现,执行不同逻辑。

@Service
public class StrategyServiceImpl implements StrategyService {
    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public List<ProductInfo> strategy(List<ProductInfo> products) {
        Collections.sort(products, (m, n) -> n.getOrderCount() - m.getOrderCount());
        products.stream().forEach(p -> p.setName(p.getName()+"("+p.getOrderCount()+")"));
        return products;
    }

    @Override
    public String getAppName() {
        return applicationContext.getApplicationName();
    }
}
更改排序后的 products 列表

基于上述操作,就可以继续进行上文中模块 开发与验证 的操作了。整体流程丝滑易上手,欢迎试用!

文档中的链接地址

  1. 本实验工程样例地址:https://github.com/sofastack/sofa-serverless/tree/master/samples/springboot-samples/web/tomcat
  2. web-ark-plugin 原理: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/
  3. 自动排包原理与配置文件下载:https://sofaserverless.gitee.io/docs/tutorials/module-development/module-slimming/#%E4%B8%80%E9%94%AE%E8%87%AA%E5%8A%A8%E7%98%A6%E8%BA%AB
  4. Arkctl 下载地址:https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0
  5. 本文档地址:https://sofaserverless.gitee.io/docs/tutorials/trial_step_by_step/

4 - 模块研发

4.1 - 编码规范

基础规范

  1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
  2. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
  3. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布

知识点

模块瘦身 (重要)
模块与模块、模块与基座通信 (重要)
模块测试 (重要)
模块复用基座拦截器
模块复用基座数据源
基座与模块间类委托加载原理介绍



4.2 - 模块瘦身

为什么要瘦身

为了让模块安装更快、内存消耗更小:

  • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
  • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。

一键自动瘦身

瘦身原则

构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

步骤一

在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

excludeGroupIds=org.apache*
excludeArtifactIds=commons-lang

步骤二

在模块打包插件中,引入上述配置文件:

    <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    <plugin>
        <groupId>com.alipay.sofa</groupId>
        <artifactId>sofa-ark-maven-plugin</artifactId>
        <version>2.2.5</version>
        <executions>
            <execution>
                <id>default-cli</id>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <skipArkExecutable>true</skipArkExecutable>
            <outputDirectory>./target</outputDirectory>
            <bizName>biz1</bizName>
            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
            <packExcludesConfig>rules.txt</packExcludesConfig>
            <webContextPath>biz1</webContextPath>
            <declaredMode>true</declaredMode>
            <!--					打包、安装和发布 ark biz-->
            <!--					静态合并部署需要配置-->
            <!--					<attach>true</attach>-->
        </configuration>
    </plugin>

步骤三

打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

基本原理

SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

  1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
  2. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。

基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

手动排包瘦身

模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
image.png

如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
image.png
确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
image.png

在 pom 中统一排包(更彻底)

有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

<dependency>
    <groupId>com.serverless.mymodule</groupId>
    <artifactId>mymodule-core</artifactId>
    <exclusions>
          <exclusion>
              <groupId>org.springframework</groupId>
              <artifactId>*</artifactId>
          </exclusion>
          <exclusion>
              <groupId>org.apache.commons</groupId>
              <artifactId>*</artifactId>
          </exclusion>
          <exclusion>
              <groupId>......</groupId>
              <artifactId>*</artifactId>
          </exclusion>
    </exclusions>
</dependency>

在 sofa-ark-maven-plugin 中指定排包

通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

 <plugin>
      <groupId>com.alipay.sofa</groupId>
      <artifactId>sofa-ark-maven-plugin</artifactId>
      <executions>
          <execution>
              <id>default-cli</id>
              <goals>
                  <goal>repackage</goal>
              </goals>
          </execution>
      </executions>
      <configuration>
          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
          <outputDirectory>../../target</outputDirectory>
          <bizName>mymodule</bizName>
          <finalName>mymodule-${project.version}-${timestamp}</finalName>
          <bizVersion>${project.version}-${timestamp}</bizVersion>
          <webContextPath>/mymodule</webContextPath>
      </configuration>
  </plugin>


4.3 - 模块与模块、模块与基座通信

基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

Spring 环境

基座调用模块

只能使用 SpringServiceFinder

@RestController
public class SampleController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {

        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
                "studentProvider", Provider.class);
        Result result = studentProvider.provide(new Param());

        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
                "teacherProvider", Provider.class);
        Result result1 = teacherProvider.provide(new Param());
        
        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
                Provider.class);
        for (String beanName : providerMap.keySet()) {
            Result result2 = providerMap.get(beanName).provide(new Param());
        }

        return "hello to ark master biz";
    }
}

模块调用基座

方式一:注解 @AutowiredFromBase

@RestController
public class SampleController {

    @AutowiredFromBase(name = "sampleServiceImplNew")
    private SampleService sampleServiceImplNew;

    @AutowiredFromBase(name = "sampleServiceImpl")
    private SampleService sampleServiceImpl;

    @AutowiredFromBase
    private List<SampleService> sampleServiceList;

    @AutowiredFromBase
    private Map<String, SampleService> sampleServiceMap;

    @AutowiredFromBase
    private AppService appService;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {

        sampleServiceImplNew.service();

        sampleServiceImpl.service();

        for (SampleService sampleService : sampleServiceList) {
            sampleService.service();
        }

        for (String beanName : sampleServiceMap.keySet()) {
            sampleServiceMap.get(beanName).service();
        }

        appService.getAppName();

        return "hello to ark2 dynamic deploy";
    }
}

方式二:编程API SpringServiceFinder

@RestController
public class SampleController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {

        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
        String result = sampleServiceImplFromFinder.service();
        System.out.println(result);

        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
        for (String beanName : sampleServiceMapFromFinder.keySet()) {
            String result1 = sampleServiceMapFromFinder.get(beanName).service();
            System.out.println(result1);
        }

        return "hello to ark2 dynamic deploy";
    }
}

模块调用模块

参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

方式一:注解 @AutowiredFromBiz

@RestController
public class SampleController {

    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
    private Provider studentProvider;

    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
    private Provider teacherProvider;

    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
    private List<Provider> providers;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {

        Result provide = studentProvider.provide(new Param());

        Result provide1 = teacherProvider.provide(new Param());

        for (Provider provider : providers) {
            Result provide2 = provider.provide(new Param());
        }

        return "hello to ark2 dynamic deploy";
    }
}

方式二:编程API SpringServiceFinder

@RestController
public class SampleController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {

        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
        Result result1 = teacherProvider1.provide(new Param());

        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
        for (String beanName : providerMap.keySet()) {
            Result result2 = providerMap.get(beanName).provide(new Param());
        }

        return "hello to ark2 dynamic deploy";
    }
}

完整样例

SOFABoot 环境

请参考该文档



4.4 - 模块本地开发

Arkctl 工具安装

方法一:

  1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
  2. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。

方法二:

  1. 二进制列表 中下载对应的二进制并加入到本地 path 中。

本地快速部署

你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

准备:

  1. 在本地启动一个基座。
  2. 打开一个模块项目仓库。

执行命令:

# 需要在仓库的根目录下执行。
# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
arkctl deploy

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

准备:

  1. 在本地启动一个基座。
  2. 准备一个构建好的 jar 包。

执行命令:

arkctl deploy /path/to/your/pre/built/bundle-biz.jar

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

准备:

  1. 在远程已经运行起来的基座 pod。
  2. 打开一个模块项目仓库。
  3. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。

执行命令:

# 需要在仓库的根目录下执行。
# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
arkctl deploy --pod {namespace}/{podName}

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

准备:

  1. 在本地启动一个基座。
  2. 打开一个多模块 Maven 项目仓库。

执行命令:

# 需要在仓库的根目录下执行。
# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
arkctl deploy --sub ./path/to/your/sub/module

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 5: 查询当前基座中已经部署的模块。

准备:

  1. 在本地启动一个基座。

执行命令:

arkctl status

场景 6: 查询远程 k8s 环境基座中已经部署的模块。

准备:

  1. 在远程 k8s 环境启动一个基座。
  2. 确保本地有 kube 证书以及有关权限。

执行命令:

arkctl status --pod {namespace}/{name}

4.5 - 模块测试

本地调试

您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
Arklet HTTP 接口主要提供了以下能力:

  1. 部署和卸载模块。
  2. 查询所有已部署的模块信息。
  3. 查询各项系统和业务指标。

部署模块

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 

请求体样例:

{
    "bizName": "test",
    "bizVersion": "1.0.0",
    // local path should start with file://, alse support remote url which can be downloaded
    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
}

部署成功返回结果样例:

{
  "code":"SUCCESS",
  "data":{
    "bizInfos":[
      {
        "bizName":"dynamic-provider",
        "bizState":"ACTIVATED",
        "bizVersion":"1.0.0",
        "declaredMode":true,
        "identity":"dynamic-provider:1.0.0",
        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
        "priority":100,
        "webContextPath":"provider"
      }
    ],
    "code":"SUCCESS",
    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
  }
}

部署失败返回结果样例:

{
  "code":"FAILED",
  "data":{
    "code":"REPEAT_BIZ",
    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
  }
}

卸载模块

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 

请求体样例:

{
    "bizName":"dynamic-provider",
    "bizVersion":"1.0.0"
}

卸载成功返回结果样例:

{
  "code":"SUCCESS"
}

卸载失败返回结果样例:

{
  "code":"FAILED",
  "data":{
    "code":"NOT_FOUND_BIZ",
    "message":"Uninstall biz: test:1.0.0 not found."
  }
}

查询模块

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 

请求体样例:

{}

返回结果样例:

{
  "code":"SUCCESS",
  "data":[
    {
      "bizName":"dynamic-provider",
      "bizState":"ACTIVATED",
      "bizVersion":"1.0.0",
      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
      "webContextPath":"provider"
    },
    {
      "bizName":"stock-mng",
      "bizState":"ACTIVATED",
      "bizVersion":"1.0.0",
      "mainClass":"embed main",
      "webContextPath":"/"
    }
  ]
}

获取帮助

Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 

请求体样例:

{}

返回结果样例:

{
    "code":"SUCCESS",
    "data":[
        {
            "desc":"query all ark biz(including master biz)",
            "id":"queryAllBiz"
        },
        {
            "desc":"list all supported commands",
            "id":"help"
        },
        {
            "desc":"uninstall one ark biz",
            "id":"uninstallBiz"
        },
        {
            "desc":"switch one ark biz",
            "id":"switchBiz"
        },
        {
            "desc":"install one ark biz",
            "id":"installBiz"
        }
    ]
}

本地构建如何不改变模块版本号

添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

<profile>
    <id>local</id>
    <build>
        <plugins>
            <plugin>
                <groupId>com.alipay.sofa</groupId>
                <artifactId>sofa-ark-maven-plugin</artifactId>
                <configuration>
                    <finalName>${project.artifactId}-${project.version}</finalName>
                    <bizVersion>${project.version}</bizVersion>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

单元测试

模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。



4.6 - 复用基座拦截器

诉求

基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

解法

为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

步骤 1:

基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

public interface AnnotionService {
    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
}

步骤 2:

在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

@Service
@SofaService(uniqueId = "facadeAroundHandler")
public class FacadeAroundHandler implements AnnotionService {

    private final static Logger LOG = LoggerConst.MY_LOGGER;

    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("开始执行")
        joinPoint.proceed();
        log.info("执行完成")
    }
}

步骤 3:

在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

//注意,这里不必申明成一个bean,不要加@Component或者@Service
@Aspect
public class FacadeAroundAspect {

    @SofaReference(uniqueId = "facadeAroundHandler")
    private AnnotionService facadeAroundHandler;

    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
    public void facadeAroundPointcut() {
    }

    @Around("facadeAroundPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        return facadeAroundHandler.doAround(joinPoint);
    }
}

步骤 4:

使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

@Configuration
public class MngAspectConfiguration {
    @Bean
    public FacadeAroundAspect facadeAroundAspect() {
        return new FacadeAroundAspect();
    }
    @Bean
    public EnvRouteAspect envRouteAspect() {
        return new EnvRouteAspect();
    }
    @Bean
    public FacadeAroundAspect facadeAroundAspect() {
        return new FacadeAroundAspect();
    }
    
}

步骤 5:

模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

@SpringBootApplication
@ImportResource("classpath*:META-INF/spring/*.xml")
@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
public class ModuleBootstrapApplication {
    public static void main(String[] args) {
        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
        	.web(WebApplicationType.NONE);
        builder.build().run(args);
    }
}


4.7 - 复用基座数据源

建议

强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

SpringBoot 解法

在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
参考 demo:/sofa-serverless/samples/springboot-samples/db/mybatis/biz1

通过SpringBeanFinder.getBaseBean获取到基座的 Bean 对象,然后注册成模块的 Bean:


@Configuration
@MapperScan(basePackages = "com.alipay.sofa.biz1.mapper", sqlSessionFactoryRef = "mysqlSqlFactory")
@EnableTransactionManagement
public class MybatisConfig {

    //tips:不要初始化一个基座的DataSource,当模块被卸载的是,基座数据源会被销毁,transactionManager,transactionTemplate,mysqlSqlFactory被销毁没有问题

    @Bean(name = "transactionManager")
    public PlatformTransactionManager platformTransactionManager() {
        return (PlatformTransactionManager) getBaseBean("transactionManager");
    }

    @Bean(name = "transactionTemplate")
    public TransactionTemplate transactionTemplate() {
        return (TransactionTemplate) getBaseBean("transactionTemplate");
    }

    @Bean(name = "mysqlSqlFactory")
    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法

        DataSource dataSource = (DataSource) getBaseBean("dataSource");
        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
        mysqlSqlFactory.setDataSource(dataSource);
        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mappers/*.xml"));
        return mysqlSqlFactory;
    }
}

SOFABoot 解法

如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
image.png


@Configuration
@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
@EnableTransactionManagement
public class MybatisConfig {

    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    
    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"

    @Bean(name = "transactionManager")
    public PlatformTransactionManager platformTransactionManager() {
        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
    }

    @Bean(name = "transactionTemplate")
    public TransactionTemplate transactionTemplate() {
        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
    }

    @Bean(name = "mysqlSqlFactory")
    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
        mysqlSqlFactory.setDataSource(dataSource);
        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml"));
        return mysqlSqlFactory;
    }
}


4.8 - 静态合并部署

介绍

SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 用户可以把 Biz 包统一放置在某个目录中,然后通过启动参数告知基座扫描这个目录,以此完成静态合并部署(详情见下描述)。如此,开发不需要考虑相互之间依赖冲突问题,Biz 之间则通过 @SofaService 和 @SofaReference 发布/引用 JVM 服务(SOFABoot,SpringBoot 还在建设中 )进行交互。

步骤 1:模块应用打包成 Ark Biz

如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz 包,详见 Ark Biz 介绍。 Ark Biz 包使用 Maven 插件 sofa-ark-maven-plugin 打包生成。


<build>
    <plugin>
        <groupId>com.alipay.sofa</groupId>
        <artifactId>sofa-ark-maven-plugin</artifactId>
        <version>${sofa.ark.version}</version>
        <executions>
            <execution>
                <id>default-cli</id>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</build>

步骤 2:将上述 jar 包移动到指定目录。

把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

mv /path/to/your/biz/jar /home/sofa-ark/biz/

步骤 3:启动基座,并通过 -D 参指定 biz 目录

java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar

步骤 4:验证 Ark Biz(模块)启动

在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

telnet localhost 1234

然后执行如下命令查看模块列表:

biz -a

此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
上述操作可以通过 SOFAArk 静态合并部署样例 体验。



4.9 - 模块中官方支持的中间件客户端

在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
注意,这里 “已经支持” 需要在基座 POM 中引入相关客户端依赖(强烈建议使用 SpringBoot Starter 方式引入相关依赖),同时在模块 POM 中也引入相关依赖并设置 * provided* 将依赖委托给基座加载。


中间件客户端版本号备注
JDK8.x
17.x
已经支持
SpringBoot>= 2.3.0 或 3.x已经支持
JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
SpringBoot Cloud>= 2.7.x已经支持
基座和模块完整使用样例可参见此处
SOFABoot>= 3.9.0 或 4.x已经支持
JMXN/A已经支持
需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
<dependency>
  <groupId>com.alipay.sofa.serverless</groupId>
  <artifactId>sofa-serverless-adapter-log4j2</artifactId>
  <version>${最新版 SOFAServerless 版本}</version>
  <scope>provided</scope> <!– 模块需要 provided –>
  </dependency>
基座和模块完整使用样例参见此处
slf4j-api1.x 且 >= 1.7已经支持
tomcat7.x、8.x、9.x、10.x
及以上均可
已经支持
基座和模块完整使用样例可参见此处
netty4.x已经支持
基座和模块完整使用样例可参见此处
sofarpc>= 5.8.6已经支持
dubbo3.x已经支持
基座和模块完整使用样例及注意事项可参见此处
grpc1.x 且 >= 1.42已经支持
基座和模块完整使用样例及注意事项可参见此处
protobuf-java3.x 且 >= 3.17已经支持
基座和模块完整使用样例及注意事项可参见此处
apollo1.x 且 >= 1.6.0已经支持
基座和模块完整使用样例及注意事项可参见此处
nacos2.1.x已经支持
基座和模块完整使用样例及注意事项可参见此处
kafka-client>= 2.8.0 或
>= 3.4.0
已经支持
基座和模块完整使用样例可参见此处
rocketmq4.x 且 >= 4.3.0已经支持
基座和模块完整使用样例可参见此处
jedis3.x已经支持
基座和模块完整使用样例可参见此处
xxl-job2.x 且 >= 2.1.0已经支持
需要在模块里声明为 compile 依赖独立使用
mybatis>= 2.2.2 或
>= 3.5.12
已经支持
基座和模块完整使用样例可参见此处
druid1.x已经支持
基座和模块完整使用样例可参见此处
mysql-connector-java8.x已经支持
基座和模块完整使用样例可参见此处
postgresql42.x 且 >= 42.3.8已经支持
mongodb4.6.1已经支持
基座和模块完整使用样例可参见此处
hibernate5.x 且 >= 5.6.15已经支持
j2cache任意已经支持
需要在模块里声明为 compile 依赖独立使用
opentracing0.x 且 >= 0.32.0已经支持
elasticsearch7.x 且 >= 7.6.2已经支持
jaspyt1.x 且 >= 1.9.3已经支持
OKHttp-已经支持
需要放在基座里,请使用模块自动瘦身能力
io.kubernetes:client10.x 且 >= 10.0.0已经支持
net.java.dev.jna5.x 且 >= 5.12.1已经支持
prometheus-待验证支持

4.11 -

5 - 模块运维

5.1 - 模块上线与下线

模块上线

在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

apiVersion: serverless.alipay.com/v1alpha1
kind: ModuleDeployment
metadata:
  labels:
    app.kubernetes.io/name: moduledeployment
    app.kubernetes.io/instance: moduledeployment-sample
    app.kubernetes.io/part-of: module-controller
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: module-controller
  name: moduledeployment-sample
spec:
  baseDeploymentName: dynamic-stock-deployment
  template:
    spec:
      module:
        name: provider
        version: '1.0.2'
        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
  replicas: 2
  operationStrategy:  # 此处可自定义发布运维策略
    upgradePolicy: installThenUninstall
    needConfirm: true
    useBeta: false
    batchCount: 2
  schedulingStrategy: # 此处可自定义调度策略
    schedulingPolicy: Scatter

ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

模块下线

在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

kubectl delete yourmoduledeployment --namespace yournamespace

其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。



5.2 - 模块发布

模块发布

修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

apiVersion: serverless.alipay.com/v1alpha1
kind: ModuleDeployment
metadata:
  labels:
    app.kubernetes.io/name: moduledeployment
    app.kubernetes.io/instance: moduledeployment-sample
    app.kubernetes.io/part-of: module-controller
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: module-controller
  name: moduledeployment-sample
spec:
  baseDeploymentName: dynamic-stock-deployment
  template:
    spec:
      module:
        name: provider
        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
  replicas: 2
  operationStrategy:
    upgradePolicy: install_then_uninstall
    needConfirm: true
    grayTimeBetweenBatchSeconds: 0
    useBeta: false
    batchCount: 2
  schedulingStrategy:
    schedulingPolicy: scatter

如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

模块回滚

重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。



5.3 - 基座和模块不兼容发布

步骤 1

修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

步骤 2

修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

步骤 3

使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。



5.4 - 模块扩缩容与替换

模块扩缩容

修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

apiVersion: serverless.alipay.com/v1alpha1
kind: ModuleDeployment
metadata:
  labels:
    app.kubernetes.io/name: moduledeployment
    app.kubernetes.io/instance: moduledeployment-sample
    app.kubernetes.io/part-of: module-controller
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: module-controller
  name: moduledeployment-sample
spec:
  baseDeploymentName: dynamic-stock-deployment
  template:
    spec:
      module:
        name: provider
        version: '1.0.2'
        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
  operationStrategy:
    upgradePolicy: installThenUninstall
    needConfirm: true
    useBeta: false
    batchCount: 2
  schedulingStrategy: # 此处可自定义调度策略
    schedulingPolicy: Scatter  

如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

模块替换

在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

kubectl delete yourmodule --namespace yournamespace

其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。



5.5 - 模块发布运维策略

运维策略

为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

字段名字段解释取值范围取值解释
batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
false 表示不启用 beta 分组
needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
false 表示不启用分组确认
grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次

调度策略

可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

保护机制

(正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

对等和非对等

您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。



5.6 - 独立使用 Arklet

Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处



5.7 - 模块信息查看

查看某个基座上所有安装的模块名称和状态

kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status

kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status

查看某个基座上所有安装的模块详细信息

kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>

kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>

替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

5.8 - 模块Service

ModuleService 简介

K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 模块也支持 Module 相关的 Service ,在模块发布时自动创建一个 service 来服务模块,将安装在一个或一组 Pod 的模块公开为网络服务。 具体见:OperationStrategy.ServiceStrategy

apiVersion: serverless.alipay.com/v1alpha1
kind: ModuleDeployment
metadata:
  labels:
    app.kubernetes.io/name: moduledeployment
    app.kubernetes.io/instance: moduledeployment-sample
    app.kubernetes.io/part-of: module-controller
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: module-controller
  name: moduledeployment-sample-provider
spec:
  baseDeploymentName: dynamic-stock-deployment
  template:
    spec:
      module:
        name: provider
        version: '1.0.2'
        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
  replicas: 1
  operationStrategy:
    needConfirm: false
    grayTimeBetweenBatchSeconds: 120
    useBeta: false
    batchCount: 1
    upgradePolicy: install_then_uninstall
    serviceStrategy:
      enableModuleService: true
      port: 8080
      targetPort: 8080
  schedulingStrategy:
    schedulingPolicy: scatter

字段解释

OperationStrategy.ServiceStrategy 字段解释如下:

字段解释取值范围
EnableModuleService开启模块servicetrue or false
Port公开的端口1 到 65535
TargetPortpod上要访问的端口1 到 65535

示例

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace

自动创建的模块的 service

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2023-11-03T09:52:22Z"
  name: dynamic-provider-service
  namespace: default
  resourceVersion: "28170024"
  uid: 1f85e468-65e3-4181-b40e-48959a069df5
spec:
  clusterIP: 10.0.147.22
  clusterIPs:
  - 10.0.147.22
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: http
    nodePort: 32232
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    module.serverless.alipay.com/dynamic-provider: "true"
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}

5.9 - 所有 K8S 资源定义及部署方式

资源文件位置

  1. ModuleDeployment CRD 定义
  2. ModuleReplicaset CRD 定义
  3. ModuleTemplate CRD 定义
  4. Module CRD 定义
  5. Role 定义
  6. RBAC 定义
  7. ServiceAccount 定义
  8. ModuleController 部署定义

部署方式

使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。