作者 | Ohav Almog
译者 | 明知山
策划 | 丁晓昀
介 绍
在编程领域,幂等性一词听起来就像是一个复杂而古怪的概念,专门用于数学讨论或计算机科学讲座。然而,它的相关性远远超出了学术范围。
幂等性是确保软件系统可预测性、可靠性和一致性的一个关键基本原则。
在本文中,我们将揭开幂等概念的神秘面纱,探索它的含义、重要性以及它如何影响我们设计和与软件交互的方式。无论你是经验丰富的开发人员还是刚刚开始编码之旅,理解幂等性对编写更健壮、更有弹性的程序来说至关重要。
什么是幂等性?
幂等性是函数或操作的一种属性,将其应用多次与应用一次具有相同的结果。
换句话说,一个幂等函数被重复调用时,不会改变第一次调用之后的结果。
例如,在数学中,绝对值函数是幂等的,因为多次取同一个数字的绝对值,其结果不会发生改变。
无论对一个数字应用绝对值函数一次还是多次,结果都是相同的,因为它总是生成输入的非负值。
代码语言:javascript复制>> abs(-5) == abs(abs(-5)) == abs(abs(abs(-5))) # ... and so on
True
为什么云工作负载的
幂等性很重要?
在开发云应用程序时(在本示例中我们将使用 AWS 演示这个概念),掌握“至少一次”传递 / 调用的概念至关重要。这个术语意味着特定目标可以至少一次或可能多次接收事件或被事件调用。作为开发者,预见并处理同一事件被多次处理的情况至关重要。这不是“是否”会发生的问题,而是“何时”会发生的问题。这就是幂等性变得至关重要的地方。编写幂等函数确保即使一个事件被多次处理,结果也保持一致,并避免意外副作用,这有助于提高 AWS 应用程序的可靠性和健壮性。
为什么要关注至少一次传递?
如果你想要了解幂等性所带来的挑战,那么深入了解其机制就变得至关重要了。这里的解释将以 Lambda 为基础,Jit 的架构师已经写过很多这方面的东西,不过它也可以与其他服务如 SQS 或 SNS 相关。
在协调 Lambda 的异步调用时,关键是要认识到从开始到结束的执行涉及到两个不同的过程。初始过程涉及将事件放入队列,而后续过程则围绕从这个队列检索事件展开。考虑到多节点数据库的复杂特性和最终一致性的概念,偶尔会出现两个并发执行器并行处理相同调用事件的情况。
为了了解这些事件发生的频率,我做了一个实验,编写了一个由 EventBridge 事件触发的 Lambda 函数,发送大量的事件来唤醒 Lambda。我监测了 Lambda 在同一事件上被其 ID 触发的频率。我的实验表明,在成千上万次运行中,同一事件会发生多个并发执行。
设计好的幂等函数
写出自然幂等的函数是有可能的。我们以一个负责将数据库中项目的状态更新为“已完成”的函数为例子。这个函数被归类为幂等函数,因为无论它被调用多少次,项目的状态都将为“已完成”。注意,只要没有外部因素(如监听器或触发器)监视数据库表中的变更,这个幂等假设就成立。
代码语言:javascript复制def handler(event: EventBridgeEvent, __) -> None:
executions = boto3.resource('dynamodb').Table('Executions')
executions.update_item(
Key={'id': event['detail']['id']},
UpdateExpression='SET #status = :status',
ExpressionAttributeNames={'#status': 'status'},
ExpressionAttributeValues={':status': 'COMPLETED'},
)
作为一般性规则,建议尽可能将函数设计为幂等的。这样,开发人员就不需要在其代码中手动处理幂等性,从而降低复杂性和未来的维护成本。
不是自然幂等的函数
有些函数的设计不是幂等的。例如,向客户发送通知消息的函数可能不是幂等的,因为如果函数在同一个事件上运行两次,客户将收到两条通知消息,这会导致不良的用户体验。相反,我们希望客户只收到一条通知消息。这就是幂等性的作用所在,也是处理幂等性最重要的地方。
使用 Lambda Powertools
解决幂等性问题
我们明白,并不是每个函数都是幂等的。我们的 Lambda 偶尔会被相同的事件调用,那么我们该怎么办?
幸运的是,AWS 提供了一个很好的解决方案,使用他们的开发工具包 Powertools for AWS Lambda 来处理幂等性,甚至还有一个专门的工具包。该工具包提供了“idempotent”装饰器,你可以配置它来处理相同事件的多次执行。
它的工作原理是对事件内部可配置的特定值进行哈希处理,这些值可以标识特定事件的唯一性,并将每个事件的执行状态存储在数据库中。
到达函数上下文中的第一个唯一性事件将作为存储层中的项保持起来。当发生对同一事件的第二次调用时,装饰器就会知道执行已经开始或已经结束了,并将中止第二次执行。
在 AWS 中常用的存储层是 DynamoDB,它提供了一致性读取能力。不深入研究细节,上面的示例应该像下面这样使用装饰器。
例子
我们来仔细地看一下如何使用幂等性装饰器。
代码语言:javascript复制from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer
from aws_lambda_powertools.utilities.idempotency import IdempotencyConfig
@idempotent(
persistence_store=DynamoDBPersistenceLayer(table_name='IdempotencyTable'),
config=IdempotencyConfig(
event_key_jmespath='id',
raise_on_no_idempotency_key=True,
),
)
def handler(event: EventBridgeEvent, __: Any) -> None:
logger.info(f"Event: {event}")
ExecutionsManager.from_event(event).complete_execution()
可以看到,幂等性装饰器配置了一个持久化层,本例中是一个叫作IdempotencyTable
的 DynamoDB 表。此外,通过在event_key_jmespath
参数中传递id
,装饰器知道只使用id
属性来创建事件对象的唯一哈希。raise_on_no_idempotency_key
设置为True
,避免出现事件中缺少id
的情况,这种情况是非预期的。
测 试
向代码库中添加了幂等性装饰器后,尽管不是纯代码,但测试它是否配置正确并按预期运行是一个好习惯。
在 Jit,我们发现了一种有效的测试幂等性装饰器的方法。我们利用 moto(AWS 基础设施的 Python 模拟库)来模拟 Lambda 函数被相同事件调用两次的场景。
代码语言:javascript复制from test_utils.aws import idempotency
def test_complete_execution_handler(executions_manager):
idempotency.create_idempotency_table()
# It's important to import the handler after moto context is in action
from src.handlers.complete_execution import handler
# Call the handler for the first time
handler(event, None)
# Validate an idempotency key was created
idempotency.assert_idempotency_table_item_count(expected=1)
# Assert status changed to completed and completed_at has updated
execution = executions_manager.get_item(...)
assert execution.status == COMPLETED
assert execution.completed_at_ts >= datetime.utcnow().timestamp() - 1
# Call the handler for the second time
complete_execution_handler(event, None) # noqa
idempotency.assert_idempotency_table_item_count(expected=1)
assert executions_manager.get_item(...) == execution # Assert nothing has changed (idempotency worked)
让我们来分解一下:
- 创建幂等性表:在 moto 上下文中创建幂等性表。由于幂等性表可以在 AWS 基础设施中的多个服务之间共享,因此开发一个测试实用程序来创建表并从各种测试中调用它是可行的。
- 在 moto 上下文中导入处理程序:第二步是在激活 moto 上下文之后导入处理程序。这一点至关重要,因为 moto 上下文模拟了 boto3 客户端,而 boto3 客户端是在导入期间在装饰器中初始化的。
- 首次调用处理程序:首次调用处理程序,并验证是否在幂等表中成功创建了幂等键。
- 验证状态和完成:下一步确认执行状态已更改为“completed”,并且“completed_at”时间戳已更新。这可确保 Lambda 函数正确执行了任务。
- 第二次调用处理程序:最后,第二次调用处理程序,并确保没有再次创建幂等性键,并且执行的属性保持不变。这表明 Lambda 函数是幂等的,并且不会在同一事件上再次运行。
一个小提示,也有助于理解装饰器的工作原理,就是调试和跟踪代码行,查看和验证第二次执行是否真的没有发生。
总 结
我希望这篇文章能更清楚地说明为什么幂等性是确保系统更强的可预测性、可靠性和一致性的基本实践。虽然失败的操作不是常态,而是异常情况,但至少一次传递一直是云系统实现幂等性的主要原因之一。
需要注意的是,在本文中,使用 AWS Lambda 与 Python 作为示例编程语言。然而,这些挑战对于其他编程语言和服务也是有效的。例如,在 SQS 中,开发人员可以在标准队列和 FIFO 队列之间做出选择。标准队列传递至少一次,而 FIFO 提供了确保一次性处理的功能,但与标准队列相比,吞吐量较低,成本较高。在这种情况下,开发人员不需要实现任何额外的逻辑,只需使用现有的解决方案即可。
现在有很多优秀的工具可用于实现幂等性实践,并在部署到生产环境之前测试其有效性。通过上面的示例,我提供了一个简单而常见的示例来说明你也可以做到。只要你遵循示例和测试流程,就可以确信你的幂等性代码按预期运行,并在 AWS 基础设施上提供可靠性和一致性。
查看英文原文:
https://www.infoq.com/articles/idempotence-aws-serverless-architecture/
声明:本文为 InfoQ 翻译,未经许可禁止转载。