这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

产品文档

1 - 产品介绍

1.1 - 简介与适用场景

简介

SOFAServerless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以比较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。
随着各行各业的信息化数字化转型,企业面临越来越多的研发效率、协作效率、资源成本和服务治理痛点,接下来带领大家逐一体验这些痛点,以及它们在 SOFAServerless 中是如何被解决的。

适用场景

痛点 1:应用构建发布慢或者 SDK 升级繁琐

传统应用镜像化构建一般要 3 - 5 分钟,单机代码发布到启动完成也要 3 - 5 分钟,开发者每次验证代码修改或上线代码修改,都需要经历数次 6 - 10 分钟的构建发布等待,严重影响开发效率。此外,每次 SDK 升级(比如中间件框架、rpc、logging、json 等),都需要修改所有应用代码并重新构建发布,对开发者也造成了不必要的打扰。
通过使用 SOFAServerless 通用基座与配套工具,您可以低成本的将应用切分为 “基座” 与 “模块”,其中基座沉淀了公司或者某个业务部门的公共 SDK,基座升级可以由专人负责,对业务开发者无感,业务开发者只需要编写模块。在我们目前支持的 Java 技术栈中,模块就是一个 SpringBoot 应用代码包(FatJar),只不过 SpringBoot 框架本身和其他的企业公共依赖在运行时会让基座提前加载预热,模块每次发布都会找一台预热 SpringBoot 的基座进行热部署,整个过程类似 AppEngine,能够帮用户实现应用 10 秒级构建发布公共 SDK 升级无感

痛点 2:长尾应用资源成本高

在企业中,80% 的应用只服务了不到 20% 的流量,同时伴随着业务的变化,企业存在大量的长尾应用,这些长尾应用 CPU 使用率长期不到 10%,造成了极大的资源浪费
通过使用 SOFAServerless 合并部署与配套工具,您可以低成本的实现多个应用的合并部署,从而解决企业应用过度拆分和低流量业务带来的资源浪费节约成本

这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。

痛点 3:企业研发协作效率低

在企业中,一些应用需要多人开发协作。在传统研发模式下,每一个人的代码变更都需要发布整个应用,这就导致应用需要以赶火车式的方式进行研发迭代,大家需要统一一个时间窗口做迭代开发,统一的时间点做发布上线,因此存在大量的需求上线相互等待、环境机器抢占、迭代冲突等情况。
通过使用 SOFAServerless,您可以方便的将应用拆分为一个基座与多个功能模块,一个功能模块就是一组代码文件。不同的功能模块可以同时进行迭代开发和发布运维,模块间互不感知互不影响,这样就消除了传统应用迭代赶火车式的相互等待,每个模块拥有自己的独立迭代,需求交付效率因此得到了极大提升。如果您对模块额外启用了热部署方式(也可以每次发布模块重启整个基座),那么模块的单次构建+发布也会从普通应用的 6 - 10 分钟减少到十秒级。

痛点 4:难以沉淀业务资产提高中台效率

在一些中大型企业中,会沉淀各种业务中台应用。中台一般封装了业务的公共 API 实现,和 SPI 定义。其中 SPI 定义允许中台上的插件去实现各自的业务逻辑,流量进入中台应用后,会调用对应的 SPI 实现组件去完成相应的业务逻辑。中台应用内的组件,业务逻辑一般不复杂,如果拆出去部署为独立应用会带来高昂的资源成本和运维成本,而且构建发布速度很慢,严重加剧研发负担影响研发效率。
通过使用 SOFAServerless,您可以方便的将中台应用拆分一个基座和多个功能模块。基座可以沉淀比较厚的业务依赖、公共逻辑、API 实现、SPI 定义等(即所谓的业务资产),提供给上面的模块使用。模块使用基座的能力可以是对象之间或 Bean 之间的直接调用,代码几乎不用改造。而且多个模块间可以同时进行迭代开发和发布运维,互不感知互不影响协作交付效率得到了极大提升。此外对于比较简单的模块还可以开启热部署,单次构建+发布也会从普通应用的 6 - 10 分钟减少到 30 秒内。

痛点 5:微服务演进成本高

企业里不同业务有不同的发展阶段,因此应用也拥有自己的生命周期。

初创期:一个初创的应用一般会先采用单体架构

增长期:随着业务增长,应用开发者也随之增加。此时您可能不确定业务的未来前景,也不希望过早把业务拆分成多个应用以避免不必要的维护、治理和资源成本,那么您可以用 SOFAServerless 低成本地将应用拆分为一个基座和多个功能模块,不同功能模块之间可以并行研发运维独立迭代,从而提高应用在此阶段的研发协作和需求交付效率。

成熟期:随着业务进一步增长,您可以使用 SOFAServerless 低成本地将部分或全部功能模块拆分成独立应用去研发运维。

长尾期:部分业务在经历增长期或者成熟期后,也可能慢慢步入到低活状态或者长尾状态,此时您可以用 SOFAServerless 低成本地将这些应用一键改回模块合并部署到一起实现降本增效

可以看到 SOFAServerless 支持企业应用低成本地在初创期、增长期、成熟期、长尾期之间平滑过渡甚至来回切换,从而轻松让应用架构与业务发展保持同步。
应用生命周期演进



1.2 - 行业背景

微服务的问题

应用架构从单体应用发展到微服务,结合软件工程从瀑布模式到当前的 DevOps 模式的发展,解决了可扩展、分布式、分工协作等问题,为企业提供较好的敏捷性与执行效率,带来了明显的价值。但该模式发展至今,虽然解决了一些问题,也有微服务的一些问题慢慢暴露出来,在当前已经得到持续关注:

基础设施复杂

认知负载高

image.png
当前业务要完成一个需求,背后实际上有非常多的依赖、组件和平台在提供各种各样的能力,只要这些业务以下的某一个组件出现异常被业务感知到,都会对业务研发人员带来较大认知负担和对应恢复的时间成本。
image.png
异常种类繁多

运维负担重

业务包含的各个依赖也会不断迭代升级,例如框架、中间件、各种 sdk 等,在遇到

  1. 重要功能版本发布
  2. 修复紧急 bug
  3. 遇到重大安全漏洞

等情况时,这些依赖的新版本就需要业务尽可能快的完成升级,这造成了两方面的问题:

对于业务研发人员

这些依赖的升级如果只是一次两次那么就不算是问题,但是一个业务应用背后依赖的框架、中间件与各类 sdk 是很多的,每一个依赖发布这些升级都需要业务同学来操作,这么多个依赖的话长期上就会对业务研发同学来说是不小的运维负担。另外这里也需要注意到业务公共层对业务开发者来说也是不小的负担。

对于基础设施人员

类似的对于各个依赖的开发人员自身,每发布一个这样的新版本,需要尽可能快的让使用的业务应用完成升级。但是业务研发人员更关注业务需求交付,想要推动业务研发人员快速完成升级是不太现实的,特别是在研发人员较多的企业里。

启动慢

每个业务应用启动过程都需要涉及较多过程,造成一个功能验证需要花费较长等待时间。
image.png

发布效率低

由于上面提到的启动慢、异常多的问题,在发布上线过程中需要较长时间,出现异常导致卡单需要恢复处理。发布过程中除了平台异常外,机器异常发生的概率会随着机器数量的增多而增多,假如一台机器正常完成发布(不发生异常)的概率是 99.9%,也就是一次性成功率为 99.9%,那么100台则是 90%,1000台则降低到了只有 36.7%,所以对于机器较多的应用发布上线会经常遇到卡单的问题,这些都需要研发人员介入处理,导致效率低。

协作与资源成本高

单体应用/大应用过大

image.png

多人协作阻塞

业务不断发展,应用会不断变大,这主要体现在开发人员不断增多,出现多人协作阻塞问题。

变更影响面大,风险高

业务不断发展,线上流量不断增大,机器数量也不断增多,但当前一个变更可能影响全部代码和机器流量,变更影响面大风险高。

小应用过多

image.png
在微服务发展过程中,随着时间的推移,例如部分应用拆分过多、某些业务萎缩、组织架构调整等,都会出现线上小应用或者长尾应用不断积累,数量越来越多,像蚂蚁过去3年应用数量增长了 3倍。
image.png

资源成本高

这些应用每个机房都需要几台机器,但其实流量也不大,cpu 使用率很低,造成资源浪费。

长期维护成本

这些应用同样需要人员来维度,例如升级 SDK,修复安全漏洞等,长期维护成本高。

问题必然性

微服务系统是个生态,在一个公司内发展演进几年后,参考28定律,少数的大应用占有大量的流量,不可避免的会出现大应用过大和小应用过多的问题。
然而大应用多大算大,小应用多少算多,这没有定义的标准,所以这类问题造成的研发人员的痛点是隐匿的,没有痛到一定程度是较难引起公司管理层面的关注和行动。

如何合理拆分微服务

微服务如何合理拆分始终是个老大难的问题,合理拆分始终没有清晰的标准,这也是为何会存在上述的大应用过大、小应用过多问题的原因。而这些问题背后的根因是业务与组织灵活,与微服务拆分的成本高,两者的敏捷度不一致。

微服务的拆分与业务和组织发展敏捷度不一致

image.png
业务发展灵活,组织架构也在不断调整,而微服务拆分需要机器与长期维护的成本,两者的敏捷度不一致,导致容易出现未拆或过度拆分问题,从而出现大应用过大和小应用过多问题。这类问题不从根本上解决,会导致微服务应用治理过一波之后还会再次出现问题,导致研发同学始终处于低效的痛苦与治理的痛苦循环中。

不同体量企业面对的问题

image.png

行业尝试的解法

当前行业里也有很多不错的思路和项目在尝试解决这些问题,例如服务网格、应用运行时、平台工程,Spring Modulith、Google ServiceWeaver,有一定的效果,但也存在一定的局限性:

  1. 从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分
  2. 只解决其中部分问题
  3. 存量应用接入改造成本高

SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。



1.3 - 架构介绍

1.3.1 - 架构原理

模块化应用架构

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

模块的定义

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

  1. SpringBoot 打包生成的 jar 包
  2. 一个模块: 一个 SpringContext + 一个 ClassLoader
  3. 热部署(升级的时候不需要启动进程)

模块的隔离与共享

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

  1. Ark Container 提供多 ClassLoader 运行环境
  2. Arklet 来管理模块生命周期
  3. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  4. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  5. SpringContext Manager 提供 Bean 与服务发现调用机制
  6. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader

image.png

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

隔离方面

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

共享方面

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

模块间通信

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

模块的可演进

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

  1. 模块不占资源所以拆分没有资源成本
  2. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  3. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  4. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

模块化架构的优势

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

平台架构

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

image.png
组件视图

在整个平台里,需要四个组件:

  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  3. 控制面组件 ModuleController
    1. ModuleDeployment 提供模块发布与运维能力
    2. ModuleScheduler 提供模块调度能力
    3. ModuleScaler 提供模块伸缩能力

1.3.2 - 基座与模块间类委托加载原理介绍

多模块间类委托加载

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

当前类委托加载机制

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. 模块可以扫描到基座里的所有类:
    • 优势:模块可以引入较少依赖
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
  3. 模块不能扫描到基座里的任何资源:
    • 优势:不会与基座重复初始化相同的 Bean
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
  4. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。

使用时需要注意事项

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

类委托的最佳实践

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

强制委托加载

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

使用方法

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

优点

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

缺点

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

自定义委托加载

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. 基座里预置一些公共依赖(可选)。
  3. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座:
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
            <plugin>
                <groupId>com.alipay.sofa</groupId>
                <artifactId>sofa-ark-maven-plugin</artifactId>
                <configuration> 
                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
                    <declaredMode>true</declaredMode>
                </configuration>
            </plugin>

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

  1. 只有模块声明过的依赖才可以委托给基座加载。

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

优点

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

缺点

对模块瘦身的依赖较强。

对比与总结

依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低

结论:推荐自定义委托加载方式

  1. 模块自定义委托加载 + 模块瘦身。
  2. 模块开启 declaredMode。
  3. 基座预置依赖。

declaredMode 开启方式

开启条件

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

# 如果是 SOFABoot,则:
# 配置健康检查跳过 JVM 服务检查
com.alipay.sofa.boot.skip-jvm-reference-health-check=true
# 忽略未解析的占位符
com.alipay.sofa.ignore.unresolvable.placeholders=true

开启方式

模块打包插件里增加如下配置:
image.png

开启后的副作用

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。


2 - 快速开始

实验 1:一键实现多应用合并部署

合并部署是指:选定一个应用作为底座,然后将多个其它应用合并部署到这个底座之上,从而实现长尾应用的极致资源降本。典型业务场景为应用的低成本交付 以及 微服务过度拆分一键重新合并。

  1. 选定一个应用作为底座(SOFAServerless 术语叫基座),将普通应用一键升级为基座
  2. 选定一个应用作为上层应用(SOFAServerless 术语叫模块),将其一键转为模块应用并完成合并部署
    您也可以直接使用 官方 Demo 和文档 在本地完成实验。

小贴士:无论基座还是模块,接入 SOFAServerless 后,同一套代码分支既能像原来一样独立启动,又能做到合并部署。



实验 2:一键体验应用秒级热部署

步骤 1:本地软件安装

下载安装 go(建议 1.20 或以上)、dockerminikubekubectl

步骤 2:一键启动 SOFAServerless

使用 git 拉取 GitHub sofa-severless 项目:https://github.com/sofastack/sofa-serverless
module-controller 目录下执行 make dev 命令一键部署环境,会自动执行 minikube service 命令弹出网页,由于此时您还没有发布模块,所以网页不会有任何内容显示。

步骤 3:秒级发布模块

执行以下命令:

kubectl apply -f config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml

即可秒级发布上线模块应用。请等待本地 Module CR 资源 Status 字段值变更为 Available**(约 1 秒,表示模块发布完毕)**,再刷新步骤 2 自动打开的网页,即可看到一个简单的卖书页面,这个卖书逻辑就是在模块里实现的:
image.png

步骤 4:清理本地环境

您可以使用 make undev 删除所有本地资源,清理本地环境。



欢迎大家学习 SOFAServerless 视频教程

点击此处查看 SOFAServerless 平台与研发框架视频培训教程。

3 - 视频教程

SOFAServerless 模块本地开发与上线视频教程

小贴士: 仅需两分钟时间



该视频的详细文字版教程请点击此处查看。

SOFAServerless 平台和研发框架完整视频教程

步骤 1:点击此处注册开源学堂账号。

步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

4 - 用户手册

4.1 - 基座接入

4.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>

启动验证

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



4.2 - 模块接入

4.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

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

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

4.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 - 模块研发

4.4.1 - 编码规范

基础规范

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

知识点

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



4.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.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.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.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.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.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.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.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.4.10 - SOFAArk 关键用户文档

模块生命周期

Ark 事件机制

Ark 自身日志



4.4.11 -

4.5 - 模块运维

4.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 一样能实现模块分组下线。



4.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,即可实现模块的分组回滚发布。



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

步骤 1

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

步骤 2

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

步骤 3

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



4.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 一样能实现模块替换。



4.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 发布。



4.5.6 - 独立使用 Arklet

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



4.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

4.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: {}

4.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 部署。



5 - 参与社区

5.1 - 开放包容理念

核心价值观

SOFAServerless 社区的核心价值观是 “开放” 和 “包容”。社区里所有的用户、开发者完全平等,体现在如下几个方面:

  1. 社区参考了 Apache 开源项目的运作方式,对社区做出任意贡献的同学,尤其是非代码贡献的同学(文档、官网、Issue 回复、运营布道、发展建议等),都是我们的 Contributor,都有机会成为社区的 Committer 甚至是 PMC(Project Management Committee)成员。

  2. 社区所有的 OKR、RoadMap、讨论、会议、技术方案等都是完全开放的,所有人都可以看见,并且都可以参与其中,社区会认证倾听、考虑大家的所有建议和意见,一旦采纳就会确保执行落地。希望大家带着无所顾虑、求同尊异的心态参与 SOFAServerless 社区。

  3. 社区不限地域国籍,所有源代码必须是英文注释确保大家都能理解,官网也是中英文双语。所有微信群、钉钉群、GitHub Issues 讨论都可以是中英双语。但由于当前我们主要聚焦在国内用户,因此大部分文档暂时只有中文版,未来会提供英文版。

2023 年 OKR

O1 打造社区健康、有行业影响力的 Serverless 开源产品

KR1 新增 10 个 Contributors,年底 OpenRank 指数 > 15(当前 5)、活跃度 > 50(当前 44)

KR1.1 完成 5 次布道和 5 次文章分享,触达 200 家企业,深度交流 30+ 企业。
KR1.2 形成完善的社区共建机制(包括 Issue 管理、文档、问题响应、培养与晋升机制),发布 2+ 培训课程与产品手册,共建开发者可在一周内上手,开发总吞吐率达到 20+ Issues/周。

KR2 新增 5 家企业在生产环境使用或完成试点接入(当前新增 1),3 家企业参与社区

KR2.1 产出初步行业分析报告,帮助定位适用不同场景的重点企业对象。
KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。

O2 打造技术先进、效果显著的降本增效解决方案

KR1 落地模块化技术实现机器减少 30%、部署验证耗时降低至 30 秒、需求交付效率提升 50%

KR1.1 搭建 1 分钟快速试用平台,完善的文档、官网与配套支持,用户可在 10 分钟完成一个模块拆分。
KR1.2 完成 20 种中间件和三方包治理,同时形成多应用与热卸载评测和自动检测标准。
KR1.3 模块具备热部署启动耗时降低至 10 秒级,多模块具备合并部署资源减少 30%,同时让用户需求交付效率提升 50%。
KR1.4 落地开源版 Arklet,支持 SOFABoot 和 SpringBoot。提供运维管道、指标采集、模块生命周期管理、多模块运行环境、Bean 与服务发现及调用能力。
KR1.5 落地研发工具 ArkCtl,具备快速开发验证、灵活部署(合并与独立部署)、模块低成本拆分改造能力。

KR2 运维调度 1.0 版本上线。全链路高频端到端测试用例成功率 99.9%,自身端到端耗时 P90 < 500ms

KR2.1 上线基于 K8S Operator 的开源版运维调度能力,至少具备发布、回滚、下线、扩缩容、替换、副本保持、2+ 调度策略、模块流控、部署策略、对等和非对等运维能力。
KR2.2 建设开源版 CI 和 25+ 高频端到端测试用例,不断打磨并推动端到端 P90 耗时 < 500ms、所有预演成功率> 99.9%、单测覆盖率达到行 > 80% 分支 > 60%(通过率 100%)。

KR3 开源版自动伸缩初步上线,模块具备人工画像和分时伸缩能力

RoadMap

  • 2023.08 完成 SOFABoot 完整的部署功能验证,产出兼容性 Benchmark 基线。
  • 2023.09 发布基础运维和调度系统 ModuleController 0.5 版本。
  • 2023.09 发布研发运维工具 Arkctl 与 Arklet 0.5 版本。
  • 2023.09 官网和完整用户手册上线。
  • 2023.10 新增 2+ 公司使用。
  • 2023.11 支持 SpringBoot 完整能力和 5+ 社区常用中间件。
  • 2023.11 SOFAServerless 1.0 版本上线(ModuleController、Arkctl、Arklet、SpringBoot 兼容)。
  • 2023.12 SOFAServerless 1.1 版本上线(包括基础自动伸缩、模块基础拆分工具、20+ 中间件与三方包兼容)。
  • 2023.12 新增 5+ 家公司真实使用,10+ Contributors 参与。


5.2 - 交流渠道

SOFAServerless 提供如下沟通交流渠道,欢迎加入我们一起分享、一起使用、一起收获:

SOFAServerless 社区交流与协作钉钉群:24970018417

如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户、或者有兴趣成为社区 Contributor,都可以加入该钉钉群和我们随时随地一起交流讨论、一起贡献代码。

SOFAServerless 用户微信群


如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户,都可以加入该微信群随时随地一起交流讨论。

社区双周会

每两周周二晚 19:30 - 20:30 会举办社区会议,下次社区双周会时间:2023 年 11 月 28 日 19:30 ~ 20:30,欢迎大家积极参与旁听或讨论。社区钉钉会议入会方式:
入会链接:https://meeting.dingtalk.com/j/blp36k9mTbc
钉钉会议号:90957500367
电话呼入:+867936169179 (中国大陆)、+867388953916 (中国大陆)
具体会议时间也可关注社区钉钉协作群(群号:24970018417)


每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。



5.3 - 贡献社区

5.3.1 - 本地开发测试

SOFAArk 和 Arklet

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

ModuleController

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
编译构建请在 module-controller 目录下执行:

go mod download   # if compile module-controller first time
go build -a -o manager cmd/main.go  

单元测试执行请在 module-controller 目录下执行:

make test

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

Arkctl

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


5.3.2 - 完成第一次 PR 提交

认领或提交 Issue

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

获取源码

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

拉分支

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

  • 下载代码到本地,这一步您可以选择 git/https 方式:
git clone https://github.com/您的账号名/sofa-serverless.git
git clone https://github.com/您的账号名/sofa-ark.git
  • 拉分支准备修改代码:
git branch add_xxx_feature


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

  git branch -a

如果您想切换回主干,执行下面命令:

  git checkout -b master

如果您想切换回分支,执行下面命令:

  git checkout -b "branchName"

修改代码提交到本地

拉完分支后,就可以修改代码了。

修改代码注意事项

  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
mvn clean compile

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

  • 补充单元测试代码。
  • 确保新修改通过所有单元测试。
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
mvn clean test

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

make test

也可以通过 IDE 来辅助运行。

其它注意事项

  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • 对逻辑和功能不容易被理解的地方添加注释。
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。

修改完代码后,执行如下命令提交所有修改到本地:

git commit -am '添加xx功能'

提交代码到远程仓库

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

git push origin "branchname"

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

提交合并代码到主干的请求

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

PR 流水线校验

PR 流水线校验包括:

  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  3. 执行全部单元测试且必须全部通过。
  4. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  5. 检测提交的代码是否存在安全漏洞。
  6. 检测提交的代码是否符合基本代码规范。

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

Code Review

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

合并代码到主干

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。


5.3.3 - 文档、Issue、流程贡献

文档贡献

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 提交与回复贡献

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 模板

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

Question or Bug Report

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

### Describe the question or bug

A clear and concise description of what the question or bug is.

### Expected behavior

A clear and concise description of what you expected to happen.

### Actual behavior

A clear and concise description of what actually happened.

### Steps to reproduce

Steps to reproduce the problem:

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

### Screenshots

If applicable, add screenshots to help explain your problem.

### Minimal yet complete reproducer code (or GitHub URL to code)

### Environment

- SOFAArk version:
- JVM version (e.g. `java -version`):
- OS version (e.g. `uname -a`):
- Maven version:
- IDE version:

Feature Request

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

流程贡献

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。



5.3.4 - 组织会议和运营布道

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. 与其他企业分享交流 SOFAServerless 的使用场景等。
  3. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  4. 其它运营方式。

5.4 - 社区角色与晋升

角色职责与晋升机制

SOFAServerless 社区角色参考了 Apache 开源产品组织方式,SOFAArk、Arklet、ModuleController、ArkCtl 每个组件都有各自的角色。每个组件的角色职责从低到高分别是:Contributor、Committer、PMC (Project Management Committee)、Maintainer

角色责任与权限晋升到更高角色机制
Contributor所有在社区提 Issue、回答 Issue、对外运营、提交文档内容或者提交任意代码的同学,都是相应组件的 Contributor。Contributor 拥有 Issue 提交、Issue 回复、官网或文档内容提交、代码提交(不包括代码评审)和对外发表文章权限。当 Contributor 完成合并的代码或者文档内容足够多,就可以由该组件的 PMC 成员投票晋升为 Committer。当 Contributor 回答的 Issue 或者参与的运营活动足够多,也可以被 PMC 成员投票晋升为 Committer。
Committer所有在社区积极回答 Issue、对外运营、提交文档内容或者提交代码的同学,按积极度都有可能被 PMC 成员投票晋升为 Committer。Committer 额外拥有代码评审、技术方案评审、Contributor 培养的责任与权限。对长期积极投入或持续有突出贡献的 Committer,经 PMC 成员投票可以晋升为相应组件的 PMC 成员。
PMC对相应组件持续贡献特别活跃的同学有机会晋升为 PMC 成员。PMC 成员额外拥有组件的 RoadMap 制定、技术方案和代码评审、Issue 和迭代管理、Contributor 和 Committer 培养等责任与权限。
MaintainerMaintainer 额外拥有密钥管理和仓库管理等管理员权限,除此之外在其他方面和 PMC 成员的责任与权限是完全对等的。

社区角色成员名单

SOFAArk

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Comittee)

glmapper

Committer

zjulbj5
gaosaroma
QilongZhang133
straybirdzls13
caojie0911

Contributor

lylingzhen10
khotyn
FlyAbner (260+ 行提交,提名 Comitter?)
alaneuler
sususama
ujjboy
JoeKerouac
Lunarscave
HzjNeverStop
AiWu4Damon
vchangpengfei
HuangDayu
shenchao45
DalianRollingKing
nobodyiam
lanicc
azhsmesos
wuqian0808
KangZhiDong
suntao4019
huangyunbin
jiangyunpeng
michalyao
rootsongjc
Zwl0113
tofdragon
lishiguang4
hionwi
343585776
g-stream
zkitcast
davidzj
zyclove
WindSearcher
lovejin52022
smalljunHw
vchangpengfei
sq1015
xwh1108
yuanChina
blysin
yuwenkai666
hadoop835
gitYupan
thirdparty-core
Estom
jijuanwang
DCLe-DA
linkoog
springcoco
zhaowwwjian
xingcici
ixufeng
jnan806
lizhi12q
kongqq
wangxiaotao00
由于篇幅有限,23 年之前提交 Issue 的 Contributor 不在此一一列举,也同样感谢大家对 SOFAArk 的使用和咨询

Arklet

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

TomorJM

Committer

暂无

Contributor

glmapper
Lunarscave
lylingzhen

ModuleController

Maintainer

gold300jin

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

liu-657667
Charlie17Li
lylingzhen

Arkctl

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

暂无


5.6 - Arklet 技术文档

5.6.1 - Arklet 架构设计与接口设计

概述

Arklet 为 SofaArk 基础和模块的交付提供了一个操作接口。有了 Arklet,Ark Biz 的发布和操作可以轻松灵活地进行。

Arklet 是由 ArkletComponent 内部构建的

image

  • ApiClient: 负责与外界交互的核心组件
  • CommandService: Arklet 对外暴露能力指令定义和扩展
  • OperationService: Ark Biz 与 SofaArk 交互,进行添加、删除、修改和封装基本能力
  • HealthService: 基于健康和稳定性,计算基础、Biz、系统等其他指标

他们之间的协作如图所示 overview

当然,您也可以通过实现 ArkletComponent 接口来扩展 Arklet 的组件功能

命令扩展

Arklet 外部公开了指令 API,并通过每个 API 映射的 CommandHandler 内部处理指令。

CommandHandler 相关的扩展属于 CommandService 组件的统一管理

您可以通过继承 AbstractCommandHandler 来自定义扩展命令

内置命令 API

以下所有的指令 api 都使用 POST(application/json) 请求格式访问 arklet

启用了 http 协议,默认端口是 1238

您可以设置 sofa.serverless.arklet.http.port JVM 启动参数覆盖默认端口

查询支持的命令

  • URL: 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"
        }
    ]
}

安装一个 biz

  • URL: 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."
  }
}

卸载模块

  • URL: 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."
  }
}

Switch a biz

  • URL: 127.0.0.1:1238/switchBiz
  • 输出样例:
{
    "bizName":"dynamic-provider",
    "bizVersion":"1.0.0"
}
  • 输出样例:
{
  "code":"SUCCESS"
}

查询所有 Biz

  • URL: 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":"/"
    }
  ]
}

查询健康状况

  • URL: 127.0.0.1:1238/health

以下根据不同的输入参数,获取到不同的状态信息

查询健康状况

  • 输入样例:
{}
  • 输出样例:
{
  "code": "SUCCESS",
  "data": {
    "healthData": {
      "jvm": {
        "max non heap memory(M)": -9.5367431640625E-7,
        "java version": "1.8.0_331",
        "max memory(M)": 885.5,
        "max heap memory(M)": 885.5,
        "used heap memory(M)": 137.14127349853516,
        "used non heap memory(M)": 62.54662322998047,
        "loaded class count": 10063,
        "init non heap memory(M)": 2.4375,
        "total memory(M)": 174.5,
        "free memory(M)": 37.358726501464844,
        "unload class count": 0,
        "total class count": 10063,
        "committed heap memory(M)": 174.5,
        "java home": "****\\jre",
        "init heap memory(M)": 64.0,
        "committed non heap memory(M)": 66.203125,
        "run time(s)": 34.432
      },
      "cpu": {
        "count": 4,
        "total used (%)": 131749.0,
        "type": "****",
        "user used (%)": 9.926451054656962,
        "free (%)": 81.46475495070172,
        "system used (%)": 6.249762806548817
      },
      "masterBizInfo": {
        "webContextPath": "/",
        "bizName": "bookstore-manager",
        "bizState": "ACTIVATED",
        "bizVersion": "1.0.0"
      },
      "pluginListInfo": [
        {
          "artifactId": "web-ark-plugin",
          "groupId": "com.alipay.sofa",
          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
          "pluginName": "web-ark-plugin",
          "pluginUrl": "file:/****/2.2.3-SNAPSHOT/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
          "pluginVersion": "2.2.3-SNAPSHOT"
        },
        {
          "artifactId": "runtime-sofa-boot-plugin",
          "groupId": "com.alipay.sofa",
          "pluginActivator": "com.alipay.sofa.runtime.ark.plugin.SofaRuntimeActivator",
          "pluginName": "runtime-sofa-boot-plugin",
          "pluginUrl": "file:/****/runtime-sofa-boot-plugin-3.11.0.jar!/",
          "pluginVersion": "3.11.0"
        }
      ],
      "masterBizHealth": {
        "readinessState": "ACCEPTING_TRAFFIC"
      },
      "bizListInfo": [
        {
          "bizName": "bookstore-manager",
          "bizState": "ACTIVATED",
          "bizVersion": "1.0.0",
          "webContextPath": "/"
        }
      ]
    }
  }
}

查询系统健康信息

  • 输入样例:
{
  "type": "system",
  // [OPTIONAL] if metrics is null -> query all system health info
  "metrics": ["cpu", "jvm"]
}
  • 输出样例:
{
  "code": "SUCCESS",
  "data": {
    "healthData": {
      "jvm": {...},
      "cpu": {...},
//      "masterBizHealth": {...}
    }
  }
}

查询模块健康信息

  • 输入样例:
{
  "type": "biz",
  // [OPTIONAL] if moduleName is null and moduleVersion is null -> query all biz
  "moduleName": "bookstore-manager",
  // [OPTIONAL] if moduleVersion is null -> query all biz named moduleName
  "moduleVersion": "1.0.0"
}
  • 输出样例:
{
  "code": "SUCCESS",
  "data": {
    "healthData": {
      "bizInfo": {
        "bizName": "bookstore-manager",
        "bizState": "ACTIVATED",
        "bizVersion": "1.0.0",
        "webContextPath": "/"
      }
//      "bizListInfo": [
//        {
//          "bizName": "bookstore-manager",
//          "bizState": "ACTIVATED",
//          "bizVersion": "1.0.0",
//          "webContextPath": "/"
//        }
//      ]
    }
  }
}

查询插件健康信息

  • 输入样例:
{
  "type": "plugin",
  // [OPTIONAL] if moduleName is null -> query all biz
  "moduleName": "web-ark-plugin"
}
  • 输出样例:
{
  "code": "SUCCESS",
  "data": {
    "healthData": {
      "pluginListInfo": [
        {
          "artifactId": "web-ark-plugin",
          "groupId": "com.alipay.sofa",
          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
          "pluginName": "web-ark-plugin",
          "pluginUrl": "file:/****/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
          "pluginVersion": "2.2.3-SNAPSHOT"
        }
      ]
    }
  }
}

使用端点查询健康状况

使用端点获取 k8s 模块的健康信息

默认配置

  • 端点暴露包括:*
  • 端点基本路径:/
  • 端点服务器端口:8080

http 代码结果

  • HEALTHY(200):如果所有健康指标都是健康的,获取健康信息
  • UNHEALTHY(400):一旦健康指标不健康,获取健康信息
  • ENDPOINT_NOT_FOUND(404):找不到端点路径或参数
  • ENDPOINT_PROCESS_INTERNAL_ERROR(500):获取健康过程中抛出错误

查询所有健康信息

curl 127.0.0.1:8080/arkletHealth
  • 输出样例
{   
    "healthy": true,
    "code": 200,    
    "codeType": "HEALTHY",    
    "data": {        
        "jvm": {...},        
        "masterBizHealth": {...},        
        "cpu": {...},        
        "masterBizInfo": {...},        
        "bizListInfo": [...],        
        "pluginListInfo": [...]    
    }
}  

查询所有 biz/plugin 健康信息

curl: 127.0.0.1:8080/arkletHealth/{moduleType} (moduleType 必须在 ['biz', 'plugin'])
  • 输出样例
{   
   "healthy": true,
   "code": 200,    
   "codeType": "HEALTHY",    
   "data": {        
       "bizListInfo": [...],  
       // "pluginListInfo": [...]      
   }
}  

查询单个 biz/plugin 健康信息

curl 127.0.0.1:8080/arkletHealth/{moduleType}/moduleName/moduleVersion (moduleType must in ['biz', 'plugin'])
  • 输出样例:
{   
   "healthy": true,
   "code": 200,    
   "codeType": "HEALTHY",    
   "data": {        
       "bizInfo": {...},  
       // "pluginInfo": {...}      
   }
}

5.6.2 - 如何发布 Arklet 版本

触发 github Action 发布到 snapshot staging

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
image.png

该 action 需要手动触发执行
image.png
执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

发布到 Release staging

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
点击左侧的 Staging Repositories:

搜索刚才记录的 ID:

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

仓库包同步与搜索

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。


5.7 - ModuleController 技术文档

5.7.1 - ModuleController 架构设计

介绍

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

基本架构

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。

功能清单和 RoadMap

  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)

5.7.2 - CRD 模型设计

CRD 模型对比

K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。

ModuleDeployment CRD 模型

image

Module CRD 模型

image

ModuleTemplate CRD 模型

image

ModuleReplicaSet CRD 模型

image


5.7.3 - 核心代码结构

image.png

image.png


核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。



5.7.4 - 模块生命周期

模块生命周期

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

image

模块状态机

image


5.7.5 - 核心流程时序图

模块首发

image

模块二发

image

模块下线

image

对等基座扩容

image

对等基座缩容

image

5.8 - Arkctl 技术文档

5.8.1 - Arkctl 技术文档

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


5.9 - 多模块运行时适配或最佳实践

5.9.1 - log4j2 的多模块化适配

为什么需要做适配

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

普通应用 log4j2 的初始化

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

该方法会根据 loggerContext 来判断是否已经初始化过了

这里在多模块下会存在问题一

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

  1. 获取到日志配置文件
  2. 解析日志配置文件里的变量值 这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的

获取日志配置文件

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

解析日志配置值

配置文件里有一些变量,例如这些变量

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

预期多模块合并下的日志

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

多模块适配点

  1. getLoggerContext() 能拿到模块自身的 LoggerContext

  2. 需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

模块改造方式

详细查看源码

5.9.2 - ehcache 的多模块化最佳实践

为什么需要最佳实践

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

最佳实践的几个要求

  1. 基座里必须引入 ehcache,模块里复用基座

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 image.png

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, image.png 如果 net.sf.ehcache.CacheManager 是。这里会走到 java native 方法上做判断,从当前类所在的 ClassLoader 里查找 net.sf.ehcache.CacheManager 类,所以基座里必须引入这个依赖,否则会报 ClassNotFound 的错误。 image.png

  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, image.png image.png 这里在 new 对象时,需要先获得对象所属类的 CacheManager 是基座的 CacheManager。这里也不能讲 CacheManager 由模块 compile 引入,否则会出现一个类由多个不同 ClassLoader 引入导致的问题。 image.png

所以结论是,这里需要全部委托给基座加载。

最佳实践的方式

  1. 模块 ehcache 排包瘦身委托给基座加载
  2. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  3. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
 <plugin>
    <groupId>com.google.code.maven-replacer-plugin</groupId>
    <artifactId>replacer</artifactId>
    <version>1.5.3</version>
    <executions>
        <!-- 打包前进行替换 -->
        <execution>
            <phase>prepare-package</phase>
            <goals>
                <goal>replace</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!-- 自动识别到项目target文件夹 -->
        <basedir>${build.directory}</basedir>
        <!-- 替换的文件所在目录规则 -->
        <includes>
            <include>classes/j2cache/*.properties</include>
        </includes>
        <replacements>
            <replacement>
                <token>ehcache.ehcache.name=f6-cache</token>
                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
            </replacement>

        </replacements>
    </configuration>
</plugin>
  1. 需要把 FactoryBean 的 shared 设置成 false
@Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();

        // 需要把 factoryBean 的 share 属性设置成 false
        factoryBean.setShared(true);
//        factoryBean.setShared(false);
        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
        return factoryBean;
    }

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 image.png image.png

最佳实践的样例

样例工程请参考这里

5.9.3 -

6 - 常见 FAQ

6.1 - 常见问题列表

问题 1-1:模块 compile 引入 springboot 依赖,模块安装时报错

java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
解决方式

模块需要做好瘦身,参考这里:模块瘦身

问题 1-2:模块安装找不到 ServerlessApplicationListener

报错信息如下:

com.alipay.sofa.ark.exception.ArkLoaderException: [ArkBiz Loader] module1:1.0-SNAPSHOT : can not load class: com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
解决方式

请在模块里面添加如下依赖:

<dependency>
    <groupId>com.alipay.sofa.serverless</igroupId>
    <artifactId>sofa-serverless-app-starter</artifactId>
    <version>0.5.5</version>
</dependency>

或者升级 sofa-serverless 版本到最新版本

问题 1-3: 通过 go install 无法安装 arkctl

执行如下命令,报错

go install serverless.alipay.com/sofa-serverless/v1/arkctl@latest

报错信息如下:

go: serverless.alipay.com/sofa-serverless/v1/arkctl@latest: module serverless.alipay.com/sofa-serverless/v1/arkctl: Get "https://proxy.golang.org/serverless.alipay.com/sofa-serverless/v1/arkctl/@v/list": dial tcp 142.251.42.241:443: i/o timeout
解决方式

arkctl 是作为 sofa-serverless 子目录的方式存在的,所以没法直接 go get,可以从这下面下载执行文件, 请参考安装 arkctl

问题 1-4:模块安装报 Master biz environment is null

解决方式,升级 sofa-serverless 版本到最新版本

<dependency>
    <groupId>com.alipay.sofa.serverless</igroupId>
    <artifactId>sofa-serverless-app-starter</artifactId>
    <version>${最新版本号}</version>
</dependency>

问题 1-5:模块静态合并部署无法从制定的目录里找到模块包

解决方式,升级 sofa-serverless 版本到最新版本

<dependency>
    <groupId>com.alipay.sofa.serverless</igroupId>
    <artifactId>sofa-serverless-app-starter</artifactId>
    <version>${最新版本号}</version>
</dependency>

6.2 - 如果模块独立引入 SpringBoot 框架部分会怎样?

由于多模块运行时的逻辑在基座引入和加载,例如一些 Spring 的 Listener。如果模块启动使用完全自己的 SpringBoot,则会出现一些类的转换或赋值判断失败,例如:

CreateSpringFactoriesInstances

image.png

name = ‘com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener’, ClassUtils.forName 获取到的是从基座 ClassLoader 的类
image.png
而 type 是模块启动时加载的,也就是使用模块 BizClassLoader 加载。
image.png
此时这里做 isAssignable 判断,则会报错。

Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener

所以模块框架这部分需要委托给基座加载。