具有作业亲和力的作业队列

时间:2019-05-22 19:00:41

标签: design-patterns message-queue distributed dispatcher job-queue

我目前正面临一个问题,我可以肯定有一个正式名称,但是我不知道该在网上搜索什么。我希望如果我能描述问题和解决方案,那么有人可以告诉我设计模式的名称(如果有一个与我要描述的设计相匹配)。

基本上,我想拥有一个工作队列:我有多个创建工作的客户端(发布者),以及许多处理这些工作的工人(消费者)。现在,我想将发布者创建的作业分发给各个消费者,这基本上可以使用几乎任何消息队列以及跨队列进行负载平衡的方法来完成,例如使用RabbitMQ甚至MQTT 5。

但是,现在事情变得复杂了……每个工作都指一个外部实体,比如说一个用户。我想要的是按顺序处理单个用户的作业,但并行处理多个用户的作业。我不要求用户X的作业始终去工作者Y,因为无论如何它们都应该按顺序处理。

现在我可以使用RabbitMQ及其一致的哈希交换来解决此问题,但是当新的工作人员进入集群时,我就会发生数据争夺,因为RabbitMQ不支持重新定位已经在队列中的工作。

MQTT 5也不支持:在这里,这种想法被称为“粘性共享订阅”,但这不是官方的。它可能是MQTT 6的一部分,也可能不是。谁知道。

我还研究了NSQ,NATS和其他一些经纪人。他们中的大多数甚至都不支持这种非常特定的情况,而那些确实使用一致哈希的情况,则存在前面提到的数据竞速问题。

现在,如果在作业到达时代理不将作业分类到队列中,但是如果它跟踪是否已处理特定用户的作业,则问题将消失:如果是,则应延迟所有作业。该用户的其他作业,但其他用户的所有作业仍应处理。这是AFAICS,无法使用RabbitMQ等人实现。

我很确定我不是唯一拥有用例的人。我可以例如考虑到用户将视频上传到视频平台,尽管上传的视频是并行处理的,但单个用户上传的所有视频都是按顺序处理的。

因此,总而言之:我所描述的名字是用一个普通名字知道的吗?诸如分布式作业队列之类的东西? 具有任务相似性的任务调度程序?还是其他?我尝试了很多术语,但没有成功。这可能意味着没有解决方案,但是如上所述,很难想象我是地球上唯一遇到此问题的人。

有什么想法我可以寻找的吗?并且:是否有实现此目的的工具?有协议吗?

PS:仅使用预定义的路由密钥是不可行的,因为用户ID(我在这里只是作为伪造的示例)基本上是UUID,因此可以有数十亿个,因此我还需要更多动态。因此,一致性哈希基本上是正确的方法,但是如上所述,为了避免数据争用,分发必须逐个进行而不是预先进行。

9 个答案:

答案 0 :(得分:1)

对每个实体的处理订单都有严格的要求,这具有挑战性。

每个已发布的任务要运行多长时间?如果它们总是很短,那么您可以通过散列来分发任务,并且只要每次改变形状时都消耗正在运行的作业的工人池,而不会损失很多生产力。

如果它们运行时间较长,则可能会太慢。在那种情况下,您还可能让工作人员在执行期间,从他们消耗的每个任务的user_id的快速中央服务(如Redis或类似工具)中获取原子建议锁。该服务还可以按用户ID范围或您拥有的内容分别进行可伸缩分区。如果在接收任务和执行任务带来的第一个副作用之间有足够的距离,则工作人员甚至不需要在成功执行锁之前就阻止成功获取锁,因此可能看不到锁的显着增加。潜伏。失败可能很少发生:如果您已经在user_id上使用了某种一致的哈希方案来分配工作,则它们的确很少发生,并且仍然仅在工作池拓扑更改时发生。您至少应该使用散列分布来确保只有两个工人在争夺锁:旧的和新的。

如果授予的锁是按照先到先得的顺序提供的,并且请求锁的速度比工作池拓扑更改的速度快(也就是说,当工作人员从发布者那里收到工作后就排队等待锁) ,甚至可以在拓扑快速变化的情况下为您提供很好的订购保证。

答案 1 :(得分:1)

Apache Qpid代理支持称为message groups的功能,其中路由密钥和工作线程之间的关系是动态的,并基于当前流量。

消费排序意味着对于给定的组,代理将不允许未解决的未确认消息发送给多个消费者。

这意味着在给定的时间,只有一个使用者可以处理来自特定组的消息。当使用者确认所有获取的消息时,代理可以将来自该组的下一个未决消息传递给其他使用者。

这可以更好地利用工人:

请注意,不同的消息组不会互相阻止传递。例如,假设一个队列包含来自两个不同消息组的消息-例如组“ A”和组“ B”-并将它们排队,以使“ A”的消息位于“ B”的前面。如果组“ A”的第一条消息正在被客户端使用,则其余“ A”消息将被阻止,但是“ B”组的消息可供其他使用者使用-即使它在队列“ A”组的“后面”。

尽管如此,此功能可能会以较高的性能价格when compared to other brokers带来。如今4 5对Qpid的兴趣不大。

编辑:还有其他经纪人也提供此功能:ActiveMQActiveMQ Artemis EDIT2:事实证明ActiveMQ中的“消息组”和Artemis的工作方式不同-组给工作人员的分配是静态的(粘性)而不是动态的。

答案 2 :(得分:0)

  

我想要的是一个工作队列:我有多个创建工作的客户端(发布者),以及许多处理这些工作的工作者(消费者)。现在,我想将发布者创建的作业分发给各个消费者,这基本上可以使用几乎任何消息队列以及跨队列进行负载平衡的方法来完成,例如使用RabbitMQ甚至MQTT 5。

     

但是,现在事情变得复杂了……每个工作都指一个外部实体,比如说一个用户。我想要的是按顺序处理单个用户的作业,但并行处理多个用户的作业。我不要求用户X的作业始终去工作者Y,因为无论如何它们都应该按顺序处理。

即使不是这种特殊的用例,我还是在两个月前对(动态)任务调度[0] [1]进行了调查,但没有发现类似的事情。

我阅读的每个调度算法都具有一些其他所有任务共有的属性,例如优先级,年龄,入队时间,任务名称(以及扩展的平均处理时间)。如果您的任务都已链接到用户,则可以构建计划程序,该计划程序会考虑user_id从队列中选择任务。

但是我想,您不想构建自己的调度程序,无论如何都是浪费的,因为从这种需求的经验来看,现有的消息队列可以实现您的需求。

要总结您的要求,您需要:

  

一个调度程序,每个用户只能同时运行一个任务。

解决方案是使用分布式锁,例如REDIS distlock,并在任务开始之前获取该锁,并在任务执行期间定期刷新它。如果有相同用户的新任务进入并尝试执行,它将无法获取该锁并将其重新排队。

这是一个伪代码:

def my_task(user_id, *args, **kwargs):
    if app.distlock(user_id, blocking=False):
        exec_my_task(user_id, *args, **kwargs)
    else:
        raise RetryTask()

别忘了刷新释放

采用类似的方法来强制搜寻器中每个请求之间的robots.txt延迟。

答案 3 :(得分:0)

Cadence Workflow能够以最小的努力支持您的用例。

这是可以满足您要求的稻草人设计:

  • 使用userID作为工作流ID向用户工作流发送signalWithStart请求。它要么向工作流程传递信号,要么首先启动工作流程并向其传递信号。
  • 对该工作流的所有请求均由它缓冲。 Cadence严格保证打开状态下只能存在一个具有给定ID的工作流。因此,可以保证所有信号(事件)都将在属于用户的工作流中进行缓冲。
  • 内部工作流程事件循环逐个分发这些请求。
  • 当缓冲区为空时,工作流可以完成。

以下是用Java实现它的工作流程代码(也支持Go客户端):

public interface SerializedExecutionWorkflow {

    @WorkflowMethod
    void execute();

    @SignalMethod
    void addTask(Task t);
}

public interface TaskProcessorActivity {
    @ActivityMethod
    void process(Task poll);
}

public class SerializedExecutionWorkflowImpl implements SerializedExecutionWorkflow {

    private final Queue<Task> taskQueue = new ArrayDeque<>();
    private final TaskProcesorActivity processor = Workflow.newActivityStub(TaskProcesorActivity.class);

    @Override
    public void execute() {
        while(!taskQueue.isEmpty()) {
            processor.process(taskQueue.poll());
        }
    }

    @Override
    public void addTask(Task t) {
        taskQueue.add(t);
    }
}

然后通过信号方法将任务排队到工作流中的代码:

private void addTask(WorkflowClient cadenceClient, Task task) {
    // Set workflowId to userId
    WorkflowOptions options = new WorkflowOptions.Builder().setWorkflowId(task.getUserId()).build();
    // Use workflow interface stub to start/signal workflow instance
    SerializedExecutionWorkflow workflow = cadenceClient.newWorkflowStub(SerializedExecutionWorkflow.class, options);
    BatchRequest request = cadenceClient.newSignalWithStartRequest();
    request.add(workflow::execute);
    request.add(workflow::addTask, task);
    cadenceClient.signalWithStart(request);
}

与使用队列进行任务处理相比,Cadence具有许多其他优点。

  • 构建具有无限到期间隔的指数重试
  • 故障处理。例如,它允许执行一个任务,如果在配置的时间间隔内两次更新均未成功,则该任务会通知另一服务。
  • 支持长时间运行的心跳操作
  • 能够实现复杂的任务依赖性。例如,在无法恢复的故障(SAGA)的情况下实现呼叫链或补偿逻辑的链接
  • 完全了解更新的当前状态。例如,当使用队列时,您就会知道队列中是否有某些消息,并且需要其他数据库来跟踪总体进度。使用Cadence可以记录每个事件。
  • 能够取消正在进行的更新。
  • 分布式CRON支持

请参见介绍Cadence编程模型的the presentation

答案 4 :(得分:0)

只要锁冲突不经常发生,amirouche所描述的就是一个简单的解决方案。如果这样的话,您将浪费大量时间在工作人员上,以获取他们必须拒绝的消息并让消息代理重新排队。

Actor模型/ Actor框架可以很好地解决此类问题。一些示例包括Akka,Orleans,Protoactor和Cadence(如上所述,尽管Candence不仅仅是一个actor框架)。这些框架可能会变得非常复杂,但是它们的核心可以确保一次处理单个参与者的消息,但一次允许处理多个参与者(您的方案中每个用户ID会有一个参与者)。这些框架使您摆脱了所有消息路由和并发性,从而大大简化了实现,并且从长远来看应该更加健壮/可扩展。

答案 5 :(得分:0)

Kafka完全支持您的需求。您需要配置一个密钥,kafka将确保所有具有相同密钥的消息将被顺序处理。

答案 6 :(得分:-1)

通过搜索“按类别排序的工作队列” ,我可以找到关于this所描述行为的讨论。

不幸的是,看起来他们没有能够解决您的问题。

有一个answer to a prior question,它建议不要将任何类型的消息代理服务用于任何类型的对订单敏感或对业务逻辑敏感的任务,原因可能与您所适用的原因无关,也可能不适用在做。它还指出了一种技术,该技术似乎可以完成您想做的事情,但可能无法很好地满足当前任务的需要。

如果您选择了stickiness,它将可以巧妙地解决您的问题,并且效率最低。当然,粘性有其自身的失败模式。没有理由认为您会找到实现您所要进行权衡的实现。

我认为,因为您在这里提出了问题,所以每个用户的顺序性很重要。在您提供的示例中,在视频平台处理上传内容的过程中,顺序性冲突并不重要。更广泛地说,大多数需要大量吞吐量负载平衡的工作队列的人并不需要 strong 保证处理事情的顺序。

如果最终需要自己构建事物,则将有很多选择。我得到的印象是您期望获得巨大的吞吐量,高度并行化的体系结构以及低的用户ID冲突率。在这种情况下,您可以考虑维护先决条件的列表:
出现新任务时,平衡器将在所有进行中的,已分配的和尚未分配的作业中搜索与作业密钥(user_id)相匹配的任何作业。
如果存在匹配项,则将新作业添加到尚未分配的列表中,最旧的作业将共享其密钥作为前提。
每次工作完成时,工作人员都需要检查尚未分配的清单,以查看它是否刚刚完成了任何人的先决条件。如果是这样,工作人员可以标记该子作业以进行分配,也可以只处理该子作业本身。
当然,这有其自身的故障模式。您必须权衡取舍。

答案 7 :(得分:-1)

Kafka可以存储一段时间,因此可以提供帮助,因此您可以再次对其进行轮询

答案 8 :(得分:-1)

如果我正确理解了您的情况,则认为您所描述的功能与Message SessionsAzure Service Bus中的工作方式非常相似。

在将消息放入队列之前,您基本上将消息的SessionId属性设置为UserId

每个使用者将依次锁定消息的会话,这些消息将属于同一用户。完成后,消费者可以继续进行下一个可用会话。

此外,Azure Functions最近发布了Service Bus Sessions支持,该支持正在预览中,但您可以轻松实现所有这些功能。

不幸的是,我还不太了解该功能是否存在于开源替代产品之一中,但我希望这会有所帮助。