1 - 4.1 基座接入

1.1 - 4.1.1 SpringBoot 或 SOFABoot 升级为基座

SpringBoot 或 SOFABoot 升级为 Koupleless 基座

前提条件

  1. SpringBoot 版本 >= 2.1.9.RELEASE(针对 SpringBoot 用户)
  2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
  3. 本地安装 maven,且 maven 版本大于 3.9.0

注意:SpringBoot 版本 == 2.1.9.RELEASE,见 SpringBoot 2.1.9 升级为基座

接入步骤

代码与配置修改

修改 application.properties

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

修改主 pom.xml

<properties>
    <sofa.ark.version>2.2.15</sofa.ark.version>
    <!-- 不同jdk版本,使用不同koupleless版本,参考:https://koupleless.io/docs/tutorials/module-development/runtime-compatibility-list/#%E6%A1%86%E6%9E%B6%E8%87%AA%E8%BA%AB%E5%90%84%E7%89%88%E6%9C%AC%E5%85%BC%E5%AE%B9%E6%80%A7%E5%85%B3%E7%B3%BB -->
    <koupleless.runtime.version>1.3.2</koupleless.runtime.version>
</properties>
<dependency>
    <groupId>com.alipay.sofa.koupleless</groupId>
    <artifactId>koupleless-base-starter</artifactId>
    <version>${koupleless.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>
    <version>${sofa.ark.version}</version>
</dependency>

<!-- 为了让三方依赖和 koupleless 模式适配,需要引入以下构建插件 -->
<build>
    <plugins>
        <plugin>
            <groupId>com.alipay.sofa.koupleless</groupId>
            <artifactId>koupleless-base-build-plugin</artifactId>
            <version>${koupleless.runtime.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>add-patch</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

其它版本接入

SpringBoot 2.1.9 升级为基座

在修改以上配置后,还需额外修改以下配置:

主 pom 引入 sdk
<!-- 注意放在构建 pom 的第一个依赖位置 -->
<dependency>
    <groupId>com.alipay.sofa.koupleless</groupId>
    <artifactId>koupleless-base-starter</artifactId>
    <version>${koupleless.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>
    <version>${sofa.ark.version}</version>
</dependency>
<dependency>
    <groupId>com.github.oshi</groupId>
    <artifactId>oshi-core</artifactId>
    <version>3.9.1</version>
</dependency>

<!-- 为了让三方依赖和 koupleless 模式适配,需要引入以下构建插件 -->
<build>
    <plugins>
        <plugin>
            <groupId>com.alipay.sofa.koupleless</groupId>
            <artifactId>koupleless-base-build-plugin</artifactId>
            <version>${koupleless.runtime.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>add-patch</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
修改基座启动类

在 koupleless 版本 1.1.0 及以上,无需改动。

在 koupleless 版本 1.1.0 以下,在基座 Springboot 启动类的注解 @SpringBootApplication 中,排除 HealthAutoConfiguration 类,如下:

import com.alipay.sofa.koupleless.arklet.springboot.starter.health.HealthAutoConfiguration;
@SpringBootApplication(exclude = { HealthAutoConfiguration.class })
public class BaseApplication {
    public static void main(String[] args) {
        SpringApplication.run(BaseApplication.class, args);
    }
}

启动验证

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



2 - 4.2 模块接入

Koupleless 模块接入

2.1 - 4.2.1 存量 SpringBoot 或 SOFABoot 升级为模块

存量 SpringBoot 或 SOFABoot 升级为 Koupleless 模块

模块的创建有四种方式,本文介绍第二种方式:

  1. 大应用拆出多个模块
  2. 存量应用改造成一个模块
  3. 直接脚手架创建模块
  4. 普通代码片段改造成一个模块

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

前提条件

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

接入步骤

步骤 1:修改 application.properties

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

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

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

<properties>
    <sofa.ark.version>2.2.15</sofa.ark.version>
    <!-- 不同jdk版本,使用不同koupleless版本,参考:https://koupleless.io/docs/tutorials/module-development/runtime-compatibility-list/#%E6%A1%86%E6%9E%B6%E8%87%AA%E8%BA%AB%E5%90%84%E7%89%88%E6%9C%AC%E5%85%BC%E5%AE%B9%E6%80%A7%E5%85%B3%E7%B3%BB -->
    <koupleless.runtime.version>1.2.3</koupleless.runtime.version>
</properties>
<!-- 模块需要引入的依赖,主要用户跨模块间通信 --> 
<dependencies>
    <dependency>
        <groupId>com.alipay.sofa.koupleless</groupId>
        <artifactId>koupleless-app-starter</artifactId>
        <version>${koupleless.runtime.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<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,支持独立部署时使用,如果不需要可以删除;注意需要放在 sofa-ark-maven-plugin 的后面  -->
    <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"
}'

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

  1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider
  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 包启动,点击此处查看详情。

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

模块的创建有四种方式,本文介绍第三种方式:

  1. 大应用拆出多个模块
  2. 存量应用改造成一个模块
  3. 直接脚手架创建模块
  4. 普通代码片段改造成一个模块

从脚手架里创建模块的方式比较简单,只需要在 idea 里创建工程里传入脚手架的 maven 坐标即可。

<dependency>
    <groupId>com.alipay.sofa.koupleless</groupId>
    <artifactId>koupleless-common-module-archetype</artifactId>
    <version>{koupleless.runtime.version}</version>
</dependency>

该脚手架创建出来的模块,已经集成模块打包插件和自动瘦身配置,可以直接打包成模块安装在基座上,或者本地直接独立启动。

2.3 - 4.2.3 Java 代码片段作为模块

Java 代码片段作为模块

模块的创建有四种方式,本文介绍第四种方式:

  1. 大应用拆出多个模块
  2. 存量应用改造成一个模块
  3. 直接脚手架创建模块
  4. 普通代码片段改造成一个模块

本文介绍 Java 代码片段升级为模块的操作和验证步骤,仅需加一个 ark 打包插件 + 配置模块瘦身 即可实现 Java 代码片段一键升级为模块应用,并且能做到同一套代码分支,既能像原来 Java 代码片段一样独立启动,也能作为模块与其它应用合并部署在一起启动。

前提条件

  • jdk8
    • sofa.ark.version >= 2.2.14-SNAPSHOT
    • koupleless.runtime.version >= 1.3.1-SNAPSHOT
  • jdk17/jdk21
    • sofa.ark.version >= 3.1.7-SNAPSHOT
    • koupleless.runtime.version >= 2.1.6-SNAPSHOT

接入步骤

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

<properties>
    <sofa.ark.version>${见上述前提条件}</sofa.ark.version>
    <!-- 不同jdk版本,使用不同koupleless版本,参考:https://koupleless.io/docs/tutorials/module-development/runtime-compatibility-list/#%E6%A1%86%E6%9E%B6%E8%87%AA%E8%BA%AB%E5%90%84%E7%89%88%E6%9C%AC%E5%85%BC%E5%AE%B9%E6%80%A7%E5%85%B3%E7%B3%BB -->
    <koupleless.runtime.version>${见上述前提条件}</koupleless.runtime.version>
</properties>
<!-- 模块需要引入的依赖,主要用户跨模块间通信 --> 
<dependencies>
    <dependency>
        <groupId>com.alipay.sofa.koupleless</groupId>
        <artifactId>koupleless-app-starter</artifactId>
        <version>${koupleless.runtime.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<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>
            <declaredMode>true</declaredMode>
        </configuration>
    </plugin>
    
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <executions>
            <execution>
                <goals>
                    <goal>jar</goal>
                </goals>
                <phase>package</phase>
                <configuration>
                    <classifier>lib</classifier>
                    <!-- Ensure other necessary configuration here -->
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

步骤 2: 增加初始化逻辑

在代码片段添加:MainApplication.init() 来初始化容器。

public static void main(String[] args) {
        // 初始化模块的实例容器
        MainApplication.init();

        // ...
    }

在模块和基座的通信上,模块将实例注册在容器中,基座通过SpringServiceFinder获取模块实例,我们以biz3 为例:

  1. biz3 实现了以 AppService 为接口的两个实例:Biz3AppServiceImplBiz3OtherAppServiceImpl
public class Biz3OtherAppServiceImpl implements AppService {
    // 获取基座的bean
    private AppService baseAppService = SpringServiceFinder.getBaseService(AppService.class);

    @Override
    public String getAppName() {
        return "biz3OtherAppServiceImpl in the base: " + baseAppService.getAppName();
    }
}

public class Biz3AppServiceImpl implements AppService {
  // 获取基座的bean
  private AppService baseAppService = SpringServiceFinder.getBaseService(AppService.class);

  public String getAppName() {
    return "biz3AppServiceImpl in the base: " + baseAppService.getAppName();
  }
}

其中,模块获取基座的 bean 方式为:SpringServiceFinder.getBaseService(XXX.class),详细可见:模块和基座通信模块调用基座的方式二:编程API SpringServiceFinder

  1. biz3 将这两个类的实例注册到容器中:
public static void main(String[] args) {
        // 初始化模块的实例容器
        MainApplication.init();

        // 注册实例到模块容器中
        MainApplication.register("biz3AppServiceImpl", new Biz3AppServiceImpl());
        MainApplication.register("biz3OtherAppServiceImpl", new Biz3OtherAppServiceImpl());

        }
  1. 基座中获取 biz3 中的实例:
@RestController
public class SampleController {

    // 通过注解获取 biz3 中的指定实例
    @AutowiredFromBiz(bizName = "biz3", bizVersion = "0.0.1-SNAPSHOT", name = "biz3AppServiceImpl")
    private AppService biz3AppServiceImpl;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String hello() {
        System.out.println(biz3AppServiceImpl.getAppName());

        // 通过 api 获取 biz3 中的指定实例
        AppService biz3OtherAppServiceImpl = SpringServiceFinder.getModuleService("biz3", "0.0.1-SNAPSHOT",
                "biz3OtherAppServiceImpl", AppService.class);
        System.out.println(biz3OtherAppServiceImpl.getAppName());

        // 通过 api 获取 biz3 中 AppService.class 的所有实例
        Map<String, AppService> appServiceMap = SpringServiceFinder.listModuleServices("biz3",
                "0.0.1-SNAPSHOT", AppService.class);
        for (AppService appService : appServiceMap.values()) {
            System.out.println(appService.getAppName());
        }
        return "hello to ark master biz";
    }
}

其中,SpringBoot / SOFABoot 基座可以通过 @AutowiredFromBiz 注解或 SpringServiceFinder.getModuleService() 编程API 获取模块中的实例,详细可见:模块和基座通信基座调用模块

步骤 3:自动化瘦身模块

一般来说,代码片段式的模块依赖比较简单,您可以自行将模块中与基座一致的依赖的 scope 设置成 provided,或使用 ark 打包插件的自动化瘦身能力,自动瘦身模块里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。

步骤 4:构建成模块 jar 包

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

实验:验证模块能合并部署

  1. 启动上一步(验证能独立启动步骤)的基座
  2. 发起模块部署

可以参考样例中 biz3 的模块部署:https://github.com/koupleless/samples/blob/main/springboot-samples/service/README-zh_CN.md

3 - 4.4 模块研发

Koupleless 模块研发

3.1 - 4.3.1 编码规范

Koupleless 编码规范

基础规范

  1. Koupleless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。

  2. 模块里要独立使用 System.setProperties() 与 System.getProperties(),请在基座 main 方法里增加 MultiBizProperties.initSystem(),详细可参考 samples
  3. 如果使用了模块热卸载能力,您可以使用如下 API 装饰模块代码中声明的 ExecutorService(典型如各种线程池)、Timer、Thread 对象,在模块卸载时, Koupleless Arklet 客户端会尝试自动清理被装饰器装饰过的 ExecutorService、Timer、Thread:
    • 在模块代码中,装饰需要自动清理的 ExecutorService,底层会调用 ExecutorService 对象的 shutdownNow 和 awaitTermination 接口,会尽可能优雅释放线程(不保证 100% 释放,比如线程一直在等待),具体用法:
      ShutdownExecutorServicesOnUninstallEventHandler.manageExecutorService(myExecutorService);
      
      其中,myExecutorService 需要是 ExecutorService 的子类型。 您也可以在模块 SpringBoot 或 SOFABoot properties 文件中配置 com.alipay.koupleless.executor.cleanup.timeout.seconds 指定线程池 awaitTermination 的优雅等待时间。

    • 在模块代码中,装饰需要自动清理的 Timer,底层会调用 Timer 对象的 cancel,具体用法:
      CancelTimersOnUninstallEventHandler.manageTimer(myTimer);
      
    • 在模块代码中,装饰需要自动清理的 Thread,底层会强行调用 Thread 对象的 stop,具体用法:
      ForceStopThreadsOnUninstallEventHandler.manageThread(myThread);
      
      注意:JDK 并不推荐强行 stop 线程,会导致线程非预期的强行释放锁,可能引发非预期问题。除非您确定线程被暴力关闭不会引发相关问题,否则慎用。

  4. 如果使用了模块热卸载能力,并且还有其他资源、对象需要清理,您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要的资源和对象, 也可以在 Spring XML 定义 Bean 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method

  5. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更), 请参见基座与模块不兼容发布

知识点

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



3.2 - 4.3.2 模块瘦身

Koupleless 模块瘦身

为什么要瘦身

Koupleless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的类隔离。模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。

基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,从而模块消耗的内存非常少,启动也能非常快

其次,模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所以需要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。

所谓模块瘦身,就是让基座已经有的 Jar 依赖不参与模块打包构建,从而实现上述两个好处:

  • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒
  • 在热部署热卸载场景下,不替换或重启基座也能热部署热卸载数百次

瘦身原则

构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。

在不同场景下,复杂应用可以选择不同的方式瘦身。

场景及相应的瘦身方式

场景一:基座和模块协作紧密,如中台模式 / 共库模式

在基座和模块协作紧密的情况下,模块应该在开发时就感知基座的部分facade类和基座正使用的依赖版本,并按需引入需要的依赖。 模块打包时,仅打包两种依赖:基座没有的依赖,模块和基座版本不一致的依赖。

因此,需要让基座:

  1. 统一管控模块依赖版本,让模块开发时就知道基座有哪些依赖,风险前置,而且模块开发者按需引入部分依赖,无需指定版本。

需要让模块:

  1. 打包时,仅打包基座没有的依赖、和基座版本不一致的依赖,降低模块瘦身成本

步骤一 打包“基座-dependencies-starter”

目标

该步骤将打出 “基座依赖-starter”,用于统一管控模块依赖版本。

基座bootstrap pom增加配置

注意:以下配置中的 dependencyArtifactId 需要修改,一般为${baseAppName}-dependencies-starter

<build>
<plugins>
    <plugin>
        <groupId>com.alipay.sofa.koupleless</groupId>
        <artifactId>koupleless-base-build-plugin</artifactId>
        <!--        koupleless.runtime.version >= 1.3.0 -->
        <version>${koupleless.runtime.version}</version>
        <configuration>
            <!--生成 starter 的 artifactId(groupId和基座一致),这里需要修改!!-->
            <dependencyArtifactId>${baseAppName}-dependencies-starter</dependencyArtifactId>
            <!--生成jar的版本号-->
            <dependencyVersion>0.0.1-SNAPSHOT</dependencyVersion>
            <!-- 调试用,改成 true 即可看到打包中间产物 -->
            <cleanAfterPackageDependencies>false</cleanAfterPackageDependencies>
        </configuration>
    </plugin>
  </plugins>
</build>

本地测试

  1. 打包基座 dependency-starter jar:在基座根目录执行命令:
mvn com.alipay.sofa.koupleless:koupleless-base-build-plugin::packageDependency -f ${基座 bootstrap pom 对于基座根目录的相对路径} 

构建出来的 pom 在 outputs 目录下,也会自动安装至本地的 maven 仓库。

注意,该步骤不会将 “基座依赖-starter” 上传至 maven 仓库。欢迎后续讨论补充 “上传至 maven 仓库” 的方案。

步骤二 模块修改打包插件和 parent

目标

  1. 模块开发时,将步骤一中的 “基座-dependencies-starter” 作为模块项目的 parent,统一管理依赖版本;
  2. 修改模块打包插件,模块打包时只将“基座没有的依赖”、“与基座版本不一致的依赖”打包进模块,而不用手动配置“provided”,自动实现模块瘦身

模块根目录的 pom 中配置 parent:

<parent>
   <groupId>com.alipay</groupId>
   <artifactId>${baseAppName}-dependencies-starter</artifactId>
   <version>0.0.1-SNAPSHOT</version>
</parent>

模块打包的 pom 中配置 plugin:

<build>
   <plugins>
       <plugin>
           <groupId>com.alipay.sofa</groupId>
           <artifactId>sofa-ark-maven-plugin</artifactId>
           <!--           since ${sofa.ark.version} >= 2.2.13    -->
           <version>${sofa.ark.version}</version>
           <executions>
               <execution>
                   <id>default-cli</id>
                   <goals>
                       <goal>repackage</goal>
                   </goals>
               </execution>
           </executions>
           <configuration>
               <!-- 配置 “基座-dependencies-starter” 的标识,规范为:'${groupId}:${artifactId}':'version' -->
               <baseDependencyParentIdentity>com.alipay:${baseAppName}-dependencies-starter:0.0.1-SNAPSHOT</baseDependencyParentIdentity>
           </configuration>
       </plugin>
   </plugins>
</build>

步骤三 配置模块依赖白名单

对于部分依赖,即使模块和基座使用的依赖版本一致,但模块打包时也需要保留该依赖,即需要配置模块瘦身依赖白名单。这部分功能我们将在 7 月底推出。

步骤四 打包构建

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

场景二:基座和模块协作松散,如多应用合并部署节省资源

在基座和模块协作松散的情况下,模块不应该在开发时感知基座正使用的依赖版本,因此模块更需要注重模块瘦身的低成本接入,可以配置模块打包需要排除的依赖。

方式一:SOFAArk 配置文件排包

步骤一

SOFAArk 模块瘦身会读取两处配置文件:

  • “模块项目根目录/conf/ark/bootstrap.properties”,比如:my-module/conf/ark/bootstrap.properties
  • “模块项目根目录/conf/ark/bootstrap.yml”,比如:my-module/conf/ark/bootstrap.yml

配置方式

bootstrap.properties (推荐)

在「模块项目根目录/conf/ark/bootstrap.properties」中按照如下格式配置需要下沉到基座的框架和中间件常用包,比如:

# excludes config ${groupId}:{artifactId}:{version}, split by ','
excludes=org.apache.commons:commons-lang3,commons-beanutils:commons-beanutils
# excludeGroupIds config ${groupId}, split by ','
excludeGroupIds=org.springframework
# excludeArtifactIds config ${artifactId}, split by ','
excludeArtifactIds=sofa-ark-spi
bootstrap.yml (推荐)

在「模块项目根目录/conf/ark/bootstrap.yml」中按照如下格式配置需要下沉到基座的框架和中间件常用包,比如:

# excludes 中配置 ${groupId}:{artifactId}:{version}, 不同依赖以 - 隔开
# excludeGroupIds 中配置 ${groupId}, 不同依赖以 - 隔开
# excludeArtifactIds 中配置 ${artifactId}, 不同依赖以 - 隔开
excludes:
  - org.apache.commons:commons-lang3
  - commons-beanutils:commons-beanutils
excludeGroupIds:
  - org.springframework
excludeArtifactIds:
  - sofa-ark-spi

步骤二

升级模块打包插件 sofa-ark-maven-plugin 版本 >= 2.2.12

    <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    <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>biz1</bizName>
            <webContextPath>biz1</webContextPath>
            <declaredMode>true</declaredMode>
        </configuration>
    </plugin>

步骤三

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

您可点击此处查看完整模块瘦身样例工程。

3.3 - 4.3.3 模块启动

模块启动

模块启动参数

模块有两种部署方式:静态合并部署和热部署。

静态合并部署模块不支持配置启动参数。模块大部分的启动参数可以放在模块配置(application.properties)中,如配置 profile 时:将启动参数中的 –spring.profiles.active=dev,配置为 application.properties 中的 spring.profiles.active=true。

热部署模块支持配置启动参数。如:使用 arklet 通过 web 请求安装模块时,可以配置启动参数和环境变量:

curl --location --request POST 'localhost:1238/installBiz' \
--header 'Content-Type: application/json' \
--data '{
    "bizName": "${Module Name}",
    "bizVersion": "${Module Version}",
    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar",
    "args": ["--spring.profiles.active=dev"],
    "env": {
        "XXX": "YYY"
    }
}'

模块启动加速

模块启动加速的设计思路

模块启动加速的总体思路是:

  1. 基座提前启动好服务,这个只需要基座提前引入依赖即可
  2. 模块通过各种方式复用基座的服务,可以通过如下的方式复用基座服务包括,具体使用哪种方式需要根据实际情况分析,有疑问可以社区群里交流:
    1. 通过类 static 变量的共享达到复用
    2. 通过基座封装一些服务的接口 api,模块直接调用这些 api 来复用基座的服务。
    3. 通过注解的方式获取基座对象的代理,Koupleless 提供的 @AutowiredFromBase 、@AutowiredFromBiz、SpringServiceFinder 工具类 ,dubbo 或者 SOFARpc 的一些支持 jvm service 调用的注解。
    4. 通过跨模块查找对象的方式,直接获取基座对象,如 Koupleless 提供的 SpringBeanFinder 工具类

这里隐含了一个问题,那就是模块为了能顺利调用基座服务,需要使用一些模型类,所以模块一般都需要将该服务对应的依赖引入进来,这导致模块启动的时候会扫描到这些服务的配置,从而再次初始化这些服务,这会导致模块启动一些不需要的服务,并且启动变慢,内存消耗增加。所以要让模块启动加速实际上要完成三件事情:

  1. 基座提前启动好服务
  2. 模块禁止启动这些服务,这是本文要详细介绍的
  3. 模块复用基座服务

模块如何禁止启动部分服务

Koupleless 1.1.0 版本开始,提供了如下的配置能力:

koupleless.module.autoconfigure.exclude=xxx,xxxx,xxx # 模块启动时不需要启动的服务 AutoConfiguration
koupleless.module.autoconfigure.include=xxx,xxx,xxx # 模块启动时需要启动的服务 AutoConfiguration,如果某个服务同时配置了 include 和 exclude,则会启动该服务
koupleless.module.initializer.skip=xxx,xxx,xxx # 模块启动时需要跳过的 initializer

该配置可以在基座里配置,也可以在模块里配置。如果在基座里配置,则所有模块都会生效,如果在模块里配置,则只有该模块生效,并且模块里的配置会覆盖基座的配置。基座里的配置方式:

ark.common.env.share.keys=koupleless.module.autoconfigure.exclude,koupleless.module.autoconfigure.include,koupleless.module.initializer.skip
koupleless.module.autoconfigure.exclude=xxx,xxxx,xxx # 所有模块启动时不需要启动的服务 AutoConfiguration
koupleless.module.autoconfigure.include=xxx,xxx,xxx # 所有模块启动时需要启动的服务 AutoConfiguration,如果某个服务同时配置了 include 和 exclude,则会启动该服务
koupleless.module.initializer.skip=xxx,xxx,xxx # 所有模块启动时需要跳过的 initializer

benchmark

详细 benchmark 还待补充

3.4 - 4.3.4 模块与模块、模块与基座通信

Koupleless 模块与模块、模块与基座通信

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

Spring 环境

模块里引入依赖

<dependency>
    <groupId>com.alipay.sofa.koupleless</groupId>
    <artifactId>koupleless-app-starter</artifactId>
    <version>${koupleless.runtime.version}</version>
    <scope>provided</scope>
</dependency>

基座调用模块

只能使用 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 环境

请参考该文档



3.5 - 4.3.5 模块本地开发与调试

Koupleless 模块本地开发与调试

Arkctl 工具安装

Arkctl 模块安装主要提供自动打包和部署能力,包括调用 mvn 命令自动构建模块为 jar 包,调用 arklet 提供的 api 接口进行完成部署。ArkCtl 安装方式可以参照文档:arkctl 安装本地环境开发验证小节。

安装方法一: 使用 golang 工具链

  1. golang 官网 下载对应的 golang 版本,版本需要在 1.21 以上。
  2. 执行 go install github.com/koupleless/arkctl@v0.2.1 命令,安装 arkctl 工具。

安装方式二: 下载二进制文件

  1. 根据实际运行操作系统,下载 arkctl
  2. 将对应的二进制解压并放到合适的系统变量 PATH 所在的目录里。
  3. 在基座和模块已经改造完成后,启动好基座后,可以使用 arkctl 快速完成构建与部署,将模块部署到基座中。

Linux/Mac 如何找到 PATH 的值?

终端执行

echo $PATH
# 选择一个目录,将 arkctl 放到该目录下

Windows 下如何找到 PATH 的值?

按 Windows + R 键,输入 cmd,然后按 Enter 打开命令提示符。在命令提示符窗口中,输入以下命令并按 Enter:

echo %PATH%

注意,在 Windows 环境下,如果开启 Windows Defender,浏览器下载二进制时可能会误报,提示如下:


报错原因可参考 [go官方文档](https://go.dev/doc/faq#virus) 。此报错可以忽略,放心下载。

由于 Arkctl 部署其实是调用 API 的方式来完成的,如果不想使用命令行工具,也可以直接使用 Arklet API 接口 完成部署操作。当然我们也提供了 telnet 的方式来部署模块,详细可查看这里

本地快速部署

你可以使用 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 包到本地运行的基座中。

准备:

  1. 在本地启动一个基座

执行命令:

arkctl deploy ./path/to/your/biz/

注意该命令适用于模块可以独立构建的(可以在biz目录里成功执行 mvn package 等命令),则该命令会自动构建该模块,并部署到基座中。

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

准备:

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

执行命令:

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

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

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

准备:

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

执行命令:

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

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

场景 6: 如何更快的使用该命令

可以在 IDEA 里新建一个 Shell Script,配置好运行的目录,然后输入 arkctl 相应的命令,如下图即可。

模块本地调试

模块与基座出于同一个 IDEA 工程中

因为 IDEA 工程里能看到模块代码,模块调试与普通调试没有区别。直接在模块代码里打断点,基座通过 debug 方式启动即可。

模块与基座在不同 IDEA 工程中

  1. 基座启动参数里增加 debug 配置 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000,然后启动基座
  2. 模块添加 remote jvm debug, 设置 host 为 localhost -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
  3. 模块里打断点
  4. 这时候安装模块后就可以调试了

查看部署状态

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

准备:

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

执行命令:

arkctl status

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

准备:

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

执行命令:

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

通过 arthas 查看运行时模块状态与信息

获取所有 Biz 信息

vmtool -x 1 --action getInstances --className com.alipay.sofa.ark.container.model.BizModel --limit 100

如:
image.png

获取特定 Biz 信息

# 请替换 ${bizName}
vmtool -x 1 --action getInstances --className com.alipay.sofa.ark.container.model.BizModel --limit 100 | grep ${bizName}  -A 4

如:
image.png

获取特定 BizClassLoader 对应的 Biz 信息

# 请替换 ${BizClassLoaderHashCode}
vmtool -x 1 --action getInstances --className com.alipay.sofa.ark.container.model.BizModel --limit 100 | grep ${BizClassLoaderHashCode}  -B 1 -A 3

如:
image.png

3.6 - 4.3.6 复用基座数据源

Koupleless 模块复用基座数据源

使用场景

建议数据源下沉到基座,模块中尽可能复用基座数据源,避免模块在安装卸载过程中反复创建、销毁数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。 如果对模块的启动时间和内存占用不太关心的,可以不用复用基座数据源,直接在模块中创建数据源,那么模块按照普通应用方式定义数据源即可无需参考本文。

SpringBoot 解法

在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
参考 demo:/koupleless/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


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

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


3.7 - 4.3.7 复用基座拦截器

Koupleless 模块复用基座拦截器

诉求

基座中会定义很多 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。如果是 SpringBoot,则使用 @AutowiredFromBase 注入基座上的 FacadeAroundHandler

注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

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

    // 如果是 SOFABoot,则使用 @SofaReference,如果是 SpringBoot,则使用 @AutowiredFromBase
    @SofaReference(uniqueId = "facadeAroundHandler")
    //@AutowiredFromBase
    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);
    }
}


3.8 - 4.3.8 线程池使用

Koupleless 线程池使用

背景

多个模块模块和基座共用一个线程池时,由于线程池执行任务使用的线程 Classloader 可能和创建该任务时使用的 Classloader 不一致,从而导致线程池执行任务时出现 ClassNotFound 异常。

因此,当多个模块或模块和基座共用一个线程池时,为了保持线程池执行任务时使用的 Classloader 和创建该任务时使用的 Classloader 一致,我们需要对线程池做一些修改。

⚠️注意:各模块使用各自的线程池,不会有此问题。

java常用的线程池使用方式有4种:

  1. 直接创建线程任务,提交到线程池中,如:Runnable, Callable, ForkJoinTask
  2. 自定义ThreadPoolExecutor,并提交到 ThreadPoolExecutor
  3. 通过 Executors 创建线程池,并提交到 ExecutorService, ScheduledExecutorService, ForkJoinPool
  4. SpringBoot 用户提交到 ThreadPoolTaskExecutor, SchedulerThreadPoolTaskExecutor

本文将介绍每一种方式在 Koupleless 上的使用方式。

使用方式

1. 直接创建线程任务,提交到线程池中

原来的方式:


threadPool.execute(new Runnable(){
    public void run() {
        //do something
    }
});


threadPool.execute(new Callable<String>(){
public String call() {
        //do something
        return "mock";
 }
});

如果保持 threadPool 不变,则需要将 Runnable 包装成 KouplelessRunnable,将 Callable 包装成 KouplelessCallable,如下:

// Runnable
// wrap方法
threadPool.execute(KouplelessRunnable.wrap(new Runnable(){
    public void run() {
        //do something
    }
});

// 或者直接new KouplelessRunnable
threadPool.execute(new KouplelessRunnable(){
    public void run() {
        //do something
    }
});

// Runnable
// wrap方法
threadPool.execute(KouplelessCallable.wrap(new Callable<String>(){
    public String call() {
        //do something
        return "mock";
    }
});

// 或者直接new KouplelessRunnable
threadPool.execute(new KouplelessCallable<String>(){
    public String call() {
        //do something
        return "mock";
    }
});

2. 自定义ThreadPoolExecutor

原来的方式:

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

threadPool.execute(new Runnable(){
    public void run() {
        //do something
    }
});


threadPool.execute(new Callable<String>(){
public String call() {
        //do something
        return "mock";
 }
});

如果要保持 Runnable 和 Callable 不变,则有两种改造方式:

  1. 将 threadPool 修改为 KouplelessThreadPoolExecutor
  2. 或者使用 kouplelessExecutorService。

首先,举例第一种改造方式:将 threadPool 修改为 KouplelessThreadPoolExecutor。如下:

// 将 ThreadPoolExecutor 修改为 KouplelessThreadPoolExecutor
ThreadPoolExecutor threadPool = new KouplelessThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

threadPool.execute(new Runnable(){
    public void run() {
        //do something
    }
});


threadPool.execute(new Callable<String>(){
public String call() {
        //do something
        return "mock";
 }
});

然后,举例第二种改造方式:使用 kouplelessExecutorService。如下:

// 使用 KouplelessExecutorService
ExecutorService executor        = new KouplelessExecutorService(new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));

// 用 executor 执行任务
executor.execute(new Runnable(){
    public void run() {
        //do something
    }
});
executor.execute(new Callable<String>(){
public String call() {
        //do something
        return "mock";
 }
});

3. 使用二方包中的 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor

原来的方式:

ThreadPoolExecutorA executorService = new ThreadPoolExecutorA();

executorService.execute(new Runnable(){
    public void run() {
        //do something
    }
});


executorService.execute(new Callable<String>(){
public String call() {
        //do something
        return "mock";
 }
});

ScheduledThreadPoolExecutorA scheduledExecutorService = new ScheduledThreadPoolExecutorA();

scheduledExecutorService.execute(new Runnable(){
    public void run() {
        //do something
    }
});

scheduledExecutorService.execute(new Callable<String>(){
    public String call() {
        //do something
        return "mock";
    }
});

如果要保持 Runnable 和 Callable 不变,则需要使用 kouplelessExecutorService, kouplelessScheduledExecutorService,如下:

// 使用 KouplelessExecutorService
ExecutorService executor        = new KouplelessExecutorService(new ThreadPoolExecutorA());

// 用 executor 执行任务
executor.execute(new Runnable(){
    public void run() {
        //do something
    }
});
executor.execute(new Callable<String>(){
    public String call() {
        //do something
    return "mock";
    }
});


// 使用 KouplelessScheduledExecutorService
ScheduledExecutorService scheduledExecutor = new KouplelessScheduledExecutorService(new ScheduledThreadPoolExecutorA());

// 用 scheduledExecutor 执行任务
scheduledExecutor.execute(new Runnable(){
    public void run() {
        //do something
    }
});
scheduledExecutor.execute(new Callable<String>(){
    public String call() {
        //do something
        return "mock";
    }
});

4. 通过 Executors 创建线程池

原来的方式:

ExecutorService executorService = Executors.newFixedThreadPool(6);

executorService.execute(new Runnable(){
    public void run() {
        //do something
    }
});


executorService.execute(new Callable<String>(){
public String call() {
        //do something
        return "mock";
 }
});

ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

scheduledExecutorService.execute(new Runnable(){
    public void run() {
        //do something
    }
});

scheduledExecutorService.execute(new Callable<String>(){
    public String call() {
        //do something
        return "mock";
    }
});

如果要保持 Runnable 和 Callable 不变,则需要使用 kouplelessExecutorService, kouplelessScheduledExecutorService,如下:

// 使用 KouplelessExecutorService
ExecutorService executor        = new KouplelessExecutorService(Executors.newFixedThreadPool(6));

// 用 executor 执行任务
executor.execute(new Runnable(){
    public void run() {
        //do something
    }
});
executor.execute(new Callable<String>(){
    public String call() {
        //do something
    return "mock";
    }
});

// 使用 KouplelessScheduledExecutorService
ScheduledExecutorService scheduledExecutor = new KouplelessScheduledExecutorService(Executors.newSingleThreadScheduledExecutor());

// 用 scheduledExecutor 执行任务
scheduledExecutor.execute(new Runnable(){
    public void run() {
        //do something
    }
});
scheduledExecutor.execute(new Callable<String>(){
    public String call() {
        //do something
        return "mock";
    }
});

5. SpringBoot 用户提交到 ThreadPoolTaskExecutor, SchedulerThreadPoolTaskExecutor

由于 koupeless 已经对 springboot(2.3.0-2.7.x) 的 ThreadPoolTaskExecutor 和 SchedulerThreadPoolTaskExecutor 做了适配,所以可以直接使用。

@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;

@Autowired
private SchedulerThreadPoolTaskExecutor schedulerThreadPoolTaskExecutor;

threadPoolTaskExecutor.execute(new Runnable(){
    public void run() {
        //do something
    }
});

schedulerThreadPoolTaskExecutor.execute(new Runnable(){
    public void run() {
        //do something
    }
});

3.9 - 4.3.9 模块多配置

为什么要多配置

在不同场合下,一份模块代码会部署到不同的应用中,但需要使用不同的配置。

怎么使用

步骤一:在不同场合下,给一份模块代码打包时,配置不同的 bizName,如:biz1, biz2

<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>
        <!-- 不同场合配置不同的bizName,如:biz1, biz2 -->
        <bizName>biz1</bizName>
        <!-- ... 其它属性 -->
    </configuration>
</plugin>

步骤二:在模块的 resources 目录下,新增文件。其中 config, biz1 和 biz2 为文件夹:

  • config/biz1/application.properties

  • config/biz2/application.properties

步骤三:用不同的 bizName(biz1,biz2),打包出两个不同的 ark-biz 文件:

  • biz1-0.0.1-SNAPSHOT-ark-biz.jar

  • biz2-0.0.1-SNAPSHOT-ark-biz.jar

步骤四:在不同场合下,安装不同的 ark-biz 模块。模块启动时,将根据不同的 bizName 读取不同的配置文件:

  • config/biz1/application.properties

  • config/biz2/application.properties

原理

模块启动时,根据模块名称与 spring.profiles.active 字段,读取以下文件为属性源:

  • config/${bizName}/application-${profile}.properties
  • config/${bizName}/application.properties

如果未设置 spring.profiles.active,则读取以下文件为属性源:

  • config/${bizName}/application.properties

3.10 - 4.3.10 多模块集成测试

English | 简体中文

为什么我们需要集成测试框架?

如果没有集成测试框架,在验证 koupleless 模块逻辑时,开发者的验证步骤是繁琐的,需要做如下步骤:

  1. 启动一个基座进程。
  2. 构建模块 jar 包。
  3. 安装模块。
  4. 调用模块的 http 接口(或其他方法)验证逻辑。

如果逻辑不符合预期,开发者需要重复上述步骤, 这样的验证流程是非常低效的。 为了提高开发者的验证效率,我们决定提供 koupleless 集成测试框架,让开发者能够在一个进程内同时启动基座和模块。

集成测试框架

原理

集成测试框架通过增强基座的类加载器和模块的类加载行为,来模拟多模块部署的场景。 具体的源代码可以参照 koupleless-test-suite

如何使用

以 webflux-samples 为例子。webflux-samples 的项目结构如下:

我们新建一个 maven module:

首先该 module 需要添加集成测试框架依赖:


<dependency>
    <groupId>com.alipay.sofa.koupleless</groupId>
    <artifactId>koupleless-test-suite</artifactId>
    <version>${koupleless.runtime.version}</version>
</dependency>

然后我们需要添加基座和模块的依赖:

<!-- 基座依赖 -->
<dependency>
    <groupId>com.alipay.sofa.web.webflux</groupId>
    <artifactId>demowebflux</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <classifier>lib</classifier>
</dependency>
        <!-- 模块依赖 -->
<dependency>
<groupId>com.alipay.sofa.web.webflux</groupId>
<artifactId>bizwebflux</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

接着,我们需要编写集成测试用例:

    public static void setUp() {
    TestMultiSpringApplication multiApp = new TestMultiSpringApplication(
            MultiSpringTestConfig
                    .builder()
                    .baseConfig(
                            BaseSpringTestConfig
                                    .builder()
                                    // 传入基座的启动类。
                                    .mainClass(DemoWebfluxApplication.class)
                                    .build()
                    )
                    .bizConfigs(
                            Lists.newArrayList(
                                    BizSpringTestConfig
                                            .builder()
                                            .bizName("biz")
                                            // 传入模块的启动类。
                                            .mainClass(BizWebfluxApplication.class)
                                            .build()))
                    .build());
    multiApp.run();
}

最后,在 IDEA 里启动测试,我们会发现基座和模块的 Spring 容器都启动了。这样我们就可以在一个进程内验证多模块的逻辑。
如此,我们就完成了一个集成测试用例。

总结

通过上面的实验,我们验证了可以通过 koupleless 集成测试框架,来快速验证多模块的逻辑,提高开发者的验证效率。

3.11 - 4.3.11 静态合并部署

Koupleless 模块静态合并部署

介绍

SOFAArk 提供了静态合并部署能力,Base 包(基座应用) 在启动时,可以启动已经构建完成的 Biz 包(模块应用),默认获取模块的方式为:本地目录、本地文件URL、远程URL。

此外,SOFAArk 还提供了静态合并部署的扩展接口,开发者可以自定义获取 Biz 包(模块应用) 的方式。

使用方式

步骤一:模块应用打包成 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>
        <configuration>
            <!--                默认100,数值越大越后面安装,koupleless runtime 版本大于等于 1.2.2             -->
            <priority>200</priority>
        </configuration>
    </plugin>
</build>

步骤二:基座获取需要合并部署的 Ark Biz

要求:

  • jdk8
    • sofa.ark.version >= 2.2.12
    • koupleless.runtime.version >= 1.2.3
  • jdk17/jdk21
    • sofa.ark.version >= 3.1.5
    • koupleless.runtime.version >= 2.1.4

方式一:使用官方默认获取方式,支持本地目录、本地文件URL、远程URL

1. 基座配置本地目录、本地文件URL、远程URL

开发者需要在基座的 ark 配置文件中(conf/ark/ark.propertiesconf/ark/ark.yml)指定需要合并部署的 Ark Biz 包,支持:

  • 本地目录
  • 本地文件URL(windows 系统为 file:\\, linux 系统为 file://)
  • 远程URL(支持 http://,https://

其中,本地文件URL、远程URL 配置在 integrateBizURLs 字段中,本地目录配置在 integrateLocalDirs 字段中。

配置方式如下:

integrateBizURLs=file://${xxx}/koupleless_samples/springboot-samples/service/biz1/biz1-bootstrap/target/biz1-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar,\
  file://${xxx}/koupleless_samples/springboot-samples/service/biz2/biz2-bootstrap/target/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar,\
  https://oss.xxxxx/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar
integrateLocalDirs=/home/${xxx}/sofa-ark/biz,\
  /home/${xxx}/sofa-ark/biz2

integrateBizURLs:
  - file://${xxx}/springboot-samples/service/biz2/biz2-bootstrap/target/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar
  - file://${xxx}/koupleless_samples/springboot-samples/service/biz2/biz2-bootstrap/target/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar
integrateLocalDirs:
  - /home/${xxx}/sofa-ark/biz
  - /home/${xxx}/sofa-ark/biz2
2. 基座配置打包插件目标 integrate-biz

基座 bootstrap 的 pom 中给 koupleless-base-build-plugin 添加 integrate-biz,如下:

<plugin>
    <groupId>com.alipay.sofa.koupleless</groupId>
    <artifactId>koupleless-base-build-plugin</artifactId>
    <version>${koupleless.runtime.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>add-patch</goal>
<!--                用于静态合并部署-->
                <goal>integrate-biz</goal>
            </goals>
        </execution>
    </executions>
</plugin>

执行打包后,如果自行解压打包的 jar 文件,可以在 classPath/SOFA-ARK/biz 中看到指定的模块 ark-biz 包。

方式二:使用自定义获取方式

1. Ark 扩展机制原理

Ark 扩展机制原理介绍

2. 实现 AddBizToStaticDeployHook 接口

基座/基座二方包中,实现 AddBizToStaticDeployHook 接口,以 AddBizInResourcesHook 为例,如下:

@Extension("add-biz-in-resources-to-deploy")
public class AddBizInResourcesHook implements AddBizToStaticDeployHook {
    @Override
    public List<BizArchive> getStaticBizToAdd() throws Exception {
        List<BizArchive> archives = new ArrayList<>();
        // ...
        archives.addAll(getBizArchiveFromResources());
        return archives;
    }

    protected List<BizArchive> getBizArchiveFromResources() throws Exception {
        // ... 读取资源中的Ark Biz包
        return archives;
    }
}
3. 配置 spi

在 resources 目录下添加 /META-INF/services/sofa-ark/ 目录,再在 /META-INF/services/sofa-ark/ 添加一个 名为 com.alipay.sofa.ark.spi.service.biz.AddBizToStaticDeployHook 的文件,文件里面内容为 hook 类的全限定名:

com.alipay.sofa.ark.support.common.AddBizInResourcesHook

重新打包基座。

步骤三:启动基座

JVM 添加参数,配置: -Dsofa.ark.embed.static.biz.enable=true

3.12 - 4.3.12 各版本兼容性关系表与适配验证的组件列表

Koupleless 各版本兼容性关系表与适配验证的组件列表

框架自身各版本兼容性关系

用户可根据实际 jdk 和 springboot 版本按需引入 Koupleless 版本

JDKSpringBootSOFA-ARKKoupleless
1.82.x2.x.x1.x.x
173.0.x, 3.1.x3.0.7(no update anymore)2.0.4(no update anymore)
17 & 213.2.x and above3.1.x2.1.x

koupleless 的 sdk 最新版本请查看 https://github.com/koupleless/runtime/releases


各组件兼容性报告

在 Koupleless 模块中,官方目前支持并兼容常见的中间件客户端。
注意,这里 “已经支持” 需要在基座 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.koupleless</groupId>
  <artifactId>koupleless-adapter-log4j2</artifactId>
  <version>${最新版 Koupleless 版本}</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已经支持
基座和模块完整使用样例及注意事项可参见此处
模块独立 system.propertieskoupleless 已支持参考这个 PR
模块独立 envkoupleless 已支持参考这个 PR
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-待验证支持
skywalking-官方不支持一个进程多个 service_name,通过每个模块打印日志到独立目录才实现 tracing 的隔离,可参考 logging 的 samples
宝蓝德9.5.5.004已经支持,请查看这里
东方通7.0.E.6_P7已经支持,请查看这里

3.13 - 4.3.13 Koupleless 配置

Koupleless 各项配置

打包构建阶段

基座打包插件配置

插件参数配置

完整的 koupleless-base-build-plugin 插件配置模板如下:

<plugin>
  <groupId>com.alipay.sofa.koupleless</groupId>
  <artifactId>koupleless-base-build-plugin</artifactId>
  <version>${koupleless.runtime.version}</version>
  <executions>
    <execution>
      <goals>
        <goal>add-patch</goal>
        <!-- 用于静态合并部署-->
        <goal>integrate-biz</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
      <!--基座打包存放目录,默认为工程 build 目录-->
      <outputDirectory>./target</outputDirectory>
      
      <!--打包 starter 的 groupId,默认为工程的 groupId-->
      <dependencyGroupId>${groupId}</dependencyGroupId>
      
      <!--打包 starter 的 artifactId-->
      <dependencyArtifactId>${baseAppName}-dependencies-starter</dependencyArtifactId>
      
      <!--打包 starter 的版本号-->
      <dependencyVersion>0.0.1-SNAPSHOT</dependencyVersion>
      
      <!-- 调试用,改成 true 即可看到打包 starter 的中间产物 -->
      <cleanAfterPackageDependencies>false</cleanAfterPackageDependencies>
  </configuration>
</plugin>

静态合并部署的配置

开发者需要在基座的 ark 配置文件中(conf/ark/bootstrap.propertiesconf/ark/bootstrap.yml)指定需要合并部署的 Ark Biz 包,支持:

  • 本地目录
  • 本地文件URL(windows 系统为 file:\\, linux 系统为 file://)
  • 远程URL(支持 http://,https://

其中,本地文件URL、远程URL 配置在 integrateBizURLs 字段中,本地目录配置在 integrateLocalDirs 字段中。

配置方式如下:

integrateBizURLs=file://${xxx}/koupleless_samples/springboot-samples/service/biz1/biz1-bootstrap/target/biz1-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar,\
  file://${xxx}/koupleless_samples/springboot-samples/service/biz2/biz2-bootstrap/target/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar,\
  https://oss.xxxxx/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar
integrateLocalDirs=/home/${xxx}/sofa-ark/biz,\
  /home/${xxx}/sofa-ark/biz2

integrateBizURLs:
  - file://${xxx}/springboot-samples/service/biz2/biz2-bootstrap/target/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar
  - file://${xxx}/koupleless_samples/springboot-samples/service/biz2/biz2-bootstrap/target/biz2-bootstrap-0.0.1-SNAPSHOT-ark-biz.jar
integrateLocalDirs:
  - /home/${xxx}/sofa-ark/biz
  - /home/${xxx}/sofa-ark/biz2

模块打包插件配置

插件参数配置

完整的 sofa-ark-maven-plguin 插件配置模板如下:

<plugins>
    <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>
                <configuration>
                    <!--ark 包和 ark biz 的打包存放目录,默认为工程 build 目录-->
                    <outputDirectory>./target</outputDirectory>

                    <!--设置应用的根目录,用于读取 ${base.dir}/conf/ark/bootstrap.application 配置文件,默认为 ${project.basedir}-->
                    <baseDir>./</baseDir>

                    <!--生成 ark 包文件名称,默认为 ${artifactId}-->
                    <finalName>demo-ark</finalName>

                    <!--是否跳过执行 goal:repackage,默认为false-->
                    <skip>false</skip>

                    <!--是否打包、安装和发布 ark biz,详细参考 Ark Biz 文档,默认为false-->
                    <attach>true</attach>

                    <!--设置 ark 包的 classifier,默认为空-->
                    <arkClassifier>ark</arkClassifier>

                    <!--设置 ark biz 的 classifier,默认为 ark-biz-->
                    <bizClassifier>ark-biz</bizClassifier>

                    <!--设置 ark biz 的 biz name,默认为 ${artifactId}-->
                    <bizName>demo-ark</bizName>

                    <!--设置 ark biz 的 biz version,默认为 ${artifactId}-->
                    <bizVersion>0.0.1</bizVersion>

                    <!--设置 ark biz 的 启动优先级,值越小优先级越高,${artifactId}-->
                    <priority>100</priority>

                    <!--设置 ark biz 的启动入口,默认会搜索被打 org.springframework.boot.autoconfigure.SpringBootApplication 注解且含有 main 方法的入口类-->
                    <mainClass>com.alipay.sofa.xx.xx.MainEntry</mainClass>

                    <!--设置是否将 scope=provided 的依赖打包,默认为 false-->
                    <packageProvided>false</packageProvided>

                    <!--设置是否生成 Biz 包,默认为true-->
                    <keepArkBizJar>true</keepArkBizJar>

                    <!--针对 Web 应用,设置 context path,默认为 /,模块应该配置自己的 webContextPath,如:biz1 -->
                    <webContextPath>/</webContextPath>

                    <!--打包 ark biz 时,排除指定的包依赖;格式为: ${groupId:artifactId} 或者 ${groupId:artifactId:classifier}-->
                    <excludes>
                        <exclude>org.apache.commons:commons-lang3</exclude>
                    </excludes>

                    <!--打包 ark biz 时,排除和指定 groupId 相同的包依赖-->
                    <excludeGroupIds>
                        <excludeGroupId>org.springframework</excludeGroupId>
                    </excludeGroupIds>

                    <!--打包 ark biz 时,排除和指定 artifactId 相同的包依赖-->
                    <excludeArtifactIds>
                        <excludeArtifactId>sofa-ark-spi</excludeArtifactId>
                    </excludeArtifactIds>

                    <!--打包 ark biz 时,配置不从 ark plugin 索引的类;默认情况下,ark biz 会优先索引所有 ark plugin 的导出类,
                    添加该配置后,ark biz 将只在ark biz内部加载该类,不再优先委托 ark plugin 加载-->
                    <denyImportClasses>
                        <class>com.alipay.sofa.SampleClass1</class>
                        <class>com.alipay.sofa.SampleClass2</class>
                    </denyImportClasses>

                    <!--对应 denyImportClasses 配置,可以配置包级别-->
                    <denyImportPackages>
                        <package>com.alipay.sofa</package>
                        <package>org.springframework.*</package>
                    </denyImportPackages>

                    <!--打包 ark biz 时,配置不从 ark plugin 索引的资源;默认情况下,ark biz 会优先索引所有 ark plugin 的导出资源,
                    添加该配置后,ark biz 将只在ark biz内部寻找该资源,不在从 ark plugin 查找-->
                    <denyImportResources>
                        <resource>META-INF/spring/test1.xml</resource>
                        <resource>META-INF/spring/test2.xml</resource>
                    </denyImportResources>
                  
                     <!--ark biz 仅能找到自己在pom 中声明过的依赖,默认为 false-->
                    <declaredMode>true</declaredMode>

                    <!--打包 ark biz 时,仅打包基座没有的依赖、模块与基座不同版本的依赖。该参数用于指定“基座的依赖管理”标识,“基座的依赖管理”需要作为模块 pom 的 parent ,以 ${groupId}:${artifactId}:${version} 标识 -->
                    <baseDependencyParentIdentity>${groupId}:${artifactId}:${version}</baseDependencyParentIdentity>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

模块瘦身配置

SOFAArk 模块瘦身会读取两处配置文件:

  • “模块项目根目录/conf/ark/bootstrap.properties”,比如:my-module/conf/ark/bootstrap.properties
  • “模块项目根目录/conf/ark/bootstrap.yml”,比如:my-module/conf/ark/bootstrap.yml

bootstrap.properties

在「模块项目根目录/conf/ark/bootstrap.properties」中按照如下格式配置需要下沉到基座的框架和中间件常用包,比如:

# excludes config ${groupId}:{artifactId}:{version}, split by ','
excludes=org.apache.commons:commons-lang3,commons-beanutils:commons-beanutils
# excludeGroupIds config ${groupId}, split by ','
excludeGroupIds=org.springframework
# excludeArtifactIds config ${artifactId}, split by ','
excludeArtifactIds=sofa-ark-spi

bootstrap.yml

在「模块项目根目录/conf/ark/bootstrap.yml」中按照如下格式配置需要下沉到基座的框架和中间件常用包,比如:

# excludes 中配置 ${groupId}:{artifactId}:{version}, 不同依赖以 - 隔开
# excludeGroupIds 中配置 ${groupId}, 不同依赖以 - 隔开
# excludeArtifactIds 中配置 ${artifactId}, 不同依赖以 - 隔开
excludes:
  - org.apache.commons:commons-lang3
  - commons-beanutils:commons-beanutils
excludeGroupIds:
  - org.springframework
excludeArtifactIds:
  - sofa-ark-spi

开发阶段

Arklet 配置

端口配置

基座启动时,在JVM参数中配置端口,默认为 1238

-Dkoupleless.arklet.http.port=XXXX

模块运行时配置

健康检查的配置

基座的 application.properties 配置:

# 或者不配置 management.endpoints.web.exposure.include
management.endpoints.web.exposure.include=health
# 如果需要展示所有信息,则配置以下内容
management.endpoint.health.show-components=always
management.endpoint.health.show-details=always
# 不忽略模块启动状态
koupleless.healthcheck.base.readiness.withAllBizReadiness=true

Web Gateway 配置

在传统应用拆出模块时,由于每个模块都有自己的 webContextPath,上游调用方需要修改请求路径。为了避免修改,可以在 application.properties 或 application.yaml 中配置 Web Gateway 转发规则,让上游调用方无需修改。

在配置上,可以配置三种策略:

  • 域名匹配:指定 符合HostA的请求 转发到 模块A
  • 路径匹配:指定 符合PathA的请求 转发到 模块A的特定PathB
  • 域名和路径同时匹配:指定 符合HostA且PathA的请求 转发到 模块A的特定PathB

application.yaml 配置样例如下:

koupleless:
  web:
    gateway:
      forwards:
# host in [a.xxx,b.xxx,c.xxx] path /${anyPath} --forward to--> biz1/${anyPath}
        - contextPath: biz1
        - hosts:
            - a
            - b
            - c
# /idx2/** -> /biz2/**, /t2/** -> /biz2/timestamp/**
        - contextPath: biz2
        - paths:
            - from: /idx2
            - to: /
            - from: /t2
            - to: /timestamp
# /idx1/** -> /biz1/**, /t1/** -> /biz1/timestamp/**
        - contextPath: biz1
        - paths:
            - from: /idx1
            - to: /
            - from: /t1
            - to: /timestamp

application.properties 配置样例如下:

# host in [a.xxx,b.xxx,c.xxx] path /${anyPath} --forward to--> biz1/${anyPath}
koupleless.web.gateway.forwards[0].contextPath=biz1
koupleless.web.gateway.forwards[0].hosts[0]=a
koupleless.web.gateway.forwards[0].hosts[1]=b
koupleless.web.gateway.forwards[0].hosts[2]=c
# /idx2/** -> /biz2/**, /t2/** -> /biz2/timestamp/**
koupleless.web.gateway.forwards[1].contextPath=biz2
koupleless.web.gateway.forwards[1].paths[0].from=/idx2
koupleless.web.gateway.forwards[1].paths[0].to=/
koupleless.web.gateway.forwards[1].paths[1].from=/t2
koupleless.web.gateway.forwards[1].paths[1].to=/timestamp
# /idx1/** -> /biz1/**, /t1/** -> /biz1/timestamp/**
koupleless.web.gateway.forwards[2].contextPath=biz1
koupleless.web.gateway.forwards[2].paths[0].from=/idx1
koupleless.web.gateway.forwards[2].paths[0].to=/
koupleless.web.gateway.forwards[2].paths[1].from=/t1
koupleless.web.gateway.forwards[2].paths[1].to=/timestamp

3.14 - 4.3.14 SOFAArk 关键用户文档

模块生命周期

Ark 事件机制

Ark 自身日志



4 - 5. Module Controller V2 模块运维

Koupleless Module Controller V2架构下的模块运维

4.1 - 5.1 模块发布

Koupleless 模块上线与下线

注意:当前 ModuleController v2 仅在 K8S 1.24 版本测试过,没有在其它版本测试,ModuleController V2依赖了部分K8S特性,K8S的版本不能低于V1.10。

模块上线

ModuleController V2支持以任意Pod的发布方式进行模块发布上线,包含但不仅限于裸pod发布、Deployment、DaemonSet、StatefulSet。下面以Deployment为例演示模块的发布流程,其他方式可以参考Deployment中template的配置:

kubectl apply -f samples/module-deployment.yaml --namespace yournamespace

完整内容如下:

apiVersion: apps/v1  # 指定api版本,此值必须在kubectl api-versions中
kind: Deployment  # 指定创建资源的角色/类型
metadata:  # 资源的元数据/属性
  name: test-module-deployment  # 资源的名字,在同一个namespace中必须唯一
  namespace: default # 部署在哪个namespace中
spec: # 资源规范字段
  replicas: 1
  revisionHistoryLimit: 3 # 保留历史版本
  selector: # 选择器
    matchLabels: # 匹配标签
      app: test-module-deployment
  strategy: # 策略
    rollingUpdate: # 滚动更新
      maxSurge: 30% # 最大额外可以存在的副本数,可以为百分比,也可以为整数
      maxUnavailable: 30% # 示在更新过程中能够进入不可用状态的 Pod 的最大值,可以为百分比,也可以为整数
    type: RollingUpdate # 滚动更新策略
  template: # 模版
    metadata: # 资源的元数据/属性
      labels: # 设定资源的标签
        module-controller.koupleless.io/component: module # 必要,声明pod的类型,用于module controller管理
        # deployment unique id
        app: test-module-deployment-non-peer
    spec: # 资源规范字段
      containers:
        - name: biz1 # 必要,声明module的bizName,需与pom中声明的artifactId保持一致
          image: https://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/biz1-web-single-host-0.0.1-SNAPSHOT-ark-biz.jar
          env:
            - name: BIZ_VERSION # 必要,声明module的biz_version,value需与pom中声明的version保持一致
              value: 0.0.1-SNAPSHOT
      affinity:
        nodeAffinity: # 必要,声明基座选择器,保证模块被调度到指定的基座上
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: base.koupleless.io/stack
                    operator: In
                    values:
                      - java # 多语言环境下可能有其他技术栈,必填
                  - key: base.koupleless.io/version
                    operator: In
                    values:
                      - 1.1.1 # 指定的基座版本,必填,至少需要一个
                  - key: base.koupleless.io/name
                    operator: In
                    values:
                      - base  # 指定的基座bizName,必填,至少需要一个
      tolerations: # 必要,允许pod被调度到基座node上
        - key: "schedule.koupleless.io/virtual-node"
          operator: "Equal"
          value: "True"
          effect: "NoExecute"

其中所有的配置与普通Deployment一致,除必填项外,可添加其他Deployment的配置实现自定义能力。

后续模块的更新也可以直接通过更新模块 Deployment 的 Container Image 和 BIZ_VERSION 来实现,复用 Deployment 的 RollingUpdate 来实现分批更新。

Module Controller 通过控制同一个基座上的 Pod 状态更新顺序实现了滚动更新过程中的模块流量无损,具体过程为:

  1. Deployment 更新后会根据更新策略配置先创建出新版本模块的 Pod
  2. K8S Scheduler 将这些 Pod 调度到 VNode 上,此时这些 VNode 上安装了老版本的模块
  3. Module Controller 监听到 Pod 成功调度,发起新版本模块的安装
  4. 安装完成之后,Module Controller 会检查当前基座上所有模块的状态,然后对所关联的 Pod 根据创建时间进行排序,按序更新状态,从而使得旧版本模块对应的 Pod 会先变成 Not Ready 状态,其后新版本模块对应的 Pod 才会变成 Ready
  5. Deployment 控制器在监听到新创建出来的 Pod Ready 之后会开始清理旧版本 Pod,在这一过程中 Deployment 会优先选择当前状态为 Not Ready 的 Pod 进行删除,而此时同一个基座上的老版本 Pod 已经 Not Ready,会被删除,从而避免其他基座上的 Ready 状态的旧版本模块 Pod 先被删除

在上述过程中,不会出现某一个基座上无模块的情况,从而保证了模块更新过程中的流量无损。

查看模块状态

这一需求可以通过查看nodeName为基座对应node的Pod来实现。首先需要了解基座服务与node的对应关系。

在Module Controller V2的设计中,每一个基座会在启动时随机生成一个全局唯一的UUID作为基座服务的标识,对应的node的Name则将包含这一ID。

除此之外,基座服务的IP与node的IP是一一对应的,也可以通过IP来筛选对应的基座Node。

因此,可以通过以下命令查看某个基座上安装的所有Pod(模块),和对应的状态。

kubectl get pod -n <namespace> --field-selector status.podIP=<baseIP>

kubectl get pod -n <namespace> --field-selector spec.nodeName=virtual-node-<baseUUID>

模块下线

在 K8S 集群中删除模块的Pod或其他控制资源即可完成模块下线,例如,在Deployment部署的场景下,可以直接删除对应的Deployment实现模块的下线:

kubectl delete yourmoduledeployment --namespace yournamespace

其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字,yournamespace 替换成您的 namespace。

如果要自定义模块发布运维策略(比如分组、Beta、暂停等),可参考模块发布运维策略

样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除Deployment一样能实现模块分组下线。

模块扩缩容

由于ModuleController V2完全复用了K8S的Pod编排方案,扩缩容只发生在ReplicaSet、Deployment、StatefulSet等部署方式上,扩缩容可以按照各自对应的扩缩容方式实现,下面以Deployment为例:

kubectl scale deployments/yourdeploymentname --namespace=yournamespace --replicas=3

其中 yourdeploymentname 替换成您的 Deployment name,yournamespace 替换成您的 namespace,replicas参数设置为希望扩/缩容到的数量。

也可以通过API调用实现扩缩容策略。

模块替换

在ModuleController v2中,模块与Container是强绑定的关系,如果想实现模块的替换,需要执行更新逻辑,更新模块所在Pod上的模块对应Image地址。

具体的替换方式随模块部署的方式不同而略有区别,例如,直接更新Pod信息会在原地进行模块的替换,Deployment会执行配置的更新策略(例如滚动更新,先创建新版本的Pod,再删除旧版本的Pod),DaemonSet也会执行配置的更新策略,与Deployment不同,DaemonSet是先删除后创建的逻辑,可能会造成流量损失。

模块回滚

由于与原生的Deployment兼容,因此可以采用Deployment的回滚方式实现模块回滚。

查看deployment历史。

kubectl rollout history deployment yourdeploymentname

回滚到指定版本

kubectl rollout undo deployment yourdeploymentname --to-revision=<TARGET_REVISION>

其他运维问题

模块流量 Service 实现方案

可以通过创建原生Service的方式创建模块的Service,仅当基座与ModuleController部署在同一VPC中时才能够正常提供服务。

由于目前基座与ModuleController并不一定部署在同一个VPC下,两者之间通过MQTT消息队列实现交互。基座node会集成基座所在Pod的IP,模块所在Pod会集成基座node的IP,因此,当基座本身与ModuleController不属于同一个VPC的时候,这里模块的IP实际上是无效的,因此无法对外提供服务。

可能的解决方案是在Service上的LB层做转发,将对应Service的流量转发到基座所在K8S的对应IP的基座服务上。后续将根据实际使用情况对这一问题进行评估与优化。

基座和模块不兼容发布

  1. 首先部署一个module的Deployment,其中Container指定为最新版本的模块代码包地址,nodeAffinity指定新版本基座的名称和版本信息。 此时,这一Deployment会创建出对应的Pod,但是由于还没有新版本的基座创建,因此不会被调度。

  2. 更新基座Deployment,发布新版本镜像,此时会触发基座的替换和重启,基座启动时会告知ModuleController V2控制器,会创建对应版本的node。

  3. 对应版本的基座node创建之后,K8S调度器会自动触发调度,将步骤1中创建的模块Pod调度到基座node上,进行新版本的模块安装,从而实现同时发布。



4.2 - 5.2 模块发布运维策略

Koupleless 模块发布运维策略

运维策略

为了实现生产环境的无损变更,模块发布运维基于K8S的原生调度能力提供了安全可靠的变更能力。用户可以通过业务需要使用合适的模块Pod部署方式。

调度策略

打散调度:通过Deployment的原生控制方式实现,可以通过PodAffinity配置实现打散调度。

对等和非对等

可以通过选择不同的部署方式实现对等和非对等部署策略。

对等部署

下面提供两种实现方式:

  1. 可以通过将模块部署成为DaemonSet实现,这样每当一个基座node上线时,DaemonSet控制器就会自动为其创建模块Pod,实现对等部署。

    这里需要注意,DaemonSet的滚动更新是先卸后装,请结合业务实际需求进行选择。

  2. 通过Deployment实现,相比DaemonSet,需要额外增加一个组件用于控制模块副本数与基座数量一致(正在建设中,预计下一个版本发布)。支持先装后卸,不会造成中台模式下基座流量损失。

    注意,Deployment虽然会尽量选择打散部署,但是并不能完全保证打散调度,可能会出现统一模块多次部署到同一个基座上,如果要实现强打散调度,需要在部署模块Deployment中添加Pod反亲和配置,示例如下:

apiVersion: apps/v1  # 指定api版本,此值必须在kubectl api-versions中
kind: Deployment  # 指定创建资源的角色/类型
metadata:  # 资源的元数据/属性
    name: test-module-deployment  # 资源的名字,在同一个namespace中必须唯一
    namespace: default # 部署在哪个namespace中
    labels:  # 设定资源的标签
        module-controller.koupleless.io/component: module-deployment # 资源类型标记, 用于module controller管理
spec: # 资源规范字段
    replicas: 1
    revisionHistoryLimit: 3 # 保留历史版本
    selector: # 选择器
        matchLabels: # 匹配标签
            module.koupleless.io/name: biz1
            module.koupleless.io/version: 0.0.1
    strategy: # 策略
        rollingUpdate: # 滚动更新
            maxSurge: 30% # 最大额外可以存在的副本数,可以为百分比,也可以为整数
            maxUnavailable: 30% # 示在更新过程中能够进入不可用状态的 Pod 的最大值,可以为百分比,也可以为整数
        type: RollingUpdate # 滚动更新策略
    template: # 模版
        metadata: # 资源的元数据/属性
            labels: # 设定资源的标签
                module-controller.koupleless.io/component: module # 必要,声明pod的类型,用于module controller管理
                module.koupleless.io/name: biz1
                module.koupleless.io/version: 0.0.1
        spec: # 资源规范字段
            containers:
            - name: biz1
              image: https://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/test_modules/biz1-0.0.1-ark-biz.jar
              env:
              - name: BIZ_VERSION
                value: 0.0.1
            affinity:
              nodeAffinity:
                requiredDuringSchedulingIgnoredDuringExecution:
                  nodeSelectorTerms: # 基座node选择
                      - matchExpressions:
                        - key: base.koupleless.io/stack
                          operator: In
                          values:
                              - java
                        - key: base.koupleless.io/version
                          operator: In
                          values:
                              - 1.0.0 # 模块可能只能被调度到一些特殊版本的 node 上,如有这种限制,则必须有这个字段。
                        - key: base.koupleless.io/name
                          operator: In
                          values:
                          - base  # 模块可能只能被调度到一些特殊版本的 node 上,如有这种限制,则必须有这个字段。
              podAntiAffinity: # 打散调度核心配置
                  requiredDuringSchedulingIgnoredDuringExecution:
                  - labelSelector:
                      matchLabels:
                          module.koupleless.io/name: biz1 # 与template中的label配置保持一致
                          module.koupleless.io/version: 0.0.1 # 与template中的label配置保持一致
                    topologyKey: topology.kubernetes.io/zone
            tolerations:
              - key: "schedule.koupleless.io/virtual-node" # 确保模块能够调度到基座node上
                operator: "Equal"
                value: "True"
                effect: "NoExecute"

非对等部署:可以通过将模块部署成为Deployment/ReplicaSet实现,此时将根据replica设置进行模块的部署。

分批更新

分批更新策略需要自行实现相关控制逻辑,ModuleController V2能够提供的能力是,当某个基座上先后安装同名不同版本的模块之后,安装时间较早的模块对应Pod会进入BizDeactivate状态,并进入Failed Phase。可结合这一逻辑实现分批更新逻辑。



4.3 - 5.3 健康检查

背景

健康检查的目的是获取应用在生命周期中的状态,包括:运维阶段和运行阶段的状态,以便用户根据该状态做决策。例如:如果发现应用状态为 DOWN,则表示应用存在故障,用户可以重启或替换机器。

在单应用情况下,健康检查比较简单:

  • 运维阶段状态:
    • 如果正在启动,则为 UNKNOWN;
    • 如果启动失败,则为 DOWN;
    • 如果启动成功,则为 UP。
  • 运行阶段状态:
    • 如果应用各健康检查点健康,则为 UP;
    • 如果应用各健康检查点不健康,则为 DOWN。

在多应用场景下,情况会复杂得多。 我们需要考虑多应用在运维阶段运行阶段的状态对整体应用健康状态的影响。在设计健康检查时,我们需要考虑以下2个问题:

  • 模块运维阶段,模块启动状态是否应该影响整体应用健康状态?

    在不同运维场景下,用户的期望是不同的。 koupleless 中模块运维有三种场景:

场景模块对整体应用健康状态的影响
模块热部署提供配置,让用户自行决定模块热部署结果是否影响应用整体健康状态(默认配置为:不影响整体应用原本的健康状态)
静态合并部署模块部署发生在基座启动时,模块启动状态应该直接影响整体应用的健康状态
模块回放模块回放发生在基座启动时,模块启动状态应该直接影响整体应用的健康状态
  • 模块运行阶段,模块运行状态是否应该影响整体应用健康状态?

    模块运行阶段的状态应该直接影响应用整体健康状态。

在此背景下,我们设计了多应用下的健康检查方案。

使用

前置条件

  • koupleless 版本 >= 1.2.1
  • sofa-ark 版本 >= 2.2.9

获取应用整体健康状态

基座的健康状态有 3 类:

状态含义
UP健康,表示已就绪(readiness)
UNKNOWN正在启动中
DOWN不健康(可能是启动失败,也可能是运行状态不健康)

由于 Koupleless 支持热部署模块,因此用户在获取应用整体健康状态时,可能希望模块部署是否成功影响整体应用健康状态,或不影响。

模块启动是否成功不影响整体应用健康状态(默认)

  • 特点:对于健康的基座,如果模块安装失败,不会影响整体应用健康状态。
  • 使用:和普通 Spring Boot 应用的配置一致,在基座的 application.properties 中配置:
# 或者不配置 management.endpoints.web.exposure.include
management.endpoints.web.exposure.include=health
# 如果需要展示所有信息,则配置以下内容
management.endpoint.health.show-components=always
management.endpoint.health.show-details=always
  • 访问:{baseIp:port}/actuator/health
  • 结果:
{
    // 应用整体健康状态
    "status": "UP",
    "components": {
        // 模块聚合健康状态
        "arkBizAggregate": {
            "status": "UP",
            "details": {
                "biz1:0.0.1-SNAPSHOT": {
                    "status": "UP",
                    // 可以看到模块中所有生效的 HealthIndicator 的健康状态
                    "details": {
                        "diskSpace": {
                          "status": "UP",
                          "details": {
                            "total": 494384795648,
                            "free": 272435396608,
                            "threshold": 10485760,
                            "exists": true
                            }
                        },
                        "pingHe": {
                          "status": "UP",
                          "details": {}
                        }
                    }
                }
            }
        },
        // 启动健康状态
        "masterBizStartUp": {
            "status": "UP",
            // 包括每一个模块的启动状态
            "details": {
                "base:1.0.0": {
                    "status": "UP"
                },
                "biz1:0.0.1-SNAPSHOT": {
                    "status": "UP"
                },
                "biz2:0.0.1-SNAPSHOT": {
                    "status": "DOWN"
                }
            }
        }
    }
}

不同场景下的整体健康状态

场景1:无模块基座启动

状态含义
UP基座健康
UNKNOWN基座正在启动中
DOWN基座不健康

场景2:基座启动时,静态合并部署

状态含义
UP基座和模块都健康
UNKNOWN基座正在启动中/模块正在启动中
DOWN基座启动失败/基座不健康/模块启动失败/模块不健康

场景3:基座启动后,热部署

提供配置,让用户自行决定模块热部署结果是否影响应用整体健康状态(默认配置为:不影响整体应用原本的健康状态)

默认配置:热部署场景下,模块是否安装成功不影响应用整体健康状态,如下:

状态含义
UP基座和模块都健康
UNKNOWN基座正在启动中
DOWN基座启动失败/基座不健康/模块不健康

场景4:基座运行中

状态含义
UP基座和模块都健康
UNKNOWN-
DOWN基座不健康或模块不健康

场景5:基座启动后,模块回放

模块回放是指在基座启动后,自动拉取模块基线,并安装模块。

目前未支持模块回放。

模块启动是否成功影响整体应用健康状态

  • 特点:对于健康的基座,如果模块安装失败,整体应用健康状态也会为失败。
  • 使用:在上述配置之外,需要配置 koupleless.healthcheck.base.readiness.withAllBizReadiness=true,即在基座的 application.properties 中配置:
# 或者不配置 management.endpoints.web.exposure.include
management.endpoints.web.exposure.include=health
# 如果需要展示所有信息,则配置以下内容
management.endpoint.health.show-components=always
management.endpoint.health.show-details=always
# 不忽略模块启动状态
koupleless.healthcheck.base.readiness.withAllBizReadiness=true
  • 访问:{baseIp:port}/actuator/health
  • 结果:
{
    // 应用整体健康状态
    "status": "UP",
    "components": {
        // 模块聚合健康状态
        "arkBizAggregate": {
            "status": "UP",
            "details": {
                "biz1:0.0.1-SNAPSHOT": {
                    "status": "UP",
                    // 可以看到模块中所有生效的 HealthIndicator 的健康状态
                    "details": {
                        "diskSpace": {
                          "status": "UP",
                          "details": {
                            "total": 494384795648,
                            "free": 272435396608,
                            "threshold": 10485760,
                            "exists": true
                            }
                        },
                        "pingHe": {
                          "status": "UP",
                          "details": {}
                        }
                    }
                }
            }
        },
        // 启动健康状态
        "masterBizStartUp": {
            "status": "UP",
            // 包括每一个模块的启动状态
            "details": {
                "base:1.0.0": {
                    "status": "UP"
                },
                "biz1:0.0.1-SNAPSHOT": {
                    "status": "UP"
                }
            }
        }
    }
}

不同场景下的整体健康状态

场景1:无模块基座启动

状态含义
UP基座健康
UNKNOWN基座正在启动中
DOWN基座不健康

场景2:基座启动时,静态合并部署

状态含义
UP基座和模块都健康
UNKNOWN基座正在启动中/模块正在启动中
DOWN基座启动失败/基座不健康/模块启动失败/模块不健康

场景3:基座启动后,热部署

提供配置,让用户自行决定模块热部署结果是否影响应用整体健康状态(默认配置为:不影响整体应用原本的健康状态)

当设置为 koupleless.healthcheck.base.readiness.withAllBizReadiness=true

状态含义
UP基座和模块都健康
UNKNOWN基座正在启动中/模块正在启动中
DOWN基座启动失败/模块启动失败/基座不健康/模块不健康

场景4:基座运行中

状态含义
UP基座和模块都健康
UNKNOWN-
DOWN基座不健康或模块不健康

场景5:基座启动后,模块回放

模块回放是指在基座启动后,自动拉取模块基线,并安装模块。

目前未支持模块回放。

获取单个模块的健康状态

  • 使用:和普通 springboot 的健康检查配置一致,开启 health 节点,即:在模块的 application.properties 中配置:
# 或者不配置 management.endpoints.web.exposure.include
management.endpoints.web.exposure.include=health
  • 访问:{baseIp:port}/{bizWebContextPath}/actuator/info
  • 结果:
{
    "status": "UP",
    "components": {
        "diskSpace": {
            "status": "UP",
            "details": {
                "total": 494384795648,
                "free": 270828220416,
                "threshold": 10485760,
                "exists": true
            }
        },
        "ping": {
            "status": "UP"
        }
    }
}

获取基座、模块和插件信息

  • 使用:和普通 springboot 的健康检查配置一致,开启 info 节点,即:在基座的 application.properties 中配置:
# 注意:如果用户自行配置了 management.endpoints.web.exposure.include,则需要将 health 节点配置上,否则无法访问 health 节点
management.endpoints.web.exposure.include=health,info
  • 访问:{baseIp:port}/actuator/info
  • 结果:
{
    "arkBizInfo": [
      {
        "bizName": "biz1",
        "bizVersion": "0.0.1-SNAPSHOT",
        "bizState": "ACTIVATED",
        "webContextPath": "biz1"
      },
      {
        "bizName": "base",
        "bizVersion": "1.0.0",
        "bizState": "ACTIVATED",
        "webContextPath": "/"
      }
    ],
    "arkPluginInfo": [
        {
            "pluginName": "koupleless-adapter-log4j2",
            "groupId": "com.alipay.sofa.koupleless",
            "artifactId": "koupleless-adapter-log4j2",
            "pluginVersion": "1.0.1-SNAPSHOT",
            "pluginUrl": "file:/Users/lipeng/.m2/repository/com/alipay/sofa/koupleless/koupleless-adapter-log4j2/1.0.1-SNAPSHOT/koupleless-adapter-log4j2-1.0.1-SNAPSHOT.jar!/",
            "pluginActivator": "com.alipay.sofa.koupleless.adapter.Log4j2AdapterActivator"
        },
        {
            "pluginName": "web-ark-plugin",
            "groupId": "com.alipay.sofa",
            "artifactId": "web-ark-plugin",
            "pluginVersion": "2.2.9-SNAPSHOT",
            "pluginUrl": "file:/Users/lipeng/.m2/repository/com/alipay/sofa/web-ark-plugin/2.2.9-SNAPSHOT/web-ark-plugin-2.2.9-SNAPSHOT.jar!/",
            "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator"
        },
        {
            "pluginName": "koupleless-base-plugin",
            "groupId": "com.alipay.sofa.koupleless",
            "artifactId": "koupleless-base-plugin",
            "pluginVersion": "1.0.1-SNAPSHOT",
            "pluginUrl": "file:/Users/lipeng/.m2/repository/com/alipay/sofa/koupleless/koupleless-base-plugin/1.0.1-SNAPSHOT/koupleless-base-plugin-1.0.1-SNAPSHOT.jar!/",
            "pluginActivator": "com.alipay.sofa.koupleless.plugin.ServerlessRuntimeActivator"
        }
    ]
}

4.4 - 5.4 Module Controller 部署

Koupleless Module Controller V2的部署方式

注意:当前 ModuleController v2 仅在 K8S 1.24 版本测试过,没有在其它版本测试,ModuleController V2依赖了部分K8S特性,K8S的版本不能低于V1.10。

资源文件位置

  1. Role 定义
  2. RBAC 定义
  3. ServiceAccount 定义
  4. ModuleControllerV2 部署定义

部署方式

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

如使用 Module Controller 分片集群能力,指需要将上述部署定义修改为 Deployment 版本,将 Pod Spec 中内容放到 Deployment template 中。

如需在分片集群中使用负载均衡能力,需要在 Module Controller ENV 配置中将 IS_CLUSTER 参数置为 true。

可配置参数解析

环境变量配置

以下是一些可配置的环境变量及其解释:

  • ENABLE_MQTT_TUNNEL

  • 含义: MQTT 运维管道启用标志。设置为 true 表示启用 MQTT 运维管道,如启用,请配置以下相关环境变量。

  • MQTT_BROKER

  • 含义: MQTT 代理的 URL。

  • MQTT_PORT

  • 含义: MQTT 端口号。

  • MQTT_USERNAME

  • 含义: MQTT 用户名。

  • MQTT_PASSWORD

  • 含义: MQTT 密码。

  • MQTT_CLIENT_PREFIX

  • 含义: MQTT 客户端前缀。

  • ENABLE_HTTP_TUNNEL

  • 含义: HTTP 运维管道启用标志。设置为 true 表示启用 HTTP 运维管道,可选择配置以下环境变量。

  • HTTP_TUNNEL_LISTEN_PORT

  • 含义: 模块控制器 HTTP 运维管道监听端口,默认使用 7777。

  • REPORT_HOOKS

  • 含义: 错误上报链接,支持多个链接,用;进行分割,目前仅支持钉钉机器人 webhook。

  • ENV

  • 含义: Module Controller环境,将设置为VNode标签,用于运维环境隔离。

  • IS_CLUSTER

  • 含义: 集群标志,若为 true,将使用集群配置启动 Virtual kubelet。

  • WORKLOAD_MAX_LEVEL

  • 含义: 集群配置,表示 Virtual kubelet 中工作负载计算的最大工作负载级别,默认值为 3,详细计算规则请参考 Module Controller 架构设计。

  • ENABLE_MODULE_DEPLOYMENT_CONTROLLER

  • 含义: Module Deployment Controller 启用标志,若为 true,将启动部署控制器以修改模块部署的副本和基线。

  • VNODE_WORKER_NUM

  • 含义: VNode 并发模块处理线程数,设为 1 表示单线程。

  • CLIENT_ID

  • 含义: 可选配置,Module Controller 实例ID,需保证同环境中全局唯一,默认情况下会生成随机UUID

文档参考

具体的结构和实现介绍请参考文档



4.5 - 5.5 模块信息查看

Koupleless 模块信息查看

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

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

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

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

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

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

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

4.6 - 5.6 错误码

本文主要介绍 Arklet, ModuleController, KouplelessBoard 的错误码。

ErrorCode 规则

两级错误码,支持动态组合,采用大驼峰方式,不同级别错误码之间只能用 “.” 分隔。
如 Arklet.InstallModuleFailed
一级:错误来源
二级:错误类型

Suggestion

简要说明解决方案,供上游操作参考。

Arklet 错误码

一级 错误来源

编码含义
User客户导致的错误
ArkletArklet 自身异常
ModuleController具体上游组件导致的异常
OtherUpstream未知上游导致的异常

二级 错误类型

业务类型错误来源错误类型含义解决方案
通用ArkletUnknownError未知错误(默认)请排查

ModuleControllerInvalidParameter参数校验失败请检查参数
ModuleControllerInvalidRequest操作类型非法请检查请求
OtherUpstreamDecodeURLFailedurl 解析失败请检查 url 是否合法
查询相关ArkletNoMatchedBiz模块查询失败,没有目标 biz 存在-
ArkletInvalidBizName模块查询失败,查询参数 bizName 不能为空请添加查询参数 bizName
安装相关ArkletInstallationRequirementNotMet模块安装条件不满足请检查模块安装的必要参数
ArkletPullBizError拉包失败请重试
ArkletPullBizTimeOut拉包超时请重试
UserDiskFull拉包时,磁盘已满请替换基座
UserMachineMalfunction机器故障请重启基座
UserMetaspaceFullMetaspace超过阈值请重启基座
ArkletInstallBizExecuting模块安装时,当前模块正在安装请重试

ArkletInstallBizTimedOut模块安装时,卸载老模块失败请排查
ArkletInstallBizFailed模块安装时,新模块安装失败请排查
UserInstallBizUserError模块安装失败,业务异常请检查业务代码
卸载相关ArkletUninstallBizFailed卸载失败,当前 biz 还存在在 容器中请排查
ArkletUnInstallationRequirementNotMet模块卸载条件不满足当前模块存在多版本,且卸载的版本是激活状态的,不允许卸载

ModuleController 错误码

一级 错误来源

编码含义
User客户导致的错误
ModuleControllerModuleController 自身异常
KouplelessBoard具体上游组件导致的异常
Arklet具体下游组件导致的异常
OtherUpstream未知上游导致的异常
OtherDownstream未知下游导致的异常

二级 错误类型

业务类型错误来源错误类型含义解决方案
通用ModuleControllerUnknownError未知错误(默认)请排查

OtherUpstreamInvalidParameter参数校验失败请检查参数
ArkletArkletServiceNotFound找不到基座服务请确保基座有Koupleless依赖
ArkletNetworkError网络调用异常请重试
OtherUpstreamSecretAKError签名异常请确认有操作权限
ModuleControllerDBAccessError读写数据库失败请重试
OtherUpstreamDecodeURLFailedurl 解析失败请检查 url 是否合法
ModuleControllerRetryTimesExceeded重试多次失败请排查
ModuleControllerProcessNodeMissed缺少可用的工作节点请稍后重试
ModuleControllerServiceMissed服务缺失请检查ModuleController版本是否含有该模版类型
ModuleControllerResourceConstraned资源受限(线程池、队列等满)请稍后重试
安装相关ArkletInstallModuleTimedOut模块安装超时请重试
Arklet / UserInstallModuleFailed模块安装失败请检查失败原因
ArkletInstallModuleExecuting模块安装中相同模块在安装,请稍后重试
UserDiskFull磁盘已满请替换
卸载相关OtherUpstreamEmptyIPListip 列表为空请输入要卸载的ip

ArkletUninstallBizTimedOut模块卸载超时请重试

ArkletUninstallBizFailed模块卸载失败请排查
基座相关ModuleControllerBaseInstanceNotFound基座实例不存在请确保基座实例存在

KubeAPIServerGetBaseInstanceFailed查询不到基座信息请确保基座实例存在
ModuleControllerBaseInstanceInOperation基座正在运维中请稍后重试
ModuleControllerBaseInstanceNotReady暂未读到基座数据或基座不可用请确保基座可用
ModuleControllerBaseInstanceHasBeenReplaced基座已被替换后续会新增基座实例,请等候
ModuleControllerInsufficientHealthyBaseInstance健康基座不足请扩容
扩缩容ModuleControllerRescaleRequirementNotMet扩缩容条件不满足请检查扩容机器是否足够/请检查缩容比例

⚠️注意:基座运行在不同基座实例上,如:pod。因此 BaseInstanceInOperation, BaseInstanceNotReady, BaseInstanceHasBeenReplaced, InsufficientHealthyBaseInstance 错误码可能指包括基座应用状态或基座实例的状态。

DashBoard 错误码

一级 错误来源

编码含义
KouplelessBoardKouplelessBoard 自身异常
ModuleController具体下游组件导致的异常
OtherUpstream未知上游导致的异常
OtherDownstream未知下游导致的异常

二级 错误类型

业务类型错误来源错误类型含义解决方案
通用KouplelessBoardUnknownError未知错误(默认)

OtherUpstreamInvalidParameter参数校验失败请检查参数
工单KouplelessBoardOperationPlanNotFound工单不存在请排查
KouplelessBoardOperationPlanMutualExclusion工单互斥请重试
内部错误KouplelessBoardInternalError系统内部错误请排查
KouplelessBoardThreadPoolError线程池调用异常请排查
运维ModuleControllerBaseInstanceOperationFailed运维失败请排查
ModuleControllerBaseInstanceUnderOperation运维中请重试
ModuleControllerBaseInstanceOperationTimeOut运维超时请重试
ModuleControllerOverFiftyPercentBaseInstancesUnavaliable超过50% 机器流量不可达请检查基座实例
KouplelessBoardBaselineInconsistency一致性校验失败(基线不一致)请排查
外部服务调用错误OtherDownstreamExternalError外部服务调用错误请排查
KouplelessBoardNetworkError外部服务调用超时请重试