次の方法で共有

Durable Functions 的 Azure 存储提供程序

使用Durable Functions时,Azure Storage提供程序是管理状态和业务流程的默认选项。 Azure Storage提供程序通过将实例状态和队列存储在Azure Storage帐户中来优化应用程序的性能和可伸缩性。

使用Azure存储提供程序:

  • Azure队列驱动所有函数执行。
  • Azure Tables用于存储编排和实体的状态及历史记录。
  • Azure Blob 和 Blob 租约会将编排实例和实体分布到多个应用实例(也称为工作器虚拟机)上。

让我们探讨这些Azure Storage组件如何协同工作,并影响应用的性能和可伸缩性。

存储表示形式

任务中心持久保存所有实例状态和所有消息。 有关任务中心如何跟踪业务流程进度的快速概述,请参阅 任务中心执行示例

创建任务中心时,Azure Storage提供程序会在存储帐户中设置这些组件:

  • Azure表:
    • 两个表存储历史记录和实例状态。
    • 如果启用 表分区管理器,第三个表存储分区信息。
  • Azure队列:
    • 一个存储活动消息的Azure队列。
    • 存储实例消息的一个或多个Azure队列。
      • 每个 控制队列 表示一个分区,该 分区 根据实例 ID 的哈希分配了所有实例消息的子集。
  • Azure Blob:
    • 用于租用 Blob 和/或大型消息的额外 Blob 容器。

例如,如果将任务中心 xyz 命名并设置 PartitionCount = 4,则会看到以下队列和表:

显示 Azure 存储提供程序的存储组织图,包含四个控制队列

让我们看看每个组件,并了解它所扮演的角色。

历史记录表

历史记录表是 Azure 存储表,其中包含任务中心内所有编排实例数据的历史事件,包括活动函数和子编排函数的输出负载,以及外部事件的负载。 表名称遵循格式 <TaskHubName>History。 当实例运行时,新行将添加到此表。 在此表中:

  • 分区键派生自业务流程的实例 ID。 默认情况下,实例 ID 是随机的,可确保Azure Storage中内部分区的最佳分布。
  • 行键是排序历史记录事件的序列号。

如果需要运行业务流程实例,系统会使用单个表分区中的范围查询将完整历史记录加载到内存中。 这些历史事件会重播到您的编排程序代码中,将其恢复到之前保存的检查点状态。 此方法遵循 事件溯源模式

这种方法可能会给虚拟机带来巨大的内存压力。 通过以下方法减少编排历史记录的长度和大小:

  • 将大型编排拆分为 多个子编排
  • 缩小活动函数和子协调器函数返回的输出大小。
  • 降低每台虚拟机的并发限制,以限制可同时加载到内存中的编排数量。

实例表

“实例”表包含任务中心内所有业务流程和实体实例的状态。 创建实例时,会向此表添加新行。 在此表中:

  • 分区键是业务流程实例 ID 或实体键。
  • 行键是空字符串。 每个业务流程或实体实例通常都有一行。

实例表满足来自代码的实例查询请求状态查询 HTTP API 的调用。 它最终与 历史记录 表的内容保持一致。 这种关注点分离遵循 命令和查询责任分离(CQRS)模式,该模式可有效地处理实例查询作。

使用 Instances 表的分区,可以存储数百万个业务流程实例,而不会对运行时性能或规模产生任何明显影响。 但是,实例数可能会显著影响 多实例查询 性能。 若要控制这些表存储的数据量,请考虑定期 清除旧实例数据

分区表

注意

启用 Table Partition Manager 时,此表仅在任务中心可见。 若要使用它,请在 useTablePartitionManagement 应用的 host.json中配置设置。

Partitions 表存储了 Durable Functions 应用程序的分区状态,并帮助在应用程序的工作器之间分配分区。 每个分区都有一行。

队列

函数应用的任务中心中的内部队列会触发你的协调器、实体和活动函数,并提供可靠的“至少一次”消息传递保证。 Durable Functions使用两种类型的队列:

工作项队列

在 Durable Functions 中,每个任务中心都有一个工作项队列。 它是一种基本队列,功能上与Azure Functions中的任何其他queueTrigger队列一样。 工作项队列通过从队列中逐一出队消息来触发无状态 活动函数。 每个消息都包含活动函数输入和元数据,例如要执行的函数。 当 Durable Functions 应用横向扩展到多台虚拟机时,这些虚拟机会竞争从工作项队列获取任务。

控制队列

Durable Functions中的每个任务中心都有多个控制队列。 与更简单的工作项队列相比,控制队列更为复杂。 控制队列会触发有状态的协调器和实体函数。 由于协调器和实体函数实例是有状态的单例,因此每个协调器或实体必须一次只能由一个工作线程处理。 为此,系统将每个业务流程实例或实体分配给单个控制队列。 这些控制队列跨工作者进行负载均衡,以确保每个队列一次只由一个工作者处理。 在后面的部分中查找有关此行为的更多详细信息。

控制队列包含各种业务流程生命周期消息类型,包括:

系统在单次轮询中从控制队列中最多出队 32 条消息。 这些消息包含有效负载数据和业务流程实例元数据。 如果多个取消排队的消息以相同的业务流程实例为目标,则会将其作为批处理进行处理。

后台线程不断轮询控制队列消息。 在 host.json中配置以下队列设置:

  • controlQueueBatchSize:控制每个队列轮询的批大小,默认为 32(Azure队列支持的最大值)。
  • controlQueueBufferThreshold:控制内存中缓冲的预提取控制队列消息的最大数目。 默认值因托管计划类型等因素而异。

有关这些设置的详细信息,请参阅 host.json 架构 文档。

提示

增加controlQueueBufferThreshold值可加快单个编排或实体处理事件的速度。 但是,它还会增加内存使用量。 内存使用率越高,部分来自从队列中提取更多消息,部分来自将更多业务流程历史记录提取到内存中。 减少 controlQueueBufferThreshold 该值可以有效地减少内存使用量。

队列轮询

Durable Task 扩展实现随机指数退避算法,以减少空闲队列的轮询操作对存储事务成本的影响。 当运行时找到消息时,它会立即检查另一条消息。 如果未找到任何消息,它会在重试之前等待。 在随后失败的尝试获取队列消息后,等待时间将继续增加到最长等待时间,默认为 30 秒。

您可以使用属性在文件中配置最大轮询延迟。

  • 较高的值可能会导致消息处理延迟较高,不过,在处于非活动状态的时间段后,你只会期望更高的延迟。
  • 较低的值可能会导致 存储成本增加 ,因为存储事务增加。

注意

当在 Azure Functions 消耗计划和高级计划中运行时,Azure Functions 缩放控制器每 10 秒轮询一次每个控制队列和工作项队列。 此额外轮询是确定何时激活函数应用程序实例并做出伸缩决策所必需的。 目前,10 秒间隔为常量且不可配置。

编排启动延迟

当系统将消息置于 ExecutionStarted 任务中心控制队列之一时,编排实例将启动。 在某些情况下,调度编排任务到实际开始运行之间可能会出现多秒的延迟。 在此时间间隔内,编排实例保持为 Pending 状态。 造成这种延迟的潜在原因有两个:

  • 积压的控制队列:

    如果实例的控制队列包含大量消息,则运行时可能需要一段时间才能接收和处理 ExecutionStarted 消息。 当编排并发处理大量事件时,可能会发生消息积压。 进入控制队列的事件包括:

    • 业务流程启动事件
    • 活动完成情况
    • 持久计时器
    • 终止
    • 外部事件

    如果积压工作延迟在正常情况下发生,请考虑创建具有更多分区的新任务中心。 配置更多分区会使运行时创建更多用于分配负载的控制队列。 每个分区对应于一个控制队列的 1:1,最多包含 16 个分区。

  • 退避轮询延迟:

    只有当你的应用横向扩展到两个或更多实例时,才可能遇到前面所述的控制队列的回退轮询行为。 如果出现以下问题,可以避免延迟:

    • 只有一个应用实例
    • 启动编排的应用实例也是轮询目标控制队列的同一实例

    可以通过更新 host.json 设置来减少退避轮询延迟。

Blob对象

大多数情况下,Durable Functions 不会使用 Azure 存储 Blob 来保留数据。 但是,队列和表具有大小限制,可以防止Durable Functions将所有必需的数据保存到存储行或队列消息中。

例如,当序列化时需要持久化到队列的数据超过 45 KB 时,Durable Functions 会压缩数据并将其存储在 Blob 中。 将数据保存到 Blob 存储时,Durable Functions 在表行或队列消息中存储对该 Blob 的引用。 当Durable Functions需要检索数据时,它会自动从 Blob 提取数据。 查找存储在 Blob 容器 <taskhub>-largemessages中的这些 Blob。

性能注意事项

对于需要处理较大消息的情况下,额外的压缩和 blob 操作步骤可能会导致 CPU以及I/O的延迟成本增高。 此外,Durable Functions需要在内存中加载持久化数据,并可能同时用于许多不同的函数执行。

因此,保留大型数据有效负载可能会导致内存使用率高。 若要最大程度地减少内存开销,请考虑手动持久化大型数据负载(例如,在 Blob 存储中),并传递指向该数据的引用。 然后,代码只有在需要时才能加载数据,以避免 业务流程协调程序函数重播期间出现冗余加载。

不建议将有效负载存储到本地磁盘,因为不能保证磁盘上的状态可用。 函数可以在不同虚拟机的整个生存期内执行。

配置 Azure 存储提供程序

Azure 存储提供程序是默认的存储提供程序,不需要任何显式配置、NuGet 包引用或扩展捆绑包引用。 可以在 路径下找到完整的 extensions/durableTask/storageProvider集。

连接

host.json 中的 connectionName 属性是对环境配置的引用,该配置指定应用应如何连接到Azure Storage。 它可能指定:

  • 多个应用程序设置的共享前缀的名称,共同定义基于标识的连接。 托管身份使用Microsoft Entra身份验证来为您的存储帐户提供最安全的连接。
  • 包含连接字符串的应用程序设置名称。 若要获取连接字符串,请按照 管理存储帐户访问密钥中显示的步骤进行操作。

如果配置的值既是单个设置的完全匹配,也是其他设置的前缀匹配,则使用完全匹配。 如果未在 host.json 中指定任何值,则默认值为 AzureWebJobsStorage

基于身份的连接

如果您使用的是 2.7.0 或更高版本的扩展以及 Azure 存储提供程序,您可以让应用使用 Microsoft Entra 身份,而不是使用带有机密的连接字符串。 为此,需要定义公共前缀下的设置,该前缀映射到触发器和绑定配置中的 connectionName 属性。

若要对Durable Functions使用基于标识的连接,请配置以下应用设置:

财产 环境变量模板 说明 示例值
Blob 服务 URI <CONNECTION_NAME_PREFIX>__blobServiceUri 存储帐户的 blob 服务的数据层 URI,使用 HTTPS 协议。 https://<storage_account_name>.blob.core.chinacloudapi.cn
队列服务 URI <CONNECTION_NAME_PREFIX>__queueServiceUri 存储帐户队列服务的数据平面 URI,采用 HTTPS 协议。 https://<storage_account_name>.queue.core.chinacloudapi.cn
表格服务 URI <CONNECTION_NAME_PREFIX>__tableServiceUri 存储帐户的表格服务所用数据层 URI,采用 HTTPS 方案。 https://<storage_account_name>.table.core.chinacloudapi.cn

可以设置附加属性以自定义连接。 请参阅 基于标识的连接的通用属性

在 Azure Functions 服务中托管时,基于标识的连接将使用托管标识。 默认情况下使用系统分配的标识,但可以使用 credentialclientID 属性来指定用户分配的标识。 请注意,不支持配置具有资源 ID 的用户分配的标识。 在其他上下文(如本地开发)中运行时,将改用开发人员标识,尽管可以进行自定义。 请参阅 使用基于标识的连接进行本地开发

向身份授予权限

无论使用何种标识,都必须具有执行所需操作的权限。 对于大多数 Azure 服务,这意味着需要在 Azure RBAC 中分配角色,可以使用内置角色或自定义角色来提供这些权限。

重要

某些权限可能由并非所有上下文都需要的目标服务公开。 尽可能遵循最小权限原则,仅授予身份所需的权限。 例如,如果应用只需要从数据源进行读取即可,则使用仅具有读取权限的角色。 为该服务分配一个同时具备写入权限的角色是不恰当的,因为这对于只需读取的操作来说权限过多。 同样,你也希望确保角色分配的范围仅限于需要读取的资源。

你将需要创建一个角色分配,以便在运行时提供对 Azure 存储的访问权限。 所有者等管理角色是不够的。 在正常操作中使用 Durable Functions 扩展时,建议使用以下内置角色:

根据所编写的代码,应用程序可能需要更多权限。 如果使用默认行为或将 connectionName 显式设置为“AzureWebJobsStorage”,请参阅使用标识连接到主机存储,了解其他权限注意事项。

存储帐户的选择

Durable Functions 会在配置的 Azure Storage 帐户中创建其使用的队列、表和 Blob。 可以指定要在 host.json 文件中使用的帐户

  • durableTask/storageProvider/connectionStringName 设置(Durable Functions 2.x)
  • durableTask/azureStorageConnectionStringName设置(Durable Functions 1.x)
{
  "extensions": {
    "durableTask": {
      "storageProvider": {
        "connectionStringName": "MyStorageAccountAppSetting"
      }
    }
  }
}

为 Durable 函数应用选择存储帐户时,请记住以下注意事项:

  • 对于性能敏感的工作负荷,请配置默认帐户以外的存储帐户(AzureWebJobsStorage)。 由于Durable Functions大量使用Azure Storage,使用专用存储帐户可将Durable Functions存储使用情况与Azure Functions主机的内部使用情况隔离开来。
  • 使用 Azure Storage 提供程序时,需要使用标准常规用途Azure Storage帐户。 目前不支持所有其他存储帐户类型。
  • 建议为Durable Functions使用旧版 v1 常规用途存储帐户。 对于Durable Functions工作负荷,较新的 v2 存储帐户可能更昂贵。 详细了解Azure Storage帐户类型

编排器横向扩展

虽然可以通过弹性添加更多虚拟机来无限横向扩展活动函数,但单个业务流程协调程序实例和实体被限制为居住在单个分区中。 最大分区数受 partitionCounthost.json 设置的约束。

注意

一般来说,业务流程协调程序函数应该是轻量级的,并且不需要大量的计算能力。 无需创建大量控制队列分区即可实现编排的高吞吐量。 你应该在无状态活动函数中执行大部分高负载工作,这些函数能够无限拓展。

您在 host.json 文件中定义控制队列的数量。 以下示例host.json代码片段将 durableTask/storageProvider/partitionCount 属性 (durableTask/partitionCount in Durable Functions 1.x) 设置为 3。 你拥有的控件队列数与分区数一样多。

{
  "extensions": {
    "durableTask": {
      "storageProvider": {
        "partitionCount": 3
      }
    }
  }
}

可以配置包含 1 到 16 个分区的任务中心。 如果未指定值,则默认分区计数为 4

在低流量情境中,您的应用程序进行缩减,因此很少有工作者管理分区。 例如,在下图中,可以看到协调器 1 至 6 在分区间进行负载均衡。 同样,分区(例如活动分区)跨工作人员进行负载均衡。 分区会在工作节点之间进行负载均衡,无论启动多少个协调器。

显示由少量辅助角色管理的分区的缩减规模编排的关系图。

如果您正在使用 Azure Functions 消耗计费计划或弹性高级计划,或者配置了基于负载的自动缩放,随着流量的增加,将会分配更多的工作程序,而分区最终将在所有工作程序之间实现负载均衡。 如果继续向外扩展,最终每个分区由单个工作线程管理。

活动在所有工作节点之间继续进行负载均衡,如下图所示。

显示第一个横向扩展编排的示意图,其中分区分布在工作节点之间。

任意给定时间内最大并发活动编排的上限等于为应用分配的工作器数量乘以maxConcurrentOrchestratorFunctions 的值。

当分区跨工作器完全横向扩展时,可以更精确地实现此上限。 完全横向扩展时,由于每个辅助角色只有一个 Functions 主机实例,所以在并发业务流程协调程序实例的最大数目等于分区数乘以

注意

在这种情况下,“活动”意味着业务流程或实体加载到了内存中并在处理新事件 。 如果业务流程或实体正在等待更多事件(例如活动函数的返回值),它将从内存中卸载,且不再被视为“活动”。 只有在有新事件要处理时,业务流程和实体才会重新加载到内存中。 单个虚拟机上可以运行的编排或实体的总数没有实际的最大限制,即使它们全部处于“正在运行”状态。 唯一的限制是同时活跃的编排或实体实例的数量。

下图演示了一个完全扩展的场景,其中添加了更多编排器,但有些编排器处于非活动状态,显示为灰色。

示意图显示第二次横向扩展时编排更多,其中部分处于非活动状态。

在横向扩展期间,控制队列租约可能会在 Functions 主机实例之间重新分配,以确保分区的均匀分布。 这些租用在内部以 Azure Blob 存储租用的形式实现,确保一个业务流程实例或实体每次仅在一个主机实例上运行。 如果将任务中心配置为具有三个分区(因此有三个控制队列),则业务流程实例和实体可以在所有三个租用托管主机实例之间进行负载均衡。 可以添加更多虚拟机来增加活动函数执行的容量。

下图演示了 Azure Functions 主机如何与横向扩展环境中的存储实体交互。

Diagram 显示了 Azure Functions 主机在横向扩展期间如何与存储实体进行交互。

所有虚拟机都在工作项队列中争用消息。 但是,只有三个虚拟机可以从控制队列获取消息,并且每个虚拟机锁定一个控制队列。

业务流程实例和实体分布在所有控制队列实例之间。 对业务流程的实例 ID 或实体名称和密钥对进行哈希处理以实现分布。 由于业务流程实例 ID 默认为随机 GUID,实例会平均分布在所有控制队列中。

扩展会话

扩展会话是一种缓存机制,即使编排和实体在处理完消息后仍保留在内存中。 启用扩展会话时,通常会看到基础持久存储的 I/O 减少,吞吐量总体提高。

可以通过在durableTask/extendedSessionsEnabled文件中将true设置为host.json来启用扩展会话。 可以使用此设置 durableTask/extendedSessionIdleTimeoutInSeconds 来控制空闲会话在内存中停留的时间:

{
  "extensions": {
    "durableTask": {
      "extendedSessionsEnabled": true,
      "extendedSessionIdleTimeoutInSeconds": 30
    }
  }
}

请注意此设置的两个潜在缺点:

  • 函数应用的总体内存使用率增加,因为空闲实例不会尽快从内存中卸载。
  • 如果有多个并发、不同且生存期较短的协调器或实体函数执行,您可能会看到吞吐量整体下降。

例如,如果您将durableTask/extendedSessionIdleTimeoutInSeconds设置为 30 秒,那么即便是执行时间小于 1 秒的短生命周期协调程序或实体函数执行周期,仍然会在 30 秒内继续占用内存。 这种消耗还会计入前面所述的 durableTask/maxConcurrentOrchestratorFunctions 配额,从而可能导致其他业务流程协调程序或实体函数无法运行。

扩展会话对编排器和实体功能有不同的影响。 让我们来探讨它们如何与协调器函数一起工作。

协调器函数回放

如前所述,系统通过使用History 表中的内容重运行编排器函数。 默认情况下,编排器函数代码在每次一批消息从控制队列中出列时都会重播。 即使您使用的是扇出/扇入模式并等待所有任务完成,由于批量处理任务响应,仍然会在一段时间内发生重播。 启用扩展会话时,业务流程协调程序函数实例会停留在内存中更长的时间,并且无需完整历史记录重播即可处理新消息。

通常,可以在以下情况下观察到扩展会话的性能改进:

  • 您并发运行的编排实例数量有限。
  • 您的编排具有大量顺序操作(例如,数百个活动函数调用),这些操作能够快速完成。
  • 编排会扇出和扇入大量在大致同时完成的动作。
  • 业务流程协调程序函数需要处理大型消息或执行任何 CPU 密集型数据处理。

在所有其他情况下,通常看不到业务流程协调程序函数的任何可观测性能改进。

注意

只有在完全开发和测试协调器函数之后,才应使用这些设置。 默认的激进重播行为可用于检测开发时业务流程协调程序函数代码约束冲突,因此默认被禁用。

性能目标

下表显示了 方案文章的预期 最大 吞吐量数

“Instance”是指在Azure App Service中单个小型虚拟机(A1)上运行的业务流程协调程序函数的单个实例。 在所有情况下,这些数字都假定你启用了 扩展会话。 实际结果可能因函数代码执行的 CPU 或 I/O 工作而异。

场景 最大吞吐量
顺序活动执行 每秒五个活动,每个实例
并行活动执行(扇出) 每个实例每秒 100 个活动
并行响应处理(扇入) 每个实例每秒 150 个响应
外部事件处理 每个实例每秒 50 个事件
实体操作处理 每秒 64 个操作

如果未看到预期的吞吐量数,并且 CPU 和内存使用率看起来正常,请检查原因是否与 存储帐户的运行状况相关。 Durable Functions扩展可能会给Azure Storage帐户带来大量负载,并且负载足够高可能会导致存储帐户限制。

提示

在某些情况下,可以通过提高 controlQueueBufferThresholdhost.json 设置的值来提升外部事件、活动扇入和实体操作的吞吐量。 将此值增大到超过其默认值后,持久任务框架存储提供程序可使用更多内存来主动预提取这些事件,从而减少从 Azure 存储控制队列中的消息取消排队时的相关延迟。 有关详细信息,请参阅 host.json 参考文档。

高吞吐量处理

Azure Storage后端体系结构对Durable Functions的最大理论性能和可伸缩性提出了一定限制。 如果测试显示在 Azure Storage 上的 Durable Functions 不符合您的吞吐量要求,则应考虑改用针对 Durable Functions 的 Netherite 存储提供程序。

比较各种基本方案的可实现吞吐量

Netherite 存储后端由 Microsoft Research 设计和开发。 它使用 Azure 事件中心FASTER 数据库技术,运行在 Azure Page Blobs 之上。 与其他提供程序相比,Netherite 的设计可以实现吞吐量更高的业务流程和实体处理。 在某些基准方案中,与默认Azure Storage提供程序相比,吞吐量增加了一个以上的数量级。

要详细了解 Durable Functions 支持的存储提供程序以及它们之间的比较,请参阅 Durable Functions 存储提供程序文档。

后续步骤