Durable Functions 中的性能和缩放 (Azure Functions)
若要优化性能和可伸缩性,必须了解 Durable Functions 的独特缩放特征。 本文介绍如何根据负载缩放辅助角色,以及如何优化各种参数。
辅助角色缩放
任务中心概念的基本优势是可以持续调整处理任务中心工作项的辅助角色数量。 具体而言,如果更快地处理工作,则应用程序可以添加更多的辅助角色(横向扩展);如果工作量对辅助角色而言并不饱和,可以删除辅助角色(横向缩减)。 如果任务中心完成空闲,甚至可以缩减至零。 缩减为零即表示完全没有辅助角色;只有缩放控制器和存储需要保持活动状态。
下图演示了此概念:
自动缩放
与消耗计划和弹性高级计划中运行的所有 Azure Functions 一样,Durable Functions 支持通过 Azure Functions 缩放控制器自动缩放。 缩放控制器监视消息和任务在处理之前需要等待的时间。 根据这些延迟,它可以决定添加或删除辅助角色。
注意
从 Durable Functions 2.0 开始,可以将函数应用配置为在 Elastic Premium 计划中受 VNET 保护的服务终结点中运行。 在此配置中,Durable Functions 触发器启动缩放请求而不是缩放控制器。 有关详细信息,请参阅运行时缩放监视。
在高级计划中,自动缩放有助于让辅助角色数量(进而让运营成本)始终与应用程序当前的负载大致成正比。
CPU 使用率
在单个线程上执行业务流程协调程序函数,确保多次重播后执行可以是确定性的。 由于采用这种单线程执行,业务流程协调程序函数线程不得执行 CPU 密集型任务、执行 I/O 或出于任何原因而阻塞。 应将需要 I/O、阻塞或多个线程的任何工作移到活动函数中。
活动函数的行为与队列触发的正则函数完全相同。 它们可以安全地执行 I/O、执行 CPU 密集型操作和使用多个线程。 由于活动触发器是无状态的,因此可以任意横向扩展到不限数量的 VM。
实体函数也在单个线程上执行,操作是逐个处理的。 但是,实体函数对于可执行的代码类型没有任何限制。
函数超时
与所有 Azure Functions 一样,活动、业务流程协调程序和实体函数会受到相同的函数超时。 作为一般规则,Durable Functions 处理函数超时的方式与处理应用程序代码引发的未经处理的异常相同。
例如,如果某一活动超时,则会将函数执行记录为失败,并通知业务流程协调程序而其处理超时的方式与处理任何其他异常一样:如果调用指定,则重试,或者可能执行异常处理程序。
实体操作批处理
为了提高性能和降低成本,单个工作项可以执行一整批实体操作。 在消耗计划中,每个批处理随后都会作为单个函数执行计费。
默认情况下,最大批大小为 50(对于消耗计划)和 5000(对于所有其他计划)。 也可在 host.json 文件中配置最大批大小。 如果最大批大小为 1,则有效地禁用了批处理。
注意
如果单个实体操作需要很长时间来执行,则限制最大批大小以降低函数超时的风险可能有所帮助,尤其是在消耗计划中。
实例缓存
通常,若要处理业务流程工作项,辅助角色必须执行以下两个步骤
- 提取业务流程历史记录。
- 使用历史记录重播业务流程协调程序代码。
如果同一辅助角色正在处理同一业务流程的多个工作项,则存储提供程序可以通过缓存辅助角色内存中的历史记录来优化此过程,从而无需执行第一个步骤。 此外,它还可以缓存中间执行业务流程协调程序,这样也就无需执行第二个步骤,也就是历史记录重播这一步。
缓存的典型效果是针对基础存储服务减少 I/O,并整体改善吞吐量和延迟。 另一方面,缓存会增加辅助角色的内存消耗。
Azure 存储提供程序和 Netherite 存储提供程序当前支持实例缓存。 下表对它们进行比较。
Azure 存储提供程序 | Netherite 存储提供程序 | MSSQL 存储提供程序 | |
---|---|---|---|
实例缓存 | 支持 (仅 .NET 进程内辅助角色) |
支持 | 不支持 |
默认设置 | 已禁用 | 已启用 | 不适用 |
机制 | 扩展会话 | 实例缓存 | 不适用 |
文档 | 请参阅扩展会话 | 请参阅实例缓存 | 不适用 |
提示
缓存可以减少重播历史记录的频率,但并非完全不需要重播。 开发业务流程协调程序时,强烈建议在禁用缓存的配置上对其进行测试。 若要在开发时检测业务流程协调程序函数代码约束,这种强制重播行为会有所帮助。
缓存机制的比较
提供程序使用不同的机制来实现缓存,并提供不同的参数来配置缓存行为。
- 扩展会话(由 Azure 存储提供程序使用)将中间执行业务流程协调程序保留在内存中,直到它们保持在空闲状态一段时间。 控制此机制的参数为
extendedSessionsEnabled
和extendedSessionIdleTimeoutInSeconds
。 有关详细信息,请参阅 Azure 存储提供程序文档的扩展会话部分。
注意
扩展会话仅在 .NET 进程内辅助角色中受支持。
- 实例缓存(由 Netherite 存储提供程序使用)将所有实例的状态(包括其历史记录)保存在辅助角色的内存中,同时跟踪使用的总内存。 如果缓存大小超过
InstanceCacheSizeMB
所配置的限制,则会逐出最近使用最少的实例数据。 如果CacheOrchestrationCursors
设置为 true,缓存还会存储中间执行业务流程协调程序以及实例状态。 有关详细信息,请参阅 Netherite 存储提供程序文档的实例缓存部分。
注意
实例缓存适用于所有语言 SDK,但 CacheOrchestrationCursors
选项仅适用于 .NET 进程内辅助角色。
并发限制
单个辅助角色实例可以同时执行多个工作项。 这有助于提高并行度并更有效地利用辅助角色。 但如果辅助角色尝试同时处理过多的工作项,则可能会耗尽其可用资源,例如 CPU 负载、网络连接数或可用内存。
若要确保不过度使用单个辅助角色,可能需要限制每个实例的并发性。 通过限制在每个辅助角色上同时运行的函数数量,我们可以避免耗尽该辅助角色的资源限制。
注意
并发限制仅适用于本地,用于限制每个辅助角色当前处理的内容。 因此,这些限制不会限制系统的总吞吐量。
提示
在某些情况下,限制每个辅助角色的并发工作实际上可以提高系统的总吞吐量。 每个辅助角色执行的工作减少会让缩放控制器添加更多辅助角色来满足队列需求,因此会增加总吞吐量。
限制的配置
可以在 host.json 文件中配置活动、业务流程协调程序和实体函数的并发限制。 相关的设置为活动函数的 durableTask/maxConcurrentActivityFunctions
,以及业务流程协调程序和实体函数的 durableTask/maxConcurrentOrchestratorFunctions
。 这些设置控制加载到单个辅助角色上的内存中的业务流程协调程序、实体或活动函数的最大数目。
注意
业务流程和实体仅在主动处理事件或操作时(或启用实例缓存时)才加载到内存中。 执行其逻辑并等待在业务流程协调程序函数代码中命中 await
(C#) 或 yield
(JavaScript、Python)语句后,可从内存中将其卸载。 从内存中卸载的业务流程和实体不计入 maxConcurrentOrchestratorFunctions
限制。 即使数百万个业务流程或实体处于“正在运行”状态,它们也仅在加载到活动内存时才会计入限制。 如果业务流程正在等待活动完成执行,那么计划活动函数的业务流程同样不计入限制。
Functions 2.0
{
"extensions": {
"durableTask": {
"maxConcurrentActivityFunctions": 10,
"maxConcurrentOrchestratorFunctions": 10
}
}
}
Functions 1.x
{
"durableTask": {
"maxConcurrentActivityFunctions": 10,
"maxConcurrentOrchestratorFunctions": 10
}
}
语言运行时注意事项
你选择的语言运行时可能会对函数施加严格的并发限制。 例如,采用 Python 或 PowerShell 编写的 Durable Functions 应用可能只支持在单个 VM 上一次运行一个函数。 如果没有仔细考虑,可能会导致严重的性能问题。 例如,如果业务流程协调程序扇出到 10 个活动,但语言运行时将并发数量限制为一个函数,那么 10 个活动函数中有 9 个将会停滞,以等待运行机会。 此外,这 9 个停滞的活动将无法对任何其他辅助角色进行负载均衡,因为 Durable Functions 运行时已将它们加载到内存中。 如果活动函数长时间运行,就特别容易出现问题。
如果使用的语言运行时施加了并发限制,则应该更新 Durable Functions 并发设置,以匹配语言运行时的并发设置。 这样可确保 Durable Functions 运行时不会尝试同时运行比语言运行时允许的并发数量更多的函数,从而将任何挂起的活动均衡到其他 VM。 例如,如果 Python 应用将并发数量限制为 4 个函数(可能只在一个语言工作进程上配置了 4 个线程,或在 4 个语言工作进程上配置了 1 个线程),则应将 maxConcurrentOrchestratorFunctions
和 maxConcurrentActivityFunctions
配置为 4。
有关 Python 的详细信息和性能建议,请参阅在 Azure Functions 中提高 Python 应用的吞吐量性能。 此 Python 开发人员参考文档中提到的技术可能会对 Durable Functions 性能和可伸缩性产生重大影响。
分区计数
部分存储提供程序使用分区机制,并允许指定 partitionCount
参数。
使用分区时,辅助角色不会直接竞争单个工作项。 而是先将工作项分组到 partitionCount
分区中。 然后将这些分区分配给辅助角色。 这种分区形式的负载分布方法有助于减少需要的存储访问总数。 此外还可以启用实例缓存并改进区域,因为会创建关联:同一实例的所有工作项都由同一辅助角色处理。
注意
分区限制横向扩展,因为大多数 partitionCount
辅助角色都可以处理已分区队列的工作项。
下表显示为各个存储提供程序分区了哪些队列,以及 partitionCount
参数的允许范围和默认值。
Azure 存储提供程序 | Netherite 存储提供程序 | MSSQL 存储提供程序 | |
---|---|---|---|
实例消息 | Partitioned | Partitioned | 未分区 |
活动消息 | 未分区 | Partitioned | 未分区 |
默认值 partitionCount |
4 | 12 | 不适用 |
最大值 partitionCount |
16 | 32 | 不适用 |
文档 | 请参阅业务流程协调程序横向扩展 | 请参阅分区计数注意事项 | 不适用 |
警告
创建任务中心后,就无法再更改分区计数。 因此,建议将其设置为足够大的值,以符合任务中心实例的未来横向扩展要求。
分区计数的配置
可以在 host.json 文件中指定 partitionCount
参数。 以下示例 host.json 片段将 durableTask/storageProvider/partitionCount
属性(或 Durable Functions 1.x 中的 durableTask/partitionCount
)设置为 3
。
Durable Functions 2.x
{
"extensions": {
"durableTask": {
"storageProvider": {
"partitionCount": 3
}
}
}
}
Durable Functions 1.x
{
"extensions": {
"durableTask": {
"partitionCount": 3
}
}
}
有关尽量减少调用延迟的注意事项
在正常情况下,(对活动、业务流程协调程序、实体等的)调用请求的处理速度应该相当快。 但是,不能保证任何调用请求的最大延迟,因为它取决于以下因素:应用服务计划的缩放行为类型、并发设置和应用程序积压工作的大小。 因此,我们建议在压力测试方面进行投资来测量和优化应用程序的尾部延迟。
性能目标
规划对生产应用程序使用 Durable Functions 时,必须在规划过程中提前考虑性能要求。 一些主要的使用方案包括:
- 顺序活动执行:此方案描述的业务流程协调程序函数逐个运行一系列活动函数。 它与函数链接示例非常类似。
- 并行活动执行:此方案描述的业务流程协调程序函数使用扇出扇入模式并行执行多个活动函数。
- 并行响应处理:此方案是扇出扇入模式的后半部分。 它侧重于扇入性能。 必须注意,与扇出不同,扇入是由单个业务流程协调程序函数实例执行的,因此只能在单个 VM 上运行。
- 外部事件处理:此方案表示一次等待一个外部事件的单个业务流程协调程序函数实例。
- 实体操作处理:此方案测试单个计数器实体处理恒定操作流的速度。
我们在存储提供程序的相应文档中提供了这些方案的吞吐量数字。 具体而言:
提示
与扇出不同,扇入操作限制为单个 VM。 如果应用程序使用扇出扇入模式,并且你关注扇入性能,请考虑在多个子业务流程之间分割活动函数扇出。