事件驱动带来的挑战及功能背景
函数计算异步调用
函数计算非常适合构建事件驱动的应用。这类应用往往通过不同的事件触发器(如 OSS 触发器、时间触发器、消息队列触发器等)来触发一次请求,并调用具体的微服务模块来处理不同逻辑。函数计算按请求量计费、高度弹性的特性以及原生 Serverless 化的架构能够很好的为业务执行提供算力并控制成本。作为各类云服务的黏合剂,用户在函数计算中也能够很容易的使用各类云服务作为 BaaS 层来实现存储、数据持久化、记录日志等需求。
在我们的实际用户场景中,有很大一类是通过触发器进行数据处理(比如图片处理、视频转码)任务。这类任务实时性不高,但希望能够保证成功率及自动化程度,降低人为介入次数。当然,在实际的使用的过程中,用户有时会面临以下几个方面的挑战。
异步调用面临的挑战
异步事件的可观测性
基于事件驱动的应用自然也存在各类挑战,其中一点就是整个服务链路的可观测性。不同于单体或模块化的应用,一个事件在微服务化(或 Serverless 化)的应用中是非常难追寻的。整个链路中存在各类用于解耦的消息队列、存储等,往往很多链路上同时存在多个异步调用。这类异步调用为问题排查带来了比较大的挑战。”我从哪里来“、”我到哪里去“成为了微服务间消息传递需要额外关注的地方。
同事件驱动的应用一样,用户要想准确知道一个异步调用消息到底是否被成功消费,失败是因为丢失了还是其他原因要花费很多周折。比如某个用户的任务(函数)偶尔会执行超时(数据量导致),但是用户无法感知也无法自动化处理这类超时失败的场景。往往都是很久之后发现数据缺失,之后再进行手动补偿及修复后重新触发函数。目前解决这类问题比较常见的一些做法就是在函数中打印日志,或者根据调用结果在函数中调用一些其他服务(如 MNS)来通知执行情况。这类解法不但增加了业务无关的工作量,也难以保证数据不被遗漏(比如错误出现在打日志前、其他服务暂时不可用等)。
如果我们能够提供一些指标,并在用户函数执行成功/失败时调用其他服务通知执行结果,那么用户将很容易的使用该功能进行执行结果的自动化处理。
事件驱动闭环
在另外的一些场景下,用户不仅希望函数计算能够作为事件的消费者,也希望函数能够作为事件的生产者。比如,函数开发者希望能够将用户的请求经过简单处理直接转为事件,并触发后续流程;或者希望由某个函数定时生产一些事件来触发一系列处理流程。
一个比较典型的场景是使用 OSS 触发器调用函数来处理用户上传的图片。但用户如果一次上传的图片很多,可能造成函数执行超时。这类问题的解决思路就是将函数从消费者的角度转变为生产者,将一次处理的数据划分为多个可稳定执行完成的大小分片。比如 OSS 触发函数后,这个函数不进行处理,而是将数据分类分片,生成一批事件交由实际处理函数来进行处理。使用 Serverless 工作流是很好的解决方案,但是如果用户流程逻辑非常简单,又希望快速设计服务原型,如果我们能够提供一种在函数执行完成后可靠的调用其他服务的功能,用户的接入成本将大大减少。
异步调用策略的可定制性
为保证用户函数因偶发的失败导致执行成功率下降,函数计算默认会为用户异步执行的函数错误进行 3 次重试。但在一些情况下,用户可能不希望进行重试,或希望进行更多的重试。比如:
- 用户某个任务(函数)经常执行超时(与输入的待处理数据大小有关),如果第一次超时,则重试也会超时。此时用户不希望执行出错就进行重试而造成资源浪费;
- 函数可重入,但部分场景失败率较高,用户希望能够增加重试次数尽可能增加异步调用成功率;
- 用户某个高峰时间段因为系统 bug,发出了过多的异步调用请求,结果导致业务正常的异步请求受到阻塞,系统整体延时及失败率增加。
此时如果能提供一种方式,用户可以配置这个重试次数及消息超时丢弃的策略,那么可以很好的解决上述问题。
异步调用配置
上述挑战的解决方案
函数计算最近上线的异步目标配置功能为上述场景提供了解决方案。
异步调用后执行目标
通过设置异步调用的 Destination,函数计算可以根据每次异步请求的执行情况(成功或失败)来调用不同的”目标“。目前支持的目标有函数计算和消息服务(MNS 主题/队列)。函数计算将会收集异步调用的一些基本情况(如请求 Payload、函数异常错误码、函数返回、重试次数等)并发往目标服务,用户可根据这些数据来执行具体的后续策略。
比如,用户可以设置异步函数的成功/失败调用目标。当异步函数执行结果符合设置时,函数计算将确保调用异步目标,以便执行后续的处理逻辑。用户也可以使用该工能将函数变为生产者,如划分数据集并串行的调用一系列函数按序执行。
配置异步调用的策略(重试次数及消息存活时间)
为了提供更加灵活的异步执行自定义策略,函数计算同时开放了异步配置的消息最大存活时间(MaxAsyncEventAgeInSeconds)以及最大重试次数(MaxAsyncRetryAttemps),用户可以通过自定义以上参数对异步调用进行配置及设置。
异步调用配置的使用方式
目前,我们支持通过控制台、SDK 或 API 来配置异步调用目标。配置的结构如下:
{
"DestinationConfig": {
"OnSuccess": {
"Destination": "acs:fc:{region}:{account}:services/{service_name}.{Qualifier}/functions/{function_name}}",
},
"OnFailure": {
"Destination": "acs:mns:{region}:{account}:/queues/{queue_name}/messages"
}
}
"MaxAsyncEventAgeInSeconds": 100,
"MaxAsyncRetryAttemps": 1
}
下面对设置的异步调用数据结构进行解释:
DestinationConfig
OnSuccess
如果配置了 Destination 的 OnSuccess 目标,函数计算将在执行成功后调用配置的目标服务。如果配置目标为函数计算,您甚至可以实现一个小型的工作流来完成您的任务,比如将一些处理结果通过 destination 传递到另外一个函数,避免诸如单个函数的最大 10 min 执行时长等限制。
OnFailure
如果配置了 Destination 的 OnFailure 目标,函数计算将在执行失败后调用配置的目标服务。失败情况包括系统的一些内部错误、函数中的各类异常。配置失败后的调用目标可以降低异步函数开发过程中的调试难度、提高线上运行的函数的可观测性。您可以通过配置失败目标来进行一些诸如资源回收、监控报警的需求。
调用 Destination 时传递的数据
当调用结果符合异步目标配置时,我们将调用对应服务,并发送相应数据。发送的数据内容如下:
{
"timestamp": "2020-08-20T12:00:00.000Z",
"requestContext": {
"requestId": "xxx",
"functionArn": "acs:fc:::services/{serviceName}/functions/{functionName}",
"condition": "FunctionResourceExhausted",
"approximateInvokeCount": 3
},
"requestPayload": "",
"responseContext": {
"statusCode": 200,
"functionError": ""
},
"responsePayload": ""
}
我们传递的数据包括了调用基本信息(requestContext:异步请求ID、请求的函数、请求结果以及异步调用次数)、异步调用输入 (requestPayload),调用结果信息(responseContext:调用状态码以及函数错误)以及异步调用函数输出(responsePayload)。这些数据覆盖了异步函数从输入到执行输出的大部分信息,可以在 Destination 目标中使用。在使用 MNS 作为目标时我们会将上述数据作为消息写入 MNS 队列/主题,在使用 FC 作为目标时我们会将上述数据作为 Event 调用函数。
MaxAsyncRetryAttempts
该参数设置了出错最大重试次数。当您的函数不是可重入的,或不希望在执行错误时被多次调用,可以通过设置该参数进行重试限制。
MaxAsyncEventAgeInSeconds
该参数可以设置异步消息最大存活时长。当您的异步调用任务较多,可能会出现消息积压时,为了保证后面的高优先级任务不被饿死,可以设置消息的最大存活时长。当消息超过该时长后,将不触发函数并被直接丢弃。配置该参数后,当 获取消息的时间 - 异步消息入队时间 > MaxAsyncEventAgeInSeconds
时,该消息将直接被抛弃,并记录 AsyncEventExpiredDropped
指标,进而保证后面的正常请求不被阻塞。
异步目标配置更为详细说明及使用文档,见 文档。
示例项目
为方便理解,我们创建了一个示例项目展示如何在实际业务中使用异步配置功能。该项目配置了异步调用目标为 mns 队列,实现了异步消息死信队列的功能,方便业务人员感知异步函数执行失败的消息并进行后续处理。具体的项目代码见:
https://github.com/awesome-fc/async-invocation-dlq
总结
函数的异步调用方式有利于拉平负载,提高任务的成功率,但也带来了一系列挑战。我们结合用户的实际场景,可总结为下述几类:
- 用户希望根据不同的业务场景,对于异步执行失败的错误进行不同的重试策略;
- 用户希望异步消息的存活时间有一个有效期。如果消息积压,希望优先处理后面更为重要的消息,而不是对所有的消息不加区别的按序处理;
- 用户希望函数计算能够提供一种方式,即在函数执行后系统帮忙执行一些调用其他服务的逻辑,以便进行错误处理、资源回收或生产后续事件。
为解决这些问题,函数计算提供了异步配置功能,用户可以配置函数的异步执行策略并配置异步执行后的调用目标。该功能提高了函数的异步执行任务的可观测性,使得函数计算也可以成为事件驱动应用的生产者。异步调用配置不但扩展了用户使用函数计算连接不同云服务的方式,也简化了函数在处理这类需求的逻辑,方便用户更专注于具体的业务。