消息队列8:RabbitMq的QOS实验

本文详细研究了RabbitMQ的消费者行为,比较了自动应答和手动应答的差异,重点讲解了QoS在控制消息流量和负载均衡中的作用。通过实例演示了队列处理、消息积压及不同QoS设置下的消费者性能。

环境:

  • win10
  • rabbitmq-3.8.8
  • .net core 3.1
  • RabbitMQ.Client 6.2.1
  • vs2019

安装RabbitMq环境参照:

实验目的:

  • 探索RabbitMq消费者的处理模式
  • 验证RabbitMq的队列在自动应答和手动应答中的表现
  • 探索Qos的作用

一、说明

这里探索队列向消费者派发消息的模式;
消费者拿到消息后的处理模式;
自动应答时的消息处理情况;
开启Qos并关闭自动应答时的消息处理情况;

这里实验使用的代码风格是工作者模式

二、RabbitMq的消费者处理模式

结论:
给消费者绑定的事件是单线程顺序执行的,并不会并发执行,也就是说消费者接收到的消息会在单线程模式下按照分发的顺序一个一个去处理。
这个结论可以《消息队列5:rabbitmq的WorkQueue模式》很容易看到。

三、RabbitMq的开启消息自动应答时的消息处理情况

结论:
队列默认将消息依次分发到不同的消费者,就像发牌一样,按顺序一个一个来;
当消费者绑定队列开启自动应答时(autoAck:true),队列就只管按顺序将消息派发了,不管消费者的处理速度如何(自动应答嘛),此时如果消费者处理速度慢的话,消息可能在消费者那里产生积压,所以当消费者处理速度慢的时候不要开启自动应答。

下面实验消息在消费者端产生积压的情况:
准备:

  1. 一个生产者,每秒产生消息;
  2. 两个消费者,每秒消费一条消息(速度和产出一致);

实验过程:

先开启生产者,当产出到第10条消息时,开启消费者1,此时消费者1和生产者将始终保持10条消息的差距(但生产者的消息已经全部发送到消费者1了,只是消费者1自己处理的慢)。
当生产者产出到第20条消息时,开启消费者2,此时消费者2将从第20条消息开始消费,并且是隔条消费,这个很好理解,因为从第20条消息开始,队列将以此派分消息到两个消费者,而此时消费者1仍然在第10条消息处不紧不慢的消费者,因为它已经积压的10-20条连续消息还未处理完。
等生产者产出到第30条消息时,消费者1也已经消费掉积压的10-20条的连续消息,然而消费者1仍然有20-30中的其中5条消息积压者,而消费者2的速度从一开始就是紧紧根据生产者的。
当生产者产出到第40条消息时,消费者1经过同样的时间(10秒)也将消息从第20条消费到了40条,这之后,生产者和两个消费者的速度将保持一致。

它们的消费情况如下图所示:
在这里插入图片描述

他们的代码如下:
生产者:

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

namespace Send
{
    class Program
    {
        static void Main(string[] args)
        {
            //1. 实例化连接工厂
            var factory = new ConnectionFactory();
            //2. 建立连接
            using (var connection = factory.CreateConnection())
            {
                var t = Task.Factory.StartNew(() =>
                  {
                      //3. 创建信道
                      var channel = connection.CreateModel();
                      {
                          //4. 声明队列
                          channel.QueueDeclare(
                                queue: "hello",
                                durable: false,
                                exclusive: false,
                                autoDelete: false,
                                arguments: null
                                );
                          //5. 发送数据包
                          var index = 1;
                          while (true)
                          {
                              string message = $"chanel1-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 测试消息:{index}";
                              var body = Encoding.UTF8.GetBytes(message);
                              channel.BasicPublish(exchange: "", routingKey: "hello", basicProperties: null, body: body);
                              Console.WriteLine(" [x] Sent {0}", message);
                              Thread.Sleep(1000);
                              index++;
                          }
                      }
                  }, TaskCreationOptions.LongRunning);
                t.Wait();
            }
        }
    }
}

消费者:

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

namespace Receive
{
    class Program
    {
        static void Main(string[] args)
        {
            //1.实例化连接工厂
            var factory = new ConnectionFactory()
            {
                HostName = "localhost",
                Port = 5672,
                UserName = "guest",
                Password = "guest",
                VirtualHost = "/"
            };
            //2. 建立连接
            using (var connection = factory.CreateConnection())
            {
                //3. 创建信道
                using (var channel = connection.CreateModel())
                {
                    //4. 声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    //5. 构造消费者实例
                    var consumer = new EventingBasicConsumer(channel);
                    //6. 绑定消息接收后的事件委托
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        Console.WriteLine($" [x] [Thread:{Thread.CurrentThread.ManagedThreadId}] Received {message}");
                        Thread.Sleep(1000);//模拟耗时
                        Console.WriteLine(" [x] Done");
                    };
                    //7. 启动消费者
                    channel.BasicConsume(queue: "hello", autoAck: true, consumer: consumer);
                    Console.WriteLine(" Press [enter] to exit.");
                    Console.ReadLine();
                }
            }
        }
    }
}

运行后效果如下:
在这里插入图片描述

实验代码下载:https://download.csdn.net/download/u010476739/18168224

四、关于Qos

参照:https://www.rabbitmq.com/consumer-prefetch.html

我们知道在RabbitMq中:

  • IConnection:表示一个真实的Tcp连接;
  • IModel:表示Tcp连接上的信道,为的是复用Tcp连接;
  • 生产者和消费者的消息收发都是通过信道传递的。
  • 生产者通过信道将消息发送到RabbitMq;
  • 消费者通过信道接收RabbitMq队列分发的消息;

从上面的实验也看出来了,当消费者绑定队列并指定自动应答时,队列会将消息依次分发到所有的消费者,而不管消费者的处理速度情况(是否有积压)。
所以,默认应答仅适合那种消费非常快的情况。

RabbitMq允许消费者绑定队列时关闭自动应答,关闭后,消费者处理完消息后手动向RabbitMq发送确认完成消息,并拉取下一条消息(如果有的话)。这样就有个好处是,能者多干,有利于整个系统的负载均衡。
不过有个新的问题,就是每次新消息的获取都需要先手动和RabbitMq确认上次消息已消费完成,这样就影响了消费的速度。RabbitMq考虑到这种问题,并提供了一种模式Qos,就是允许消费者在处理速度有限的情况下也可以多储存一部分消息,而不用每次手动确认拉取新消息。基于这种模式,队列在分发消息时也就观察消费者的储存积压消息的容量,如果还有消息积压的空间就派发消息,没有的话就不派发。

如下图所示:
在这里插入图片描述

在RabbitMq中允许对队列和信道做消息的数量限制,但只推荐作用在队列上,这里实验也只在队列上做实验,因为在信道上限制消息的数量影响太大。

4.1 情形1: 关闭自动应答、不设置qos

准备一个生产者和一个消费者,速度都是1个/每秒。
先让生产者先跑到第20条消息,这样队列里面就积压了20条消息,然后开启消费者,可以看到生产者和消费者之间始终有20条消息的差距。
此时,观察RabbitMq的web面板,可以看到,这个队列hello未确认的消息稳定在20条。。。

代码如下:
生产者:

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

namespace Send
{
    class Program
    {
        static void Main(string[] args)
        {
            //1. 实例化连接工厂
            var factory = new ConnectionFactory();
            //2. 建立连接
            using (var connection = factory.CreateConnection())
            {
                var t = Task.Factory.StartNew(() =>
                  {
                      //3. 创建信道
                      var channel = connection.CreateModel();
                      {
                          //4. 声明队列
                          channel.QueueDeclare(
                                queue: "hello",
                                durable: false,
                                exclusive: false,
                                autoDelete: false,
                                arguments: null
                                );
                          //5. 发送数据包
                          var index = 1;
                          while (true)
                          {
                              string message = $"chanel1-{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} 测试消息:{index}";
                              var body = Encoding.UTF8.GetBytes(message);
                              channel.BasicPublish(exchange: "", routingKey: "hello", basicProperties: null, body: body);
                              Console.WriteLine(" [x] Sent {0}", message);
                              Thread.Sleep(1000);
                              index++;
                          }
                      }
                  }, TaskCreationOptions.LongRunning);
                t.Wait();
            }
        }
    }
}

消费者:

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

namespace Receive
{
    class Program
    {
        static void Main(string[] args)
        {
            //1.实例化连接工厂
            var factory = new ConnectionFactory()
            {
                HostName = "localhost",
                Port = 5672,
                UserName = "guest",
                Password = "guest",
                VirtualHost = "/"
            };
            //2. 建立连接
            using (var connection = factory.CreateConnection())
            {
                //3. 创建信道
                using (var channel = connection.CreateModel())
                {
                    //4. 声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    ////5. 声明信道上的Qos限制,作用在队列上
                    //channel.BasicQos(
                    //    prefetchSize: 0,//最多传输的内容的大小的限制,0为不限制,但据说prefetchSize参数,RabbitMQ暂未对其没有实现。
                    //    prefetchCount: 5,//会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该消费者Consumer将阻塞block掉,直到有消息进行ack确认
                    //    global: false // true/false,表示是否将上面设置应用于channel,简单点说,就是上面限制是信道channel级别的还是消费者consumer级别。
                    //);
                    //6. 构造消费者实例
                    var consumer = new EventingBasicConsumer(channel);
                    //7. 绑定消息接收后的事件委托
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        Console.WriteLine($" [x] [Thread:{Thread.CurrentThread.ManagedThreadId}] Received {message}");
                        Thread.Sleep(1000);//模拟耗时
                        Console.WriteLine(" [x] Done");
                        //multiple: 是否批量确认,这里单独确认即可
                        channel.BasicAck(ea.DeliveryTag, multiple: false);
                    };
                    //8. 启动消费者
                    channel.BasicConsume(queue: "hello", autoAck: false, consumer: consumer);
                    Console.WriteLine(" Press [enter] to exit.");
                    Console.ReadLine();
                }
            }
        }
    }
}

效果截图:
在这里插入图片描述
在这里插入图片描述

4.2 情形2:关闭自动应答,设置Qos的个数为5

准备一个生产者和一个消费者,速度都是1个/每秒;
还上面一样的步骤,先让生产者跑一会,到第20条消息时开启消费者,在上面的实验中,RabbitMq的web面板中稳定显示有20条消息未应答(因为没有Qos,队列中有消息就会分发到消费者)。
但是在这里,我们会观察到RabbitMq的web面板中稳定显示有5条消息未应答,有15条消息处于Ready状态,总共有20条消息。

代码:
生产者的代码未改动,消费者中的代码去掉Qos的注释即可。

实验效果如下图所示:
在这里插入图片描述
在这里插入图片描述

4.3 关于Qos的声明

Qos的声明会作用到后面新绑定的消费者上,所以,当一个队列上需要绑定多个消费者的时候,我们可以在每次绑定前重新声明Qos,如下面的代码:

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

namespace Receive
{
    class Program
    {
        static void Main(string[] args)
        {
            //1.实例化连接工厂
            var factory = new ConnectionFactory();
            //2. 建立连接
            using (var connection = factory.CreateConnection())
            {
                //3. 创建信道
                using (var channel = connection.CreateModel())
                {
                    //4. 声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    // 5.声明信道上的Qos限制,作用在队列上
                    channel.BasicQos(
                        prefetchSize: 0,
                        prefetchCount: 5,
                        global: false
                    );
                    //6. 构造消费者实例
                    var consumer = new EventingBasicConsumer(channel);
                    //7. 绑定消息接收后的事件委托
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        Console.WriteLine($" [x] [Thread:{Thread.CurrentThread.ManagedThreadId}] Received {message}");
                        Thread.Sleep(1000);//模拟耗时
                        Console.WriteLine(" [x] Done");
                        channel.BasicAck(ea.DeliveryTag, multiple: false);
                    };
                    //8. 启动消费者
                    channel.BasicConsume(queue: "hello", autoAck: false, consumer: consumer);

                    //连续声明5个消费者绑定到同一队列
                    for (var i = 0; i < 5; i++)
                    {
                        channel.BasicQos(
                            prefetchSize: 0,
                            prefetchCount: (ushort)(i + 1),
                            global: false
                        );
                        consumer = new EventingBasicConsumer(channel);
                        consumer.Received += (model, ea) =>
                        {
                            var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                            Console.WriteLine($" [x] 2 [Thread:{Thread.CurrentThread.ManagedThreadId}] Received {message}");
                            Thread.Sleep(1000);
                            Console.WriteLine(" [x] 2 Done");
                            channel.BasicAck(ea.DeliveryTag, multiple: false);
                        };
                        channel.BasicConsume(queue: "hello", autoAck: false, consumer: consumer);
                    }
                    Console.WriteLine(" Press [enter] to exit.");
                    Console.ReadLine();
                }
            }
        }
    }
}

运行后,查看rabbitmq的web面板,可以看到每个消费者消息预取的长度如下:
在这里插入图片描述

4.4 单个消费者应用程序如何并发消费队列

场景是这样的,有一个pdf转word队列,我写了一个消费者处理器,它会连续不断的从队列中拉取消息。
如果,我们想提高并发数量,只需要多部署几台机器就可以了。
但实际情况是,单台机器的性能还可以,同时转换个几十个不成问题,总不能每提高一个并发就买一台机器吧?
这时,我们在一台机器上将消费者处理器运行几十次,这样单个机器就能拥有几十个并发了,但是,开启几十个进程合适嘛?显然不合适!
所以,最终我们期望可以配置单个进程的并发数量。比如,我们可以配置消费者处理器的并发处理能力为30,这样起一个进程就可以满足30个并发了。

不过,我们写代码的时候要注意一下,一个消费者要对应一个信道。
因为,一个通道内的消息处理顺序是串行的,无论是几个队列或几个消费者。(这里实验的效果是这样的,欢迎找到反例。。。)

正确的写法如下:

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

namespace Receive
{
    class Program
    {
        static void Main(string[] args)
        {
            var factory = new ConnectionFactory();
            using (var connection = factory.CreateConnection())
            {
                //加入有10个并发
                var concurrent = 10;
                for (int i = 0; i < concurrent; i++)
                {
                    //每一个并发单独创建一个信道
                    var channel = connection.CreateModel();
                    channel.QueueDeclare(queue: "hello0", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    channel.BasicQos(
                        prefetchSize: 0,
                        prefetchCount: 5,
                        global: false
                    );
                    var consumer = new EventingBasicConsumer(channel);
                    var cc = i;
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} [x] {cc} [Thread:{Thread.CurrentThread.ManagedThreadId}] Received {message}");
                        Thread.Sleep(6000);
                        Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} [x] {cc} Done");
                        channel.BasicAck(ea.DeliveryTag, multiple: false);
                    };
                    channel.BasicConsume(queue: "hello0", autoAck: false, consumer: consumer);
                }
                Console.ReadLine();
            }
        }
    }
}

在这里插入图片描述

4.5 其他注意事项:

当调用chanel的消息确认时,必须使用接收消息时的chanel对象,如下所示:

private void Consumer_Received(object sender, BasicDeliverEventArgs e)
 {
 	try
     {
     }
     catch (Exception ex)
     {
         logger.LogError($"[{e.DeliveryTag}]:{ex?.Message}");
     }
     finally
     {
         var consumer = sender as EventingBasicConsumer;
         consumer.Model.BasicAck(e.DeliveryTag, false);
     }
 }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jackletter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值