作为Serverless架构的一种典型形态, 函数即服务(function as a service, FaaS)架构将业务抽象为细粒度的函数, 并且提供弹性的自动伸缩等自动化运维功能, 能够大幅降低运维成本. 当前, 许多在线服务系统中的一些高并发、高可用、灵活多变的业务(如支付、红包等)都已经迁移到了FaaS平台上, 但是大量传统单体应用还是难以利用FaaS架构的优势. 针对这一问题, 提出了一种基于动态和静态分析的单体应用FaaS改造方法. 该方法针对指定的单体应用API, 通过动态分析和静态分析相结合的方式识别并剥离其实现代码和依赖, 然后按照函数模板完成代码重构. 针对函数在高并发场景下的冷启动问题, 该方法利用基于IO多路复用的主从多线程Reactor模型优化了函数模板, 提高了单个函数实例的并发处理能力. 基于该方法实现了针对Java语言的原型工具Codext, 在开源Serverless平台OpenFaaS上, 面向4个开源单体系统进行了实验验证.
As a typical form of the Serverless architecture, the function as a service (FaaS) architecture abstracts the business into fine-grained functions, and provides automatic operation and maintenance functionality such as auto-scaling, which can greatly reduce the operation and maintenance costs. Some of the high concurrent, high available, and high flexible services (such as payment, red packet, etc.) in many online service systems have been migrated to the FaaS platform, but a large number of traditional monolithic applications still find it difficult to take advantage of the FaaS architecture. In order to solve this problem, a FaaS migration approach for monolithic applications based on dynamic and static analysis is proposed in this study. This approach identifies and strips the implementation code and dependencies for the specified monolithic application API by combining dynamic and static analysis, and then completes the code refactoring according to the function template. Aiming at the cold-start problem of functions in high concurrency scenario, this approach uses the master-slave multithreaded Reactor model based on IO multiplexing to optimize the function template and improve the concurrency processing capability of a single function instance. Based on this approach, Codext, a prototype tool for Java language, is implemented and experimental verification is carried out on OpenFaaS, an open source Serverless platform, for four open source monolithic applications.
传统的单体架构软件应用将所有代码一起打包、构建和部署. 随着需求的变化和业务的增长, 单体架构应用将变得越来越臃肿、代码复杂度不断提高、技术债务不断积累、维护成本大幅度提高, 从而导致新特性的交付周期变长、新技术难以应用、外部需求变化以及业务增长带来的性能需求难以满足[
函数即服务(function as a service, FaaS)是Serverless架构的一种典型形态. FaaS将应用的业务逻辑封装为在无状态的容器中运行、由事件触发、临时性(可能只一次调用即释放)并且完全由第三方云平台管理的函数[
为了应对业务增长和需求变化, 许多采用单体架构的企业都选择向更适应云计算特点的架构演进. 许多企业选择将单体应用重构拆分为微服务应用, 但这一过程十分困难. Netflix公司花费了7年时间才完成从单体到微服务的架构迁移[
针对以上问题, 本文提出了一种基于动态和静态分析的单体应用FaaS改造方法. 该方法能够针对指定的API进行函数抽取和模板化改造, 并进行相应的性能优化. 该方法通过动态分析和静态分析相结合的方式获得指定API的完整方法调用执行轨迹, 其中, 动态分析通过运行测试用例获得动态执行轨迹, 静态分析补充未覆盖的执行分支和方法调用. 在此基础上, 通过测试驱动的方法确保指定API的实现代码及相关的配置和依赖都已实现完整剥离, 然后自动将相关实现代码及其配置和依赖迁移至函数模板. 此外, 我们利用基于IO多路复用的主从多线程Reactor模型[
目前, FaaS平台最流行的程序设计语言为Python和NodeJS, 因为其语言特性能有效避免冷启动问题, 大量新的FaaS应用都使用了NodeJS和Python. 但是在过去很长一段时间内, Java为构建构建大型应用的主要语言, 很多遗留应用也为Java应用, 其中蕴含着大量的改造需求. 为了验证方法的可行性, 我们基于字节码增强技术(Javaagent/Javasist[
单体应用的重构、改造和升级一直是软件工程领域的一个热门话题. 近几年来, 由于微服务架构的兴起, 越来越多的单体应用向微服务架构迁移[
目前, 已经有了很多关于单体应用的微服务化拆分的算法和工具. Abdullah等人[
由于微服务架构固有的复杂性, 微服务拆分过程需要考虑诸多因素, 以上所提到的研究都只是从某一个或几个方面来对单体应用进行微服务拆分, 缺少对微服务特性的全面分析. 微服务化至少需要对代码和数据库进行拆分, 需要解决前后台分离、服务通信方式重构、代码重构和迁移、数据表修改以及存储过程和数据库事务应该如何迁移等问题[
除了单体系统向微服务架构迁移固有的复杂性和缺乏拆分评价标准以外, 企业还需要完善的基础设施和运维人员来支撑上层微服务系统的运行和维护, 这极大地提高了企业的成本. 为了节省成本, 越来越多的微服务系统选择使用云服务. Villamizar等人研究发现: 使用云服务的微服务架构比单体应用节省70%的成本, 使用Serverless (AWS Lambda)的微服务系统比使用云服务自运维的微服务系统更加节约成本[
为了避免大量的运维成本开销和微服务拆分的复杂性, 已经有不少的单体应用开始向Serverless架构迁移. Goli等人[
冷启动问题是Serverless不容忽视的, 冷启动时间过长将会带来各类问题[
本文提出了单体应用向FaaS迁移的方案, 充分参考了上述单体应用微服务化和Serverless化迁移工作的优缺点, 通过测试用例驱动得到运行时方法执行轨迹, 并结合静态代码分析的方式, 将有迁移需求的API相关代码迁移至FaaS函数模板, 并通过函数模板自动化生成容器镜像, 最后通过容器镜像实例化函数. 该迁移方法通过对指定API的迁移, 避免了微服务迁移的复杂性; 并通过平台托管治理维护的方式, 最大化降低了运维成本. 除此之外, 我们综合使用了代码依赖精简、基于IO多路复用的主从多线程Reactor的高性能函数模板等方式来降低冷启动带来的影响, 特别是在高并发场景下频繁扩容导致冷启动影响加剧的情况.
几乎所有的软件应用系统中, 不管是使用命令式编程语言或者函数式编程语言编写, 其业务逻辑都封装在方法中. 一个对外提供服务的API(下文称外部API)通常包含多个内部方法, 一次对外部API的调用可能会经过部分或者全部这些内部方法. 这些方法可能会依赖其他代码文件中的定义的变量(常量)或者类型(比如Java语言中的Class、Golang和C语言中的struct等)以及第三方依赖提供的API. 本文所提出的单体应用外部API迁移方法, 在测试环境中, 通过测试用例驱动指定外部API运行, 并通过运行时监控和静态代码分析得到该外部API相关的内部方法调用; 然后以这些内部方法为主线, 分析其相关源码文件的抽象语法树(遍历抽象语法树, 对与外部API无关的代码元素进行剪枝); 最后, 通过剪枝后的抽象语法树生成该外部API相关的代码. 抽取出的代码通过人工调整和测试之后会被迁移到函数模板, 然后对迁移后的代码进行依赖精简, 最后通过函数模板构建函数容器镜像. 上述用到的动态分析、静态分析和依赖分析工具, 针对不同的语言有不同的实现, 因此本方法可以适用于不同的程序设计语言编写的应用程序.
本文所提出的方法适合于满足以下两个条件的单体应用后端的外部API(即前端或第三方客户端可直接访问的API): 无状态或具有状态管理机制(即状态可以迁移至外部存储); 具备较完备的API测试用例集(目的是驱动动态分析). 改造后的FaaS实现能够更好地应对突发或流量不可预测的场景, 并支持业务快速迭代.
方法流程如
单体应用外部API迁移方法流程
本文代码迁移的方法需要尽可能完整地搜集外部API依赖的方法. 目前主要存在两种方法搜集调用轨迹的方法, 即运行时动态分析和静态代码分析. 这两种方法都存在一定的缺陷: 动态分析过于依赖测试用例的完整性和分支覆盖度; 静态分析主要通过抽象语法树分析得到服务调用的拓扑结构, 难以分析程序运行时的行为, 比如动态绑定. 本方法充分考虑了动态分析和静态分析的缺陷, 使用测试用例驱动的运行时动态方法执行轨迹日志分析和静态代码抽象语法树分析相结合的方式搜集方法调用.
动态分析大多数都是根据语言的特性, 采用不同的实现方式, 比如代码插桩、字节码增强等. 动态分析过程针对不同语言有不同工具, 配置过程和方法调用搜集过程也可能不同. 本文方法统一将其分别抽象为
主流编程语言对应的动态调用监控工具
语言 | 工具 | 语言 | 工具 |
C | gprof/valgrind | C++ | CodeViz/SourceInsight |
Java | Java-callgraph | Python | pycallgraph |
Golang | go-callvis | NodeJS | njsTrace |
静态分析主要通过分析代码抽象语法树补全动态分析中测试用例未覆盖的方法调用, 主要基于动态调用轨迹, 分析抽象语法树中动态分析未覆盖的方法调用, 包括
主流编程语言对应的抽象语法树分析工具
语言 | 工具 | 语言 | 工具 |
C | Clang | C++ | foonathan/cppast |
Java | Javaparser | Python | 内置ast.py |
Golang | fatih/astrewrite | NodeJS | ajaxorg/treehugger |
静态抽象语法树分析补全方法调用过程
算法1详细介绍了
(1) 遍历映射中的键, 通过键(全限定名前缀)和单体应用源码根目录
(2) 使用算法2遍历第(1)步中抽象语法树的方法声明节点, 若方法声明对应的方法签名已经存在于映射的值(
(3) 将第(2)步中测试用例未覆盖的方法调用节点加入
(4) 依次从
(5) 随后得到
输入: 输入全限定名前缀-方法签名集合映射
输出: 补全后的全限定名前缀-方法签名集合映射
1:
2:
3:
4: 根据
5:
6: 根据
7: 生成
8:
9:
10:
11:
12: 根据
13: 根据
14: 生成
15:
16:
17:
算法2描述了基于广度优先遍历的方法调用轨迹补全算法, 该算法主要通过广度优先遍历(BFS)遍历抽象语法树的方法调用和方法声明语法树, 搜索
(1) 遍历ast中所有的方法声明, 如果方法声明节点对应的方法签名存在于
(2) 遍历第(1)步中的方法调用子抽象语法树, 若该方法未存在于
输入: 抽象语法树
1:
2: //遍历抽象语法树中的方法声明节点
3:
4:
5: //遍历抽象语法树中的方法调用节点
6:
7: 根据
8:
9: //补全
10:
11:
12:
13: //若不存在key为
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
抽象语法树分析是外部API相关代码提取的核心部分, 如
算法3的主要分为3步: (1) 通过
输入: 补全的全限定名前缀-方法签名集合
输出: 剪枝后的抽象语法树集合
1:
2:
3:
4:
5: //根据
6:
7: //去除未被调用方法的方法声明
8:
9:
10:
11: 遍历
12: 根据变量的类型分析导入(
13:
14:
15:
16:
17:
18: 分析
19: 根据变量的类型分析导入(
20:
21:
22:
23:
24:
25: //变量和类型的声明可能在其他源码文件中, 所以需要对这些变量和类型的声明重新构建
26:
27:
28:
29:
30:
31:
32: //分别对
33:
34:
35:
36:
外部API相关代码提取的最后一步(
测试和人工调整主要是为了保证迁移后的函数代码质量和语义与迁移前外部API一致, 整个过程都在与单体应用一致的测试环境中进行. 在测试和人工调整前, 将生成的代码、相关配置和所有的依赖迁移至同一临时目录下, 并保持和原单体应用目录结构相对一致. 测试和人工调整的具体流程如
测试和人工调整流程
此部分对应
函数模板构建和容器镜像生成流程
(1) 根据单体应用的编程语言, 选择对应编程语言的函数模板. 将临时目录测试完毕的代码、配置迁移至函数模板的处理器目录下, 并保持目录结构相对一致, 然后调整函数启动入口代码, 即在函数入口中调用迁移的函数代码.
(2) 去除函数代码中未使用的第三方依赖. 本文实现主要针对Java语言, 基于依赖管理工具Maven的依赖分析插件maven-dependency-plugin对未使用的依赖进行分析, 然后根据依赖分析结果分析依赖配置文件, 去除未使用依赖的配置. 对于其他语言, 也具备相应的依赖分析工具, 比如NodeJS有npm工具、Python有pip工具、Golang有Go module工具.
(3) 将原单体应用的构建打包配置、运行时环境配置和函数启动配置填入函数模板的容器配置中.
(4) 根据容器配置, 首先将源码构建打包, 然后配置运行环境. 容器启动时, 将自动运行函数启动配置中的函数启动命令.
本文使用基于IO多路复用的主从多线程Reactor模型实现了高并发的函数模板, 以减轻高流量场景下函数频繁水平扩容带来的冷启动影响. 如
相对于基于阻塞IO和其他非阻塞IO的服务器, 主从多线程Reactor模型有更好的处理并发的能力. 如
主从多线程Reactor模型
本文基于第2节单体应用外部API迁移方法实现了针对Maven工具管理的Java应用的的迁移工具Codext, 基于第3节冷启动优化实现了基于Netty的主从多线程的Reactor模型的Java语言函数模板.
如
Codext工具工作原理
(1) projectRoot: 单体应用源码根目录.
(2) pkgPrefix: 包名前缀(遵循Java package命名规范[
(3) outPath: 迁移目的地址, 即临时目录的路径.
(4) methodLogPath: 方法调用链搜集的日志路径.
(5) reservedFile: 迁移需要保留的配置文件(比如./src/main/resource, pom.xml, mybatis mapper配置等).
方法调用链搜集模块基于Javaagent和Javasist的字节码增强技术实现了运行时无侵入式方法调用搜集, 该模块封装了修改字节码的逻辑, 该逻辑被打成jar包, 在应用启动时, 通过JVM参数-javaagent加载. 具体流程如
方法调用日志示例
日志预处理模块主要包含过滤和聚合两个过程: 首先, 通过日志前缀过滤出日志文件中与方法调用信息相关的行; 然后将其读入内存, 以
键
抽象语法树分析模块基于Javaparser工具对抽象语法树进行分析, 主要用来补全测试用例未覆盖的方法调用以及对抽象语法树进行剪枝, 从而得到函数相关代码的抽象语法树, 主要流程如下.
1. 构建AST: 遍历
2. 方法调用补全: 主要根据算法1分析遍历分析
3. 抽象语法树代码元素依赖分析: 该步骤主要通过算法2从方法声明子语法树、类型声明子语法树两个层次对抽象语法树进行分析.
(1) 方法声明子抽象语法树(
Javaparser抽象语法树分析涉及的类型和示例
编号 | AST元素类型 | 示例 |
1 | ArrayType | |
2 | ArrayCreationExpr | new Object[ |
3 | CastExprt | ( |
4 | CatchClause | |
5 | ClassExpr | |
6 | ClassOrInterfaceDeclaration | |
7 | ClassOrInterfaceType | |
8 | NormalAnnotationExpr | @ |
9 | ObjectCreationExpr | |
10 | Parameter | |
11 | ThrowStmt | |
12 | TypeExpr | |
13 | VariableDeclarator | |
14 | AssignExpr | |
15 | FieldAccessExpr | |
16 | FieldDeclaration | |
17 | ConstructionDeclaration | |
18 | ImportDeclaration | |
19 | InstanceOfExpr | |
20 | MethodCallExpr | |
21 | MethodDeclaration | |
22 | MethodReferenceExpr | |
23 | TypeParameter | 〈 |
(2) 类型声明子语法树分析: 搜集类型(类、接口、注解、枚举)中除了方法声明之外依赖(方法声明依赖的元素在(1)中已经分析)的变量和类型. 类的依赖分析主要包括
将所有依赖的类统一添加到
4. 抽象语法树剪枝: 剪枝过程如算法2所示, 在Java实现中, 从类型语法树和根语法树两个层次进行剪枝, 最终得到剪枝后的函数相关的抽象语法树.
(1) 类型抽象语法树剪枝: 根据
(2) 根抽象语法树剪枝: 根据
代码生成模块将
(1) 根据临时目录路径配置outPath, 生成临时目录.
(2) 根据Maven规范生成对应的源码目录结构和配置文件目录结构.
(3) 遍历剪枝后的每一个抽象语法树
(4) 使用Javaparser Lexical-Preserving组件将相应的抽象语法树生成与单体应用源码中风格一致的java代码(如
使用Javaparser生成的抽象语法树和对应的源码
(5) 最后, 将配置项reservedFiles列表中的配置文件迁移至临时目录, 并保持项目结构与单体应用一致.
依赖管理模块主要包含依赖分析和依赖精简两个部分.
● 依赖分析通过Maven插件maven-dependency-plugin分析临时目录中测试好的函数代码, 并生成相应的分析日志. 如
依赖精简流程
● 依赖精简通过maven-model工具分析项目配置文件POM, 并在POM文件中去除依赖分析日志中的声明了但未被使用的依赖配置.
根据第3节中基于IO多路复用的主从多线程Reactor模型函数原理, 我们基于Netty和OpenFaaS模板规范[
函数模板结构和运行时请求处理流程
(1) 函数业务逻辑代码: 此部分包含了所有业务代码及其相关的配置和依赖.
(2) 函数入口: 函数入口负责启动一个HTTP服务器, 并负责请求的连接建立、解码、编码等一系列请求处理工作.
(3) HTTP消息模型: 封装了HTTP的处理逻辑, 以方便函数业务逻辑对HTTP对象的处理.
如
我们设计了代码迁移工具Codext有效性实验和函数性能优化实验: 前者基于开源项目对Codext工具进行了功能和性能测试, 后者测试了依赖精简对冷启动的优化和大流量场景下高并发函数模板对函数实例频繁扩容带来的冷启动问题的优化.
如
实验项目信息
项目 | API | 技术栈 | 后端源码总行数 |
PSS | /student/login | Framework: Springboot |
1 719 |
/login | |||
/choices-overview | |||
/choice | |||
/major | |||
StarChair | /register | Framework: Springboot |
6 683 |
/applyConference | |||
/getUncheckedConference | |||
/changeApplicationStatus | |||
/getAllPassedMeetings | |||
Newbee-mall | /login | Framework: SpringBoot |
7 519 |
/goods/detail/{goodId} | |||
/shop-cart | |||
/shop-cart/{shopCartItemId} | |||
/personal/updateInfo | |||
My-Blog | /admin/login | Framework: SpringBoot |
4 304 |
/admin/blogs/save | |||
/blog/{blogId} | |||
/admin/comments/delete | |||
/admin/blogs/update |
本实验对比了开发者分别在对项目熟悉和陌生的情况下, 人工和使用Codext工具迁移指定API的效率; 同时定义了“类修改比”“方法修改比”和“代码行修改比”这3个指标来评估Codext工具的有效性.
本实验在单台Linux虚机上进行, 具体服务器、工具和环境信息配置见
服务器配置信息
OS | CPU | 内存 | 磁盘 |
CentOS Linux release |
Intel(R) Xeon(R) CPU E5-2690 v2@3.00 GHz |
24 GB | 107 GB |
Java和Maven信息
Java | JRE | JVM | Maven |
java version |
Java(TM) SE Runtime Environment |
Java HotSpot(TM) 64-Bit Server VM |
Apache Maven |
我们选择了4名在校计算机相关专业的研究生作为实验人员, 此前他们并不熟悉将会被分配到的实验项目, 但熟悉上述实验项目的相关技术栈和Docker技术. 实验开始前, 首先对4位同学进行培训, 让他们了解Codext工具的使用流程, 然后基于测试项目进行练习, 直到能够熟练流畅使用Codext工具成功提取出单体应用的API代码, 并将其构建打包为Docker镜像. 完成培训后, 为每个实验人员分配一个开源项目, 并指定5个API完成3组实验: (1) 在完全陌生的条件下, 将指定的5个API人工迁移至函数模板, 并统计所用时间;
(2) 给予实验人员对项目充分的熟悉时间, 然后再将指定的5个API人工迁移至函数模板, 并统计所用时间; (3) 使用Codext工具将指定的5个API迁移至函数模板, 并统计所用时间.
对于第1组和第2组人工迁移实验, 以及第3组需要人工调整代码的部分, 允许实验人员使用自己熟悉的集成开发环境和其他必要的工具. 实验完成后, 统计实验人员对3组实验的反馈.
3组实验时间消耗对比
项目 | API | 迁移类数 | 迁移方法数 | 第1组耗时 |
第2组耗时 |
第3组耗时 |
PSS | /student/login | 11 | 33 | 1 975 | 1 063 | 568 |
/login | 9 | 55 | 1 673 | 1 007 | 839 | |
/choices-overview | 12 | 33 | 2 573 | 1 090 | 780 | |
/choice | 9 | 41 | 1 907 | 1 333 | 601 | |
/major | 9 | 30 | 2 095 | 1 110 | 823 | |
HardChair | /register | 11 | 63 | 1 836 | 1 297 | 986 |
/applyConference | 14 | 54 | 3 791 | 1 764 | 922 | |
/getUncheckedConference | 15 | 43 | 2 210 | 1 109 | 721 | |
/changeApplicationStatus | 16 | 53 | 3 762 | 1 566 | 1 009 | |
/getAllPassedMeetings | 15 | 44 | 2 427 | 1 221 | 863 | |
Newbee-mall | /login | 13 | 41 | 4 042 | 1 769 | 795 |
/goods/detail/{goodId} | 15 | 67 | 3 539 | 1 325 | 437 | |
/shop-cart | 14 | 56 | 4 537 | 1 998 | 908 | |
/shop-cart/{shopCartItemId} | 12 | 28 | 2 387 | 999 | 546 | |
/personal/updateInfo | 14 | 47 | 2 555 | 1 079 | 653 | |
My-Blog | /admin/login | 14 | 34 | 2 075 | 1 264 | 975 |
/admin/blogs/save | 13 | 66 | 3 340 | 1 788 | 754 | |
/blog/{blogId} | 16 | 97 | 4 329 | 2 130 | 638 | |
/admin/comments/delete | 9 | 21 | 1 875 | 971 | 803 | |
/admin/blogs/update | 13 | 70 | 3 339 | 1 434 | 764 |
每个API的3组实验分别占实验总时长比例
根据实验人员的反馈, 他们认为, 在对项目陌生的情况下, 人工迁移和使用Codext工具迁移相比, 以下操作花费时间较多: (1) 寻找API起始代码的位置; (2) 某些隐式调用的方法难以确认, 花费时间较多; (3) 频繁的文件创建和复制粘贴. 在对项目完全熟悉的情况下, 他们认为, Codext工具主要节约了代码文件创建和复制粘贴的时间. 同时, 他们还认为, Codext工具可以自动化去除迁移后代码不需要的依赖, 这是人工迁移很难做到的.
我们定义了以下3个对代码的修改指标来判断Codext工具对API代码提取的有效性.
(1) 类修改比: 修改类的数量/最终函数代码中类的数量.
(2) 方法修改比: 修改方法数量/最终函数代码中方法的数量.
(3) 代码行修改比: 修改行数量/最终函数代码中代码行的数量.
统计规则: 若对任意一个类的非方法代码进行了任意代码行修改(包括增加和删除行), 则对修改类和修改代码行数量加1; 若对任意一个方法进行了任意代码行修改, 则对修改类、方法和代码行数量加1; 若对非类代码(包声明和导入语句)进行了修改, 则将修改代码行数量加1. 所有的统计不包括配置文件和配置代码.
同时, 我们基于源码总行数和耗时来衡量Codext工具的性能. 实验结果见
代码迁移有效性实验结果
项目 | API | 源码总行数 | 类修改比 | 方法修改比 | 代码行修改比 | 耗时(ms) |
PSS | /student/login | 1 719 | 6/11 | 9/33 | 34/599 | 809 |
/login | 3/9 | 23/55 | 30/507 | 822 | ||
/choices-overview | 4/12 | 10/33 | 71/551 | 1 011 | ||
/choice | 3/9 | 8/41 | 30/509 | 793 | ||
/major | 3/9 | 9/30 | 34/557 | 838 | ||
HardChair | /register | 6 683 | 5/11 | 24/63 | 164/910 | 1 003 |
/applyConference | 5/14 | 6/54 | 49/927 | 910 | ||
/getUncheckedConference | 6/15 | 7/43 | 75/781 | 1 610 | ||
/changeApplicationStatus | 7/16 | 7/53 | 86/942 | 1 387 | ||
/getAllPassedMeetings | 6/15 | 7/44 | 75/774 | 1 218 | ||
Newbee-mall | /login | 7 519 | 1/13 | 2/41 | 7/935 | 4 359 |
/goods/detail/{goodId} | 0/15 | 0/67 | 0/972 | 3 513 | ||
/shop-cart | 2/14 | 2/56 | 23/1, 010 | 3 019 | ||
/shop-cart/{shopCartItemId} | 0/12 | 0/28 | 0/726 | 2 797 | ||
/personal/updateInfo | 0/14 | 0/47 | 0/876 | 2 842 | ||
My-Blog | /admin/login | 4 304 | 2/14 | 6/34 | 59/675 | 4 209 |
/admin/blogs/save | 2/13 | 2/66 | 14/829 | 3 604 | ||
/blog/{blogId} | 4/16 | 1/97 | 19/1, 227 | 4 206 | ||
/admin/comments/delete | 1/9 | 1/21 | 4/373 | 3 483 | ||
/admin/blogs/update | 1/13 | 1/70 | 4/831 | 4 904 |
上述部分代码未正确迁移的主要原因如下:
(1) 静态代码分析补全测试用例未覆盖的方法调用, 某些方法调用无法使用静态分析精确识别其方法签名和所属类(或对象), 所以难以定位这类方法调用对应的方法声明, 从而导致这类方法及其相关依赖无法被分析, 主要存在以下3种情况: (a) 方法调用为接口方法, 难以识别其动态绑定时调用的方法; (b) 方法调用为父类方法, 但是运行时绑定为子类, 未能识别子类; (c) Java8新特性Lambda表达式中匿名类型对函数的调用, 不能识别匿名类型的调用.
(2) 一些框架使用了字节码增强技术导致某些类被声明, 但是在加载时被动态拦截修改字节码, 并且加载后的全类名发生了变化, 导致运行时并没有使用被声明的类, 最终导致Codext工具误判这部分类与当前API无关.
性能实验在基于Kubernetes[
Kubernetes集群信息
Kubernetes版本 | 主节点数量 | 从节点数量 | 网络插件 | Docker版本 |
v1.19.2 | 1 | 4 | Calico v3.16.1 | v19.03.13 |
Linux服务器(Kubernetes节点)配置信息
操作系统版本信息 | 处理器信息 | 内存 | 磁盘 |
CentOS Linux release |
Intel(R) Xeon(R) CPU E5-2690 v2@3.00 GHz |
24 GB | 107 GB |
实验所需的容器镜像已经提前构建好, 通过docker pull命令将其提前拉取到每个Kubernetes节点上, 并设置Kubernetes的镜像拉取策略为IfNotPresent, 即如果本地存在某个镜像, 则不会从远程进行拉取. 这样避免了每次扩容都需要从远程拉取镜像带来的网络开销对实验的干扰.
本实验总共分为10组, 每组实验使用Jmeter对集群进行持续30 s的压力测试, 并保持每组实验的并发数从1000/s递增至10000/s. 函数实例在集群中配置了CPU核数为50 m (0.05个物理CPU核心)和HPA以实现自动伸缩, 伸缩范围为1−999, 伸缩条件为CPU使用率超过50%. 实验分别统计了高并发函数模板和OpenFaaS函数模板执行情况、返回时间和吞吐量等信息, 见
自动伸缩条件下, 高并发函数模板在持续30 s的不同并发数下的性能指标
执行情况(持续30 s) | 返回时间(ms) | 吞吐量 | |||||||||
总请求数量 | 并发数 | 失败 | 失败率 | 平均 | 最小 | 最大 | 中位 | P90 | P95 | P99 | 请求数/s |
30 000 | 1000/s | 909 | 3.03 | 6 447.06 | − | 139 070 | 2 699.00 | 5 400.90 | 7 601.00 | 15 366.00 | 123.20 |
60 000 | 2000/s | 4 048 | 6.75 | 5 464.64 | − | 140 288 | 1 800.00 | 4 899.00 | 6 401.95 | 13 444.58 | 210.12 |
90 000 | 3000/s | 5 846 | 6.50 | 7 797.00 | − | 139 515 | 1 505.00 | 5 160.90 | 6 199.00 | 18 298.00 | 277.47 |
120 000 | 4000/s | 13 586 | 11.32 | 10 220.80 | − | 144 461 | 900.00 | 4 900.00 | 6 701.00 | 30 680.00 | 299.67 |
150 000 | 5000/s | 40 098 | 26.73 | 6 647.58 | − | 144 100 | 300.00 | 2 698.00 | 3 501.00 | 131 019.91 | 465.33 |
180 000 | 6000/s | 68 958 | 38.31 | 5 409.46 | − | 145 159 | 1 499.00 | 5 900.00 | 8 585.00 | 130 554.00 | 513.59 |
210 000 | 7000/s | 102 572 | 48.84 | 7 144.61 | − | 174 080 | 1 900.00 | 5 201.00 | 6 399.00 | 13 394.63 | 478.39 |
240 000 | 8000/s | 133 786 | 55.74 | 4 204.70 | − | 136 000 | 100.00 | 1 400.00 | 3 975.95 | 131 518.00 | 711.37 |
270 000 | 9000/s | 161 478 | 59.81 | 3 864.31 | − | 140 288 | 203.00 | 3 803.00 | 9 728.95 | 131 023.44 | 798.95 |
300 000 | 10000/s | 194 222 | 64.74 | 3 516.78 | − | 143 617 | 198.00 | 1 600.00 | 2 900.00 | 13 159.00 | 818.48 |
自动伸缩条件下, OpenFaaS函数模板在持续30 s的不同并发数下的性能指标
执行情况(持续30 s) | 返回时间(ms) | 吞吐量 | |||||||||
总请求数量 | 并发数 | 失败 | 失败率 | 平均 | 最小 | 最大 | 中位 | P90 | P95 | P99 | 请求数/s |
30 000 | 1000/s | 1 231 | 4.10 | 7 134.11 | − | 142 142 | 2 803.00 | 5 951.00 | 15 351.80 | 131 038.88 | 104.10 |
60 000 | 2000/s | 4 925 | 8.22 | 6 626.76 | − | 132 395 | 1 945.00 | 5 103.00 | 7 203.00 | 131 067.00 | 199.55 |
90 000 | 3000/s | 7 525 | 8.36 | 8 984.32 | − | 174 798 | 1 203.00 | 5 791.00 | 6 561.00 | 168 231.00 | 264.68 |
120 000 | 4000/s | 15 896 | 13.25 | 11 351.22 | − | 193 376 | 1 056.00 | 4 590.00 | 8 655.00 | 128 509.00 | 257.33 |
150 000 | 5000/s | 48 003 | 32.00 | 7 053.69 | − | 152 714 | 457.00 | 3 489.00 | 4 312.00 | 141 536.95 | 416.36 |
180 000 | 6000/s | 70 481 | 39.16 | 7 746.08 | − | 184 319 | 1 580.00 | 6 590.00 | 9 560.00 | 169 531.54 | 425.51 |
210 000 | 7000/s | 105 136 | 50.06 | 6 895.37 | − | 191 233 | 2 600.00 | 6 310.00 | 7 523.00 | 140 560.00 | 426.40 |
240 000 | 8000/s | 144 909 | 60.38 | 8 573.21 | − | 187 904 | 200.00 | 1 900.00 | 3 621.00 | 161 057.00 | 557.41 |
270 000 | 9000/s | 182 991 | 67.77 | 5 429.67 | − | 178 476 | 303.00 | 4 600.00 | 11 631.00 | 131 063.00 | 671.47 |
300 000 | 10000/s | 213 091 | 71.03 | 4 823.03 | − | 157 963 | 259.00 | 1 893.00 | 3 251.00 | 145 312.00 | 723.12 |
自动伸缩场景下, 两种函数模板在不同并发数量下的吞吐量对比
自动伸缩场景下, 两种函数模板在不同并发数量下的执行情况对比
自动伸缩场景下, 两种函数模板在不同并发下平均延迟、中位数、P90和P95延迟对比
自动伸缩场景下两种函数模板在不同并发下P99延迟对比
综上, 高并发函数模板在大流量情况下总体并发量高于OpenFaaS函数模板, 并且能够有效地避免流量峰值到来时, 因为冷启动函数无法处理请求的问题. 同时, 请求延迟相对OpenFaaS函数模板有明显的降低, 说明高并发函数模板能够更加高效地利用多核CPU的处理能力, 在避免冷启动问题的同时提高函数性能.
为了适应互联网时代用户量大、并发要求高等软件特性, 传统的单体架构应用不得不向更高级的应用架构迁移. 单体应用向微服务架构迁移已经成了学术界和工业界一个热门的话题, 但是其中还有很多难以克服的挑战, 例如服务拆分、代码迁移、数据库迁移、分布式事务重构、需要完善的基础设施和高昂的运维成本等. 本文提出的基于动态和静态分析的单体应用FaaS改造方法规避了微服务拆分的复杂性, 通过运行时轨迹和静态代码分析将单体应用的API迁移到FaaS架构, 这种半自动化的方式能帮助开发者快速有效地将单体应用的API迁移至FaaS架构, 特别是对于文档缺失的遗留系统. 除此之外, 单体应用API迁移至FaaS架构充分利用了Serverless的优点, 提高了应用的性能, 降低了应用的维护成本. 同时, 本文提出的函数模板优化方案通过实验验证, 优于目前开源社区最优的OpenFaaS官方模板. 综上, 本文提出的函数迁移和函数优化方案能够有效地将单体应用的API迁移至FaaS架构.
未来我们会将Codext工具在大规模范围内进行验证, 提高对不同软件组件的支持(支持更多的语言、开发框架等), 将Codext做到更加自动化, 例如自动化回归测试和接口测试, 增强代码迁移的覆盖度, 争取更少的依赖人工修改.
Rechardson C. Microservices Patterns: With Examples in Java. Manning Publications, 2018. 4−7.
Ding D, Peng X, Guo XF, Zhang J, Wu YJ. Scenario-driven and bottom-up microservice decomposition for monolithic systems. Ruan Jian Xue Bao/Journal of Software, 2020, 31(11): 145−164. http://www.jos.org.cn/1000-9825/6031.htm[doi: 10.13328/j.cnki.jos.006031]
https://martinfowler.com/articles/serverless.html]]>
doi: 10.13140/RG.2.2.15007.87206]]]>
https://www.datadoghq.com/state-of-serverless/]]>
https://gotocon.com/dl/goto-amsterdam-2016/slides/RuslanMeshenberg_MicroservicesAtNetflixScaleFirstPrinciplesTradeoffsLessonsLearned.pdf]]>
Zhou X, Peng X, Xie T, Sun J, Ji C, Li W, Ding D. Fault analysis and debugging of microservice systems: Industrial survey, benchmark system, and empirical study. IEEE Trans. on Software Engineering, 2018, 47(2): 243−260. [doi: 10.1109/TSE.2018.2887384]
doi: https://doi.org/10.1145/3338906.3338961]]]>
doi: 10.1109/ICWS.2017.61]]]>
doi: 10.1109/ICSA.2018.00012]]]>
Taibi D, Lenarduzzi V, Pahl C. Processes, motivations, and issues for migrating to microservices architectures: An empirical investigation. IEEE Cloud Computing, 2017, 4(5): 22−32. [doi: 10.1109/MCC.2017.4250931]
doi: 10.1109/AIMS.2017.23]]]>
https://en.wikipedia.org/wiki/Reactor_pattern]]>
http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf]]>
https://www.javassist.org/]]>
https://javaparser.org/]]>
https://netty.io/]]>
https://www.openfaas.com/]]>
https://www.oreilly.com/programming/free/the-state-of-microservices-maturity.csp]]>
Abdullah M, Iqbal W, Erradi A. Unsupervised learning approach for Web application auto-decomposition into microservices. Journal of Systems and Software, 2019, 151: 243−257. [doi: 10.1016/j.jss.2019.02.031]
doi: 10.1007/978-3-319-44482-6_12]]]>
doi: 10.1109/ICWS.2018.00034]]]>
https://martinfowler.com/articles/microservices.html]]>
doi: 10.1109/CCGrid.2016.37]]]>
doi: 10.1145/3375555.3384380]]]>
doi: 10.1109/IWoR.2019.00008]]]>
https://help.aliyun.com/document_detail/160531.html?spm=a2c4g.11186623.6.829.203c3956LDQxwk]]>
https://help.aliyun.com/document_detail/160531.html?spm=a2c4g.11186623.6.829.203c3956LDQxwk]]>
doi: 10.1007/978-981-10-5026-8_1]]]>
et al. Serverless architectures with AWS Lambda. https://d1.awsstatic.com/whitepapers/serverless-architectures-with-aws-lambda.pdf]]>
doi: 10.1109/ICDCSW.2017.36]]]>
https://en.wikipedia.org/wiki/Cold_start_(computing)]]>
doi: 10.1145/3423211.3425682]]]>
https://github.com/jeremydaly/lambda-warmer]]>
https://docs.openfaas.com/architecture/autoscaling/]]>
Stevens R. Unix Network Programming. 3rd ed., Prentice Hall, 1990. 202−237.
https://docs.oracle.com/javase/specs/jls/se6/html/packages.html#7.7]]>
https://maven.apache.org/guides/mini/guide-naming-conventions.html]]>
https://github.com/openfaas/templates]]>
https://github.com/kylinxiang70/openfaas-template]]>
https://github.com/LeBW/preference-selection-system]]>
https://github.com/FudanSELab/StarChair]]>
https://github.com/newbee-ltd/newbee-mall]]>
https://github.com/ZHENFENG13/My-Blog]]>
https://kubernetes.io/]]>
https://jmeter.apache.org/]]>