Java& RabbitMQ - 排队&多线程 - 或Couchbase作为作业队列

时间:2012-09-05 08:07:05

标签: java multithreading mongodb rabbitmq couchbase

我有一个Job Distributor在不同Channels上发布消息。

此外,我希望有两个(以及将来更多)Consumers谁负责不同的任务并在不同的机器上运行。 (目前我只有一个,需要扩展它)

让我们为这些任务命名(仅举例):

  • FIBONACCI(生成斐波那契数字)
  • RANDOMBOOKS(生成随机句子来写一本书)

这些任务最长可达2-3小时,应平均分配给每个Consumer

每个消费者都可以拥有x 并行主题来处理这些任务。 所以我说:(这些数字只是例子,将被变量取代)

  • 机器1可以为FIBONACCI消耗3个并行作业,为RANDOMBOOKS消耗5个并行作业
  • 机器2可以为FIBONACCI消耗7个并行作业,为RANDOMBOOKS消耗3个并行作业

我怎样才能实现这个目标?

我是否必须为每个x启动Channel主题,以便在每个Consumer上进行收听?

我什么时候需要确认?

我目前仅针对一个Consumer的方法是:为每个任务启动x个线程 - 每个线程都是一个实现Runnable的Defaultconsumer。在handleDelivery方法中,我调用basicAck(deliveryTag,false),然后完成工作。

此外:我想将一些任务发送给特殊消费者。如何与上述公平分配相结合实现这一目标?

这是publishing

的代码
String QUEUE_NAME = "FIBONACCI";

Channel channel = this.clientManager.getRabbitMQConnection().createChannel();

channel.queueDeclare(QUEUE_NAME, true, false, false, null);

channel.basicPublish("", QUEUE_NAME,
                MessageProperties.BASIC,
                Control.getBytes(this.getArgument()));

channel.close();

这是我Consumer

的代码
public final class Worker extends DefaultConsumer implements Runnable {
    @Override
    public void run() {

        try {
            this.getChannel().queueDeclare(this.jobType.toString(), true, false, false, null);
            this.getChannel().basicConsume(this.jobType.toString(), this);

            this.getChannel().basicQos(1);
        } catch (IOException e) {
            // catch something
        }
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Control.getLogger().error("Exception!", e);
            }

        }
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] bytes) throws IOException {
        String routingKey = envelope.getRoutingKey();
        String contentType = properties.getContentType();
        this.getChannel().basicAck(deliveryTag, false); // Is this right?
        // Start new Thread for this task with my own ExecutorService

    }
}

在这种情况下,课程Worker会启动两次:FIBUNACCI一次,RANDOMBOOKS一次

更新

正如答案所述,RabbitMQ不是最好的解决方案,但Couchbase或MongoDB拉方法最好。我对这些系统不熟悉,有没有人可以向我解释一下,如何实现这一目标?

5 个答案:

答案 0 :(得分:7)

这是一个关于如何在couchbase上构建它的概念视图。

  1. 您有一些机器可以处理作业,还有一些机器(可能是相同的机器)可以创建作业。
  2. 您可以在couchbase的存储桶中为每个作业创建一个文档(并将其类型设置为"作业"或者如果您将其与该存储桶中的其他数据混合在一起的话)。
  3. 每个职位描述以及要完成的具体命令可以包括创建时间,到期时间(如果有特定时间到期)以及某种生成的工作值。此工作值将是任意单位。
  4. 每个工作的消费者都会知道它一次可以做多少工作单位,有多少可用(因为其他工人可能正在工作。)
  5. 因此,如果一台机器具有10个工作单元,可以完成6个工作单元,则可以查询4个或更少工作单位的工作。
  6. 在couchbase中有一些视图,这些视图是逐步更新的map / reduce作业,我认为你只需要这里的地图阶段。您可以编写一个视图,通过该视图查询到期时间,输入系统的时间和工作单位数。通过这种方式,您可以获得4个或更少工作单位的最迟期工作。"
  7. 这种查询,随着容量的释放,将首先获得最多的过期工作,尽管你可以获得最大的过期工作,如果没有,那么最大的未逾期工作。 (其中"逾期"是当前时间与工作截止日期之间的差值。)
  8. Couchbase视图允许这样非常复杂的查询。虽然它们逐步更新,但它们并非完全实时。因此,你不会寻找一份工作,而是一份求职者名单(按你的意愿订购)。
  9. 因此,下一步是获取候选作业列表并检查第二个位置 - 可能是锁定文件的膜库(例如:RAM缓存,非持久性)。锁文件将有多个阶段(这里你使用CRDT做一些分区解析逻辑或任何最适合你需要的方法。)
  10. 由于这个桶基于ram,它比视图更快,并且从总状态开始具有更少的延迟。如果没有锁定文件,则创建状态标志为"临时"的文件。
  11. 如果另一个工作人员获得相同的工作并看到锁定文件,那么它可以跳过该候选人并在列表中执行下一个工作。
  12. 如果有两个工人试图为同一个工作创建锁文件,就会发生冲突。在冲突的情况下,你可以踢。或者您可以拥有逻辑,其中每个工作人员对锁定文件进行更新(CRDT解析因此使这些幂等元素可以合并兄弟姐妹)可能会输入一个随机数或一些优先级数字。
  13. 在指定的时间段(可能是几秒钟)之后,工作人员会检查锁定文件,如果它不必参与任何种族分辨率更改,它会更改锁定文件的状态。 ;临时"到"采取"
  14. 然后它会更新作业本身,状态为""或其他一些这样的,以便当其他工人正在寻找可用的工作时,它不会出现在观点中。
  15. 最后,您要添加另一个步骤,在执行查询之前获取上面描述的这些求职者,您会进行特殊查询以查找已执行的作业,但涉及的工作人员已经死亡。 (例如:过期的工作)。
  16. 了解工人何时死亡的一种方法是,放入membase存储桶的锁文件应该有一个到期时间,最终会导致它消失。可能这个时间可能很短,工人只需触摸它就可以更新到期时间(这在couchbase API中得到支持)
  17. 如果一名工人死亡,最终其锁定文件将消失,孤立的工作将被标记为"采取"但没有锁定文件,这是寻找工作的工人可以寻找的条件。
  18. 总而言之,每个工作者都会对孤立的作业进行查询,如果有的话,检查是否依次检查是否存在锁定文件,如果没有,则创建一个并且它遵循上面的常规锁定协议。如果没有孤立的作业,则它会查找过期的作业,并遵循锁定协议。如果没有过期的工作,那么它只需要最旧的工作并遵循锁定协议。

    当然,如果没有"过期"对于你的系统,如果及时性并不重要,那么你可以使用另一种方法来代替最旧的工作。

    另一种方法可能是在1-N之间创建一个随机值,其中N是一个相当大的数字,比如工人数量的4倍,并且每个工作都用这个值标记。每当一个工人去寻找工作时,它就可以掷骰子,看看是否有任何有这个号码的工作。如果没有,它会再次这样做,直到找到具有该号码的作业。这样,而不是多个工人争夺少数"最老的"或者是最高优先级的工作,以及更多的锁争用可能性,它们会被分散出来......代价是que中的时间比FIFO情况更随机。

    随机方法也适用于你必须适应负载值的情况(这样一台机器不会承担太多的负载)而不是拿最老的候选者,只需要一个随机的候选人形成可行的工作列表并尝试这样做。

    编辑以添加:

    在步骤12中,我说"可能输入随机数"我的意思是,如果工人知道优先级(例如:哪个人最需要完成工作),他们可以将一个代表这个的数字放入文件中。如果没有"需要"这项工作,然后他们都可以掷骰子。他们用骰子的角色更新这个文件。然后他们两个都可以看着它,看看对方是怎么回事。如果他们输了,那么他们就会踢,而另一个工人知道它有它。这样,您可以解决哪个工作人员在没有大量复杂协议或协商的情况下完成工作。我假设这两个工作者都在这里击中相同的锁文件,它可以用两个锁文件和一个查找所有这些文件的查询来实现。如果经过一段时间后,没有工人推出更高的数字(并且新员工认为他的工作会知道其他人已经开始工作,所以他们会跳过它)你可以安全地接受工作,因为你知道你是只有工人才能工作。

答案 1 :(得分:5)

首先让我说我没有使用Java与RabbitMQ进行通信,因此我无法提供代码示例。这应该不是问题,因为那不是你要问的问题。这个问题更多的是关于您的应用程序的一般设计。

让我们稍微分解一下,因为这里有很多问题。

将任务划分给不同的消费者

这样做的一种方法是使用循环法,但这是相当粗略的,并没有考虑到不同的任务可能需要不同的时间才能完成。那么该怎么办。那么一种方法是将prefetch设置为1。预取意味着消费者在本地缓存消息(注意:消息尚未消耗)。通过将此值设置为1,不会发生预取。这意味着您的消费者只会知道并且只有内存中正在处理的消息。这使得只有在工作人员空闲时才能接收消息。

何时确认

通过上述设置,可以从队列中读取消息,将其传递给您的某个线程,然后确认该消息。对所有可用线程执行此操作-1。您不想确认最后一条消息,因为这意味着您将打开以接收另一条消息,您将无法将该消息传递给您的某个工作人员。当其中一个线程结束时,那就是当你确认该消息时,这样你就会总是让你的线程处理某些事情。

传递特殊消息

这取决于你不想做什么,但总的来说我会说你的制作人应该知道他们传递的是什么。这意味着您可以将其发送到某个交换机,或者更确切地说是将某个路由密钥发送到某个路由密钥,该路由密钥会将此消息传递给正确的队列,该队列将让消费者收听该消息,知道如何处理该消息。

我建议您阅读AMQP和RabbitMQ,这可能是一个很好的startingpoint

警告

我的提案和您的设计中存在一个主要缺陷,那就是我们在实际完成处理之前ACK消息。这意味着当(不是)我们的应用程序崩溃时,我们无法重新创建ACKed消息。如果您事先知道要启动多少个线程,这可以解决。我不知道你是否可以动态更改预取计数,但不知怎的,我对此表示怀疑。

一些想法

从RabbitMQ的经验来看,虽然有限,但您不应该害怕创建交换和队列,如果正确完成,这些可以极大地改善和简化您的应用程序设计。也许你不应该有一个启动一堆消费者线程的应用程序。相反,您可能希望使用某种类型的包装器,根据系统中的可用内存或类似内容启动使用者。如果你这样做,你可以确保没有消息丢失,如果你的应用程序崩溃,因为如果你这样做,你当然会在你完成它时确认消息。

推荐阅读

如果有些事情不清楚,或者我错过了你的观点,请告诉我,如果可以的话,我会尝试扩展我的答案或改进它。

答案 2 :(得分:3)

以下是我对你问题的看法。正如@Daniel在他的回答中提到的,我认为这更多的是建筑原则问题而不是实施问题。一旦架构清晰,实现就变得微不足道了。

首先,我想谈谈与调度理论有关的事情。这里有很长时间运行的任务,如果没有以正确的方式安排它们,您将(a)以低于满容量的速度运行服务器或(b)花费更长的时间来完成任务,而不是以其他方式完成任务。 。所以,我对你的调度范例有一些问题:

  1. 您是否有能力估算每项工作需要多长时间?
  2. 这些工作是否有与之相关的截止日期,如果是,它是如何确定的?
  3. 在这种情况下RabbitMQ是否合适?

    我不相信RabbitMQ是发送极长期工作的正确解决方案。事实上,由于RabbitMQ不适合这项工作,我认为你有这些问题。默认情况下,在从队列中删除作业以确定下一个应该处理的作业之前,您对作业没有足够的了解。其次,正如@ Daniel的回答所提到的,你可能无法使用内置的ACK机制,因为每当连接到一个作业时,一个作业可能会被重新排队。 RabbitMQ服务器失败。

    相反,我会寻找像MongoDB或Couchbase这样的东西来存储你的"队列"为了工作。然后,您可以完全控制调度逻辑,而不是依赖RabbitMQ强制执行的内置循环。

    其他考虑因素:

      

    此外,我希望有两个(以及将来更多)消费者,他们从事不同的任务并在不同的机器上运行。 (目前我只有一个,需要扩展它)

    在这种情况下,我不认为您想要使用基于推送的消费者。相反,使用基于拉的系统(在RabbitMQ中,这将被称为Basic.Get)。通过这样做,您将负责工作安排

      

    消费者1有3个用于FIBONACCI的线程和5个用于RANDOMBOOKS的线程。   消费者2有7个线程用于FIBONACCI,3个线程用于RANDOMBOOKS。   我怎样才能做到这一点?

    在这种情况下,我不确定我理解。你有一个fibonacci工作,你在服务器上以某种方式并行执行它?或者您希望您的服务器同时执行多个fibonacci个工作?假设后者,您将创建线程以在服务器上执行工作,然后将作业分配给它们,直到所有线程都已满。当线程可用时,您将轮询队列以启动另一个作业。

    您遇到的其他问题:

      
        
    • 我是否必须为每个频道启动x线程以监听每个消费者?
    •   
    • 我什么时候需要回答?
    •   
    • 我目前仅针对一个消费者的方法是:为每个
    • 启动x个帖子   
    • 任务 - 每个线程都是一个实现Runnable的Defaultconsumer。在handleDelivery方法中,我调用basicAck(deliveryTag,false)然后完成工作。
    •   
    • 此外:我想将一些任务发送给特殊消费者。如何与上述公平分配相结合实现这一目标?
    •   

    我认为,如上所述,一旦您将调度责任从RabbitMQ服务器转移到您的个人消费者(以及消费者,我的意思是消费线程),上述问题将不再是问题。此外,如果您使用更多数据库驱动的东西(比如Couchbase),您将能够自己编写这些东西,并且可以完全控制逻辑。

    使用Couchbase

    虽然有关如何将Couchbase用作队列的详细说明超出了本问题的范围,但我可以提供一些指示。

    • 首先,您想阅读Couchbase
    • 我建议将作业存储在Couchbase存储桶中,并依赖索引视图列出可用作业。如何为每个作业定义一个键有很多选项,但作业本身需要序列化为JSON。也许使用ServiceStack.Text
    • 当处理作业时,需要有一些逻辑来标记作业在Couchbase中的状态。您需要使用CAS method来确保其他人没有在您拥有的同时处理该作业。
    • 您需要某种策略来清除队列中失败和已完成的作业。

    摘要

    1. 不要将RabbitMQ用于此
    2. 使用每个作业的参数来提出智能调度算法。一旦我了解了你的工作性质,我就能帮助你。
    3. 根据#2中的算法将作业拉入工作程序,而不是从服务器中推送它们。
    4. 提出自己的方法来跟踪整个系统中的作业状态(排队,运行,失败,成功等)以及何时/是否重新发送停滞的作业。

答案 3 :(得分:1)

如果使用弹簧或愿意使用弹簧,那么您可以使用弹簧监听器容器支撑来实现它。这将为您提供类似的回调类型的编程模型。

Spring AMQP Reference documentation

的示例代码
@Configuration
public class ExampleAmqpConfiguration {

    @Bean
    public MessageListenerContainer messageListenerContainer() {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(rabbitConnectionFactory());
        container.setQueueName("some.queue");
        container.setMessageListener(exampleListener());
        return container;
    }

    @Bean
    public ConnectionFactory rabbitConnectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        return connectionFactory;
    }

    @Bean
    public MessageListener exampleListener() {
        return new MessageListener() {
            public void onMessage(Message message) {
                System.out.println("received: " + message);
            }
        };
    }
}

答案 4 :(得分:0)

最近我推动了分支bug18384,它改变了回调被发送到Consumer实现的方式。

在此更改之后,Connection会维护一个调度线程,用于将回调发送给使用者。这将释放消费者在Connection和Channel上调用阻塞方法。

Twitter上提出了一个关于使其可配置的问题,允许将自定义Executor插入ConnectionFactory。我想概述为什么这很复杂,讨论可能的实现,看看是否有很多兴趣。

首先,我们应该确定每个Consumer应该只在一个线程中接收回调。如果情况并非如此,则会出现混乱,消费者需要担心自己的线程安全性超出初始化安全性。

对于所有消费者而言,只有一个调度线程,这种消费者 - 线程配对很容易受到尊重。

当我们引入多个线程时,我们必须确保每个Consumer只与一个线程配对。使用Executor抽象时,这可以防止每个回调调度被包装在Runnable中并发送到Executor,因为您无法保证将使用哪个线程。

为了解决这个问题,可以将Executor设置为运行'n'长时间运行的任务(n是Executor中的线程数)。这些任务中的每一个都将调度指令从队列中拉出并执行它们。每个消费者与一个调度指令队列配对,可能是在循环的基础上分配的。这不是太复杂,并且将在Executor中提供跨线程的调度负载的简单平衡。

现在,仍有一些问题:

  1. Executor中的线程数不一定是固定的(与ThreadPoolExecutor一样)。
  2. 无法通过Executor或ExecutorService找出有多少线程。因此,我们无法知道要创建多少个调度指令队列。
  3. 但是,我们当然可以引入一个ConnectionFactory.setDispatchThreadCount(int)。在幕后,这将创建一个Executors.newFixedThreadPool()和正确数量的调度队列和调度任务。

    我有兴趣听到是否有人认为我忽略了一些更简单的方法来解决这个问题,事实上如果这甚至值得解决。