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



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 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。



3 - 架构介绍

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 提供模块伸缩能力

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

开启后的副作用

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