本文提供有关在应用程序中同时使用 Azure 事件中心和 Azure Functions 时优化可伸缩性和性能的指南。
函数分组
通常,函数将工作单元封装在事件处理流中。 例如,函数可以将事件转换为新的数据结构,或为下游应用程序扩充数据。
在 Functions 中,函数应用为函数提供了执行上下文。 函数应用行为适用于由函数应用托管的所有函数。 函数应用中的函数将一起部署并一起缩放。 函数应用中的所有函数必须使用同一语言。
将函数分组到函数应用中的方式可能会影响函数应用的性能和缩放功能。 可以根据访问权限、部署和调用代码的使用模式进行分组。
有关分组和其他方面的 Functions 最佳做法的指南,请参阅可靠的 Azure Functions 的最佳做法和提高 Azure Functions 的性能和可靠性。
以下列表是对函数进行分组的指南。 本指南考虑了存储和使用者组方面:
在函数应用中托管单个函数:如果事件中心触发了函数,为了减少该函数和其他函数之间的争用,可以在函数自己的函数应用中隔离该函数。 如果其他函数属于 CPU 或内存密集型,则隔离会特别重要。 此方法之所以有用是因为每个函数都有自己的内存占用和使用模式,可能会直接影响托管它的函数应用的缩放。
为每个函数应用创建自己的存储帐户:避免在函数应用之间共享存储帐户。 此外,如果函数应用使用存储帐户,请不要将该帐户用于其他存储操作或需求。 请务必避免共享由事件中心触发的函数的存储帐户,因为此类函数由于检查点的原因可能具有大量存储事务。
为每个函数应用创建专用使用者组:使用者组是事件中心的视图。 不同的使用者组具有不同的视图,这意味着状态、位置和偏移量可能会有所不同。 通过使用者组,多个消耗应用程序可以具有各自的事件流视图,并且可以按自身步调和偏移量独立读取流。 有关使用者组的详细信息,请参阅 Azure 事件中心中的功能和术语。
一个使用者组具有一个或多个与之关联的使用者应用程序,一个使用者应用程序可以使用一个或多个使用者组。 在流处理解决方案中,每个使用者应用程序相当于一个使用者组。 函数应用是使用者应用程序的典型示例。 下面的关系图提供了从事件中心读取数据的两个函数应用的示例,其中每个应用都具有自己的专用使用者组:
不要在函数应用与其他使用者应用程序之间共享使用者组。 每个函数应用都应是一个具有自己分配的使用者组的不同应用程序,以确保每个使用者的偏移完整性并简化事件流式处理体系结构中的依赖项。 这种配置以及为事件中心触发的每个函数提供其自己的函数应用和存储帐户有助于奠定实现最佳性能和缩放的基础。
函数托管计划
函数应用有几个托管选项,审查它们的功能很重要。 有关这些托管选项的详细信息,请参阅 Azure Functions 托管选项。 记下各个选项的缩放方式。
消耗计划是默认选项。 消耗计划中的函数应用独立缩放,在避免长时间运行的任务时最为高效。
高级版和专用版计划通常用于托管比较消耗 CPU 和内存的多个函数应用和函数。 使用专用计划,可以在 Azure 应用服务计划中按常规应用服务计划费率运行函数。 请务必注意,这些计划中的所有函数应用共享分配给计划的相同资源。 如果函数具有不同的负载配置文件或独特要求,则最好将它们托管在不同的计划中,特别是在流处理应用程序中。
Azure 容器应用为在 Azure Functions 上开发、部署和管理容器化函数应用提供集成支持。 这使您可以在完全托管的基于 Kubernetes 的环境中运行事件驱动的函数,并内置了对开源监控、mTLS、Dapr 和 KEDA 的支持。
事件中心缩放
在部署事件中心命名空间时,需要正确设置几项重要设置,以确保峰值性能和缩放。 本部分重点介绍事件中心的标准层,以及该标准层在你使用 Functions 时会影响缩放的独特功能。 要详细了解事件中心层级,请参阅基本层、标准层、高级层与专用层。
事件中心命名空间对应于 Kafka 群集。 有关事件中心和 Kafka 如何相互关联的信息,请参阅什么是适用于 Apache Kafka 的 Azure 事件中心。
了解吞吐量单位(TU)
在事件中心标准层中,吞吐量被归类为每单位时间内进入命名空间以及从中读取的数据量。 TU 是吞吐量容量的预购单位。
TU 按小时计费。
命名空间中的所有事件中心会共享 TU。 要正确计算容量需求,必须考虑所有应用程序和服务,包括发布者和使用者。 Functions 会影响发布到事件中心和从事件中心读取的字节数和事件数。
确定 TU 数的重点在于入口点。 但是,使用者应用程序的聚合(包括处理这些事件的速率)也必须包括在计算中。
有关事件中心吞吐量单位的详细信息,请参阅吞吐量单位。
使用自动扩充进行纵向扩展
可以在事件中心命名空间上启用自动扩充,以适应负载超过配置的 TU 数的情况。 使用自动扩充可防止限制应用程序,并有助于确保处理(包括事件引入)继续而不会中断。 由于 TU 设置会影响成本,因此使用自动扩充有助于解决有关过度预配的问题。
自动扩充是事件中心的一项功能,经常与自动缩放混淆,尤其是在无服务器解决方案的上下文中。 但与自动缩放不同,当不再需要增加容量时,自动扩充不会纵向缩减。
如果应用程序需要的容量超过允许的最大 TU 数,请考虑使用事件中心高级层或专用层。
分区和并发函数
创建事件中心时,必须指定分区数。 分区计数将保持不变且无法更改,除非是从高级层和专用层进行更改。 当事件中心触发函数应用时,并发实例数可能会等于分区数。
在消耗和高级托管计划中,函数应用实例会根据需要动态横向扩展以满足分区数。 专用托管计划在应用服务计划中运行函数,并要求手动配置实例或设置自动缩放方案。 有关详细信息,请参阅 Azure Functions 的专用托管计划。
最终,在分区数与函数应用实例数或使用者之间形成一对一关系是在流处理解决方案中实现最大吞吐量的理想目标。 要实现最佳并行度,请在一个使用者组中包括多个使用者。 对于 Functions,此目标会转换为计划中函数的许多实例。 结果被称为分区级并行度或最大并行度,如下图所示:
配置尽可能多的分区来实现最大吞吐量和提供更高事件量的可能性似乎是有意义的。 但在配置多个分区时,需要考虑几个重要因素:
- 分区越多,吞吐量越大:由于并行度就是使用者(函数实例)的数目,因此分区越多,并发吞吐量就会越高。 当与其他使用者应用程序共享某个事件中心指定数量的 TU 时,上述事实非常重要。
- 函数越多,需要的内存越多:随着函数实例数的增加,计划中资源的内存占用量也会增加。 有时候,对于使用者而言,分区过多可能会导致性能降低。
- 存在来自下游服务反压的风险:随着生成更多的吞吐量,你会面临压倒性的下游服务风险,或承受来自下游服务的背压。 在考虑对周围资源产生的后果时,必须将使用者扇出考虑在内。 可能的后果包括来自其他服务的限制、网络饱和和其他形式的资源争用。
- 可以对分区进行稀疏填充:包含许多分区和少量事件的组合可能会导致数据在分区之间稀疏分布。 相反,使用较少数量的分区可以提供更好的性能和资源使用。
可用性和一致性
如果未指定分区键或 ID,则事件中心会将传入事件路由到下一个可用分区。 这种做法可提供高可用性,并有助于增加使用者的吞吐量。
当需要对一组事件进行排序时,事件生成者可以指定将特定分区用于该组的所有事件。 从该分区进行读取的使用者应用程序会按正确顺序接收这些事件。 这种权衡提供了一致性,但损害了可用性。 除非必须保留事件的顺序,否则不要使用此方法。
对于 Functions,当事件发布到特定分区并且事件中心触发的函数获得对同一分区的租约时,就可以实现排序。 目前,不支持通过事件中心输出绑定来配置分区的功能。 相反,最佳方法是使用事件中心 SDK 之一发布到特定分区。
有关事件中心如何支持可用性和一致性的详细信息,请参阅事件中心中的可用性和一致性。
事件中心触发器
本部分重点介绍优化事件中心触发的函数的设置和注意事项。 因素包括批处理、采样和会影响事件中心触发器绑定行为的相关功能。
被触发函数的批处理
可以将事件中心触发的函数配置为处理一批事件或一次处理一个事件。 成批处理事件消除了函数调用的一些开销,因此效率更高。 除非你需要仅处理单个事件,否则,应当将函数配置为在被调用时处理多个事件。
为事件中心触发器绑定启用批处理的方式因语言而异:
- 当函数的 function.json 文件中的基数属性设置为多个时,JavaScript、Python 和其他语言会启用批处理。
- 在 C# 中,如果在 EventHubTrigger 属性中为类型指定了数组时,将自动配置基数。
有关如何启用批处理的详细信息,请参阅适用于 Azure Functions 的 Azure 事件中心触发器。
触发器设置
host.json 文件中的多个配置设置在 Functions 的事件中心触发器绑定性能特征中发挥关键作用:
- maxEventBatchSize:此设置表示函数在被调用时可接收的最大事件数。 如果接收的事件数小于此数量,则仍会使用尽可能多的可用事件来调用函数。 无法设置最小批大小。
- prefetchCount:预提取计数是优化性能时最重要的设置之一。 基础 AMQP 通道将参考此值来确定要为客户端提取和缓存的消息数。 预提取计数应大于或等于 maxEventBatchSize 值,并且通常设置为该数量的倍数。 将此值设置为小于 maxEventBatchSize 的数字会对性能造成影响。
- batchCheckpointFrequency:当函数进行批处理时,此值会确定创建检查点的速率。 默认值为 1,这意味着每当函数成功处理单个批次时,都会存在一个检查点。 检查点是在分区级别为使用者组中的每个读者创建的。 有关此设置如何影响事件的重播和重试的信息,请参阅事件中心触发的 Azure 函数:重播和重试(博客文章)。
执行多个性能测试以确定要为触发器绑定设置的值。 建议以增量方式更改设置,并以一致方式测量以微调这些选项。 对于大多数事件处理解决方案而言,默认值是一个合理的起点。
检查点
检查点可标记或提交分区事件序列中的读者位置。 在对事件进行处理并且满足批次检查点频率设置时,Functions 主机负责执行检查点操作。 有关检查点的详细信息,请参阅 Azure 事件中心中的功能和术语。
以下概念有助于理解检查点与函数的事件处理方式之间的关系:
- 异常仍计为成功:如果在处理事件时函数进程没有崩溃,则函数的完成情况将被视为成功,即时出现异常也是如此。 函数完成后,Functions 主机将评估 batchCheckpointFrequency。 如果到了检查点的时间,就会创建一个检查点,而无论是否出现异常。 异常不会影响检查点这一事实不应对正确使用异常检查和处理产生影响。
- 批次频率非常重要:在大容量事件流式处理解决方案中,将 batchCheckpointFrequency 设置更改为大于 1 的值会很有用。 增大此值可以降低检查点创建速率,因此可以降低存储 I/O 操作数。
- 重播可能会发生:每次通过事件中心触发器绑定来调用某个函数时,该函数会使用最新的检查点来确定要从何处继续进行处理。 每个使用者的偏移是在分区级别针对每个使用者组保存的。 如果在上次调用函数时没有出现检查点,则会发生重播,然后会再次调用该函数。 有关重复和重复项技术的详细信息,请参阅幂等性。
在考虑错误处理和重试的最佳做法时,了解检查点至关重要,本文稍后将讨论此主题。
遥测采样
Functions 为 Application Insights 提供内置支持,后者是 Azure Monitor 的一个扩展,可提供应用程序性能监视功能。 借助此功能,可以记录有关函数活动、性能、运行时异常等信息。 有关详细信息,请参阅 Application Insights 概述。
此项强大的功能提供了一些会影响性能的关键配置选项。 有关监视和性能的一些值得注意的设置和注意事项包括:
- 启用遥测采样:对于高吞吐量方案,应评估所需的遥测和信息量。 可考虑使用 Application Insights 中的遥测采样功能,以避免因收集不必要的遥测数据和指标而降低函数性能。
- 配置聚合设置:检查并配置将数据聚合并发送到 Application Insights 的频率。 此配置设置位于 host.json 文件中,该文件中还包含许多其他与采样和日志记录相关的选项。 有关详细信息,请参阅配置聚合器。
- 禁用 AzureWebJobDashboard:对于面向 Functions 运行时版本 1.x 的应用,此设置会将连接字符串存储到 Azure SDK 使用的一个存储帐户,以保留 WebJobs 仪表板的日志。 如果使用 Application Insights 而不是 WebJobs 仪表板,则应移除此设置。 有关详细信息,请参阅 AzureWebJobsDashboard。
如果启用 Application Insights 而不进行采样,则会发送所有遥测数据。 发送有关所有事件的数据可能会对函数的性能产生不利影响,尤其是在高吞吐量事件流式处理方案中。
利用采样并持续评估监视功能所需的适量遥测数据是对于实现最佳性能非常重要。 应将遥测用于常规平台健康状况评估并偶尔用于故障排除,不应用于捕获核心业务指标。 有关详细信息,请参阅配置采样。
输出绑定
使用适用于 Azure Functions 的事件中心输出绑定以简化从函数发布到事件流的过程。 使用此绑定的好处包括:
- 资源管理:绑定会为你处理客户端和连接生命周期,并降低端口耗尽和连接池管理出现问题的可能性。
- 更少的代码:绑定对基础 SDK 进行抽象化,减少了发布事件所需的代码量。 它可帮助你编写更易于编写和维护的代码。
- 批处理:批处理受多种语言支持,可高效地发布到事件流。 批处理可以提高性能,有助于简化发送事件的代码。
强烈建议查看 Functions 支持的语言列表以及这些语言的开发人员指南。 每种语言的“绑定”部分都提供了详细的示例和文档。
在发布事件时进行批处理
如果函数仅发布单个事件,则配置绑定以返回值是一种常见方法,如果函数执行始终以发送事件的语句结束,则此方法非常有用。 应仅将此技术用于仅返回一个事件的同步函数。
当将多个事件发送到某个流时,建议使用批处理来提高性能。 批处理允许绑定以最高效的方式发布事件。
C#、Java、Python 和 JavaScript 都支持使用输出绑定将多个事件发送到事件中心。
使用进程内模型输出多个事件 (C#)
从 C# 函数中发送多个事件时,可使用 ICollector 和 IAsyncCollector 类型。
- 同步函数和异步函数都可以使用 ICollector<T>.Add() 方法。 调用后,它会立即执行添加操作。
- IAsyncCollector<T>.AddAsync() 方法可准备要发布到事件流的事件。 如果编写异步函数,则应使用 IAsyncCollector 以更好地管理已发布的事件。
有关使用 C# 发布单个和多个事件的示例,请参阅适用于 Azure Functions 的 Azure 事件中心输出绑定。
使用独立工作进程模型输出多个事件 (C#)
根据 Functions 运行时版本的不同,独立工作进程模型将支持传递给输出绑定的参数的不同类型。 对于多个事件,使用数组来封装集合。 建议查看独立模型的输出绑定属性和使用情况详细信息,并记下扩展版本之间的差异。
限制和回压
限制注意事项不仅适用于事件中心,也适用于 Azure Cosmos DB 等 Azure 服务的输出绑定。 请务必熟悉应用于这些服务的限制和配额,并相应地进行规划。
要处理进程内模型的下游错误,可以将 AddAsync 和 FlushAsync 包装在 .NET Functions 的异常处理程序中,以便捕获来自 IAsyncCollector 的异常。 另一个选项则是直接使用事件中心 SDK,而不是使用输出绑定。
如果利用独立模型来实现函数,那么在返回输出值时,应该使用结构化异常处理来负责任地捕获异常。
函数代码
本部分介绍了编写代码以处理事件中心触发的函数中的事件时必须考虑的关键方面。
异步编程
建议编写函数以使用异步代码并避免阻塞调用,尤其是在涉及 I/O 调用时。
下面是编写函数以进行异步处理时应遵循的准则:
- 全部异步或全部同步:如果函数配置为异步运行,则所有 I/O 调用应是异步的。 在大多数情况下,部分异步的代码比完全同步的代码更糟。 请选择异步或同步,并始终坚持这一选择。
- 避免阻塞调用:与立即返回的异步调用相反,阻塞调用仅在调用完成后才会返回调用方。 C# 中的一个示例是在异步操作中调用 Task.Result 或 Task.Wait。
有关阻塞操作的更多信息
在异步操作中使用阻塞调用可能会导致线程池饥饿与函数进程崩溃。 发生崩溃的原因在于阻塞调用需要创建另一个线程来补偿当前正在等待的原始调用。 因此,它需要两倍的线程才能完成该操作。
当涉及事件中心时,避免使用这种异步中同步方法尤其重要,因为函数崩溃不会更新检查点。 下次调用该函数时,它可能以此循环结束,并因函数最终超时而似乎停滞不前或进展缓慢。
要对这种现象进行故障排除,通常首先要检查触发器设置,并运行可能涉及增加分区计数的实验。 调查还可能导致更改多个批处理选项,例如最大批大小或预提取计数。 给人的印象是,这是一个吞吐量问题或配置设置问题,只需要进行相应调整便可解决。 但核心问题在于代码本身,因此必须在代码中才能解决。
作者
本文由 Microsoft 维护, 最初由以下参与者撰写。
主要作者:
- David Barkol | 首席解决方案专家 GBB
若要查看非公开的 LinkedIn 个人资料,请登录到 LinkedIn。
后续步骤
在继续操作之前,请考虑查看以下相关文章:
- 监视 Azure Functions 中的执行
- Azure Functions 可靠事件处理
- 针对完全相同的输入设计 Azure Functions
- ASP.NET Core 异步指导。
- 适用于 Azure Functions 的 Azure 事件中心触发器