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

3
我有一个Azure函数应用程序运行在消耗计划下,其中包含使用EventGridTrigger触发的函数以及触发这些函数的Event Grid Topic。 我依赖于重试功能以提高可靠性。如果函数没有通过从函数返回来确认事件,我会看到事件被重新传递。 到目前为止一切顺利。
我正在使用带有SQL Server的EF Core,并具有用于并发管理的时间戳/行版本列。稍后会详细说明。
文档概述了事件网格具有“至少一次”传递。这意味着事件的消费者应该是幂等的。我们有一个多步骤的过程,我们正在修改此过程以使其成为幂等,因此我们正在考虑各种情况。这个问题涉及到一个特定的情况。
我理解的“至少一次”是指同一事件可以多次触发一个函数,并且一个事件可以同时由两个函数实例处理;也许一个正在花费很长时间,或者网格只是将其传送了两次。
如果情况是这样的,并且两个函数实例同时处理一个事件,则可能一个成功,一个失败-可能是并发异常,或者函数可能由代码或平台生成其他异常。这些平台异常很少见,但我们已经看到了这种迹象(调用仅失败;我们已经看到了单个调用,并且已经看到事件被重新传递)。
我们从未看到任何迹象表明一个事件同时由两个实例处理;我的问题假设这是可能的。在这种情况下,我不确定重新传递会发生什么:
- “成功”实例首先完成,然后第二个实例无法通过超时或未处理的异常确认事件。 - “失败”实例通过抛出异常首先完成,然后“成功”实例完成并确认事件。
在这两种情况中,一个实例确认了事件,另一个实例没有。有人知道是否会在这些情况中重新传递事件吗?这些实例运行的顺序是否改变答案?
实际问题更为复杂:我们正在进行多步骤过程,并在每个步骤结束时保存到数据库。因此,计划仅允许处理每个步骤一次。在两个实例同时运行的情况下,我们利用EF Core中的并发验证来防止第二个实例在移动到下一个“步骤”时保存数据。但我们不知道是否要确认事件-因为其他实例可能在后续步骤中遇到问题。我们很“乐观”,认为这些问题会很少见,但我们希望获得尽可能多的自动恢复。
任何见解都将有所帮助。我们真的不能改变多步骤过程的方式;首先,多个步骤允许我们跟踪进度。
提前感谢。
1个回答

3

我在文档中找不到有实质性的信息,因此我自己进行了一些测试,因为这个问题让我很感兴趣。我还包括了一个潜在的"解决方案"来解决您的多步问题,或者说,它还没有幂等(但是吗?)。请浏览整篇文字,它在最后。

首先,如果第一次调用超过默认的30秒响应时间限制,只要两个实例运行相同的事件,那么两个实例都可以运行同一个事件,这就是为什么提到“至少一次”传递的原因。例如:

  1. 事件已发布
  2. 实例A接收事件
  3. 实例A运行超过30秒
  4. 将事件添加到“重试队列”,因为实例A尚未回复
  5. Instance A完成,总共耗时35秒
  6. 从重试队列中发布超过10秒的事件
  7. 实例B接收事件

我真的认为,两个实例的执行持续时间会影响EventGrid的反应。

根据Retry schedule文档:

如果端点在3分钟内响应,则Event Grid将尽最大努力从重试队列中删除事件,但仍可能收到重复事件。

这可以解释为,只要一个实例成功响应,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秒后最有可能被添加到队列中的重试要么被移除/替换了,要么它的预定执行时间被"更新":

  • 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实际上知道第一个实例尚未失败,仍在运行,因此它实际上还没有将重试尝试添加到队列中。然而,它在第一次实例失败后添加了它,而且如预期的那样,在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仅查看时间戳的分钟部分。我们可以从中得出的一件事是,第一个实例的延迟失败不会触发第三次调用。
让我们改变异常抛出的时间;第二次调用将会抛出一个异常,像这样:
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

之后,由于重试策略的延迟时间超过了函数实例的闲置冷却时间(静态字典显然从内存中删除,因为实例已关闭),该方法继续失败。

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

除了这些简单测试显示的内容之外,可能还有更多内容。如果您确实必须知道,我建议在GitHub上开启一个问题票据。

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

我不确定我完全理解您的“多部分步骤”。但是,如果您可以将该部分包装到单个会话中,则可以将EventGridTrigger与启用会话的ServiceBusTrigger链接起来。

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

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

在此处阅读有关带“消息会话”的ServiceBusTrigger在此处(查看isSessionsEnabled属性)(您可以搜索“session”以查找与会话相关的页面更多内容,它们分散在各处)。


首先,感谢您的详细回复!我计划处理这种情况,所以至少我不会浪费时间担心永远不会发生的事情。当我说“多步骤过程”时,我的意思是我需要完成几件事才能完成操作:读取一些数据,将该数据与数据库中的记录关联,构建内存中的文件,写入Azure存储等。一旦某些数据与一个记录相关联,我就不想再次进行关联。我会调查您的建议-再次感谢。 - Phil Mar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接