Serverless从入门到进阶:架构、原理与实践
上QQ阅读APP看书,第一时间看更新

3.6 运行时和自定义运行时

本节将通过介绍运行时和自定义运行时,对函数运行环境的架构进行说明,让读者进一步了解FaaS的执行原理和调度机制,实现自定义函数的运行环境。

3.6.1 运行时和自定义运行时的概念

FaaS平台的每个函数的底层是基于轻量化虚拟机(MicroVM)实现的,而每个函数都需要通过运行时环境的承载来提供服务,因此可以将运行时看作每个函数的执行环境。函数、运行环境和轻量化虚拟机的层级关系如图3-8所示。

046-1

图3-8 函数、运行环境和轻量化虚拟机的层级关系

当前主流云厂商的函数平台都提供常用运行环境的预置和封装,并能够持续支持和迭代。编程语言本身十分丰富,并且持续更新,对于平台来说,维护所有语言和版本并不现实。此外,如果用户希望针对预置运行环境安装自定义的扩展,比如PHP环境中PostgreSQL数据库的扩展Pdo_pgsql,则需要平台进一步开放限制,实现支持自定义插件和扩展包的安装。当前,各大云厂商已经通过开放公共容器镜像等方式,支持函数挂载容器镜像,从而实现自定义开发和依赖安装。此外,用户也可以通过自定义运行时来实现这一需求。接下来我们对运行时和自定义运行时的原理进行分析,实现自定义运行环境。

1. 运行时的生命周期

标准运行时的生命周期由两部分组成:初始化阶段和调用阶段。在初始化阶段,会准备运行函数需要的资源,然后拉取用户代码,对函数进行初始化,并对外暴露入口函数(handler)接口。在调用阶段,通过事件触发,对函数进行调用,并执行函数内部定义的逻辑进行响应,如图3-9所示。

046-2

图3-9 运行时的生命周期

2. 自定义运行时的生命周期

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

047-1

图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。