函数计算基于传统常驻应用所拓展的运行时扩展功能,能够有效帮助您消除闲置成本。本文介绍函数计算的运行时扩展功能原理、计费说明以及如何通过控制台、Serverless Devs和SDK配置PreFreeze和PreStop回调函数并提供示例代码。
常驻应用与FaaS运行环境
传统常驻的虚拟机或者托管容器类服务通常从实例启动到结束作为计费区间,即使该时间段没有业务请求仍纳入计费。函数计算提供1 ms计费粒度,且只在有实际请求的区间内计费,在请求以外的时间段内实例会被冷冻。这样基本消除了完全事件驱动的计费模型的闲置成本。然而冷冻机制也会打破一些传统架构下long-running进程的假设,加大应用迁移的难度。由于函数计算拥有特殊的运行环境,面对没有冷启动的场景,例如常用的开源分布式链路追踪Tracing Analysis库和第三方APM解决方案等,将无法追踪并正确上报其数据。
阻碍传统应用平滑迁移至Serverless架构的痛点如下。
异步背景指标数据延迟或丢失:如果在请求期间没有发送成功,则可能被延迟至下一次请求,或者数据点被丢弃。
同步发送指标增加延迟:如果在每个请求结束后都调用类似Flush接口,不仅增加了每个请求的延迟,对于后端服务也产生了不必要的压力。
函数优雅下线受阻:实例关闭时应用有清理连接、关闭进程、上报状态等需求。在函数计算中实例的下线时机开发者无法掌握,也缺少Webhook通知函数实例下线事件。

编程模型扩展
函数计算针对上述痛点发布了运行时扩展(Runtime Extensions)功能。该功能在现有的HTTP服务编程模型上扩展,在已有的HTTP服务器的模型中增加了PreFreeze和PreStop Webhooks,扩展开发者实现HTTP handler,监听函数实例生命周期事件。

PreFreeze
在每次函数计算服务决定冷冻当前函数实例前,函数计算服务会调用HTTP GET /pre-freeze路径,扩展开发者负责实现相应逻辑以确保完成实例冷冻前的必要操作,例如等待指标发送成功等。函数调用InvokeFunction的时间不包含PreFreeze hook的执行时间。
PreStop
在每次函数计算决定停止当前函数实例前,函数计算服务会调用HTTP GET /pre-stop路径,扩展开发者负责实现相应逻辑以确保完成实例释放前的必要操作,如关闭数据库链接,以及上报、更新状态等。
注意事项
PreFreeze和PreStop回调函数输入参数没有event参数。
PreFreeze和PreStop回调函数无返回值,在函数末尾增加返回逻辑无效。
所有Runtime均支持配置PreStop回调函数;Python、PHP及C# Runtime不支持配置PreFreeze回调函数。
如果使用Java Runtime,您需要将fc-java-core更新至1.4.0及以上版本,否则无法使用PreFreeze和PreStop扩展回调函数。具体操作,请参见HTTP请求处理程序(HTTP Handler)。
如果您使用的是非Web Server模式的Custom Container Runtime,则配置的PreFreeze和PreStop回调无效。
计费说明
唤起PreFreeze或PreStop中产生的费用计费方式与InvokeFunction计费方式一致。具体信息,请参见计费方式。扩展HTTP hooks请求数不计费。扩展在单实例多并发的场景下依然适用,假设同时有多个Invoke请求在相同实例执行,在所有请求都结束后,即将冷冻实例之前,会调用一次PreFreeze hook。
以下图为例,函数规格为1 GB,从t1 PreFreeze开始到t6请求2结束的时间段(假设为1秒),则实例执行时间为t6-t1,消耗1s * 1 GB=1 CU。

前提条件
通过控制台配置PreFreeze和PreStop回调
当您使用控制台创建函数时,函数计算不支持您配置PreFreeze及PreStop回调,您需要在更新函数时配置该回调函数。
- 登录函数计算控制台,在左侧导航栏,单击服务及函数。
- 在顶部菜单栏,选择地域,然后在服务列表页面,单击目标服务。
- 在函数管理页面,单击目标函数操作列的配置。
在编辑函数配置页面的实例生命周期回调区域,设置回调程序与超时时间,然后单击保存。
说明每一个扩展函数都需要配置单独的回调程序和超时时间,其中回调程序格式为[文件名].[扩展函数名]。例如在Python Runtime中,创建函数时指定的PreStop回调为index.preStopHandler,那么文件名为index.py,PreStop函数名为preStopHandler。
配置扩展函数后,您需要在代码执行中实现对应的函数。
单击函数代码页签,在代码编辑区域,输入扩展函数代码。
例如,您配置的PreStop回调程序为
index.preStopHandler
,则需要实现preStopHandler函数。本文以运行环境为Node.js 12的事件函数为例。关于不同Runtime中扩展函数的示例代码,请参见示例代码。说明在线IDE支持PHP、Python、Node.js和Custom Runtime;但不支持Java、Go和.NET这类编译性语言,以及Custom Container。
单击代码编辑器上方的部署代码,然后单击测试函数。
通过Serverless Devs配置PreFreeze和PreStop回调
使用Serverless Devs配置PreFreeze、PreStop扩展函数时,配置文件示例代码如下所示:
codeUri: './code.zip'
......
instanceLifecycleConfig:
preFreeze:
handler: index.PreFreeze
timeout: 60
preStop:
handler: index.PreStop
timeout: 60
如果您需要关闭某个扩展函数,需要将扩展函数的handler
参数显示置空,否则后端默认不更新。例如关闭PreFreeze函数,您需要按照以下配置进行部署更新,此时PreFreeze函数的timeout
参数已无效。
codeUri: './code.zip'
......
instanceLifecycleConfig:
preFreeze:
handler: ""
timeout: 60
preStop:
handler: index.PreStop
timeout: 60
通过SDK配置PreFreeze和PreStop回调
您可以通过SDK部署和更新扩展函数。本文以Go SDK为例,介绍在创建函数时配置PreStop和PreFreeze函数的示例代码:
package main
import (
"fmt"
"os"
"github.com/aliyun/fc-go-sdk"
)
func main() {
/*
阿里云账号AccessKey拥有所有API的访问权限,建议您使用RAM用户进行API访问或日常运维。
建议不要把AccessKey ID和AccessKey Secret保存到工程代码里,否则可能导致AccessKey泄露,威胁您账号下所有资源的安全。
本示例以将AccessKey和AccessSecretKey保存在环境变量中实现身份验证为例。
运行本示例前请先在本地环境中设置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
在FC Runtime运行环境下,配置执行权限后,ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET环境变量会自动被设置。
*/
client, _ := fc.NewClient(
os.Getenv("ENDPOINT"),
"2016-08-15",
os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"),
os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"),
)
serviceName := "ExtensionService"
functionName := "ExtensionFunction"
createFunctionInput := fc.NewCreateFunctionInput(serviceName).WithFunctionName(functionName)
// 配置PreStop和PreFreeze函数。
preStopHook := fc.NewLifecycleHook().WithHandler("index.preStop").WithTimeout(int32(30))
preFreezeHook := fc.NewLifecycleHook().WithHandler("index.preFreeze").WithTimeout(int32(10))
instanceLifecycle := fc.NewInstanceLifecycleConfig().WithPreStopHook(preStopHook).WithPreStopHook(preFreezeHook)
createFunctionOutput, err := client.CreateFunction(createFunctionInput.WithInstanceLifecycleConfig(instanceLifecycle))
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Printf("CreateFunction response: %s \n", createFunctionOutput)
}
return
}
示例代码
本文介绍不同Runtime下,扩展函数的实现Demo。相同Runtime下,PreFreeze和PreStop函数定义一致,因此本文仅介绍一种扩展函数的实现Demo。
Python PreStop
# -*- coding: utf-8 -*-
import logging
def handler(event, context):
logger = logging.getLogger()
logger.info('handler start')
return "ok"
def pre_stop(context):
logger = logging.getLogger()
logger.info('preStop start')
Node.js PreFreeze
'use strict';
var counter = 0;
module.exports.handler = function(event, context, callback) {
counter += 2;
callback(null, String(counter));
};
module.exports.preFreeze = function(ctx, callback) {
counter += 1;
callback(null, "");
};
PHP PreStop
<?php
$counter = 0;
function fc_func_pre_stop($context) {
sleep(2);
}
function fc_func_handler($event, $context) {
global $counter;
$counter += 2;
return $counter;
}
?>
C# PreStop
using System;
using System.IO;
using Aliyun.Serverless.Core;
using Microsoft.Extensions.Logging;
namespace Aliyun.Serverless.TestHandlers
{
public class PreStopAndPojoCounter
{
public int counter;
public PreStopAndPojoCounter()
{
this.counter = 0;
}
public void PreStop(IFcContext context)
{
// todo
}
// todo
}
}
Java PreFreeze
import com.aliyun.fc.runtime.Context;
import com.aliyun.fc.runtime.PreFreezeHandler;
import java.io.IOException;
public class PreFreezeNormal implements PreFreezeHandler {
@Override
public void preFreeze(Context context) throws IOException {
// todo
}
}
查询回调函数相关日志
配置函数实例生命周期回调并执行代码实现对应的回调函数后,您可以查询实例生命周期回调函数的相关日志。
- 登录函数计算控制台,在左侧导航栏,单击服务及函数。
- 在顶部菜单栏,选择地域,然后在服务列表页面,单击目标服务。
在函数管理页面,单击目标函数,然后单击调用日志页签。
在调用请求列表页签,找到想要查询的请求行,复制实例 ID,然后单击高级日志。
页面跳转至日志服务控制台的查询和分析页面。您可以使用复制的实例ID,查询所有生命周期回调函数的Start/End日志;还可以使用
实例ID+函数实例生命周期回调关键字
查询指定回调函数的Start/End日志,例如,c-62833f38-20f1629801fa4bd***** and PreStop
。此外,您还可以根据Start/End日志中的RequestId查询请求的日志信息。如果用户日志中没有RequestId,单击
查询日志。