网络安全|详解异步任务:函数计算的任务触发去重?( 二 )


在大多数开源消息系统中(如 MQ、Kafka)一般都提供消息多副本存储及唯一消费的语义 。 函数计算所使用的消息队列(最底层为 RocketMQ)也是同样的 , 底层存储的 3 副本实现使得我们无需关注消息存储方面的稳定性 。 除此之外 , 函数计算所使用的的消息队列还具有以下特性:
1、消费的唯一性:每一个队列中的每一条消息当被消费后 , 会进入“不可见模式” 。 在此模式下 , 其他消费者无法获取该消息;
2、每条消息的实际消费者需要实时更新该模式的不可见时间;当消费者消费完成后 , 需要显示的删除该消息 。 因此 , 消息在队列中的的整个生命周期如下图所示:
图 3
Scheduler 主要负责消息的处理 , 其任务主要有以下几个部分组成:
1、根据函数计算负载均衡模块的调度策略 , 监听自身所负责的队列;
【网络安全|详解异步任务:函数计算的任务触发去重?】2、当队列中出现消息后 , 拉取消息 , 并在内存中维持一个状态:直到消息消费完成(用户实例返回函数执行结果)前 , 不断更新消息的可见时间 , 确保消息不会再次在队列中出现;
3、当任务执行完成后 , 显示删除该消息 。
在队列的调度模型方面 , 函数计算对于普通用户采用“单队列”的管理模式;即每一个用户的所有异步执行请求由一个独立队列相互隔离 , 并且由一个 Scheduler 固定负责 。 这个负载的映射关系由函数计算的负载均衡服务进行管理 , 如下图所示(我们在后续文章中还会更为详细的介绍这部分内容):
图 4
当 Scheduler 1 发生宕机或升级时 , 任务由两种执行状态:
1、如果消息还未传递到用户的执行实例中(图 2 中的步骤 3.1 ~ 3.2) , 那么当这台 Scheduler 负责的队列被其他 Scheduler 拾起后 , 消息将在消费可见期后再次出现 , 因此 Scheduler 2 将再次获取该消息 , 做到后续的触发 。
2、如果消息已经开始执行(步骤 3.2) , 当消息在 Scheduler 2 中再次出现后 , 我们依赖用户 VM 中的 Agent 进行状态管理 。 此时 Scheduler 2 将向对应的 Agent 发送执行请求;此时 Agent 发现该消息已经存在于内存中 , 那么将直接忽略执行请求 , 并将执行的结果在执行后通过此链接告知 Scheduler 2 , 进而完成 Failover 的恢复 。
用户侧业务级别的分发去重实现 函数计算系统能够做到对于单点故障下的每条消息准确的消费能力 , 但是如果用户侧对于同一条业务数据反复触发函数执行的话 , 函数计算无法识别不同消息是否在逻辑上是同一个任务 。 这种情况往往发生在网络分区 。 在图 2 中 , 如果用户调用 1 发生超时 , 此时有可能有两种情况:
1、消息未到达函数计算系统 , 任务未成功提交;
2、消息已经到达函数计算并入队 , 任务提交成功 , 但由于超时用户无法得知提交成功的信息 。
大多数情况下用户会对此次的提交进行重试 。 如果是第 2 种情况 , 那么同一个任务将被提交并执行多次 。 因此函数计算需要提供一种机制 , 保证这种场景下业务的准确性 。
函数计算提供了 TaskID 这一任务概念(StatefulAsyncInvocationID) 。 该 ID 全局唯一 。 用户每次提交任务均可以指定这样一个 ID 。 当发生请求超时时 , 用户可以进行无限次重试 。 所有的重复重试将在函数计算侧进行校验 。 函数计算内部使用 DB 对任务 Meta 数据进行存储;当有相同 ID 进入系统时该次请求将被拒绝 , 并返回 400 错误 。 此时客户端即可得知任务的提交情况 。
在实际使用中以 Go SDK 为例 , 您可以编辑如下触发任务的代码: