Azure Functions、事件网格和事件重新传递

Azure Functions, Event Grid and Event Redelivery

我在消费计划下有一个 Azure 函数应用程序 运行ning,其中包含使用 EventGridTrigger 触发的函数,以及触发这些函数的事件网格主题。我依靠重试功能来提高可靠性。如果函数不确认事件(通过函数中的 return),我会看到事件被重新传递。到目前为止一切顺利。

我将 EF Core 与 SQL 服务器一起使用,并使用 timestamp/row 版本列进行并发管理。稍后会详细介绍。

文档概述了事件网格已 'at-least-once' 交付。这意味着事件的消费者应该是幂等的。我们有一个多步骤的过程,我们正在修改这个过程是幂等的,所以我们正在考虑各种情况。本题涉及一种具体情况。

我读 'at-least-once' 的意思是一个函数可以被同一个事件触发两次,并且一个事件可以同时被该函数的两个实例处理;也许一个需要很长时间,或者网格只发送了两次。

如果是这种情况,并且两个函数实例同时处理一个事件,则其中一个可能会成功,而另一个可能会失败 - 要么出现并发异常,要么函数可能有一些代码或平台产生的其他异常。这些平台异常很少见,但我们已经看到了这种情况的迹象(调用刚刚结束;我们已经看到单次调用出现这种情况,并且已经看到事件被重新传送)。

我们从未见过任何迹象表明一个事件同时由两个实例处理;不过,我的问题假设这是可能的。在那种情况下,我不确定重新交付会发生什么:

在这两种情况下,一个个体确认事件,另一个不承认。有谁知道在这些情况下是否会重新发送事件?这些实例 运行 的顺序是否改变了答案?

实际问题更复杂:我们正在执行一个多步骤过程,并在每个步骤结束时保存到数据库中。所以计划是只允许每个步骤处理一次。在两个实例同时 运行 的情况下,我们利用 EF Core 中的并发验证来防止第二个实例在移动到下一个 'step' 时保存数据。但是我们不知道是否要确认该事件——因为另一个实例可能会在后续步骤中遇到问题。我们 'optimistic' 因为我们认为这些问题很少见,但我们希望获得尽可能多的自动恢复。

任何见解都会有所帮助。我们真的无法改变这个多步骤过程的完成方式;一方面,多个步骤使我们能够跟踪进度。

提前致谢。

我在文档中找不到任何可靠的内容。所以我 运行 自己做了一些测试,因为我对这个问题很感兴趣。我还为您的多步问题提供了一个潜在的“解决方案”,或者更确切地说,它不是幂等的(还没有?)。翻过文字墙,到最后了

首先想到的是两个实例可能 运行 同一个事件,前提是第一次调用超过默认的 30 秒响应时间,这就是提到“at-least-once”传递的原因.示例:

  1. 活动已发布
  2. 实例A拾取事件
  3. 实例 A 运行s 超过 30 秒
  4. 由于实例 A 尚未回复,事件已添加到重试队列
  5. 实例 A 已完成,总共耗时 35 秒
  6. 重试队列中的事件在重试队列中等待 10 秒后发布
  7. 实例B拾取事件

我真的认为这取决于 EventGrid 如何反应的两个实例的执行持续时间。

根据 Retry schedule 的文档:

If the endpoint responds within 3 minutes, Event Grid will attempt to remove the event from the retry queue on a best effort basis but duplicates may still be received.

这可以解释为一旦实例回复成功响应,EventGrid 将尝试清除重试队列(从上面的示例来看,实例 B 永远不应接收事件)。但是,我也对实例 A 失败时发生的情况感兴趣。

我尝试了以下代码,它会在一定延迟后从实例 A 中抛出异常:

public static class Function1
{
    private static Dictionary<string, int> _Counter = new();

    [FunctionName("Function1")]
    public static async Task Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
    {
        if (_Counter.ContainsKey(eventGridEvent.Id))
        {
            _Counter[eventGridEvent.Id]++;
            log.LogInformation($"{eventGridEvent.Id}: {_Counter[eventGridEvent.Id]}");
            return;
        }

        _Counter.Add(eventGridEvent.Id, 1);
        var delay = (long)eventGridEvent.Data;
        log.LogInformation($"Delay: {delay}");
        await Task.Delay(TimeSpan.FromSeconds(delay));
        log.LogInformation($"{eventGridEvent.Id}: {_Counter[eventGridEvent.Id]}");
        throw new Exception($"{eventGridEvent.Id} Throwing after {delay} seconds delay");
    }
}

不同延迟的结果

延迟 = 35

2022-02-06T00:34:10.798 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:34:10.7899831+00:00', Id=9d1adfe9-21bd-454c-862e-a4d8d5b312e0)
2022-02-06T00:34:10.801 [Information] Delay: 35
2022-02-06T00:34:45.807 [Information] 6d3d141d-d0ee-4e81-a648-e2497e562b84: 1
2022-02-06T00:34:45.894 [Error] Executed 'Function1' (Failed, Id=9d1adfe9-21bd-454c-862e-a4d8d5b312e0, Duration=35096ms)6d3d141d-d0ee-4e81-a648-e2497e562b84 Throwing after 35 seconds delay
2022-02-06T00:34:55.949 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:34:55.9486015+00:00', Id=58a7a6db-2f97-4bc7-b878-030b8be25711)
2022-02-06T00:34:55.949 [Information] 6d3d141d-d0ee-4e81-a648-e2497e562b84: 2
2022-02-06T00:34:55.949 [Information] Executed 'Function1' (Succeeded, Id=58a7a6db-2f97-4bc7-b878-030b8be25711, Duration=1ms)

似乎最有可能在 30 秒后添加到队列中的重试是 removed/replaced 或其计划执行已“更新”:

  • 58-13=45,
  • 实例A的执行时间为35
  • 实例 A 失败后的重试延迟为 10

延迟 = 45

2022-02-06T00:37:29.526 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:37:29.5262414+00:00', Id=c4596efa-7034-4efe-bcf9-85d502517711)
2022-02-06T00:37:29.526 [Information] Delay: 45
2022-02-06T00:38:14.524 [Information] 284fa140-1cdc-41e7-973f-d0ecd6f19692: 1
2022-02-06T00:38:14.530 [Error] Executed 'Function1' (Failed, Id=c4596efa-7034-4efe-bcf9-85d502517711, Duration=44999ms)284fa140-1cdc-41e7-973f-d0ecd6f19692 Throwing after 45 seconds delay
2022-02-06T00:38:24.590 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:38:24.5901035+00:00', Id=43a1fafa-7773-44e5-9f3a-b4bf697865a9)
2022-02-06T00:38:24.590 [Information] 284fa140-1cdc-41e7-973f-d0ecd6f19692: 2
2022-02-06T00:38:24.590 [Information] Executed 'Function1' (Succeeded, Id=43a1fafa-7773-44e5-9f3a-b4bf697865a9, Duration=0ms)

这有点出乎意料。我们希望看到第二次调用在第一次调用之前完成,大概是在第一次调用开始后 40 秒后完成。这可能表明 EventGrid 实际上知道第一个实例没有失败,并且仍在 运行ning,因此它实际上还没有将重试尝试添加到队列中。但是它确实在第一个实例失败后添加它,并且重试尝试如预期的那样在 10 秒后执行。

我想测试上面的内容,并认为幻数可能是 3 分钟(文档中也提到)而不是 30 秒。

延迟 = 185

2022-02-06T00:40:20.381 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:40:20.3809687+00:00', Id=454b815f-6ca6-4b75-9272-711c7b6bf42a)
2022-02-06T00:40:20.381 [Information] Delay: 185
2022-02-06T00:43:00.393 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:43:00.3928958+00:00', Id=23910142-d903-4b9b-bec2-a5665e8e1db7)
2022-02-06T00:43:00.393 [Information] 97d828e6-d1f3-4523-b043-1f76465de6ea: 2
2022-02-06T00:43:00.393 [Information] Executed 'Function1' (Succeeded, Id=23910142-d903-4b9b-bec2-a5665e8e1db7, Duration=0ms)
2022-02-06T00:43:25.381 [Information] 97d828e6-d1f3-4523-b043-1f76465de6ea: 2
2022-02-06T00:43:25.387 [Error] Executed 'Function1' (Failed, Id=454b815f-6ca6-4b75-9272-711c7b6bf42a, Duration=185002ms)97d828e6-d1f3-4523-b043-1f76465de6ea Throwing after 185 seconds delay

这也与文档有些矛盾。第一个实例在 40:20 处执行。在 43:00(预计 43:20),它 运行 是第二个实例,立即完成。然后第一个实例在 43:25 处失败(如预期的那样)。也许是 3 分钟而不是 30 秒。但是EventGrid只看时间戳的minute-part。我们可以从中得到的一件事是第一个实例的延迟失败不会触发第三次调用。

抛出异常的时候我们翻转一下;第二次调用将抛出异常,如下所示:

public static class Function1
{
    private static Dictionary<string, int> _Counter = new();

    [FunctionName("Function1")]
    public static async Task Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
    {
        if (_Counter.ContainsKey(eventGridEvent.Id))
        {
            _Counter[eventGridEvent.Id]++;
            throw new Exception($"{eventGridEvent.Id}: {_Counter[eventGridEvent.Id]}");
        }

        _Counter.Add(eventGridEvent.Id, 1);
        var delay = (long)eventGridEvent.Data;
        log.LogInformation($"Delay: {delay}");
        await Task.Delay(TimeSpan.FromSeconds(delay));
        log.LogInformation($"{eventGridEvent.Id} finished after {delay}: {_Counter[eventGridEvent.Id]}");
        return;
    }
}

延迟=35

2022-02-06T00:57:00.777 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:57:00.7684489+00:00', Id=16505ac4-0cb2-4b1a-8b7c-d4adf964fe14)
2022-02-06T00:57:00.780 [Information] Delay: 35
2022-02-06T00:57:35.794 [Information] 7fce3bc0-f5d3-4b3e-bd5d-4997299aa9b7 finished after 35: 1
2022-02-06T00:57:35.796 [Information] Executed 'Function1' (Succeeded, Id=16505ac4-0cb2-4b1a-8b7c-d4adf964fe14, Duration=35028ms)

30 秒后仍然没有。我跳过 45,因为我很确定它会产生与 35 相同的结果。

延迟=185

2022-02-06T00:59:04.226 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T00:59:04.2262900+00:00', Id=cac343e5-911b-4ba7-a0c7-69e883a61392)
2022-02-06T00:59:04.227 [Information] Delay: 185
2022-02-06T01:01:44.196 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T01:01:44.1962398+00:00', Id=2c65d66b-57c8-4a7d-b9ec-d54959e84e3d)
2022-02-06T01:01:44.271 [Error] Executed 'Function1' (Failed, Id=2c65d66b-57c8-4a7d-b9ec-d54959e84e3d, Duration=68ms)e051565c-9900-4cef-9786-5795c9fc7f91: 2
2022-02-06T01:02:09.240 [Information] e051565c-9900-4cef-9786-5795c9fc7f91 finished after 185: 2
2022-02-06T01:02:09.240 [Information] Executed 'Function1' (Succeeded, Id=cac343e5-911b-4ba7-a0c7-69e883a61392, Duration=185014ms)
2022-02-06T01:02:14.293 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T01:02:14.2930303+00:00', Id=729e8079-e33e-48f4-8c5a-48e102a6d9d1)
2022-02-06T01:02:14.300 [Error] Executed 'Function1' (Failed, Id=729e8079-e33e-48f4-8c5a-48e102a6d9d1, Duration=1ms)e051565c-9900-4cef-9786-5795c9fc7f91: 3
2022-02-06T01:03:14.395 [Information] Executing 'Function1' (Reason='EventGrid trigger fired at 2022-02-06T01:03:14.3956880+00:00', Id=5996df91-cecf-418f-a5b5-82d7680761f8)
2022-02-06T01:03:14.403 [Error] Executed 'Function1' (Failed, Id=5996df91-cecf-418f-a5b5-82d7680761f8, Duration=1ms)e051565c-9900-4cef-9786-5795c9fc7f91: 4

在此之后它一直失败,直到重试策略的延迟变得比 Function 实例的空闲冷却时间更长(当实例被关闭时,静态字典显然已从内存中删除)。

这可能表明 EventGrid 忽略了来自 3 分钟之前的调用的任何响应。也就是说,第一次调用的 185 秒延迟“OK”响应被忽略了。所有后续调用均失败,迫使事件排队等待重试。

它可能比这些简单的测试显示的内容更多。如果您现在真的必须这样做,我建议您在 GitHub

中打开问题单

幂等性问题的潜在解决方案

我不确定我是否完全理解您的“多部分步骤”。但是,如果您可以将该部分打包到一个会话中,则可以将 EventGridTrigger 与 session-enabled ServiceBusTrigger.

链接起来

如果您的事件具有唯一标识符 (eventGridEvent.Id),那么您可以使用与事件标识符匹配的会话 ID 构建服务总线消息。启用会话的服务总线触发器将确保一次仅调用 1 个事件(或会话)。

之所以可行,是因为两个相同的事件也共享相同的 eventGridEvent.Id。但是,您必须跟踪这些标识符。在服务总线会话结束时,您必须将 ID 标记为“已处理”。在会话开始时,检查该 ID 之前是否已被处理;如果是,请忽略该请求。

通过“消息会话”阅读更多关于 ServiceBusTrigger 的信息 here and here(查看 isSessionsEnabled 属性)(您可以查看rch for "session" 以在与会话相关的页面上找到更多信息,它分散在周围)。