Rabbit MQ 翻译二(分发任务)

这个教程中我们创建一个用于分配耗时的任务给多个工作者的工作队列,工作队列的主要思想是为了避免立即执行一个资源密集型的任务,并等待它的完成。相反,我们计划稍后去完成任务。我们封装任务为消息并将它发送给队列,后台的工作进程将抛出任务并实际的去执行作业。当你运行多个工作者,这些任务将这这些工作者之间进行分享。在web应用程序中当不能在短时间的http请求处理复杂的任务时,这个概念特别有用。

在前一个教程中我们发送一个包含了"Hello world!"的消息,现在我将发送代表复杂任务的字符串,我们没有一个真实世界的任务,如图像进行伸缩或者pdf文件被渲染,所以我们用Thread.Sleep()方法来模拟它仅仅假设我们很忙,我们在字符串中放置一定数量的.来作为其复杂性,每一个点都会占用一秒的工作。举个例子,一个模拟的任务“task ...”将花费3秒钟。

我们将上一个教程中的Send.cs的代码稍微修改下,允许从命令行发送任意的消息,这个程序将安排任务给我们的工作队列,我们命名它为NewTask.cs:

我们之前的那个Receive.cs也需要做一些修改,消息体里面的每一个点它需要伪造一秒钟的工作。它将处理RabbitMQ交付的消息并执行任务,所以我们叫他Worker.cs

 

轮询分配

使用任务队列的一个优势是能够并行工作,如果我们处理积压的工作,我们只要增加更多的工作者,用这种方式很容易规模化。

首先让我们同时运行两个工人,她们都将从一个队列里面获取消息,但究竟如何呢?让我们继续看。

我们建立3个控制台应用程序,两个工作者,这两个工作者是两个消费者,另一个是生产者。

当生产者发布一系列消息,将会把每个消息依次的交付给下一个消费者,平均每个消费者得到相同数量的消息。这种分配消息的方式我们称之为round-robin, 你可以尝试3个或者更多的的工人。

 

消息确认

做一个任务花费几秒,你可能比较好奇当其中一个消费者获得一个比较长的任务或者该任务死掉并且只有部分的任务完成,那么这会发生什么事情呢?在我们当前的代码中,一旦RabbitMQ把一个消息交给消费者后,那么则个消息就会立即从队列的内存中移除掉。这这个case中,如果你杀掉一个消费者,那么他正在处理的这个消息将被丢失掉。我们也将丢失丢所有的分配到给该特定的消费者的所有未处理的消息。但是我们不想丢掉任何消息。如果一个消费者死掉,那么我们将把这个任务交付给另一个消费者来处理。为了保证不丢失任何消息,RabbitMQ支持消息确认。消费者将送返一个确认来告诉RabbitMQ这个特定的消息已经被接收,被处理,并且RabbitMQ将删除它。如果一个消费者没有发送确认给RabbitMQ, 包括管道关闭,连接关闭或者TCP连接关闭。RabbitMQ将明白这个消息没有被完全处理,它将重新入队。如果有其他的消费者同时在线上,它将快速重新交付这个消息给另一个消费者。这种方式你能确保即使一个消费者突然死亡,也不会让消息丢失掉。这里没有任何的超时,当这个消费者死亡,RabbitMQ将把消息重新交付。即使处理这个消息要花很长时间,这也是好的。消息确认是默认被开启的,在前一个例子中,我们特定把noAck参数设置为true来关闭它的。当我们完成了工作时,是时候工人把它移除掉,并且发送一个合适的确认。

 

var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
    var body = ea.Body;
    var message = Encoding.UTF8.GetString(body);
    Console.WriteLine(" [x] Received {0}", message);

    int dots = message.Split('.').Length - 1;
    Thread.Sleep(dots * 1000);

    Console.WriteLine(" [x] Done");

    channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};
channel.BasicConsume(queue: "task_queue", noAck: false, consumer: consumer);

 

使用这段code我们能够确保即使我们通过ctrl+C来杀掉正在处理消息的工作者,也没有消息被丢失。工作者死亡之后所有未被确认的消息将被重新交付。

》》》忘记确认

丢失这个BasicAck这是一个常见的失误,这是一个简单的错误,但是后果却是很严重,当工作者死掉后消息是被重新交付,它看起来是被随机交付,但是当那些未被确认的未被释放,RabbitMQ就吃掉越来越多的内存。为了调试这种类型的失误,你能够使用rabbitmqctl 来打印这些未被释放的字段。

 

 

$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello    0       0
...done.

 

 

消息持久化

我们已经学到了如何在工作者死掉的情况下任务也不丢失,但是当我们的RabbitMQ的服务器停止以后我的队列中的任务也还是会丢失。当RabbitMQ退出或者崩溃以后,它将忘记队列和消息,除非告诉它不这么做。两件必须的事情可用来确保消息消息不丢失:我们需要同时标记队列和消息是持久的。

首先我们确保RabbitMQ不会丢失我们的队列,为了这么做,我们需要把队列声明为durable.

 

channel.QueueDeclare(queue: "hello",
                     durable: true,
                     exclusive: false,
                     autoDelete: false,
                     arguments: null);

即使它本身的命令是正确的,用我们目前的设置他还是不能运行的。那是因为我们已经定义个名叫hello的非持久化的队列,对任何尝试这么做的程序,RabbitMQ不允许用不同的参数来定义一个已经存在的消息队列,这样它将返回错误。但是这里有一个快速的解决办法,让我们把队列重新命名,如task_queue.这个queueDeclare 的声明需要生产者和消费者都要变更。在这里我们已经确保即使Rabbit服务器重启也不会丢失队列了。现在我们需要确保我们的消息也是持久的,需要靠设置IBasicProperties.SetPersistent属性的值为true.

var properties = channel.CreateBasicProperties();
properties.Persistent = true;

》》消息持久化的注意

标记消息为persistent,不能完全的保证消息不丢失,尽管告诉RabbitMQ把消息保存到磁盘上,存在一个短的时间窗口RabbitMQ已经收到了消息但是还没保存到。RabbitMQ不能够做到把每条消息同步到磁盘,因为它可能还只存到Cache中,还没真正的存储到磁盘。这个持久化保证不是很强,但是对于简单的任务队列来说已经是足够强的了,如果你需要更强的保证,你可以使用publisher confirms.

 

公平调度

你可能已经注意到调度可能不是我们想要的工作。举个例子,在一个两个工人的情况下,当所有奇数的消息是重量级而偶数的消息是轻量级的,一个工人是一直很忙,而另一个工人则几乎不用做什么事情,RabbitMQ是不知道这种情况的事情而继续平均的分派消息。这事因为当消息进入队列RabbitMQ只是负责分派消息。它是不看对于一个消费者未被确认的消息的数量,他只是盲目的把第N条消息发送给第N个消费者。

为了解决这个问题我们使用这个basicQos 方法和prefetchCount = 1来设置。这告诉RabbitMQ不要在这个时间点分配另外的消息给这个工人。或者换句话说,直到这个工人已经处理完或者确认了前一个消息才给他分派另一个新的消息。相反,它将分配消息给另一个不忙的工人。

channel.BasicQos(0, 1, false);

注意队列大小,

当所有的工人都很忙时,你的队列将被塞满,应该要保持对队列的监测,可能要增加新的工人或者其他的策略。

最终我们的NewTask.cs类的代码如下:

using System;
using RabbitMQ.Client;
using System.Text;

class NewTask
{
    public static void Main(string[] args)
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue: "task_queue",
                                 durable: true,
                                 exclusive: false,
                                 autoDelete: false,
                                 arguments: null);

            var message = GetMessage(args);
            var body = Encoding.UTF8.GetBytes(message);

            var properties = channel.CreateBasicProperties();
            properties.Persistent = true;

            channel.BasicPublish(exchange: "",
                                 routingKey: "task_queue",
                                 basicProperties: properties,
                                 body: body);
            Console.WriteLine(" [x] Sent {0}", message);
        }

        Console.WriteLine(" Press [enter] to exit.");
        Console.ReadLine();
    }

    private static string GetMessage(string[] args)
    {
        return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
    }
}

我的工人代码:

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Threading;

class Worker
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue: "task_queue",
                                 durable: true,
                                 exclusive: false,
                                 autoDelete: false,
                                 arguments: null);

            channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

            Console.WriteLine(" [*] Waiting for messages.");

            var consumer = new EventingBasicConsumer(channel);
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine(" [x] Received {0}", message);

                int dots = message.Split('.').Length - 1;
                Thread.Sleep(dots * 1000);

                Console.WriteLine(" [x] Done");

                channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
            };
            channel.BasicConsume(queue: "task_queue",
                                 noAck: false,
                                 consumer: consumer);

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

使用消息确认和BasicQos你能设置一个工作队列,持久化的选项可以在RabbitMQ重启后也能幸存。

 

posted @ 2017-03-03 00:16  康熙来乐  阅读(187)  评论(0)    收藏  举报