
3.6 运行时和自定义运行时
本节将通过介绍运行时和自定义运行时,对函数运行环境的架构进行说明,让读者进一步了解FaaS的执行原理和调度机制,实现自定义函数的运行环境。
3.6.1 运行时和自定义运行时的概念
FaaS平台的每个函数的底层是基于轻量化虚拟机(MicroVM)实现的,而每个函数都需要通过运行时环境的承载来提供服务,因此可以将运行时看作每个函数的执行环境。函数、运行环境和轻量化虚拟机的层级关系如图3-8所示。

图3-8 函数、运行环境和轻量化虚拟机的层级关系
当前主流云厂商的函数平台都提供常用运行环境的预置和封装,并能够持续支持和迭代。编程语言本身十分丰富,并且持续更新,对于平台来说,维护所有语言和版本并不现实。此外,如果用户希望针对预置运行环境安装自定义的扩展,比如PHP环境中PostgreSQL数据库的扩展Pdo_pgsql,则需要平台进一步开放限制,实现支持自定义插件和扩展包的安装。当前,各大云厂商已经通过开放公共容器镜像等方式,支持函数挂载容器镜像,从而实现自定义开发和依赖安装。此外,用户也可以通过自定义运行时来实现这一需求。接下来我们对运行时和自定义运行时的原理进行分析,实现自定义运行环境。
1. 运行时的生命周期
标准运行时的生命周期由两部分组成:初始化阶段和调用阶段。在初始化阶段,会准备运行函数需要的资源,然后拉取用户代码,对函数进行初始化,并对外暴露入口函数(handler)接口。在调用阶段,通过事件触发,对函数进行调用,并执行函数内部定义的逻辑进行响应,如图3-9所示。

图3-9 运行时的生命周期
2. 自定义运行时的生命周期
自定义运行时的生命周期如图3-10所示,可以看到,不同的触发器,如网关触发器、定时触发器,在触发事件后,会先将事件分发给Runtime API,之后由用户实现自定义运行时部分,和Runtime API通过HTTP/RPC等方式进行交互。用户实现自定义运行时主要需要考虑以下几个部分:首先需要自定义引导文件bootstrap,通过bootstrap进行初始化,和负责调度的引擎,即Runtime API进行交互,实现运行时的自定义,之后再执行函数的业务逻辑。

图3-10 自定义运行时的生命周期
因此,与标准的内置运行时相比,自定义运行时的开放性主要体现在以下三个方面。
- 支持自定义开发语言。
- 开放了运行时的初始化阶段。
- 支持通用的HTTP/RPC协议通信。
接下来我们详细解读自定义运行时每个阶段所做的工作。首先,启动运行时寻找代码包中的bootstrap文件,此阶段引导程序bootstrap做启动之前的准备和数据加载等工作,同时也需要确保该文件的权限为chmod 755。
之后进入函数的初始化阶段。该阶段之前是通过FaaS平台实现的,在自定义运行时中则开放给用户。本阶段可以做许多初始化的工作,例如上文提到的加载扩展、启动自定义插件、创建连接池等。初始化完成后,可以通过/runtime/init/ready的API通知平台初始化已经完成。
最后进入函数调用阶段,通过/runtime/invocation/next API中长轮询的方式获取时间,并调用函数处理对应事件。处理完毕后,通过/runtime/invocation/response | error等API推送函数的处理结果或者异常报告。由此可见,在函数调用和初始化阶段,都可以通过用户自定义和平台的运行时API进行交互,定义函数的生命周期。
通过自定义运行时的能力,FaaS函数平台可以完美支持Deno、.NET、WASM、Swift等编程语言,同时也能满足用户对运行时自定义扩展和插件的需求,进一步扩展FaaS函数的边界。
3.6.2 自定义运行时示例
接下来,我们通过一个Deno环境的函数示例,展示FaaS平台自定义运行时的运行原理。Deno由Node.js的创始人Ryan Dahl在2017年创立。和Node.js一样,Deno也是一个服务器运行时,它支持多种语言,可以直接运行JavaScript和TypeScript代码。对比Node.js,Deno提供了安全的执行环境,并且能够开箱即用地支持TypeScript等开发语言,在开发者中也越来越受欢迎。但当前各大云厂商的FaaS平台默认仅支持Node.js运行时,因此在本节,我们通过自定义运行时创建Deno环境。
在创建自定义运行时Custom Runtime函数前,首先需要创建运行时引导文件bootstrap和函数处理文件。其中,bootstrap是运行时入库引导程序文件。创建bootstrap文件,定义Deno的文件目录并执行入口文件entry.js,如代码清单3-2所示。
代码清单3-2 Deno环境下创建bootstrap文件
# 设置Deno缓存目录 export DENO_DIR=/tmp # 取消代理 unset http_proxy unset https_proxy # 执行入口文件 deno run --allow-net --allow-env entry.js
入口文件创建完毕后,接下来需要创建函数处理文件。函数处理文件主要包含函数逻辑的具体实现,其执行方式及参数可以通过运行时自定义实现。在本例中,创建entry.ts作为入口文件,在其中创建HTTP Server、监听固定端口、进行函数的初始化等,如代码清单3-3所示。
代码清单3-3 Deno环境的函数处理文件
const SCF_RUNTIME_API: string | undefined = Deno.env.get('SCF_RUNTIME_API'); const SCF_RUNTIME_API_PORT: string | undefined = Deno.env.get( 'SCF_RUNTIME_API_PORT', ); const READY_URL = 'http://${SCF_RUNTIME_API}:${SCF_RUNTIME_API_PORT}/runtime/init/ready'; const EVENT_URL = 'http://${SCF_RUNTIME_API}:${SCF_RUNTIME_API_PORT}/runtime/invocation/next'; const RESPONSE_URL = 'http://${SCF_RUNTIME_API}:${SCF_RUNTIME_API_PORT}/runtime/invocation/response'; const ERROR_URL = 'http://${SCF_RUNTIME_API}:${SCF_RUNTIME_API_PORT}/runtime/invocation/error'; import { app } from './src/main.jsx'; const PORT = 9000; async function post(url = '', data = {}) { const response = await fetch(url, { method: 'POST', // 默认为GET, 也支持POST、PUT、DELETE等请求方式 body: JSON.stringify(data), }); return response.text(); } async function forwardEventToRequest(event: any) { // 获取请求的事件 console.log( '++++++++ Req Url +++++++', 'http://localhost:${PORT}/${event.path}', ); const response = await fetch('http://localhost:${PORT}/${event.path}', { method: 'GET', }); const body = await response.text(); const apigwReturn = { statusCode: 200, body: body, headers: { 'Content-Type': 'text/html; charset=UTF-8', }, isBase64Encoded: false, }; return apigwReturn; } async function run() { // 事件循环 // 获取事件 const eventObj: any = await fetch(EVENT_URL); const event = await eventObj.json(); await app.start({ port: PORT }); const apigwReturn = await forwardEventToRequest(event); await app.close(); if (!event) { const error = await post(ERROR_URL, { msg: 'error handling event' }); console.log('Error response: ${error}'); } else { console.log('Send Invoke Response: ${event}'); await post(RESPONSE_URL, apigwReturn); } } // 完成 POST 请求,初始化完毕 post(READY_URL, { msg: 'deno ready' }).then(() => { console.log('Initialize finish'); run(); });
示例完整代码地址为https://github.com/serverless-plus/serverless-deno。