Jianyi1.前言无论是在大数据处理领域还是在消息处理领域,任务系统都有一个非常关键的能力——任务触发的去重。这种能力在一些对精度要求极高的场景(如金融领域)中必不可少。ServerlessTask作为一个serverless任务处理平台,也需要提供这样的保障。它在用户应用层和内部系统两个维度上具有准确的任务触发语义。本文主要以消息处理可靠性为主题,介绍函数计算的异步任务功能的技术细节,并展示如何在实际应用中使用函数计算提供的能力来增强任务执行的可靠性。2.谈任务去重在讨论异步消息处理系统时,消息处理的基本语义是绕不开的话题。在异步消息处理系统(任务系统)中,一条消息的处理流程简化如下图:图1.用户发送任务-进入队列-任务处理单元监听并获取消息-派发实际工作人员执行。任务消息在流转过程中,任何一个组件(链路)可能出现宕机等问题,都会导致消息的错误传输。一个通用的任务系统会提供最多三个级别的消息处理语义:At-Most-Once:确保一条消息最多被传递一次。当发生网络分区和系统组件宕机时,消息可能会丢失;At-Least-Once:确保消息至少传递一次。消息传递链路支持错误重试,使用消息重发机制保证下游必须收到上游消息。但是,在宕机或网络分区的情况下,同一条消息可能会被多次传输。Exactly-Once机制可以保证消息恰好传输一次。Exactlyonce并不是说??在宕机或者网络分区的情况下不会重传,而是说重传不会导致接收方的状态发生任何变化,这与发送一次不同。同样的结果。在实际生产中,往往是依靠重传机制&接收端去重(幂等)来实现ExactlyOnce。函数计算可以为任务分发提供ExactlyOnce语义,即无论在什么情况下,重复的任务都会被系统认为是同一个触发器,然后只分发一次任务。结合图1,如果要对任务进行去重,系统至少需要提供两个维度的保障:系统侧保障:任务调度系统本身的故障转移不影响消息传递的正确性和唯一性;为用户提供一种机制,可以结合业务场景,实现整个业务逻辑的触发+执行的去重。下面我们就来谈谈函数计算是如何结合简化的ServerlessTask系统架构来实现上述能力的。3.函数计算异步任务触发去重实现背景函数计算的任务系统架构如下图所示:图2首先,用户调用函数计算API发送一个任务(步骤1)到系统的API-Server,API-验证后,服务器将消息发送到内部队列(步骤2.1)。后台有异步模块实时监控内部队列(步骤2.2),然后调用资源管理模块获取运行时资源(步骤2.2-2.3)。调度模块获取运行时资源后,将任务数据发送给VM级客户端(步骤3.1),客户端将任务转发给实际用户运行资源(步骤3.2)。为了保证上述两个维度,我们需要以下层面的支持:系统端保证:在步骤2.1-3.1中,任何一个中间过程的Failover只能触发一次步骤3.2的执行,即Onlyoneuserinstance将被安排运行;用户端应用级去重能力:可以支持用户重复执行步骤1,但实际上只触发一次步骤3.2的执行。1.系统侧优雅的升级&故障转移任务分发,保证用户的消息进入函数计算系统时(即2.1步骤完成后),用户的请求会收到一个HTTP状态码为202的Response,并且用户可以认为它成功提交了一个任务。由于任务消息进入MQ,其生命周期由Scheduler维护,所以Scheduler和MQ的稳定性将直接影响到系统ExactlyOnce的实现。在大多数开源消息系统(如MQ、Kafka)中,一般都提供了消息的多副本存储和唯一消费的语义。函数计算使用的消息队列(底层是RocketMQ)也是如此。底层存储的3副本实现,让我们无需关注消息存储的稳定性。此外,函数计算使用的消息队列还具有以下特点:消费的唯一性:每个队列中的每条消息被消费后,都会进入“隐身模式”。在这种模式下,其他消费者无法获取消息;每条消息的实际消费者需要实时更新该模式的隐身时间;当consumer消费完成后,需要显式删除消息。因此,消息在队列中的整个生命周期如下图所示:图3.Scheduler主要负责消息处理,其任务主要由以下几个部分组成:根据函数计算负载的调度策略平衡模块,监控其负责的队列;当队列中出现消息时,拉取消息并在内存中维护一个状态:直到消息消费完成(用户实例返回函数执行结果),可见时间不断更新消息的消息,确保消息不会再次出现在队列中;当任务执行完成后,将显示删除消息。在队列调度模型上,函数计算对普通用户采用“单队列”管理模式;即每个用户的所有异步执行请求都由一个独立的队列相互隔离,由一个Scheduler固定负责。这个负载的映射关系由函数计算的负载均衡服务管理,如下图所示(我们会在后续文章中对这部分内容进行更详细的介绍):图4当Scheduler1宕机或升级时,任务有两种执行状态:如果消息还没有投递到用户的执行实例(图2中的步骤3.1~3.2),那么当负责这个Scheduler的队列被其他Scheduler拾取时,消息就会在visibleperiod又出现了,所以Scheduler2会再次拿到消息做后续的触发。如果消息已经开始执行(步骤3.2),当消息在Scheduler2中重新出现后,我们依赖用户VM中的Agent进行状态管理,此时Scheduler2会向对应的Agent发送执行请求;此时Agent发现内存中已经存在该消息,则直接忽略执行请求,执行完成后通过该链路将执行结果通知Scheduler2,完成Failover恢复。2、用户侧业务级分布式去重,实现单点故障下函数计算系统可以准确消费每条消息,但是如果用户侧针对同一条业务数据重复触发函数执行,函数计算无法识别不同的消息在逻辑上是同一个任务。这通常发生在网络分区中。图2中,如果用户调用1超时,此时可能有两种情况:消息没有到达函数计算系统,任务没有提交成功;没有办法知道提交是否成功。在大多数情况下,用户将重试此提交。情况2,同一个任务会被多次提交执行。因此,函数计算需要提供一种机制来保证该场景下业务的准确性。函数计算提供了TaskID(StatefulAsyncInvocationID)的任务概念。这个ID是全局唯一的。用户可以在每次提交任务时指定这样一个ID。当发生请求超时时,用户可以无限次重试。所有重复的重试都会在函数计算端进行验证。函数计算内部使用DB存储任务元数据;当相同的ID进入系统时,请求将被拒绝并返回400错误。此时客户端就可以知道任务的提交状态了。在实际使用中,以GoSDK为例,您可以编辑触发任务的代码如下:("ServiceName","FunctionName")invokeInput=invokeInput.WithAsyncInvocation().WithStatefulAsyncInvocationID("TaskUUID")invokeOutput,err:=fcClient.InvokeFunction(invokeInput)...}然后提交一个独特的任务。4.小结本文介绍FunctionComputeServerlessTask对任务触发器去重的相关技术细节,以支持对任务执行精度有严格要求的场景。使用ServerlessTask后,您无需担心任何系统组件的故障转移,您提交的每个任务都将恰好执行一次。为了支持业务端语义分布去重,您可以在提交任务时设置任务的全局唯一ID,利用函数计算提供的能力帮助您对任务进行去重。
