使用函数 initializer 接口优化应用层冷启动

背景

用户函数调用链路包括以下几个阶段:1)系统为函数分配计算资源;2)下载代码;3)启动容器并加载函数代码;4)用户函数内部进行初始化逻辑;5)函数处理请求并将结果返回。其中1,2,3步是系统层面的冷启动开销,通过对调度以及各个环节的优化,函数计算(FC)能做到负载快速增长时稳定的延时,细节详见 函数计算系统冷启动优化。第4步是函数内部初始化逻辑,属于应用层面的冷启动开销,例如深度学习场景下加载规格较大的模型、数据库场景下连接池构建、函数依赖库加载等等。为了减小应用层冷启动对延时的影响,函数计算推出了 initializer 接口,系统能识别用户函数的初始化逻辑,从而在调度上做相应的优化。

功能简介

现在用户能为函数设置 initializer 和 handler 两个入口,分别对应初始化逻辑和请求处理逻辑。系统首先调用 initializer 完成函数的初始化,成功后再调用 handler 处理请求。Initializer 是可选的,用户也可不实现,此时系统将跳过 initializer,直接调用 handler 处理请求。

引入 initializer 接口的价值:

  • 分离初始化逻辑和请求处理逻辑,程序逻辑更清晰,让用户更易写出结构良好,性能更优的代码;
  • 用户函数代码更新时,系统能够保证用户函数的平滑升级,规避应用层初始化冷启动带来的性能损耗。新的函数实例启动后能够自动执行用户的初始化逻辑,在初始化完成后再处理请求;
  • 在应用负载上升,需要增加更多函数实例时,系统能够识别函数应用层初始化的开销,更精准的计算资源伸缩的时机和所需的资源量,让请求延时更加平稳;
  • 即使在用户有持续的请求且不更新函数的情况下,FC系统仍然有可能将已有容器回收或更新,这时没有平台方(FC)的冷启动,但是会有业务方冷启动,Initializer可以最大限度减少这种情况;

Initializer 接口规范

各个 runtime 的 initializer 接口有以下共性:

  • 无自定义参数
    Initializer 不支持用户自定义参数,只能获取函数计算提供的 context 参数中的变量进行相关逻辑处理,详细介绍请参考 context文档
  • 无返回值
    用户无法从 invoke 的响应中获取 initializer 预期的返回值。如果 initializer 执行失败,用户能够通过 response 中的 X-Fc-Error-Type 和 body 来确认 initializer 无法成功执行的出错类型,建议开启 Logging 功能,便于错误定位;
  • 超时时间
    用户可单独设置 initializer 的超时时间,与 handler 的超时相互独立,但最长不超过 300 秒;
  • 执行时机
    运行函数逻辑的进程称之为函数实例,运行在容器内。系统会根据用户负载伸缩函数实例。每当有新函数实例创建时,系统会首先调用 initializer。系统保证一定 initializer 执行成功后才会执行 handler 逻辑;
  • 最多成功执行一次
    系统保证每个函数实例启动后只会成功执行一次 initializer 。如果执行失败,那么该函数实例在收到 Invoke 请求之后都会先执行 initializer ;一旦执行成功,那么该实例的生命周期内不会再执行 initializer ,收到 Invoke 请求之后只执行请求处理函数;
  • initializer 入口命名
    除 Java 外,其他 runtime 的 initializer 入口命名规范与原有的 处理函数入口命名 保持一致,格式为 “[文件名].[ initializer 名]”,其中 initializer 名可自定义。Java 需要定义一个类并实现函数计算预定义的初始化接口,细节可见 下文
  • 计量计费
    Initializer 的执行时间也会被计量,用户需要为此付费;

支持 runtime

下文将对各个 runtime 的 initializer 编写规则进行介绍:

python

当使用 Python 进行编程时,可参考下面示例:

# file :main.py
# initializer: main.my_initializer
def my_initializer(context):
    print("hello world!")

更多细节请参考 python 函数入口

nodejs

当使用 Nodejs 进行编程时,可参考下面示例:

// file :main.php
// initializer : main.my_initializer
exports.my_initializer = function(context, callback) {
    console.log('hello world!');
    callback(null, '');
};

更多细节请参考 nodejs 函数入口

php

当使用 Php 进行编程时,可参考下面示例:

// file: main.js
// initializer: main.my_initializer
<?php
function my_initializer($context) {
    echo 'hello world!' . PHP_EOL;
}
?>

更多细节请参考 php 函数入口

java

java 针对流式输入和通过泛型的方式自定义输入和输出的两种函数形式都支持 initializer,当需要在 JAVA runtime 中添加 initializer 功能时,需在原有的函数结构基础上额外实现 initializer 预定义的接口。

一个简单的流式输入和 initializer 结合的示例如下:

package example;

import com.aliyun.fc.runtime.Context;
import com.aliyun.fc.runtime.FunctionComputeLogger;
import com.aliyun.fc.runtime.StreamRequestHandler;
import com.aliyun.fc.runtime.FunctionInitializer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class HelloFC implements StreamRequestHandler, FunctionInitializer {
    @Override
    public void initialize(Context context) {
        FunctionComputeLogger logger = context.getLogger();
        logger.debug(String.format("RequestID is %s %n", context.getRequestId()));
    }

    @Override
    public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
        FunctionComputeLogger logger = context.getLogger();
        logger.debug(String.format("RequestID is %s %n", context.getRequestId()));
        output.write(new String("hello world!").getBytes());
        output.flush();
    }
}
  • 从上面的示例可以看到,在 Java runtime 中,用户的函数如果需要添加 initializer 功能,需在实现原有的 StreamRequestHandler 接口基础上,额外实现 initializer 的 FunctionInitializer 接口。
  • initializer 所属包名和类名可以自定义。"initializer" 的格式同为 {package}.{class}::{method},与 处理函数不同的是处理函数中的 method 为 handleRequest,initializer 的 method 为 initialize。根据定义可知此示例的 initializer 为 example.HelloFC::initialize
  • 在通过泛型方式自定义输入输出的函数中,添加 initializer 和上面的步骤一致,更多细节可参考函数计算官方文档 java 函数入口

开发指南

当使用 initializer 接口时,可以通过 APISDK、控制台、函数计算提供的丰富工具链(fclifun等工具)进行创建和更新。

SDK 开发

支持 initializer 的 SDK 有 Nodejs、Python、Php、Java 。以 Nodejs runtime 和 Php SDK 结合为例,创建 initializer 并进行更新操作。

对 initializer 的更新可以从有更新到无,即关闭 initializer 功能。也可以从无更新到有,即开启 initializer 功能。

  • 通过 Nodejs 编写两个简单的函数如下:
// file: main.js
exports.initializer = function(context, callback) {
    console.log('hello initializer!');
    callback(null, "");
};

exports.handler = function(event, context, callback) {
    callback(null, string("initializer"));
};
// file: new_main.js
exports.newInitializer = function(context, callback) {
    console.log('hello new initializer!');
    callback(null, "");
};

exports.handler = function(event, context, callback) {
    callback(null, string("new initializer"));
};
  • 对文件 main.js 压缩为 main.zip,文件 new_main.js 压缩为 new_main.zip
  • 通过 Php SDK 创建函数并进行更新,示例如下:
 <?php

require_once __DIR__ . '/vendor/autoload.php';
use AliyunFC\Client;

$fcClient = new Client([
    "endpoint" => '<Your Endpoint>',
    "accessKeyID" =>'<Your AccessKeyID>',
    "accessKeySecret" =>'<Your AccessKeySecret>'
]);

$serviceName = 'test_service';
$functionName = 'test_function';
// Create service.
$fcClient->createService($serviceName);

// Create function.
$fcClient->createFunction(
    $serviceName,
    array(
    'functionName'             => $functionName,
        'handler'              => 'main.handler',
        'initializer'          => 'main.initializer',
        'runtime'              => 'php7.2',
        'memorySize'           => 128,
        'description'          => "test initializer",
        'code'                         => array(
            'zipFile' => base64_encode(file_get_contents(<Your main.zip>)),
        ),
    )
);

$invkRes = $fcClient->invokeFunction($serviceName, $functionName);
// The value of $invkRes['data'] is 'initializer'

$fcClient->updateFunction(
    $serviceName,
    $functionName,
    array(
    'functionName'             => $functionName,
        'handler'              => 'main.handler',
        'initializer'          => 'main.newInitializer',
        'runtime'              => 'php7.2',
        'memorySize'           => 128,
        'description'          => "test update initializer",
        'code'                 => array(
            'zipFile' => base64_encode(file_get_contents(<Your new_main.zip>)),
        ),
    )
);

$invkRes = $fcClient->invokeFunction($serviceName, $functionName);
// The value of $invkRes['data'] is 'new initializer'

// Delete function
$fcClient->deleteFunction($serviceName, $functionName);

// Delete service.
$fcClient->deleteService($serviceName);

fcli 开发

为支持创建、更新 initializer ,fcli 为引入了两个新的参数:

  • -i —initializer string 设置 initializer,initializer 格式为 “文件名. initializer 名”。例如 hello_world.initializer 指定了 initializer 的调用入口为 hello_world.js 文件中的 initializer 函数。
  • -e —initializationTimeout int32 initializer 最大运行时间,单位为秒。

更多参数请参考 fcli开发文档

创建 initializer 操作如下:

// 在相应service目录下
// 代码存储在oss上且包含 initializer,-t 指定runtime,-h 指定函数入口,-i 指定 initializer 入口,-e 指定 initializer 超时时间,-b 指定代码所在的oss bucket,-o 指定了代码在bucket中的object key
>>> mkf myFunction -t nodejs6 -h myFunction.handler -i  myFunction.initializer -e 60 -b ossBucketName -o objectKey

更新 initializer 操作如下:

// 在相应service目录下
// 代码存储在 OSS 上且包含 initializer,-t 指定 runtime,-i 指定 initializer 入口,-e 指定 initializer 超时时间,-b 指定代码所在的 oss bucket,-o 指定了代码在 bucket 中的 object key
// 将initializer 从  myFunction.initializer 更新至 myFunction.newInitializer
>>> upf myFunction -t nodejs6 -i myFunction.newInitializer -e 30  -b ossBucketName -o objectKey

// 将initializer 从 myFunction.newInitializer 更新至空,关闭 initializer 功能。
>>> upf myFunction -t nodejs6  -i  "" -b ossBucketName -o objectKey

// 将initializer 从空更新至 myFunction.newInitializer,开启 initializer 功能。
>>> upf myFunction -t nodejs6 -i myFunction.newInitializer -e 30 -b ossBucketName -o objectKey

fun 部署

Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作,详细介绍请参考 fun开发文档

Fun 为 initializer 的新增两个 Properties,可以支持在部署原有处理函数的基础上,对新增的 initializer 同样做到一起部署。
新增的 Properties 如下:

  • Initializer:initializer 入口
  • InitializationTimeout:initializer 最大运行时间,单位为秒。

下面我们用一个简单的示例演示如何使用 fun 进行部署,并在下文控制台开发中展示该函数的运行结果:

# -*- coding: utf-8 -*-

import logging

init_flag = False
init_count = 0

def my_initializer(context):
    global init_flag
    global init_count
    init_flag = True
    init_count += 1

def my_handler(event, context):
    global init_flag
    global init_count
    logger = logging.getLogger()
    if not init_flag:
        logger.info('no initializer has executed, init_count: %d', init_count)
        return init_count
    logger.info('initializer has executed, init_count: %d', init_count)
    return init_count

针对 template.yml 文件,在原有 Properties 的基础上增加 InitializerInitializationTimeout 两个新的 Properties。

ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
  initializerDemo: # service name
    Type: 'Aliyun::Serverless::Service'
    Properties:
      Description: 'initializer demo'
    initializer: # function name
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: main.my_handler
        Initializer: main.my_initializer
        CodeUri: './'
        Description: 'Hello world with initializer!'
        Runtime: python2.7
        MemorySize: 1024
        Timeout: 300
        InitializationTimeout: 60

编写完后直接执行 fun deploy 进行一键部署即可生效,部署成功后可以到 函数计算控制台 查看 initializerdemo 服务中 initializer 函数是否已经创建成功,如果已经成功,点击执行,查看函数执行结果。

函数执行结果恒为 1,且不会走到 if not init_flag: 逻辑中。

控制台

在函数计算控制台新建函数页面的环境配置中,可根据用户的需求来决定是否开启初始化功能 (初始化开关默认关闭)。如果需要,请点击开启初始化功能 开关,并配置 initializer 入口initializer 超时时间 ; 如不需此功能,请确认没有打开 开启初始化功能 开关。更多关于函数计算控制台的操作请参考 控制台文档

下面以上文 fun 开发中使用的示例进行演示:

  • 开启初始化功能并进行配置
    使用函数 initializer 接口优化应用层冷启动
  • 运行函数
    使用函数 initializer 接口优化应用层冷启动

在函数计算控制台进行更新函数操作时,如果需要更新 initializer 入口initializer 超时时间 ,可以在控制台函数概览中对指定函数进行修改,如果将 initializer 入口 修改为空,即可关闭初始化功能。

  • 更新 initializer
    使用函数 initializer 接口优化应用层冷启动
  • 运行函数
    使用函数 initializer 接口优化应用层冷启动

总结

阿里云函数计算的 initializer 功能,可以有效的解决业务冷启动开销较大带来的痛点。同时借助函数计算丰富的工具链和多语言 SDK 的支持,可以帮助您快速上手,提高开发效率。

最后欢迎大家通过扫码加入我们用户群中,使用过程中有问题或者有其他问题可以在群里提出来。函数计算官网客户群(11721331)。

使用函数 initializer 接口优化应用层冷启动
上一篇:PostgreSQL 11 preview - compress method 接口 - 暨开放接口系列


下一篇:SQL查询数据并插入新表