以单项操作的形式协调一组分布式操作。 如果任一操作失败,将尝试以透明方式处理故障,如果故障处理失败,则撤消已执行的工作,从而使整个操作作为一个整体成功或失败。 这使分布式系统可以恢复并重试因暂时性异常、长期故障和进程故障而失败的操作,从而提高自身的复原能力。
上下文和问题
应用程序执行包含多个步骤的任务,其中有些步骤可能调用远程服务或访问远程资源。 各个步骤可以彼此独立,但它们由实施任务的应用程序逻辑进行协调。
应用程序应尽可能确保任务完成运行,并解决访问远程服务或资源时可能发生的任何故障。 导致故障的原因有很多。 例如,网络可能关闭,通信可能中断,远程服务可能停止响应或处于不稳定状态,或者可能因为资源约束而暂时无法访问某个远程资源。 在许多情况下,故障是暂时的,可使用重试模式处理。
如果应用程序检测到一个更持久的故障且无法轻松地从中恢复,则它必须能够将系统还原到一致状态,并确保整个操作的完整性。
解决方案
计划程序代理监督程序模式定义了以下参与者。 这些参与者对各个步骤进行协调,使其作为整个任务的一部分执行。
计划程序安排执行构成任务的各个步骤并协调其操作。 这些步骤可以合并到管道或工作流中。 计划程序负责确保按正确顺序执行此工作流中的步骤。 执行每个步骤时,计划程序会记录工作流的状态,例如“步骤尚未启动”、“步骤正在运行”或“步骤已完成”。状态信息还应包括完成该步骤所允许的时间上限,即最迟完成时间。 如果某个步骤要求访问远程服务或资源,计划程序会调用相应的代理,向其传递要执行的工作的详细信息。 计划程序通常使用异步请求/响应消息传送与代理通信。 这可以使用队列实现,不过也可改用其他分布式消息传送技术。
计划程序执行的功能与进程管理器模式下的进程管理器相似。 实际的工作流通常由计划程序所控制的工作流引擎来定义和实施。 此方法将工作流中的业务逻辑与计划程序相分离。
代理包含一种逻辑,该逻辑封装对任务中某个步骤引用的远程服务的调用或远程资源的访问。 通常,每个代理均包装对单个服务或资源的调用,并实现相应的错误处理和重试逻辑(受后面所述的超时约束限制)。 实现重试逻辑时,可以在所有重试尝试中传递一个稳定的标识符,使远程服务可以将它用于它可能具有的任何重复数据删除逻辑。 如果计划程序正在运行的工作流中的步骤跨不同步骤使用多个服务和资源,则每个步骤可能引用一个不同的代理(这是此模式的实现细节)。
监督程序监视由计划程序执行的任务中步骤的状态。 它定期运行(频率因系统而异),并检查由计划程序维护的步骤的状态。 如果它检测到任何超时或失败的步骤,则会安排相应的代理来恢复该步骤或执行相应的更正措施(这可能涉及修改步骤的状态)。 请注意,恢复或更正操作由计划程序和代理实施。 监督程序应只请求执行这些操作。
计划程序、代理和监督程序都是逻辑组件,其物理实现依赖于所采用的技术。 例如,多个逻辑代理可能作为单个 Web 服务的一部分实现。
计划程序在一个名为状态存储的持久数据存储中维护有关任务进度和各步骤的状态的信息。 监督程序可以使用此信息帮助确定某个步骤是否失败。 下图阐释了计划程序、代理、监督程序和状态存储之间的关系。
注意
此图展示了该模式的简化版本。 在实际实施中,可能会同时运行计划程序的多个实例,每个任务子集一个实例。 同样,系统也可以运行每个代理的多个实例,甚至运行多个监督程序。 在这种情况下,各监督程序之间必须谨慎协调工作,确保它们不会争相恢复相同的失败步骤和任务。 领导选拔模式为此问题提供了一个可行的解决方案。
当应用程序准备好运行某个任务时,它会向计划程序提交请求。 计划程序在状态存储中记录有关该任务及其步骤的初始状态信息(例如,步骤尚未启动),然后开始执行工作流定义的操作。 在计划程序启动每个步骤时,它会更新状态存储中有关该步骤状态的信息(例如,步骤正在运行)。
如果步骤引用远程服务或资源,计划程序会将消息发送到相应的代理。 除了操作的最迟完成时间外,该消息还包含代理传递到服务或访问资源所需的信息。 如果代理成功完成其操作,则向计划程序返回一个响应。 接着,计划程序可以更新状态存储中的状态信息(例如,步骤已完成),并执行下一个步骤。 此过程会一直持续到整个任务完成。
代理可以实现执行其工作所需的任何重试逻辑。 但是,如果在最迟完成时间到期前,代理未完成其工作,计划程序将假定该操作失败。 在这种情况下,代理应停止其工作,不再尝试向计划程序返回任何信息(甚至包括错误消息)或尝试执行任何形式的恢复。 设置此限制的原因是,某个步骤超时或失败后,可能会安排代理的另一个实例来运行失败的步骤(后面介绍了此过程)。
如果代理失败,计划程序不会收到响应。 该模式不会区分超时的步骤和真正失败的步骤。
如果某个步骤超时或失败,状态存储将包含一条记录,指示该步骤正在运行,但已超过最迟完成时间。 监督程序会查找类似的步骤,并尝试恢复它们。 一个可行的策略是让监督程序更新最迟完成时间值,将时间延长到足够完成该步骤,然后将消息发送到确定该步骤已超时的计划程序。接着,计划程序可以尝试重复此步骤。 但是,此设计要求任务是幂等的。 系统应包含基础结构以保持一致性。 有关详细信息,请参阅可重复基础架构、构建 Azure 应用程序以提高弹性和可用性以及资源一致性决策指南。
如果同一步骤不断失败或超时,监督程序可能需要阻止重试该步骤。为此,监督程序可以在状态存储中为每个步骤维护重试计数和状态信息。 如果此计数超过预定义的阈值,监督程序可以采取以下策略:在通知计划程序应重试该步骤前,再多等一段时间,以期故障在这段时间内得到解决。 或者,监督程序也可以向计划程序发送消息,请求通过实施补偿事务模式来撤消整个任务。 此方法依赖于计划程序和代理提供必要的信息,为成功完成的每个步骤实施补偿操作。
它不靠监督程序来监视计划程序和代理并在它们失败时将其重新启动。 系统这一块应由运行这些组件的基础结构处理。 同样,监督程序不应当去了解计划程序所执行的任务正在运行的业务操作(包括这些任务失败时如何补偿)。 这是由计划程序实现的工作流逻辑的职责。 监督程序的唯一职责是确定某个步骤是否失败,并安排重试该步骤或撤消包含该失败步骤的整个任务。
如果计划程序在发生故障后重新启动,或计划程序执行的工作流意外终止,计划程序应能够确定它发生故障时正在处理的任何即时任务的状态,并准备好从该点继续处理此任务。 此过程的实现细节可能因系统而异。 如果无法恢复该任务,则可能需要撤消该任务已执行的工作。 这可能还需要实施补偿事务。
此模式的主要优点是,系统能够从意外发生的临时故障或不可恢复的故障中复原。 系统可构造为自我修复。 例如,如果代理或计划程序失败,则可以启动一个新的代理或计划程序,监督程序可以安排继续执行任务。 如果监督程序失败,则可以启动另一个实例,该实例可以从发生故障处开始接管。 如果监督程序计划为定期运行,新实例可在预定义的时间间隔后自动启动。 可以复制状态存储,以实现更强的复原能力。
问题和注意事项
在决定如何实现此模式时,应考虑以下几点:
此模式很难实施,并且要求仔细测试系统每种可能的故障模式。
计划程序所实现的恢复/重试逻辑非常复杂,且依赖于保存在状态存储中的状态信息。 可能还需要在持久数据存储中记录实施补偿事务所需的信息。 补偿事务也可能会失败。
监督程序的运行频率非常重要。 它的运行频率应足够高,以防止任何失败的步骤长时间阻止应用程序运行,但又不能太高,以至于成为一项开销。
代理执行的步骤可能运行多次。 用于实施这些步骤的逻辑应是幂等的。
何时使用此模式
当分布式环境(比如云)中运行的进程必须能够从通信故障和/或操作失败中复原时,可使用此模式。
此模式可能不适用于不调用远程服务或访问远程资源的任务。
工作负载设计
架构师应评估如何在其工作负载的设计中使用“计划程序代理监督程序模式”,以解决 Azure Well-Architected Framework 支柱中涵盖的目标和原则。 例如:
支柱 | 此模式如何支持支柱目标 |
---|---|
可靠性设计决策有助于工作负荷在发生故障后复原,并确保它在发生故障后恢复到正常运行状态。 | 此模式使用运行状况指标来检测故障,并将任务重新路由到运行状况良好的代理,以减轻故障的影响。 - RE:05 冗余 - RE:07 自我修复 |
性能效率通过在缩放、数据和代码方面进行优化, 帮助工作负载高效地满足需求。 | 此模式使用性能和容量指标来检测当前利用率,并将任务路由到具有容量的代理。 还可以使用它来优先执行优先级更高的优先级工作,而不是优先级较低的工作。 - PE:05 缩放和分区 - PE:09 关键流 |
与任何设计决策一样,请考虑对可能采用此模式引入的其他支柱的目标进行权衡。
示例
Microsoft Azure 上部署了一个用于实现电子商务系统的 Web 应用程序。 用户可以运行此应用程序,以浏览现有产品和下订单。 其用户界面以 Web 角色运行,应用程序的订单处理元素作为一组辅助角色实现。 订单处理逻辑的一部分涉及到访问远程服务,并且系统的这一块可能很容易出现暂时性故障或更持久的故障。 为此,设计人员使用计划程序代理监督程序模式来实现系统的订单处理元素。
客户下订单时,应用程序会构造一条描述该订单的消息,并将此消息发布到队列中。 一个在辅助角色中运行的独立提交进程检索到该消息后,将订单详细信息插入订单数据库中,并在状态存储中为订单进程创建一条记录。 请注意,针对订单数据库和状态存储的插入作为同一操作的一部分执行。 提交进程旨在确保两次插入一起完成。
提交进程为订单创建的状态信息包括:
OrderID。 订单在订单数据库中的 ID。
LockedBy。 处理订单的辅助角色的实例 ID。 运行计划程序的当前辅助角色实例可能有多个,但每个订单只能由单个实例处理。
CompleteBy。 订单的最迟处理时间。
ProcessState。 处理订单的任务的当前状态。 可能的状态包括:
- 挂起。 已创建订单,但尚未开始处理。
- 正在处理。 当前正在处理订单。
- 已处理。 已成功处理订单。
- 错误。 订单处理失败。
FailureCount。 尝试处理订单的次数。
在此状态信息中,OrderID
字段从新订单的订单 ID 复制而来。 LockedBy
和 CompleteBy
字段设置为 null
,ProcessState
字段设置为 Pending
,FailureCount
字段设置为 0。
注意
在此示例中,订单处理逻辑相对简单,并且只有一个调用远程服务的步骤。 在更复杂的多步骤方案中,提交进程可能涉及多个步骤,因此会在状态存储中创建多条记录 — 每条记录描述单个步骤的状态。
计划程序也作为辅助角色的一部分运行,并实现用于处理订单的业务逻辑。 轮询新订单的计划程序实例会在状态存储中检查 LockedBy
字段为 NULL 且 ProcessState
字段为挂起的记录。 当计划程序发现新订单时,会立即使用自己的实例 ID 填充 LockedBy
字段,并将 CompleteBy
字段设置为适当的时间,将 ProcessState
字段设置为正在处理。 该代码设计为具有独占性和原子性,以确保计划程序的两个并发实例不能尝试同时处理同一个订单。
接着,计划程序运行业务工作流,以异步方式处理订单,并向其传递状态存储中 OrderID
字段的值。 处理订单的工作流从订单数据库中检索订单的详细信息,并执行其工作。 如果订单处理工作流中的某个步骤需要调用远程服务,它会使用代理。 该工作流步骤通过将一对 Azure 服务总线消息队列作为请求/响应通道,与代理通信。 下图显示了该解决方案的概要视图。
从工作流步骤发送到代理的消息描述了订单,并包含最迟完成时间。 如果代理在最迟完成时间到期前从远程服务收到响应,它会在工作流侦听的服务总线队列上发布回复消息。 工作流步骤收到有效的回复消息后,会完成其处理,计划程序则将订单状态的 ProcessState
字段设置为已处理。 此时,订单处理已成功完成。
如果在代理从远程服务收到响应前,最迟完成时间就已到期,代理只需暂停其处理,并终止订单处理。 同样,如果处理订单的工作流超过最迟完成时间,它也会终止。 在这两种情况下,状态存储中订单的状态仍设置为正在处理,但最迟完成时间指示订单处理时间已过,该过程被视为已失败。 请注意,如果访问远程服务的代理或处理订单的工作流(或两者同时)意外终止,状态存储中的信息也仍然设置为正在处理,并且最终将具有到期的最迟完成时间值。
如果代理在尝试联系远程服务时检测到一个不可恢复的非暂时性故障,它可以向工作流发回错误响应。 计划程序可以将订单状态设置为错误,并引发一个事件以向操作员发出警报。 操作员随后可以尝试手动解决故障原因,并重新提交处理失败的步骤。
监督程序定期检查状态存储,寻找具有到期的最迟完成时间值的订单。 如果监督程序发现一条记录,则会递增 FailureCount
字段的值。 如果失败计数值低于指定阈值,监督程序会将 LockedBy
字段重置为 NULL,将 CompleteBy
字段更新为新的到期时间,并将 ProcessState
字段设置为挂起。 计划程序的某个实例可以选取此订单,像以前一样执行其处理。 如果失败计数值超过指定阈值,失败的原因会被视为非暂时性。 监督程序会将订单状态设置为错误,并引发一个事件以向操作员发出警报。
在此示例中,监督程序以单独的辅助角色实现。 用户可使用各种策略来安排监督程序任务的运行,包括使用 Azure 计划程序服务(不要与此模式中的计划程序组件混淆)。 有关 Azure 计划程序服务的详细信息,请访问计划程序页。
虽然未在此示例中加以说明,但计划程序可能需要让提交订单的应用程序及时了解订单的进度和状态。 应用程序和计划程序彼此独立,以消除两者之间的任何依赖关系。 应用程序不知道处理订单的是哪个计划程序实例,计划程序也不清楚发布订单的是哪个特定的应用程序实例。
若要允许报告订单状态,应用程序可以使用自己的私有响应队列。 此响应队列的详细信息会作为请求的一部分发送至提交进程,提交进程则将此信息添加到状态存储中。 接着,计划程序向此队列发布消息,指明订单的状态(已收到请求、订单已完成、订单失败等等)。 它应在这些消息中包含订单 ID,以使它们与应用程序发出的原始请求相关联。
后续步骤
实现此模式时,以下指南可能也比较有用:
异步消息传送入门。 计划程序代理监督程序模式中的组件在运行时通常相互分离,并以异步方式通信。 该模式介绍了一些可用来基于消息队列实现异步通信的方法。
参考 6:Sagas 上的 Saga。 提供了一个示例,展示 CQRS 模式如何使用进程管理器(CQRS 旅程指南的一部分)。
相关资源
实现此模式时,以下模式也可能有用:
重试模式。 代理可以使用此模式,以透明方式重试之前失败的访问远程服务或资源的操作。 此模式在预计失败原因是暂时性的并且能够纠正时使用。
断路器模式。 代理可以在连接到远程服务或资源时,使用此模式处理所需纠正时间不定的故障。
补偿事务模式。 如果由计划程序执行的工作流无法成功完成,则可能需要撤消之前执行的所有工作。 补偿事务模式描述如何为采用最终一致性模型的操作实现这一点。 此类操作通常由执行复杂业务流程和工作流的计划程序实施。
领导选拔模式。 可能必须协调监督程序多个实例的操作,防止它们试图恢复同一失败过程。 领导选拔模式描述如何实现这一点。
Clemens Vasters 博客中的 Cloud Architecture: The Scheduler-Agent-Supervisor Pattern(云体系结构:计划程序代理监督程序模式)