Durable Functions中的版本控制挑战和缓解策略

Durable Functions中的版本控制至关重要,因为函数不可避免地在应用程序的生存期内添加、删除和更改。 Durable Functions使你能够按以前不可能的方式将函数链接在一起,并且此链接会影响处理版本控制的方式。

本文可帮助你:

快速策略比较

如果已经知道您的更改是破坏性的,请使用此表选择缓解措施。

策略 最适用于 详细信息
编排版本控制 (建议) 大多数具有中断性变更的应用程序。 内置运行时功能适用于任何存储后端。 跳转到分区
并行部署 无法使用业务流程版本控制或需要通过单独的任务中心或存储帐户进行完全隔离的应用。 跳转到分区
停止所有正在运行的实例 原型开发和本地开发中,丢失正在进行中的编排流程是可以接受的。 跳转到分区

Tip

如果要查找在运行时级别提供自动版本隔离的内置 业务流程版本控制 功能,请参阅 业务流程版本控制

重要

在部署之前,请确认你的更改是否是重大改动:

  • 是否更改了活动或实体函数 的名称、输入类型或输出类型
  • 您是否在协调程序代码中添加、删除或重新排序对活动、子协调程序、计时器或外部事件的调用?
  • 是否 重命名或删除 了正在进行的业务流程可能仍在调用的函数?

如果您对上述任何一项回答,请使用以下缓解策略之一来避免运行编排失败。

重大更改的类型

存在一些中断性变更的示例。 本文讨论最常见的类型。 所有这些功能背后的主要主题是功能代码的更改既会影响新的功能编排,也会影响现有的功能编排。

活动或实体功能签名更改

签名更改是指函数的名称、输入或输出中的更改。 如果对操作或实体函数进行此类更改,可能会中断依赖于它的任何编排器函数。 对于类型安全语言,此行为尤其如此。 如果更新业务流程协调程序函数以适应此更改,则可能会中断现有的正在进行的实例。

例如,请考虑以下协调器函数。

[FunctionName("FooBar")]
public static Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    bool result = await context.CallActivityAsync<bool>("Foo");
    await context.CallActivityAsync("Bar", result);
}

此函数获取 Foo 的结果并将其传递给 Bar。 假设需要将 Foo 的返回值从布尔值更改为 String,以支持更广泛的结果值。 结果如下所示:

[FunctionName("FooBar")]
public static Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    string result = await context.CallActivityAsync<string>("Foo");
    await context.CallActivityAsync("Bar", result);
}

此更改适用于所有新创建的编排程序函数实例,但可能会中断任何进行中的实例。 例如,请考虑这样一种情况:业务流程实例调用名为 Foo 的函数并先后返回布尔值和检查点。 如果此时部署签名更改,则检查点实例在恢复并重播调用 Foo时立即失败。 发生此失败的原因是历史记录表中的结果是布尔值,但新代码尝试将其反序列化为字符串值,从而导致意外行为,甚至导致类型安全语言的运行时异常。

此示例是函数签名更改可以中断现有实例的多种方式之一。 通常,如果业务流程协调程序需要更改调用函数的方式,则更改可能会有问题。

编排器逻辑更改

更改编排器函数代码以改变正在执行的实例的执行路径是另一类版本控制问题的根源。

请考虑以下协调器函数:

[FunctionName("FooBar")]
public static Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    bool result = await context.CallActivityAsync<bool>("Foo");
    await context.CallActivityAsync("Bar", result);
}

现在假设你想要在两个现有函数调用之间添加新的函数调用。

[FunctionName("FooBar")]
public static Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    bool result = await context.CallActivityAsync<bool>("Foo");
    if (result)
    {
        await context.CallActivityAsync("SendNotification");
    }

    await context.CallActivityAsync("Bar", result);
}

此更改在SendNotificationFoo之间添加了一个新的函数调用Bar。 不存在签名更改。 出现问题的情况是现有实例从 Bar 调用中恢复时。 在重播期间,如果原始调用Foo返回true,则协调器将重播调用SendNotification,而该调用不在其执行历史记录中。 运行时检测到此不一致,并引发非确定性协调错误,因为它在期望调用SendNotification时遇到了调用Bar。 向其他持久操作添加 API 调用(例如创建持久计时器、等待外部事件或调用子业务流程)时,可能会出现相同的问题。

缓解策略

Warning

在没有缓解策略(“不执行任何操作”方法)的情况下部署中断性变更可能会导致编排失败,并出现不确定性编排错误,状态无限期停滞或触发降低性能的低级别运行时故障。 在部署重大变更时,请始终使用以下策略之一。

与本节中的其他策略不同,业务流程版本控制是一项 内置运行时功能 ,可提供自动版本隔离。 无需管理单独的部署、任务中心或存储帐户。 相反,运行时本身会跟踪版本信息,并确保编排实例由兼容的工作线程处理。

使用业务流程版本控制:

  • 每个编排实例在创建时都会永久与一个版本相关联。
  • 业务流程协调程序函数可以相应地检查其版本和分支执行,并将旧代码路径和新代码路径保留在相同的代码库中。
  • 运行较新协调器函数版本的工作者可以继续执行由旧版本创建的协调器实例。
  • 运行时可防止运行旧版协调器函数的工作者执行新版的协调任务。

此方法需要最少的配置(版本字符串和可选的匹配策略),并且与任何 存储提供程序兼容。 这是为需要适应中断性变更的应用程序而推荐的策略,同时维护零停机部署

有关详细的配置和实现指南,请参阅 业务流程版本控制

停止所有正在进行的实例

另一个选项是停止所有正在进行的实例。 如果您使用默认的 Azure 存储 提供商 进行 Durable Functions ,请通过清除内部 control-queue 和 workitem-queue 队列的内容来停止所有实例。 或者,停止函数应用,删除这些队列,然后重启应用。 应用重启后,将自动重新创建队列。 以前的业务流程实例可能无限期保持“正在运行”状态,但它们不会将日志与失败消息混为一体,也不会对应用造成任何损害。 此方法非常适合快速原型开发,包括本地开发。

Warning

此方法需要直接访问基础存储资源,不适用于Durable Functions支持的所有存储提供程序。

并行部署

确保破坏性变更被安全部署的最可靠方法是将其与旧版本并行部署。 可以使用以下任一技术:

  • 不同的存储帐户:将所有更新部署为具有不同存储帐户的新函数应用。 这完全将新版本的状态与旧版本隔离开来。
  • 不同的任务中心:使用同一存储帐户部署函数应用的新副本,但任务中心名称已更新。 此方法为新版本创建新的存储项目,而旧版本继续使用其现有项目。

在 Azure 中进行并行部署时,可以利用 部署槽位来同时运行两个版本,其中只有一个作为活跃的 生产槽。 准备好公开新的业务流程逻辑时,将新版本交换到生产槽。

注意

本指南使用特定于Azure 存储的术语,但一般适用于所有受支持的Durable Functions存储提供程序

注意

部署插槽交换在 HTTP 和 Webhook 触发器中效果最佳。 对于队列或事件中心等非 HTTP 触发器,触发器定义应 派生自 在交换操作过程中更新的应用设置。

后续步骤