Azure设计模式之计划任务代理模式

计划任务代理模式
将一组分布式操作放在一个操作中进行统一协调。如果任何操作失败试着处理,或对已执行的工作进行撤销,来使整个操作整体成功或失败。使得分布式系统处理瞬时故障、长时间故障或失败的操作,从而可以增加灵活性。


问题背景
应用程序执行的任务可以包括多个步骤,一些可能会调用远程服务或访问远程资源。各个步骤相互独立,由实现该任务的应用程序逻辑决定。应用程序应自行解决由于访问远程服务或资源时的故障。发生故障的原因有很多。如网络可能已关闭,通信中断,远程服务没有响应或处于不稳定状态,或远程资源可能暂时无法访问,可能是由于资源限制所致。在许多情况下故障是瞬态的,可以使用重试模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/retry)来处理。


如果应用程序检测到了无法轻易恢复的长期或永久性故障,则必须先将系统恢复到一致的状态并要确保整个操作的完整性。


解决方案
计划程序代理模式定义了以下几种行为。这些行为用于协调任务中的多个步骤。


计划任务会安排执行任务的步骤,并协调其操作。这些步骤可以合并到管线或工作流中。调度程序负责确保按正确顺序执行工作流中的步骤。在执行每个步骤时,计划程序会记录工作流的状态,如"未开始"、"运行"或"已完成"。状态信息还应包括完成步骤所允许的时间上限,称为"最迟完成时间"。如果某步骤需要访问远程服务或资源,则计划任务将调用相应代理,并将要执行工作的详细信息传递给它。调度程序通常使用异步请求/响应消息与代理进行通信。建议使用队列实现,也可以使用其他分布式消息技术来实现。


日程管理器在与进程管理器

(http://www.enterpriseintegrationpatterns.com/patterns/messaging/ProcessManager.html)执行的类似。实际工作流通常由日程管理器的工作流引擎实现。使得业务逻辑与日程管理分离。



代理中封装了对远程服务的调用,或对任务步骤所引用的远程资源访问。每个代理通常封装对服务或资源的调用,包含了适当的错误处理和重试逻辑(包含了超时处理,稍后描述)。如果任务计划程序所执行工作流中的步骤中使用了多个服务和资源,则每个步骤可能引用了不同的代理(这是模式的实现细节)。


主管程序对任务计划中的每个步骤状态进行监控。定期运行(频率将是系统设定的)并检查每个步骤的状态。如果检测到超时或失败的情况,将安排适当的代理恢复步骤或执行适当的补救操作(可能涉及步骤状态的更新)。注意,恢复或补救操作是由任务计划程序和代理实现的。主管程序只是请求执行这些操作。


任务计划程序、代理和主管是逻辑组件,物理实现取决于所使用的技术。例如,可以将几个逻辑代理作为web服务的一部分来实现。


任务计划程序维护任务进度和每个步骤状态的数据存储(称为状态存储区)。主管程序可以使用此信息来确定某个步骤是否已失败。下图说明了调度程序、代理、主管和状态存储之间的关系。
Azure设计模式之计划任务代理模式


该图显示了这个模式的简化版。在实际实现中,在每个任务中可能会有多个调度程序同时运行的实例。同样,系统可以实例化多个代理,和多个主管实例。在这种情况下,主管必须谨慎协调他们的工作,以确保他们不会发生竞争,以致于重复执行了相同的失败步骤和任务。领导选举模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/leader-election)为这个问题提供了一种解决方案。


当应用程序准备运行任务时,会向任务计划程序提交一个请求。计划程序记录了任务及其步骤的初始状态(例如,未启动),然后开始执行工作流所定义的操作。当任务计划程序执行每个步骤时,也更新其在状态存储中的状态信息(例如,运行)。


如果某步骤引用了远程服务或资源,任务计划程序会向相应代理发送消息。该消息包含了代理需要传递给服务或资源的信息,以及操作的目标完成时间。如果代理成功完成其操作,它将返回对计划程序的响应。然后,计划程序更新状态存储区中的状态信息(例如,已完成)并执行下一步。此过程一直持续到整个任务完成。


代理中可以实现重试逻辑。但是,如果代理在目标完成时间内未能完成其工作,则计划程序将假定该操作已失败。在这种情况下,代理应停止其手头工作,不应返回任何内容到计划程序(包括错误消息),停止尝试任何恢复。这样限制的原因是,在某个步骤超时或失败之后,可能会安排另一个代理实例重试失败步骤(稍后将介绍这个过程)。


如果代理失败,计划程序将不会收到响应。而没有区分是超时还是失败。


如果某个步骤超时或失败,插入一条新的状态记录,步骤正在运行,但超出目标完成时间。主管程序会找到这样的步骤,并试图恢复。一种策略是主管程序延迟目标完成时间,然后向计划程序发送一条任务已超时消息。然后计划程序尝试重复执行步骤。但是,要求任务的执行是幂等操作。


如果管理员连续失败或超时,需要防止对同一步骤不断重试。因此,主管程序可以对状态存储中每个步骤的重试进行计数,以及状态信息。如果计数超过定义的阈值,则主管程序可在通知计划程序前等待较长时间,试着在这段时间内解决故障。或者主管向计划程序发送消息,通过实现补偿事务模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)来请求对整个任务进行撤消。此方法将依赖于提供必要信息的计划程序和代理,以便为成功完成的每个步骤执行补偿操作。


主管程序的目的不是为了对任务计划和代理程序进行监督然后如果失败进行重启。这部分应该由系统架构中的组件来负责完成。同样,主管不应该了解任务计划所执行任务的业务细节(包括如何补偿任务失败)。这是任务计划中实现工作流的目的。主管唯一责任是确定某任务步骤是否失败,并安排其被重复执行或撤销整个任务。


如果任务计划程序执行失败后或工作流意外终止,执行重启,计划程序应当能够确定它在失败那一刻所处理的每个任务状态,并在那里继续执行。此过程的实现细节可能会因系统而异。如果无法恢复任务,就可能需要撤消任务已执行的工作。还可能还需要实现补偿事务(https://docs.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)。该模式很重要的优点是,当临时或不可恢复的故障时,系统能够保持弹性。系统可以被设计为自我治愈的。例如,如果某代理或计划程序失败,则可以启动一个新的实例,并由主管程序安排任务从失败处继续执行。如果主管失败,则启动另一个实例,并从发生故障的位置接管。如果主管程序被安排定期执行,则可在定义好的时间间隔自动启动新实例。可以对状态存储进行拷贝,以获得更大的系统弹性。


问题和注意事项
在决定如何实现此模式时, 应考虑以下几点:
此模式可能难以实现, 需要对系统每个可能的故障情形进行完整的测试。


计划程序实现的恢复/重试逻辑可能会很复杂,并依赖状态存储中的状态信息。还可能需要将补偿事务信息存在数据存储区中。主管程序的运行频率是很重要的。它应该经常运行,以防止任何失败的步骤在较长的时间段内阻塞应用程序,但频率不应该过高,会成为系统开销。


代理执行的步骤可以被多次执行。这些步骤的逻辑应该是幂等的。


何时使用此模式
当在分布式环境(如云)中运行的进程必须在通信失败/或操作失败时保持弹性时,考虑使用此模式。该模式可能不适用于没有调用远程服务或访问远程资源的任务。


例子
电商系统的web应用程序已部署在微软Azure上。用户可以运行在应用程序浏览产品和订购。用户界面作为web角色运行,订单处理部分作为工作角色实现。订单处理逻辑的一部分会涉及远程服务访问,而这部分又可能出现瞬态或长期故障。因此,可以使用计划任务代理模式来实现系统中的订单处理。


当创建订单时,应用程序会创建一条描述订单的消息并将其发给队列。在工作角色中运行的进程查询消息,并将订单详细信息插入到"订单"数据库中,创建并保存订单记录到数据库。注意,向订单数据库和状态更新是由同一操作(事务)完成的。是为了确保两个插入一起完成。


所提交的订单信息包括:
OrderID。订单数据库中的订单ID。
LockedBy。正在处理订单的工作角色的实例ID。在任务计划中可能会有多个工作角色实例,但每个订单只能被1个实例处理。
CompleteBy。订单的预计处理时间。


ProcessState。处理订单任务的当前状态。状态可能是:
Pending。订单已创建但仍未处理。
Processing。订单正在被处理。
Processed.订单已处理。
Error。订单处理失败。


FailureCount。订单被尝试处理的次数。


在该状态信息中,OrderID字段从新订单的订单中复制。LockedBy和CompleteBy字段设置为null,ProcessState字段设置为Pending,FailureCount字段设置为0。


在本例中,订单处理逻辑相对简单,只有一个远程服务调用。在更复杂的场景中,这个过程可能涉及多个步骤, 因此可在状态存储区中创建多个记录,每一个记录描述一个步骤的状态。


任务计划程序作为工作角色的一部分运行,并实现订单处理业务逻辑。新的任务计划实例轮询检查LockedBy为null且ProcessState为Pending的记录。当任务计划找到新订单时,将自己的实例ID设为LockedBy字段的值,将CompleteBy字段设为某一时间,将ProcessState字段设为"Processing"。这部分操作被设计为独占性的原子操作,以确保两个并发实例不能同时处理同一订单。


然后任务计划中运行的工作流异步处理订单,并将状态值赋值给OrderId字段。处理订单的工作流从订单数据库中检索订单的详细信息然后执行。当订单处理工作流中的步骤需要调用远程服务时,使用代理来完成。工作流步骤使用一对Azure服务总线中的消息队列作为请求/响应通道与代理进行通信。下图演示了这种架构。
Azure设计模式之计划任务代理模式


解决方案
在工作流中的其中一个步骤是向代理发送消息,其中包括了订单信息和预计完成时间。如果代理在预计完成时间前收到远程服务的响应,则向工作流所监听的服务总线队列上发送消息。当工作流收到应答消息时就会继续处理订单,完成后调度程序会将订单的ProcessState字段设置为"Processed"。订单处理成功完成。


如果代理调用远程服务超时,代理将暂停处理并停止处理该订单。同样,如果处理订单的工作流超过了预计完成时间,也会被终止。在这两种情况下,数据库中订单状态仍为"Processing",但如果工作流的订单处理超出了总预计时间,被视为订单处理失败。注意,如果远程服务代理或处理该订单的工作流(或两者都)被意外终止,则状态信息将再次被设置为"Processing",最终会超出订单预计完成时间。


当代理与远程服务建立连接时检测到不可恢复的故障,它可将错误响应发送回工作流。任务计划可将订单状态置为错误,并触发错误警报。系统管理员可以尝试手动解决并尝试重新执行处理失败的步骤。


主管进程定期检查订单状态信息,查找处理超时的订单。如果发现记录,则递增FailureCount字段值。如果失败计数值低于设定的阈值,将LockedBy字段重置为null,并更新CompleteBy字段为新的过期时间,将ProcessState字段设置为Pending。任务计划程序的实例可以再次接收并处理订单。如果故障计数值超过了阀值,则失败的原因被看作是非瞬态故障。主管进程将订单的状态设置为"Error",并向管理员触发一个警报事件。


在本例中,主管是进程是在单独工作角色中实现的。可以使用其他策略来确定主管任务的运行形式,可使用Azure任务计划服务(不要与任务计划程序组件混淆)。有关Azure任务计划服务的详细信息,请访问这里。(https://azure.microsoft.com/en-us/services/scheduler/)


以上例子中没有演示,任务计划程序还需要负责通知提交订单进程最新的订单状态。应用程序和任务计划程序彼此隔离,以消除依赖。应用程序不知道任务计划程序的哪个实例在处理订单,任务计划程序也不知道哪个应用程序实例创建了订单。


应用程序可以使用自己的响应队列来接收推送的订单状态。该响应队列信息的将作为请求的一部分发送到订单提交进程,存在数据库。然后任务计划程序将消息发送到该队列,其中包含订单的状态(接收请求、完成订单、订单失败等)。在消息中还要包括订单ID,以便可以与应用程序的原生请求关联起来。


相关阅读
在实现此模式时,以下模式和指南也可能是相关的:


重试模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/retry)。代理可使用该模式对以前失败的远程服务或资源调用进行重试。故障原因必须是瞬时的,可自我纠正的。


断路器模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)。代理可以使用这个模式来处理连接远程服务或资源时出现错误时能有时间处理。


补偿事务模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)。如果任务计划所执行的工作流无法成功完成,可能需要撤消所完成的工作。补偿事务模式描述了在如何实现最终一致性的操作。这类操作通常由复杂业务流程或工作流的任务计划程序来实现。


异步消息入门(https://msdn.microsoft.com/library/dn589781.aspx)。计划任务代理模式中的组件通常彼此隔离并异步通信。这篇文章中描述了一些基于消息队列实现异步通信的方法。


领导者选举模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/leader-election)。需要协调领导者多个实例的操作,以防止试图恢复相同的失败进程。领导者选举模式一文中描述了如何实现这一点。


云架构: Vasters者博客中讲的任务计划-代理-主管模式。

(http://www.enterpriseintegrationpatterns.com/patterns/messaging/ProcessManager.html)



CQRS模式中如何使用进程管理器的(CQRS解决方案例子的一部分)
https://blogs.msdn.microsoft.com/clemensv/2010/09/27/cloud-architecture-the-scheduler-agent-supervisor-pattern/


微软Azure任务计划(https://azure.microsoft.com/services/scheduler/)