大家好,我是Z哥。最近在项目中使用RabbitMQ遇到了一个问题。我觉得这个问题还是普遍的。分享给大家,以免以后在同样的问题上犯错误。消息队列(MQ)是软件开发中非常常用的中间件。如果一个程序需要配合另一个程序进行数据“写”操作,并且不关心“写”的结果,就会选择它。它是一个保存消息(数据)的容器,用来保证消息一定要投递到目标程序。打个比方,消息队列就是一个邮递员,负责将信件(消息)从信源发送到目的地,并根据信件的重要程度,提供两种服务:当面确认或直达送货。RabbitMQ是一个典型的消息队列,以AMQP为标准。它的历史很悠久,大概是2007年开发的,Erlang使用的编程语言也很有年代感。有必要简单介绍一下Erlang的特点,这对我们理解RabbitMQ有很大的帮助。Erlang是一种在“虚拟机”(如JVM)上运行的解释型语言。是一种结构化的、动态类型的编程语言,内置了对并行计算的支持。用Erlang编写的应用程序运行时通常由数千个轻量级“进程”(不是传统意义上的进程)组成,它们通过消息传递相互通信。进程间的上下文切换对于Erlang来说只是一两步,比C程序的线程切换效率高很多。根据百度百科资料整理无论是什么MQ中间件,消息的生产者和消费者都需要与MQ服务器建立连接进行通信。一般这种连接都会使用TCP协议,在RabbitMQ中也不例外。大多数RabbitMQSDK将连接封装为“Connection”对象。这还没完,大部分MQ中间件还会在“Connection”的基础上增加一个“Channel”的概念,通过多路复用来提高TCP连接的利用率,因为TCP连接的建立和销毁都是非常昂贵的开销。RabbitMQ中的多路复用TCP连接模式是“非阻塞I/O”模式。关于NIO,“Non-blockingI/O”的概念,有兴趣的可以跳转阅读之前写的这篇文章。(分布式系统聚焦——阻塞和非阻塞有什么区别?)再说一句,任何解决方案都不是“银弹”。当每个Channel的流量不是很大时,复用单个Connection可以在性能瓶颈的情况下有效节省TCP连接资源。但是当Channel本身的流量非常大的时候,一个Connection被多个Channel复用会造成性能瓶颈,进而限制整体的流量。这时候就需要开辟多个Connection,将这些Channel分发给这些Connection。至于哪个Channel使用哪个Connection以及Connections和Channel的数量关系,需要根据业务本身的实际情况进行调整。Channel是AMQP中一个非常重要的概念,大部分操作都是在channel层面进行的。比如channel.exchangeDeclare、channel.queueDeclare、channel.basicPublish、channel.basicConsume等方法。RabbitMQ相关的API与AMQP紧密相连,例如channel.basicPublish对应AMQP的Basic.Publish命令。也许你要问了,Channel能不能像Connection一样复用呢?这是一个很好的问题,也是我们这次遇到问题的重点。结论是:可以,但是需要保证客户端访问Channel的线程安全,因为在Channel的另一端,在RabbitMQ的服务器端,每个Channel都是由一个单独的“进程”管理的。使用Channel导致数据帧乱序,RabbitMQ服务器会主动关闭整个Connection。所以我们这次犯的错误就是多个线程复用同一个Channel导致的问题。所以,如果你也使用了streadway/amqp库,这一点需要特别注意。但是不同语言的SDK内部实现是不一样的。我们使用了Golang的AMQP库streadway/amqp和RabbitMQ官方提供的C#版本库来模拟相同的场景。前者有问题,后者没有问题。由于时间关系,我没有具体验证C#库的源码。我的主观猜测是C#库对单个Channel做了更多线程安全的处理。最后整理了三个使用streadway/amqp库的最佳实践,大家可以看看:在01golang中使用streadway/amqp时,需要保证每个线程都有独立的Channel。streadway/amqp库中获取Channel的方法“Connection.channel()”是线程安全的。但是里面有个defaultChannelMax参数限制了Channel的个数,默认是(2<<10)-1,2047这个需要注意:02我们可以通过调用amqp.DialConfig(urlstring,configConfig)来调整限制.但是,这不是您调整多少。还需要配合RabbitMQ服务器的配置用min()函数进行处理,最后取两者的最小值。Tips:尤其是在使用云厂商的MQ产品时,很多性能参数会因为分级收费而受到限制,需要特别注意这一点。例如阿里云RabbitMQ的某个版本的实例限制是单个Connection,最多64个Channel)03和之前一样面对Erlang的简单介绍,Erlang是天生支持多进程设计的语言,所以在RabbitMQ服务器设计中,每个Queue和每个Connection都是一个独立的“进程”。因此,如果想尽可能地压榨RabbitMQ的性能,可以通过创建更多的Connections或者创建更多的Queues来实现。当然需要注意Connection创建和销毁的性能开销。
