前提

在某一次用户标签服务中大量用到异步流程,使用了RabbitMQ进行解耦。其中,为了提高消费者的处理效率针对了不同节点任务的消费者线程数和prefetch_count参数都做了调整和测试,得到一个相对合理的组合。这里深入分析一下prefetch_count参数在RabbitMQ中的作用。

prefetch_count参数的含义

先从AMQPAdvanced Message Queuing Protocol,即高级消息队列协议,RabbitMQ实现了此协议的0-9-1版本的大部分内容)和RabbitMQ的具体实现去理解prefetch_count参数的含义,可以查阅对应的文档(见文末参考资料)。AMQP 0-9-1定义了basic.qos方法去限制消费者基于某一个Channel或者Connection上未进行ack的最大消息数量上限。basic.qos方法支持两个参数:

  • global:布尔值。
  • prefetch_count:整数。

这两个参数在AMQP 0-9-1定义中的含义和RabbitMQ具体实现时有所不同,见下表:

global参数值 AMQP 0-9-1prefetch_count参数的含义 RabbitMQprefetch_count参数的含义
false prefetch_count值在当前Channel的所有消费者共享 prefetch_count对于基于当前Channel创建的消费者生效
true prefetch_count值在当前Connection的所有消费者共享 prefetch_count值在当前Channel的所有消费者共享

或者用简洁的英文表格理解:

global prefetch_count in AMQP 0-9-1 prefetch_count in RabbitMQ
false Per channel limit Per customer limit
true Per connection limit Per channel limit

这里画一个图理解一下:

上图仅仅为了区分协议本身和RabbitMQ中实现的不同,接着说说prefetch_count对于消费者(线程)和待消费消息的作用。假定一个前提:RabbitMQ客户端从RabbitMQ服务端获取到队列消息的速度比消费者线程消费速度快,目前有两个消费者线程共用一个Channel实例。当global参数为false时候,效果如下:

而当global参数为true时候,效果如下:

在消费者线程处理速度远低于RabbitMQ客户端从RabbitMQ服务端获取到队列消息的速度的场景下,prefetch_count条未进行ack的消息会暂时存放在一个队列(准确来说是阻塞队列,然后阻塞队列中的消息任务会流转到一个列表中遍历回调消费者句柄,见下一节的源码分析)中等待被消费者处理。这部分消息会占据JVM的堆内存,所以在性能调优或者设定应用程序的初始化和最大堆内存的时候,如果刚好用到RabbitMQ的消费者,必须要考虑这些"预取消息"的内存占用量。不过值得注意的是:prefetch_countRabbitMQ服务端的参数,它的设置值或者快照都不会存放在RabbitMQ客户端。同时需要注意prefetch_count生效的条件和特性(从参数设置的一些demo和源码上感知):

  • prefetch_count参数仅仅在basic.consumeautoAck参数设置为false的前提下才生效,也就是不能使用自动确认,自动确认的消息没有办法限流。
  • basic.consume如果在非自动确认模式下忘记了手动调用basic.ack,那么prefetch_count正是未ack消息数量的最大上限。
  • prefetch_count是由RabbitMQ服务端控制,一般情况下能保证各个消费者线程中的未ack消息分发是均衡的,这点笔者猜测是consumerTag起到了关键作用。

RabbitMQ客户端中prefetch_count源码跟踪

编写本文的时候引入的RabbitMQ客户端版本为:com.rabbitmq:amqp-client:5.9.0

上面说了这么多都只是根据官方的文档或者博客中的理论依据进行分析,其实更加根本的分析方法是直接阅读RabbitMQJava客户端源码,主要是针对basic.qosbasic.consume两个方法,对应的是com.rabbitmq.client.impl.ChannelN#basicQos()com.rabbitmq.client.impl.ChannelN#basicConsume()两个方法。先看ChannelN#basicQos()

这里的basicQos()方法多了一个prefetchSize参数,用于限制分发内容的大小上限,默认值0代表无限制,而prefetchCount的取值范围是[0,65535],取值为0也是代表无限制。这里的ChannelN#basicQos()实现中直接封装basic.qos方法参数进行一次RPC调用,意味着直接更变RabbitMQ服务端的配置,即时生效,同时参数值完全没有保存在客户端代码中,印证了前面一节的结论。接着看ChannelN#basicConsume()方法:

上图已经把关键部分用红圈圈出,因为整个消息消费过程是异步的,涉及太多的类和方法,这里不全量贴出,整理了一个流程图:

整个消息消费过程,prefetch_count参数并未出现在客户端代码中,又再次印证了前面一节的结论,即prefetch_count参数的行为和作用完全由RabbitMQ服务端控制。而最终Customer或者常用的DefaultCustomer句柄是在WorkPoolRunnable中回调的,这类任务的执行线程来自于ConsumerWorkService内部的线程池,而这个线程池又使用了Executors.newFixedThreadPool()去构建,使用了默认的线程工厂类,因此在Customer#handleDelivery()方法内部打印的线程名称的样子是pool-1-thread-*

这里VariableLinkedBlockingQueue就是前一节中的message queue的原型

prefetch_count参数使用

设置prefetch_count参数比较简单,就是调用Channel#basicQos()方法:

public class RabbitQos {

    static String QUEUE = "qos.test";

    public static void main(String[] args) throws Exception {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE, true, false, false, null);
channel.basicQos(2);
channel.basicConsume("qos.test", false, new DefaultConsumer(channel) { @Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("1------" + Thread.currentThread().getName());
sleep();
}
});
channel.basicConsume("qos.test", false, new DefaultConsumer(channel) { @Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("2------" + Thread.currentThread().getName());
sleep();
}
});
for (int i = 0; i < 20; i++) {
channel.basicPublish("", QUEUE, MessageProperties.TEXT_PLAIN, String.valueOf(i).getBytes());
}
sleep();
} private static void sleep() {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (Exception ignore) { }
}
}

上面是原生的amqp-client的写法,如果使用了spring-amqpspring-boot-starter-amqp),可以通过配置文件中的spring.rabbitmq.listener.direct.prefetch属性指定所有消费者线程的prefetch_count,如果要针对部分消费者线程进行该属性的设置,则需要针对RabbitListenerContainerFactory进行改造。

prefetch_count参数最佳实践

关于prefetch_count参数的设置,RabbitMQ官方有一篇文章进行了分析:《Finding bottlenecks with RabbitMQ 3.3》。该文章分析了消息流控的整个流程,其中提到了prefetch_count参数的一些指标:

这里指出了,如果prefetch_count的值超过了30,那么网络带宽限制开始占主导地位,此时进一步增加prefetch_count的值就会变得收效甚微。也就是说,官方是建议把prefetch_count设置为30。这里再参看一下spring-boot-starter-amqp中对此参数定义的默认值,具体是AbstractMessageListenerContainer中的DEFAULT_PREFETCH_COUNT

如果没有通过spring.rabbitmq.listener.direct.prefetch进行覆盖,那么使用spring-boot-starter-amqp中的注解定义的消费者线程中设置的prefetch_count就是250

笔者认为,应该综合带宽、每条消息的数据报大小、消费者线程处理的速率等等角度去考虑prefetch_count的设置。总结如下(个人经验仅供参考):

  • 当消费者线程的处理速度十分慢,而队列的消息量十分少的场景下,可以考虑把prefetch_count设置为1
  • 当队列中的每条消息的数据报十分大的时候,要计算好客户端可以容纳的未ack总消息量的内存极限,从而设计一个合理的prefetch_count值。
  • 当消费者线程的处理速度十分快,远远大于RabbitMQ服务端的消息分发,在网络带宽充足的前提下,设置可以把prefetch_count值设置为0,不做任何的消息流控。
  • 一般场景下,建议使用RabbitMQ官方的建议值30或者spring-boot-starter-amqp中的默认值250

小结

小结一下:

  • prefetch_countRabbitMQ服务端的参数,设置后即时生效。
  • prefetch_count对于AMQP-0-9-1中的定义与RabbitMQ中的实现不完全相同。
  • prefetch_count值设置建议使用框架提供的默认值或者通过分组实验结合数据报大小进行计算和评估出一个合理值。

彩蛋

笔者把文章发布到公众号和朋友圈后,笔者的师傅作了点评,指出其中的一点不足:

确实如此,prefetch_count的本质作用就是消费者的流控,官方的那篇文章也提到了网络和带宽的重要性,所以要考虑RTTRound-Trip Time,往返时延),这里的RTT概念来源于《计算机网络原理》:

The RTT includes packet-propagation delays, packet-queuing delays and packet -processing delay.

也就是说RTT = 数据包传播时延(往返)+ 数据包排队时延(路由器和交换机的)+ 数据处理时延(应用程序处理耗时,用在本文的场景就是消费者处理消息的耗时)。假设RTT中只计算网络的时延,不包含数据处理的时延,那么数据包往返需要2RTT,也就是一条消费消息处理的数据包的往返,RTT越大,那么数据传输成本越高,应该允许客户端"预取"更多的未ack消息避免消费者线程等待。这样就可以计算出单个消费者线程处理达到最饱和状态下的"预取"消息量:prefetch_count = 2RTT / 消费者线程处理单条消息的耗时。依照此概念举例:

  • RTT30ms,而消费者线程处理单条消息的耗时为10ms,此时,消费速率占优势,可以考虑把prefetch_count设置为6或者更大的值(考虑堆内存极限的限制)。
  • RTT30ms,而消费者线程处理单条消息的耗时为200msRTT占优势,消费速率滞后,此时考虑把prefetch_count设置为1即可。

思考:为什么spring-boot-starter-amqp把prefetch_count默认值设置为250这么高的值,很少开发者改动它却没有出现明显问题?

(本文完 c-4-d e-a-20201017)

深入理解RabbitMQ中的prefetch_count参数的更多相关文章

  1. 理解RabbitMQ中的AMQP-0-9-1模型

    前提 之前有个打算在学习RabbitMQ之前,把AMQP详细阅读一次,挑出里面的重点内容.后来找了下RabbitMQ的官方文档,发现了有一篇文档专门介绍了RabbitMQ中实现的AMQP模型部分,于是 ...

  2. 如何理解javaSript中函数的参数是按值传递

    本文是我基于红宝书<Javascript高级程序设计>中的第四章,4.1.3传递参数小节P70,进一步理解javaSript中函数的参数,当传递的参数是对象时的传递方式. (结合资料的个人 ...

  3. 深入理解python中函数传递参数是值传递还是引用传递

    深入理解python中函数传递参数是值传递还是引用传递 目前网络上大部分博客的结论都是这样的: Python不允许程序员选择采用传值还是传 引用.Python参数传递采用的肯定是"传对象引用 ...

  4. 理解 Python 中的可变参数 *args 和 **kwargs:

    默认参数:  Python是支持可变参数的,最简单的方法莫过于使用默认参数,例如: def getSum(x,y=5): print "x:", x print "y:& ...

  5. 基于TensorFlow理解CNN中的padding参数

    1 TensorFlow中用到padding的地方 在TensorFlow中用到padding的地方主要有tf.nn.conv2d(),tf.nn.max_pool(),tf.nn.avg_pool( ...

  6. 关于vue自定义事件中,传递参数的一点理解

    例如有如下场景 先熟悉一下Vue事件处理 <!-- 父组件 --> <template> <div> <!--我们想在这个dealName的方法中传递额外参数 ...

  7. RabbitMQ中 exchange、route、queue的关系

    从AMQP协议可以看出,MessageQueue.Exchange和Binding构成了AMQP协议的核心,下面我们就围绕这三个主要组件    从应用使用的角度全面的介绍如何利用Rabbit MQ构建 ...

  8. RabbitMQ中交换机的消息分发机制

    RabbitMQ是一个消息代理,它接受和转发消息,是一个由 Erlang 语言开发的遵循AMQP协议的开源实现.在RabbitMQ中生产者不会将消息直接发送到队列当中,而是将消息直接发送到交换机(ex ...

  9. 怎么理解js中的事件委托

    怎么理解js中的事件委托 时间 2015-01-15 00:59:59  SegmentFault 原文  http://segmentfault.com/blog/sunchengli/119000 ...

随机推荐

  1. pytest allure 生成html测试报告

    前提:需要 java 1.8 以上.python3环境 一.下载pytest pip install pytest 二.下载Allure Pytest Adaptor插件 pip install py ...

  2. Python数据类型方法整理

      前言:主要是对Python数据类型做一个整理,部分知识点源于<python3程序开发指南(第二版)>   一.Python的关键要素 1.1 要素1:数据类型  int类型 str类型 ...

  3. 看图学习 ArrayBuffers 和 SharedArrayBuffers

    原文地址:A cartoon intro to ArrayBuffers and SharedArrayBuffers 原文作者:Lin Clark 译者:黑黑 校对者:Bob 在上一篇文章中,老司机 ...

  4. oracle之二数据字典表和动态性能视图

    数据字典表和动态性能视图 Oracle提供了大量的内部表,它们记录了数据库对象的更改和修正.可以将这些内部表划分为两种主要类型:静态的数据字典表和动态的性能表.这些内部表是由oracle维护的,可以说 ...

  5. [极客大挑战 2019]Secret File wp

    通过标题考虑可能为文件包含漏洞方面 打开网页 从页面并没任何思路,查看源代码 得到有一个跳转到./Archive_room.php的超链接,打开Archive_room.php 中央有一个secret ...

  6. [LeetCode]3. 无重复字符的最长子串(滑动窗口)

    题目 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度. 示例 1: 输入: "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc ...

  7. spring boot之AOP

    首先,aop是面向对象切面,嗯,就是说不面向静态方法,我做测试demo的时候controller方法有个加了static,尴尬的是就用了那个方法测,检查了几遍配置... 参看这篇文章https://m ...

  8. Redis5设计与源码分析读后感(一)认识Redis

    一.初识redis 定义 Redis是一个开源的Key-Value数据库,通常被称为数据结构服务器,其值可以是多种常见的数据格式,且读写性能极高,且所有操作都是原子性的. 高性能的主要原因 1.基于内 ...

  9. Python-进程-进程池-原理

    进程 资源集合,调度和分配资源,说到进程就不得不提到线程,线程和进程是密不可分,进程申请了资源,但真正使用资源的是线程,其实本质上类似面向对象的思想,面向对象把数据和数据的操作封装在一个类中,进程把资 ...

  10. 基于Excel参数化你的Selenium2测试-xlrd

    本篇文章转载至苦叶子:   前言 今天我们就如何使用xlrd模块来进行python selenium2 + excel自动化测试过程中的参数化进行演示说明,以解决大家在自动化测试实践过程中参数化的疑问 ...