无服务器应用程序中重试失败的3种解决方法(附代码)

在无服务器应用程序中重试失败的3种方法

了解如何从低、中、高三个层次来处理事件驱动的工作流中的错误。

几年前,当我开始接触无服务器开发和事件驱动架构时,有很多神秘感和好奇心。我不知道事件总线的用途,我无法理解异步流程,我也不明白为什么你需要在应用程序中建立重试功能。

从严格的同步背景来看,重试是没有意义的。如果某件事情没有成功,只需再次点击按钮。虽然这在同步API调用中可能是正确的,但在许多无服务器环境中却不是这样的。

无服务器环境大量使用事件,而事件的性质是异步的。这意味着调用者在返回之前不会等待听到回应。在设计松散耦合的系统时,这是一种非常有效的方法,可以在负载下自动扩展。更不用说当Lambda函数不必等待一个进程完成时,它可以为你节省一些严重的费用。

当一个异步流程出错时,你不希望它失败并永远失去。进程失败有可能是因为网络故障或节流事件等瞬时性错误。这些类型的事件可以安全地重试,并期望通过(假设网络问题被清除或你在你的速率限制下)。

如果你的应用程序举手说 "我不干了",那么你就会有不必要的数据损失。我们不希望这样。

这一点在无服务器应用中非常重要,以至于AWS在Well-Architected Framework的无服务器应用镜头中明确地将其纳入了可靠性支柱。

今天,我们将讨论三个级别的重试能力,你可以将其添加到你的应用中,以及每个级别的利弊。

低级别重试

当你间接调用函数时,AWS已经为你处理了各种故障。它用不同类型的重试来解决不同的调用类型。

然而,当你运行同步调用时,你就得靠自己了。

兰姆达函数

我相信大家都同意,大多数Lambda函数至少会对AWS SDK进行一次调用(但希望不要超过两次)。SDK会自动为你重试失败,但如果你想自己处理问题,你可以配置它重试的次数和它的退缩方式。

当重试一个失败时,标准做法是 使用抖动器对每次重试进行指数反推.抖动是一种随机延迟,用于防止在多个执行同时失败的情况下出现连续的碰撞。

如果你使用javascript SDK,你可以为你在函数中实例化的每个客户端配置重试。

const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb'); const ddb = new DynamoDBClient({ maxAttempts: 5 });
复制代码

当客户端遇到问题时,它将在你的函数代码中抛出异常之前最多重试5次命令。如果你没有明确设置这个值,SDK将最多重试三次

如果你的函数在尝试最大次数后仍然出错,强烈建议将该事件丢到死信队列(DLQ)中进行中级重试。

步骤功能

如果你是一个步骤函数的用户,你很幸运。当涉及到重试时,这项服务为你提供了保障。你有能力单独捕捉、回退和重试每个状态

与AWS SDK相反的是,这不是自动发生的。你必须手动配置它。幸运的是,这是一个简单的配置,甚至在Workflow Studio中也支持。

步骤功能 Workflow Studio重试配置

或者如果你喜欢看ASL(亚马逊状态语言)。

"Retry": [   {     "ErrorEquals": [ "States.ALL" ],     "BackoffRate": 2,     "IntervalSeconds": 3,     "MaxAttempts": 5   } ]
复制代码

在你的重试次数已经达到上限的情况下,你会再次想把事件或输入执行丢在死信队列中。死信队列将存储输入,并允许你通过中级或高级机制重试,或使你能够手动反应。

优点- 低层重试的主要好处是它感觉是无缝的。你不需要提供额外的基础设施来处理错误,而且执行过程中也不会显示发生了问题。他们为你处理,并在他们的快乐路上。

缺点- 取决于你如何配置你的回退率,这种方法可能会产生大量的计算费用。如果一个Lambda函数正在等待指数级的回退完成,那么在它等待的过程中,你将被收取运行时间的费用。

中层重试

我们在重试阶梯中的每一级都会增加一个抽象的层次。中级重试涉及重试那些生活在死信队列中的事件。这些重试并不像我们的低级重试那样,在函数的代码级别上操作。相反,它们是特定于一个函数或状态机和一个特定的死信队列。

中层重试机制可以是Lambda函数或状态机,它可以做以下事情。

  • 从死信队列中弹出项目
  • 评估该事件是否可以重试或需要人工干预
  • 执行任何转换或验证
  • 将事件重新提交给将其添加到DLQ的函数或状态机

这种类型的重试是专门意识到将项目放入DLQ的函数或状态机的。它向计算资源提供一对一的错误处理。这意味着对于每个Lambda函数或状态机,你都有一个死信队列和一个错误管理函数。

这也意味着你有重点的、范围狭窄的资源,将在错误发生时处理它们。

简单的中级重试数据流

优点- 重试功能具有最低的权限。他们应该能够从一个队列中读取和删除消息,并调用一个状态机或Lambda函数。你还可以在重试器中做出智能决定或转换特定于失败的Lambda函数/状态机的数据。

缺点- 有相当多的资源需要跟踪和维护。如果你不小心,你可能会让自己陷入重试逻辑的无限循环,如果你不跟踪尝试。

高级重试

把重试过程抽象得更高,我们就可以看到通用的、高水平的重试。这些机制并不特别知道它在重试哪个状态机或Lambda函数。相反,它接受来自死信队列的结构化输入,并重新提交输入,不执行任何转换或业务逻辑。

一个通用的、高层次的重试机制可以是一个状态机或一组状态机,它查看账户中的所有死信队列,验证是否有要重试的消息,并处理它们。在下面的例子中,状态机是由事件桥接规则在30分钟的计时器上触发的。

死信队列预计以相同的前缀开始,即重试-,以正确识别并与有其他用途的队列分开。

高级重试状态机

当状态机处理单个消息时,它必须确定它将调用的资源类型以及它已经重试了多少次。理想情况下,这些信息来自最初被传入死信队列的结构化消息。

从死信队列中通用地重试消息

优点- 随着你的应用程序的增长,这种方法会在错误被丢入DLQ时自动捡起并重试(只要你给你的DLQ命名并适当地构造你的消息)。你的资源数量将保持较低,因为你没有特定的Lambda函数错误处理程序来拾取项目。这也迫使你以结构化DLQ日志的方式进行一定程度的治理。拥有一致的信息传递对排除错误很有帮助。

缺点- 这个状态机需要通配符访问SQS队列、Lambda函数和状态机。现在,它不需要完全的管理,但它确实需要读取和删除消息并调用状态机和函数。这不符合最小权限的原则,而且打开的门比它们应该的要大。你也没有得到重试时添加业务逻辑和转换的好处。最后,如果你的最大尝试逻辑中有一个错误,你也可能在这里让自己陷入无限循环。

关于无效性的说明

当处理分布式系统时,你不能保证完全一次交付。你也不能保证你的重试机制会在一个原始的实体上运行,这意味着操作在第一次时可能已经部分成功了。

Idempotency指的是应用程序能够多次运行相同的API调用或函数,并对系统产生相同的影响。因此,如果我重试3次相同的POST,它将导致一个实体被创建,而不是三个。

这不是开箱即用的处理方式。这是你需要自己解决的问题,特别是当你开始自己实现重试逻辑时。

Idempotency模式被清楚地定义和建立。如果你是Python用户,它甚至包括在Python Lambda powertools中,它可以快速使你的函数可以重试。

重试和空闲是相辅相成的,是aws无服务器设计原则的一部分。虽然你可以只做一个而不做另一个,但你不应该这样做。

总结

在任何规模的操作中,拥有重试错误的能力将为你节省大量的开发人员时间。他们不会把时间花在挖掘日志或不必要地进入根本原因分析上。如果一个问题可以通过 "关闭并重新打开 "来解决,那么你的无服务器应用就会为你做这件事。

如果你的应用中每天有5000个事务,平均1%的故障率,你就会有50次故障。如果其中80%是由于瞬时问题,这意味着你每天会自动解决40个问题。想象一下,如果工程和支持团队不需要经常看这些问题,他们会有多高兴。

你有多个层次的重试抽象。低层次的重试在代码层面上处理错误。中级重试处理的是函数或状态机的死信队列。高级别的重试则是对所有的东西进行一般性的处理。

我的建议是,在生产软件中坚持使用低级和中级的重试。

高级重试的广泛权限所带来的风险太高了。无服务器是关于狭窄的权限范围。拥有一个可以从任何队列中读取和删除消息或调用任何函数或状态机的状态机,只是自找麻烦。

如果你已经在Lambda函数中使用了AWS SDK,你已经在做低级别的重试了。如果你在使用步骤函数,请确保在每个状态下都包含重试逻辑。

中层重试是非常有效的。它们可以在重新提交之前查看下游或验证数据完整性。养成在你的异步工作流中包含一个处理程序来处理和识别错误的习惯。

一个自我修复的应用程序将为你节省时间和金钱。把你能做的事情自动化。它可以消除人为错误并尽可能快地解决问题。

编码愉快!

猜你喜欢

转载自juejin.im/post/7107026833266180103
今日推荐