我有一个Job Distributor
在不同Channels
上发布消息。
此外,我希望有两个(以及将来更多)Consumers
谁负责不同的任务并在不同的机器上运行。 (目前我只有一个,需要扩展它)
让我们为这些任务命名(仅举例):
FIBONACCI
(生成斐波那契数字)RANDOMBOOKS
(生成随机句子来写一本书)这些任务最长可达2-3小时,应平均分配给每个Consumer
。
每个消费者都可以拥有x
并行主题来处理这些任务。
所以我说:(这些数字只是例子,将被变量取代)
FIBONACCI
消耗3个并行作业,为RANDOMBOOKS
消耗5个并行作业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拉方法最好。我对这些系统不熟悉,有没有人可以向我解释一下,如何实现这一目标?
答案 0 :(得分:7)
这是一个关于如何在couchbase上构建它的概念视图。
总而言之,每个工作者都会对孤立的作业进行查询,如果有的话,检查是否依次检查是否存在锁定文件,如果没有,则创建一个并且它遵循上面的常规锁定协议。如果没有孤立的作业,则它会查找过期的作业,并遵循锁定协议。如果没有过期的工作,那么它只需要最旧的工作并遵循锁定协议。
当然,如果没有"过期"对于你的系统,如果及时性并不重要,那么你可以使用另一种方法来代替最旧的工作。
另一种方法可能是在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)花费更长的时间来完成任务,而不是以其他方式完成任务。 。所以,我对你的调度范例有一些问题:
我不相信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用作队列的详细说明超出了本问题的范围,但我可以提供一些指示。
CAS
method来确保其他人没有在您拥有的同时处理该作业。答案 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中提供跨线程的调度负载的简单平衡。
现在,仍有一些问题:
但是,我们当然可以引入一个ConnectionFactory.setDispatchThreadCount(int)。在幕后,这将创建一个Executors.newFixedThreadPool()和正确数量的调度队列和调度任务。
我有兴趣听到是否有人认为我忽略了一些更简单的方法来解决这个问题,事实上如果这甚至值得解决。