协调模式
事件网格 让每个系统组件都参与有关业务事务工作流的决策过程,而不是只依赖于控制中心点。
上下文和问题
在微服务体系结构中,基于云的应用程序通常被划分为几项小型服务,这些服务以端到端的方式协同处理业务事务。 为了降低服务之间的耦合度,每项服务都负责一个业务操作。 一些优点包括开发速度更快、代码库更小、可伸缩。 然而,设计高效且可伸缩的工作流是一项挑战,通常需要复杂的服务间通信。 这些服务使用明确定义的 API 相互通信。 即使单个业务操作也可能会在所有服务之间进行多个点到点调用。 常见的通信模式是使用充当编排器的集中式服务。 它确认所有传入的请求并将操作委托给相应的服务。 在此过程中,它也管理着整个业务事务的工作流。 每项服务只完成一个操作,并且不了解整个工作流。 编排模式可减少服务之间的点到点通信,但由于编排器和参与业务事务处理的其他服务之间紧密耦合,因此也存在一些缺点。 为了按顺序执行任务,编排器需要对这些服务的责任有一定的了解。 如果要添加或移除服务,现有的逻辑将中断,你需要重新连接通信路径的各部分。 虽然可以使用设计完善的编排器轻松配置工作流、添加或移除服务,但这种实现既复杂又难以维护。
解决方案
让每项服务都参与决定业务运营的处理时间和处理方式,而不是依赖于一个中心型业务流程协调程序。 实现协调的一种方法是使用异步消息传送模式来协调业务操作。 客户端请求将消息发布到消息队列。 当消息到达时,它们会被推送给对该消息感兴趣的订阅者或服务。 每项订阅的服务都按照消息的指示执行操作,并以操作的成败响应消息队列。 如果成功,服务就可以将消息推送回同一队列或不同的消息队列,以便在需要时另一项服务可以继续执行工作流。 如果操作失败,消息总线可以重试该操作。 这样,服务在它们之间协调工作流,而无需依赖于编排器或在它们之间直接通信。 因为没有点到点通信,所以这种模式有助于减少服务之间的耦合。 它还可消除编排器在必须处理所有事务时造成的性能瓶颈。
何时使用此模式
如果希望经常更新或替换服务,并最终添加或删除某些服务,请使用协调模式。 修改整个应用程序时会更加省力,并且对现有服务的干扰将降至最低。 如果在中央编排器中遇到性能瓶颈,可考虑使用此模式。 这种模式是无服务器体系结构的自然模型,其中所有服务都可以是短期服务或事件驱动的服务。 服务可以因事件而启动、执行其任务,并在任务完成后被移除。
问题和注意事项
分散编排器可能会导致在管理工作流时出现问题。 如果服务无法完成业务操作,则很难从该故障中恢复。 一种方法是让服务通过触发事件来指示失败。 订阅这些失败事件的另一项服务会采取必要的操作,例如应用补偿事务来撤消请求中的成功操作。 失败的服务也可能无法为失败触发事件。 在此情况下,请考虑使用重试和超时机制将该操作识别为失败。 有关示例,请参见“示例”一节。 当你希望并行处理独立业务操作时,实现工作流很简单。 你可以使用一个消息总线。 但如果需要按顺序进行协调,工作流可能会变得复杂。 例如,只有在服务 A 和服务 B 成功完成操作后,服务 C 才能开始操作。 一种方法是使用多个消息总线,按照所需顺序获取消息。 有关详细信息,请参阅示例部分。 如果服务数量迅速增长,协调模式将成为一项挑战。 考虑到有大量独立的移动部件,服务之间的工作流往往会变得复杂。 同时,分布式跟踪也变得困难。 编排器集中管理工作流的复原能力,并且它可能成为单一故障点。 另一方面,对于协调来说,角色分布在所有服务之间,复原能力变得不那么可靠。 每项服务不仅要负责其操作的复原能力,还要负责工作流。 这种责任对服务来说可能是一种负担,而且很难履行。 每项服务都必须重试暂时性、非暂时性和超时故障,以便根据需要正常终止请求。 此外,服务必须努力传达操作是成功还是失败,以便其他服务可以相应地采取行动。
示例
此示例显示了无人机配送应用中的协调模式。 当客户端请求取件时,该应用会分配一架无人机并通知客户端。 GitHub 上提供此模式的示例。 单个客户端业务事务需要三个不同的业务操作:创建或更新包裹、分配无人机配送包裹以及检查配送状态。 这些操作由三项微服务执行:包裹、无人机计划程序和配送服务。 这些服务使用消息传送在它们之间协作和协调请求,而不是使用中央编排器。
设计
业务事务通过多个跃点按顺序进行处理。 每个跃点都有一个消息总线和相应的业务服务。 当客户端通过 HTTP 端点发送传递请求时,引入服务会接收它,引发操作事件,并将其发送到消息总线。 总线调用订阅的业务服务并在 POST 请求中发送事件。 在接收事件时,业务服务可以以成功、失败完成操作,或请求可以超时。如果成功,服务将使用“确定”状态代码响应总线,引发新的操作事件,并将其发送到下一跃点的消息总线。 如果发生故障或超时,服务通过将 BadRequest 代码发送到发送原始 POST 请求的消息总线来报告故障。 消息总线根据重试策略重试操作。 该期限到期后,消息总线会标记失败的操作,并对整个请求停止的进行进一步处理。 此工作流持续进行,直到处理完整个请求。 该设计使用多个消息总线来处理整个业务事务。 Microsoft Azure 事件网格提供消息传送服务。 该应用部署在 Azure Kubernetes 服务 (AKS) 群集中,其中同一个 Pod 中有两个容器。 一个容器运行与事件网格交互的代表,而另一个容器运行业务服务。 在同一个 Pod 中使用两个容器的方法提高了性能和可伸缩性。 代表和业务服务共享同一网络,该网络允许低延迟和高吞吐量。 为了避免可能导致多次工作量的级联重试操作,只有事件网格重试操作,业务服务不重试操作。 它通过向死信队列 (DLQ) 发送消息来标记失败的请求。 业务服务是幂等的,以确保重试操作不会导致重复资源。 例如,包服务使用 upert 操作将数据添加到数据存储中。 该示例实现了自定义解决方案,以关联所有服务和事件网格跃点之间的调用。 下面的代码示例显示了所有业务服务之间的协调模式。 它显示了无人机配送应用事务的工作流。 为了简洁起见,异常处理和日志记录的代码已被移除。
[HttpPost]
[Route("/api/[controller]/operation")]
[ProducesResponseType(typeof(void), 200)]
[ProducesResponseType(typeof(void), 400)]
[ProducesResponseType(typeof(void), 500)]
public async Task<IActionResult> Post([FromBody] EventGridEvent[] events)
{
if (events == null)
{
return BadRequest("No Event for Choreography");
}
foreach(var e in events)
{
List<EventGridEvent> listEvents = new List<EventGridEvent>();
e.Topic = eventRepository.GetTopic();
e.EventTime = DateTime.Now;
switch (e.EventType)
{
case Operations.ChoreographyOperation.ScheduleDelivery:
{
var packageGen = await packageServiceCaller.UpsertPackageAsync(delivery.PackageInfo).ConfigureAwait(false);
if (packageGen is null)
{
//BadRequest allows the event to be reprocessed by Event Grid
return BadRequest("Package creation failed.");
}
//we set the event type to the next choreography step
e.EventType = Operations.ChoreographyOperation.CreatePackage;
listEvents.Add(e);
await eventRepository.SendEventAsync(listEvents);
return Ok("Created Package Completed");
}
case Operations.ChoreographyOperation.CreatePackage:
{
var droneId = await droneSchedulerServiceCaller.GetDroneIdAsync(delivery).ConfigureAwait(false);
if (droneId is null)
{
//BadRequest allows the event to be reprocessed by Event Grid
return BadRequest("could not get a drone id");
}
e.Subject = droneId;
e.EventType = Operations.ChoreographyOperation.GetDrone;
listEvents.Add(e);
await eventRepository.SendEventAsync(listEvents);
return Ok("Drone Completed");
}
case Operations.ChoreographyOperation.GetDrone:
{
var deliverySchedule = await deliveryServiceCaller.ScheduleDeliveryAsync(delivery, e.Subject);
return Ok("Delivery Completed");
}
return BadRequest();
}
}
相关资源
在设计协调时请考虑这些模式。
- 使用代表设计模式对业务服务进行模块化处理。
- 实现基于队列的负载均衡模式来处理工作负载高峰。
- 通过发布者-订阅者模式使用异步分布式消息传送。
- 使用补偿事务撤消一系列成功的操作,以防一个或多个相关操作失败。
- 若要详细了解如何在消息传送基础结构中使用消息代理,请参阅 Azure 中的异步消息传送选项。