使用自定义运行时支持自定义编程语言
Serverless应用的函数代码都是在FaaS当中运行的,但是在目前为止也只能选择FaaS平台所选择的编程语言开发应用。
FaaS平台所支持的编程语言有限,函数计算只支持Node.js 8和12、java和python,那么在使用不支持的语言或不支持小版本的时候就需要使用自定义运行时。
自定义运行时的原理
运行时(Runtime)是程序运行时所依赖的环境。
FaaS中的运行时就会创建函数所指定的运行环境,比如说函数计算的Node.js运行时,那就包括了Node.js运行环境以及一些内置的模块比如ali-oss、tablestore,除此之外还有Java运行时、Python运行时。
自定义运行时就是自己在FaaS自定义一个运行时环境,比如TypeScript,使用TypeScript来编写代码并且部署到FaaS平台上运行。
先来回顾下FaaS的运行原理
在FaaS当中运行时被预先定义,比如创建函数的时候可以指定runtime:nodejs12,接下来用户通过创建触发器驱动函数执行之后,Faas就会以nodejs12作为运行时来创建函数实例,函数代码也就在nodejs12这个运行时环境中执行。
怎么才能让函数在自己定义的运行时环境中执行?
安装依赖的本质就是要把函数运行所需要的依赖都打包上传至FaaS中,那能不能把函数的运行时也打包上传到Faas当中呢,也让Faas运用你上传的运行时来执行你的代码呢?
FaaS平台的自定义运行
用TypeScript编写代码
把代码和TypeScript的运行时都上传至FaaS中,然后通过特定的配置让FaaS通过自定义的TypeScript运行时来运行你的代码
runtime:custom
告诉FaaS你使用的是自定义运行时
bootstrap ts-node index.js
告诉FaaS函数启动时使用ts-node运行index.js
FaaS平台在运行函数是会有很多参数,这些参数怎么传递给自定义运行时呢?本质上是远程数据通信问题。
最简单点的在自定义运行时中实现一个Http服务,FaaS平台通过http请求把数据传递给自定义运行时。
自定义运行时就是使用自定义编程语言实现的Http服务,需要为你的Http服务指定一个启动命令。
boostrap文件示例
通用的做法把启动命令保存在名字叫做boostrap文件中,FaaS平台在创建函数实例的时候会执行boostrap文件,启动http服务。
把所有请求和参数都转发到Http服务中,让Http服务处理所有的请求。
自定义运行时的实现
源码示例
实现一个TypeScript运行时,TypeScript为jS代码增加了类型系统,可以大大提升代码的可读性和可维护性。
大多数FaaS平台都不支持TypeScript,如果你想用一个TypeScript去编写一个Serverless应用,通常把代码编译成JavaScript再运行,很明显没有直接执行TypeScript代码高效。
如果想要直接运行TypeScript的代码,可以通过ts-node来实现,所以可以基于ts-node来执行一个TypeScript的运行时,这样就可以直接使用TypeScript来编写Serverless应用了。
首先在本地创建一个TypeScript项目,然后安装必要的依赖,为了把依赖都上传到FaaS,需要将ts-node等相关的依赖都安装到项目的node目录中。
自定义运行时需要实现一个http服务来接收FaaS平台的请求。
所以接下来使用TypeScript来编写一个Http服务
这段代码启动了一个http服务,监控9000端口。
然后在本地测试,通过在安装在项目中的ts-node命令来运行这段代码。
在另一个终端使用curl命令来进行测试.
在将自定义运行时部署到FaaS之前,需要让FaaS知道如何启动你的自定义运行时。
你还要创建一个boostrap文件在文件当中添加启动命令,这样让FaaS知道如何启动自定义运行时。
添加函数计算的template.yaml配置
- • runntime的值必须为custom
- • handler属性这里没有实际的意义,但是必须要填写
将自定义运行时部署到函数计算
调用远程函数进行测试
其他无法直接安装在安装目录的编程语言,比如golang和最新版本的Node.js自定义运行时又该怎么实现呢?
如果要沿用TypeScript这种自定义启动命令的方式就需要把golang和代码打包,但是golang是直接安装在操作系统上的,依赖系统环境好像无从下手?
实现一个golang运行时
将运行环境和代码打包,这种思想是不是和容器技术很像?
容器技术就是将应用和运行所依赖环境打包为镜像,这样应用就可以轻松迁移和部署。
那能不能把golang的运行环境构建成docker镜像,然后让FaaS平台能支持自定义容器就能实现任意编程语言的运行时了。
FaaS平台也提供了自定义容器的能力
先构建一个容器镜像,然后通过函数的配置告诉FaaS平台使用你的容器镜像,在函数执行的时候FaaS平台会拉取容器镜像并启动容器执行代码。
与前面的TypeScript运行时一样在自定义容器镜像中也需要实现一个Http服务,用来接收FaaS平台所有的请求。
需要准备一个镜像仓库用来存放你的镜像。
函数计算目前只支持容器镜像服务中的镜像,所以你需要构建自定义运行时镜像然后上传到容器镜像服务中,你可以提前在容器服务中创建一个命名空间或镜像仓库,创建完毕之后记住仓库地址。
使用golang实现一个Http服务
http服务中定义了一个接口 访问/返回golang版本。
当基于容器实现自定义运行时函数计算会将容器的http请求转发到/路由。
本地测试-启动Http服务
测试
构建包含Golang运行时及代码的镜像
alpine是最小体积的golang运行环境。
构建并上传镜像
创建一个template.yaml定义函数配置
- • runtime的值等于custom-container表示该函数是自定义容器
- • 通过customContainerConfig来自定义容器镜像
部署+测试
小结
- • FaaS平台提供了有限的编程语言及版本的支持,使用自定义运行时,可以自定义编程语言进行开发
- • 自定义运行时原理时在函数中实现一个Http服务,FaaS平台将触发器事件转发到你的Http服务
- • 可以通过将运行时上传到FaaS,在bootstrap中定义启动命令来实现自定义运行时
- • 可以通过自定义容器镜像来实现任意编程语言的自定义运行时
自定义运行时是Serverless应用中非常重要的一个功能,可以突破Faas平台运行环境的限制,可以使用FaaS平台所不支持的编程语言进行开发。
基于容器实现自定义运行时你可以很方便的安装依赖,因为依赖都打包到了镜像中。
还可以平滑的将原有系统或传统应用平滑迁移到Serverless架构。
单元测试
单元测试时保证代码质量和应用稳定性的重要手段。
使用Serverless的难点
- • Serverless架构是分布式的,组成Serverless应用的函数是单独运行的,这些函数集合到一起组成分布式架构,你需要对独立函数和分布式应用都进行测试。
- • Serverless架构依赖很多云服务,比如各种FaaS、BaaS等,这些云服务很难在本地模拟
- • Serverless架构是事件驱动的,事件驱动这种异步工作模式也很难在本地模拟。
Serverless单元测试准则
越上层测试速度越慢,成本越高,所以应该写更多的单元测试。
Serverless应用依赖很多云服务,函数参数也与触发器强相关。
- • 准则一:将业务逻辑和依赖的云服务分开,保持业务代码独立,使其更易于扩展和测试
- • 准则二:对业务逻辑编写充分的单元测试保证业务代码的正确性
- • 准则三:对业务代码和云服务编写集成测试,保证应用的正确性
示例代码
保存用户信息,保存成功后并发送欢迎邮件,这段代码的业务逻辑没有和FaaS服务分开,单元测试依赖数据库和邮件服务,这些服务都需要发送网络请求。
代码重构
把存储数据和发送邮件的业务逻辑单独拆分到user类中,并且为user类提供构造函数,注入db和mailer依赖。
db和mailer初始化逻辑和依赖单独放在handler.js文件中,这个文件代码修改频率更低,这样修改后的代码就满足了准则一。
业务逻辑也不依赖任何外部服务,在单元测试时即可以使用真实的db和mailer服务,也可以模拟db和mailer服务,使单元测试简单,使得代码更易于扩展。
想将代码迁移到其他FaaS平台,不用修改业务逻辑,只需要提供handler.js。
提供一个handler.js使其适用于新的FaaS平台从而避免云厂商绑定。
需要对User类编写单元测试
首先需要选择一个测试框架,比如jest,jest可以零配置上手使用。内置的mock功能提供了完善的测试覆盖率报告等。
可以使用npm install -d jest来安装,在package.json中添加一个jest命令即可。
为了方便的管理所有测试用例,可以创建一个__test__目录,在里面新建名为users.test.js文件,用来编写Users类的测试用例。
jest默认会将test目录或包含test关键词文件当中代码用作单元测试。
Users类主要提供save方法,save方法的功能把用户信息存入数据库,然后给用户发送一封邮件,那么代码运行中可能存在这几种情况。
- • 用户信息写入数据库成功发送邮件成功
- • 用户信息写入数据库成功,发送邮件失败
- • 用户信息写入数据库失败
这个时候可能感觉有到有点困难了,写数据库或发邮件都依赖远程服务,而且还要那么多异常情况来考虑,怎么进行测试呢?
对于save方法来说本质上不需要关注远程服务,只需要考虑分支逻辑的正确性。
这个地方可以对db和mailer模拟,模拟db和mailer的各自异常情况,然后观察save方法的执行结果是否正确。
FaaS提供mock功能,可以让我们对类进行模拟。
对db.saveUser和mailer.sendWelcomeEmail函数进行模拟
所以只需要对这两个函数进行模拟,当db.saveUser执行成功,发送邮件的返回值为true。
针对save方法编写第一个测试用例
使用npm run test测试
其他两种情况的测试用例
运行单元测试并生成测试覆盖率
覆盖率100%表示所有代码都经过了测试。
模拟业务变动的情况比如回调函数不再返回userId
运行单元测试
再次运行单元测试就无法通过,从单元测试结果来看业务逻辑发生了不兼容变更。
这个时候就需要考虑这个变更对上下游的影响,避免造成线上业务风险,在确认没有风险之后,再去修改测试用例。
小结
- • 为了更好的管理代码,建议单元测试目录结构和业务代码结构保持一致
- • 将业务代码和依赖的云服务分离开来,这样才能方便测试
- • 单元测试速度足够快,因为单元测试是用来辅助开发的,运行若是非常繁琐,运行速度慢会影响开发效率
- • 单个测试要小于200毫秒,整个系统的测试要小于10分钟
- • 要隔离一切外部调用,比如不能读磁盘、不能有网络调用,不能写数据库,不能依赖环境变量,不能依赖系统时间
- • 必要时需要对外部进行模拟,用来确保单元测试不会对外部环境有所影响。由于模拟外部aip可能导致内部代码行为发生改变,所以要按照最新的外部api描述进行模拟
- • 单一职责,一个测试用例只用来验证一个行为
- • 单元测试代码是最好的文档和描述,因此单元测试需要明确代码的意图。好的单元测试应该是自描述的能对代码进行解释说明
- • 单元测试并不是测试的全部,这是用来保证单个功能、组件的正确性,在Serverless中依旧使用集成测试来验证所有的组件集成在一起时运行是否正常。在设计和编写业务代码的时候需要考虑代码是否有利于测试,在这个基础之上,业务代码的单元测试和传统的应用单元测试的方法是互通的。
- • Serverless应用由于其分布式、依赖云服务、事件驱动等特性导致编写单元测试很困难,为了方便编写单元测试,需要将业务逻辑和依赖的云服务分离开来
- • 编写单元测试时,需要考虑速度、隔离性、单一职责等因素避免单元测试称为开发的负担