使用 Azure Functions 和事件中心进行可靠的事件处理

学习如何通过使用 Azure Functions 配合 Azure 事件中心 触发器构建稳健可靠的无服务器解决方案。 本文介绍了检查点、错误处理以及断路器模式实施的最佳实践,以确保你不会丢失任何事件,并且你的事件驱动应用保持稳定和弹性。

分布式系统中事件流的挑战

假设系统以每秒 100 个事件的常量速率发送事件。 按此速率,多个并行实例每秒可消耗100个事件。

但是,请考虑以下使用事件流的挑战:

  • 事件发布者发送已损坏的事件。
  • 函数代码遇到未经处理的异常。
  • 下游系统脱机并阻止事件处理。

与在处理过程中锁定消息的 Azure 队列存储触发器不同,Azure 事件中心从流中的单个点读取每个分区。 此读取行为更像是视频播放器,可提供高吞吐量、多个使用者组和重播能力所需的优势。 事件可以从检查点向前或向后读取,但必须移动指针以处理新事件。 有关详细信息,请参阅事件中心文档中的 检查点

当流中出现错误且您选择不移动指针时,将阻止对后续事件的处理。 换句话说,如果你停下指针去处理单个事件时出现的问题,未处理的事件就会开始堆积。

无论成功还是失败,函数始终推进流的指针,从而避免死锁。 由于指针不断向前推进,函数需要适当地处理故障。

事件中心触发器如何消费事件

Azure Functions 通过循环执行以下步骤,从事件中心消费事件:

  1. 该触发器在 Azure 存储 中为事件中心的每个分区创建并持久化一个指针。
  2. 触发器默认会在批处理中接收新事件,主机尝试触发该函数,提供批次事件进行处理。
  3. 当函数完成执行时,无论有无例外,触发器都会推进指针并保存检查点到默认的主机存储账户。
  4. 如果条件阻止函数执行完成,主机无法推进指针。 当指针无法前进时,后续执行将重新处理相同的事件。

此行为揭示了一些要点:

  • 未经处理的异常可能会导致事件丢失。

    引发异常的函数执行将继续推进指针位置。 设置 重试策略 或其他重试逻辑会延迟指针前进,直到整个重试完成。

  • 函数保证 至少传递一次

    你的代码和依赖系统可能需要考虑到同一事件可以两次处理的事实。 有关详细信息,请参阅 为相同的输入设计 Azure 函数

  • Checkpoint 状态存储在 Azure 存储 中:

    触发器会将检查点(处理指针)保存在函数应用的 AzureWebJobsStorage 设置所配置的存储帐户中。 该已存储的检查点引用表示:

    • 当你更改 AzureWebJobsStorage 引用不同的存储账户时,函数会从新位置开始处理,这可能导致事件被重新处理。
    • 当事件集线器被删除并重新创建时,事件流位置(如序列号和偏移量)会被重置,而存储的检查点引用保持不变。 在这种情况下,函数可能直到手动删除检查点后才处理新事件。

处理异常

虽然所有函数代码都应在最高级别的代码中包含 try/catch 块 ,但对于使用事件中心事件的函数,拥有 catch 块更为重要。 这样,当引发异常时,catch 代码块会在指针前进之前处理该错误。

重试机制和策略

由于云中的许多异常是暂时性的,因此错误处理的第一步始终是重试作。 可以应用内置重试策略或定义自己的重试逻辑。

重试策略

Functions 为事件中心提供内置的重试策略。 使用重试策略时,你只需提出一个新的异常,主机会根据定义的策略再次处理事件。 此重试行为需要 5.x 或更高版本的事件中心扩展。 有关详细信息,请参阅重试策略

自定义重试逻辑

还可以在函数本身中定义自己的重试逻辑。 例如,可以实施遵循以下规则演示的工作流的策略:

  • 尝试处理事件三次(可能重试之间有延迟)。
  • 如果所有重试的最终结果为失败,请将事件添加到队列中,以便处理可以在流中继续。
  • 随后会处理损坏或未处理的事件。

注释

Polly 是 C# 应用程序的复原和暂时性故障处理库的示例。

非异常错误

可能会出现一些问题,而不会引发异常。 例如,请考虑请求超时或运行函数的实例崩溃的情况。 如果一个函数在未发生异常的情况下无法完成,则偏移指针永远不会前进。 如果指针未推进,则在执行失败后运行的任何实例将会继续读取相同的事件。 这种情况提供至少一次的保证。

保证每个事件至少处理一次意味着可以多次处理某些事件。 函数应用需要注意这种可能性,并且必须围绕 幂等性原则构建。

处理失败状态

你的应用在事件处理中可能能够接受地处理一些错误。 但是,还应准备好处理永久性故障状态,这可能是下游处理失败导致的。 在此类故障状态(例如下游数据存储处于脱机状态)中,函数应停止对事件触发,直到系统达到正常状态。

断路器设计模式

实现 断路器 模式时,应用可以有效地暂停事件处理,然后在解决问题后稍后恢复它。

在事件流进程中实现断路器需要两个组件:

  • 在所有实例之间共享状态,用于跟踪和监视线路的运行状况。
  • 一个可以管理线路状态的主要进程,可以是openclosed

实现细节可能会有所不同,但为了在各个实例之间共享状态,你需要一个存储机制。 可以在 Azure 存储、Redis 缓存或任何其他可由函数应用实例访问的持久服务中存储状态。

Durable FunctionsAzure 逻辑应用都提供基础结构来管理工作流和线路状态。 本文介绍如何使用逻辑应用暂停和重启函数执行,从而提供实现断路器模式所需的控制。

跨实例定义故障阈值

当多个实例同时处理事件时,需要保留的共享外部状态来监视线路的运行状况。 然后,可以根据指示失败状态的规则监视此持久状态,例如:

在所有实例的 30 秒内发生超过 100 个事件故障时,断开电路以停止对新的事件进行触发。

此监视逻辑的实现详细信息因特定应用需求而异,但一般情况下,必须创建一个系统:

  1. 将失败记录到持久存储。
  2. 在记录新失败时检查滚动计数,以确定是否满足事件失败阈值。
  3. 满足此阈值时,发出一个事件,告知系统中断线路。

使用 Azure 逻辑应用管理线路状态

Azure 逻辑应用 提供用于连接不同服务、功能和有状态编排的内置连接器。 管理电路状态是自然而然的选择。 检测线路何时必须中断后,可以生成逻辑应用来实现此工作流:

  1. 触发停止函数处理的事件网格工作流。
  2. 发送包含重启工作流的选项的通知电子邮件。

如需了解如何使用应用设置禁用和重新启用特定功能,请参阅“如何在 Azure Functions 中禁用功能”。

邮件收件人可以调查电路的健康状况,并在适当情况下通过通知邮件中的链接重新启动电路。 当工作流重启函数时,会处理来自上一个事件中心检查点的事件。

用这种方法时,你不会丢失任何事件,你按顺序处理事件,并且可以随时中断电路。

事件网格触发器的迁移策略

在区域之间或某些计划之间迁移现有函数应用时,必须在迁移过程中重新创建应用。 在这种情况下,迁移过程中,你可能有两个应用,它们都可以从同一个事件流中消费并写入同一个输出目的地。

为避免迁移过程中事件数据丢失或重复,建议 使用消费者组

  1. 为新目标应用创建新的使用者组。

  2. 将新应用中的触发器配置为使用此新的使用者组。

    通过这种方法,两个应用可以在验证过程中独立处理事件。

  3. 验证新应用是否正确处理事件。

  4. 关闭原应用或移除其订阅或用户组。