假设我有一个具有以下限制的多步骤异步过程:
- 任何工人都可以执行各个步骤
- 步骤必须按顺序执行
我正在考虑的方法:
- 插入代表整个过程的 db 行,并带有“已完成的步骤”列以跟踪进度。
- 订阅将在整个过程完成后接收消息的队列。
- 完成每个步骤后,更新数据库行并将流程中的下一步排队。
- 最后一步完成后,将“进程完成”消息排队。
- 删除数据库行。
想法?陷阱?更聪明的方法?
假设我有一个具有以下限制的多步骤异步过程:
我正在考虑的方法:
想法?陷阱?更聪明的方法?
我已经构建了一个与您在大型任务密集型文档处理系统中描述的系统非常相似的系统,并且在过去的 7 年中不得不忍受优缺点。您的方法可靠且可行,但我看到了一些缺点:
可能容易受到状态变化的影响(即,如果在所有步骤排队之前流程输入发生变化,那么后面的步骤可能与前面的步骤有不一致的输入)
比您想要的更多的基础设施,涉及数据库和队列=更多的故障点,更难设置,需要更多的文档=感觉不太对
你如何防止多个工人同时在同一个步骤上行动?换句话说,DB 行说 4 个步骤已完成,工作进程如何知道它是否可以执行 #5?它不需要知道另一个进程是否已经在处理这个问题吗?您需要以一种或另一种方式(DB 或 MQ)包括用于锁定的附加状态。
您的示例对失败具有鲁棒性,但没有解决并发问题。当您添加状态来解决并发问题时,故障处理就会成为一个严重的问题。例如,一个进程执行第 5 步,然后将 DB 行置于“工作”状态。然后,当该过程失败时,第 5 步将停留在“工作”状态。
您的协调器有点重,因为它正在执行大量同步数据库操作,我担心它可能无法像架构的其余部分那样扩展,因为只能有其中一个......这取决于与数据库事务相比,您的步骤运行时间有多长——这可能只会成为一个非常大规模的问题。
如果让我重来一遍,我肯定会将更多的编排工作推到工作进程上。因此,编排代码很常见,可以被任何工作进程调用,但我会尽可能保持中央控制进程的轻量级。我也将只使用消息队列而不是任何数据库来保持架构简单且同步性较低。
我将创建一个包含 2 个队列的交换:IN 和 WIP(正在进行中)
中央进程负责订阅进程请求,并检查 WIP 队列中的超时步骤。
1)当中央进程收到一个给定处理(X)的请求时,它调用编排代码,并将第一个任务(X1)加载到IN队列中
2) 第一个可用的工作进程 (P1) 以事务方式将 X1 出列,并将其加入 WIP 队列,并使用保守的生存时间 (TTL) 超时值。这种出队是原子的,在 IN 中没有其他 X 任务,因此没有第二个进程可以处理 X 任务。
3)如果P1突然终止,除了超时,地球上没有架构可以保存这个过程。在超时时间结束时,中央进程将在 WIP 中找到超时的 X1,并将事务性地从 WIP 中将 X1 出列并将其重新入队,并提供适当的通知。
4) 如果 P1 异常但优雅地终止,则工作进程将以事务方式将 X1 从 WIP 中出列并将其重新入队,并提供适当的通知。根据异常情况,工作进程还可以选择重置 TTL 并重试该步骤。
5) 如果 P1 无限期挂起,或超过其 TTL,则结果与 #3 相同。中央进程处理它,并且可能工作进程将在某个时候被回收——或者规则可以是在超时时回收工作进程。
6) 如果 P1 成功,那么工作进程将决定下一步,是 X2 还是 X-done。如果下一步是 X2,那么工作进程将以事务方式将 X1 从 WIP 中出列,并将 X2 入队到 IN。如果下一步是 X-done,则处理完成,可以采取适当的操作,也许这会将 X-done 排入 IN 以供编排器进行后续处理。
我建议的方法的好处是:
指定工作进程之间的争用
处理所有可能的失败场景(崩溃、异常、挂起和成功)
简单的架构可以完全用RabbitMQ实现,无需数据库,扩展性更强
由于工作人员处理下一步的确定和排队,因此有一个更轻量级的编排器,从而产生一个更具可扩展性的系统
唯一真正的缺点是它可能容易受到状态变化的影响,但这通常不会引起关注。只有您可以知道这是否会成为您系统中的问题。
我对此的最终想法是:您应该有充分的理由进行这种编排。毕竟,如果进程 P1 完成了任务 X1,现在是某个进程处理下一个任务 X2 的时候了,看起来 P1 将是一个非常好的候选者,因为它刚刚完成了 X1 并且现在可用。按照这种逻辑,一个流程应该只完成所有步骤直到完成——如果任务需要按顺序完成,为什么要混合和匹配流程呢?唯一的异步边界实际上是在客户端和工作进程之间。但我会假设您有充分的理由这样做,例如,进程可以在不同的和/或资源专用的机器上运行。