如何避免遗漏讯息
事件处理是无服务器和Azure函数中最常见的方案之一。 几周前,我写了一篇关于如何按功能处理事件的文章 ,对于这个博客,我想概述一下如何创建一个可靠的消息处理器,从而避免一路上丢失任何消息。 老实说,这个博客很容易分成两部分或三部分,但是我决定将所有内容都保留在一个帖子中。 它很冗长,但是从基础一直到高级模式,例如断路器和异常过滤器。 这些示例使用C#编写时,所有模式都可以跨任何语言运行(除非另有明确说明)。
分布式系统中事件流的挑战
想象一下,一个系统以恒定的速率发送事件-假设每秒100个事件。 从Azure Functions消耗这些事件很容易设置,并且在几分钟之内您可以让多个并行实例每秒处理这100个事件。 但是,如果事件发布者发送了损坏的事件怎么办? 还是您的实例出现故障并在执行中崩溃? 还是下游系统离线? 在保留应用程序的整体完整性和吞吐量的同时,如何处理这些问题?
有了队列,可靠的消息传递就更加自然了。 在Azure函数中,当您在队列消息上触发时,该函数可以在队列消息上创建“锁”,尝试进行处理,如果未能“释放”该锁,则另一个实例可以重新尝试并重试。 这种反复操作一直持续到达到成功或经过多次尝试(默认为4次)后,该消息才被添加到中毒队列中。 尽管单个队列消息可能处于此重试周期中,但它不能防止其他并行执行继续使其余消息出队-因此,总体吞吐量在很大程度上不受一个坏消息的影响。 但是,存储队列不能保证排序,也无法针对事件中心等服务的高吞吐量进行优化。
对于像Azure Event Hubs这样的事件流,没有锁定概念。 为了实现高吞吐量,多个使用者组和可重播性,事件中心等服务在使用事件时读起来更像磁带驱动器。 每个分区的流中只有一个“偏移”指针,您可以向前或向后读取。 在读取事件流时,如果遇到故障并决定将指针放在同一位置,它将阻止对该分区进行进一步处理,直到指针前进为止。 换句话说,如果每秒仍然有100个事件发生,并且Azure Functions在尝试处理单个不良事件时停止将指针移到新事件,则这些事件将开始堆积。 不久之后,您将积压大量不断增加的事件。
![](https://i-blog.csdnimg.cn/blog_migrate/8db1d310acb7576a8155c4dd9f4b2485.gif)
给定这种偏移量和使用者行为, 无论执行成功还是失败 , 函数都会继续在流上继续执行指针 。 这意味着您的系统和功能需要了解并结构化以处理这些行为。
Azure函数如何使用事件中心事件
Azure Functions事件中心触发器的行为如下:
- 将为事件中心的每个分区创建一个指针并将其持久保存在Azure存储中(如果您看到的话,可以在存储帐户中看到它)
- 收到新的事件中心消息时(默认为一批),主机将尝试使用一批消息来触发功能
- 如果函数完成执行 (有无异常),则指针将前进并在存储中检查点
- 如果由于某种原因阻止函数执行完成,主机将无法使指针前进,并且在随后的检查中,将再次接收相同的消息(来自先前的检查点)
- 重复步骤2-4
有一些重要的事情要注意。 首先是如果您有未处理的异常,则可能会丢失消息 ,因为即使执行导致异常的操作也会使指针前进。 第二点是,与分布式系统的规范一样,Functions保证至少一次交付。 您的代码和从属系统可能需要考虑以下事实:同一条消息可能会收到两次。 我的示例在下面显示了这两种行为以及如何对其进行编码:
对于这些测试,我执行了以下操作-我发布了100,000条消息,以按顺序进行处理(每个分区键)。 我将每条消息记录到Redis缓存中,以便对其进行处理以验证和可视化顺序和可靠性。 对于第一个测试,我编写了它,因此每100条消息都会引发一个异常,而无需任何异常处理。
当我在此示例中推送100,000条消息时,这就是我在Redis中看到的内容:
![](https://i-blog.csdnimg.cn/blog_migrate/7ef6d1380d5d8fb72b3577ca5a1fc838.png)
您会发现我错过了100-112之间的一整条消息。 那么这里发生了什么? 在某个时候,我的一个函数实例收到了有关该分区键的一批消息。 该特定批处理以112结尾,但是在消息100处引发了我的异常。 这暂停了该执行,但是功能主机继续前进并读取下一批。 从技术上讲,这些消息仍保留在事件中心中,但是我需要手动执行并重新获取100-112进行重新处理。
添加尝试捕获
最简单的解决方法是在我的代码中添加一个简单的“ try / catch”块。 现在,如果引发了异常,我可以在同一执行中捕获该异常,并在指针前进之前对其进行处理。 当我在上面的代码示例中添加了一个catch并重新运行测试时,我依次看到了所有100,000条消息。
![](https://i-blog.csdnimg.cn/blog_migrate/c8961e16b9c4f6632f03c9195d3a2996.png)
最佳实践:所有事件中心功能都需要有一个catch块
在此示例中,我使用了catch尝试将其他插入Redis,但是您可以想象其他可行的选项,例如发送通知或将事件输出到“毒药”队列或事件中心以供以后处理。
重试机制和策略
本质上可能会出现一些例外情况。 也就是说,如果过一会儿再次尝试操作,某些“打cup”或其他问题可能会消失。 在上一节中,我在catch块中进行了一次重试-但我只重试了1次,如果重试失败或抛出了自己的异常,我会很不幸,仍然会丢失100–112个事件。 有许多工具可以帮助定义更强大的重试策略,而这些工具仍使您可以保留处理顺序。
在测试中,我使用了一个名为Polly的C#故障处理库。 这使我能够定义简单和高级的重试策略,例如“尝试插入此消息3次(可能有两次重试之间的延迟)。 如果所有重试的最终结果均是失败,则将消息添加到队列中,以便我可以继续处理流并稍后处理损坏或未处理的消息。”
以及产生的Redis:
![](https://i-blog.csdnimg.cn/blog_migrate/ed1ff37130ac438741a934fa406273b4.png)
当使用更高级的异常捕获和重试策略时,值得注意的是,对于预编译的C#类库,有一个预览功能可在函数中编写“异常过滤器”。 这使您能够编写一种方法,该方法将在函数执行期间抛出未处理的异常时执行。 详细信息和示例可以在这篇文章中找到。
非异常错误或问题
我们已经解决了如果您的代码遇到异常时可能发生的异常,但是在执行过程中函数实例出现打ic或失败的情况又如何呢?
![](https://i-blog.csdnimg.cn/blog_migrate/77772e955284d94a726fea09b22e01bb.png)
如前所述-如果函数未完成执行,则偏移量指针永远不会前进,因此当新实例开始提取消息时,相同的消息将再次处理。 为了模拟这一点,在100,000条消息处理过程中,我手动停止,启动并重新启动了功能应用程序。 这是一些结果(左)。 您会注意到,尽管我处理了所有事情,并且一切都井井有条,但某些消息已被处理了多次(在700我重新处理601+之后)。 总体而言,这是一件好事,因为它至少给了我一次保证,但这确实意味着我的代码可能需要一定程度的幂等性。
断路器和停线
以上模式和行为有助于重试并尽最大努力处理任何事件。 尽管此处可能会出现一些故障,但是如果发生大量故障并且我想停止触发新事件,直到系统达到健康状态,该怎么办? 这通常是通过“断路器”模式实现的,您可以在其中断开事件过程的电路并在以后恢复。
Polly(我用于重试的库)支持某些断路器功能。 但是,当跨电路跨越多个无状态实例的分布式临时函数工作时,这些模式也无法转换。 关于如何在Polly中解决此问题 ,有一些有趣的讨论 ,但与此同时,我手动实现了它。 事件过程中的断路器需要两部分:
- 跨所有实例共享状态以跟踪和监视电路的运行状况
- 可以管理电路状态(断开或闭合)的主进程
出于我的目的,我将Redis缓存用于#1,将Azure Logic应用程序用于#2。 还有其他多种服务可以同时满足这两种需求,但是我发现这两种服务都很好用。
跨实例的故障阈值
因为我可能有多个实例同时处理事件,所以我需要共享外部状态以监视电路的运行状况。 我想要的规则是“如果所有实例在30秒内最终发生100次以上的故障,请断开电路并停止触发新消息。”
无需太深入细节( 所有这些示例都在GitHub上 ),我使用Redis中的TTL和排序集功能来滚动显示最近30秒内的失败数量。 每当添加新的故障时,我都会检查滚动窗口以查看是否已超过阈值(在过去30秒钟内超过了100),如果是,则将事件发送到Azure事件网格。 如果感兴趣的话, 这里是相关的Redis代码 。 这使我能够检测并发送事件并断开电路。
使用Logic Apps管理电路状态
我使用Azure Logic应用程序来管理电路状态,因为连接器和有状态编排很自然。 在检测到需要断开电路后,我触发了工作流程(事件网格触发器)。 第一步是停止Azure功能(带有Azure资源连接器),并发送包含一些响应选项的通知电子邮件。 然后,我可以调查电路的运行状况,当一切看起来正常时,我可以响应“启动”电路。 这将恢复工作流,然后将启动该功能,并且将从上一个事件中心检查点开始处理消息。
![](https://i-blog.csdnimg.cn/blog_migrate/c027751047e0a017ec1637788123c8b9.png)
大约15分钟前,我发送了100,000条消息,并将每100条消息设置为失败。 我大约有5,000条消息达到了故障阈值,因此向事件网格发出了一个事件。 我的Azure Logic应用立即触发,停止了该功能,并向我发送了一封电子邮件(上述)。 如果现在看一下Redis的当前状态,我会看到很多部分经过部分处理的分区,如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/73d9d9f9f2e76e3a258d4b8174701be4.png)
单击电子邮件以重新启动电路后,运行相同的Redis查询,我可以看到该函数从上一个Event Hub检查点开始执行并继续。 没有消息丢失,一切都按顺序进行了处理,只要我需要使用逻辑应用程序管理状态,就可以断开电路。
![](https://i-blog.csdnimg.cn/blog_migrate/811ce507dc9b86bfe0cc6c59dcfec156.png)
希望该博客对概述可用于使用Azure Functions可靠处理消息流的某些模式和最佳实践有所帮助。 有了这种了解,您应该能够利用功能的动态规模和消耗定价,而不必牺牲可靠性。
我提供了一个指向GitHub存储库的链接,该链接指向该示例的不同枢轴的每个分支的指针: https : //github.com/jeffhollan/functions-csharp-eventhub-ordered-processing 。 如有任何问题,请随时通过Twitter @jeffhollan与我联系。
From: https://hackernoon.com/reliable-event-processing-in-azure-functions-37054dc2d0fc