市面上的消息队列产品有很多,如老牌的 ActiveMQ、RabbitMQ ,目前我看最火的 Kafka ,还有 ZeroMQ ,去年底阿里巴巴捐赠给 Apache 的 RocketMQ ,连redis 这样的 NoSQL 数据库也支持 MQ 功能。总之这块知名的产品就有十几种,本文讲一下RabbitMQ。
消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。
消息队列(Message Queue)又叫消息中间件,是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。
消息队列中间件,一般有两种传递模式:点对点(P2P) 模式和发布/订阅(Pub/Sub)模式:
点对点模式时基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息,队列的存在使得消息的异步传输成为可能。
发布订阅模式定义了如何像一个内容节点发布和订阅消息,该内容节点称为主题(topic) 主题可以认为是消息传递的中介,消息发布者将消息发布到中介,消息订阅者从主题中订阅消息。主题使得订阅者、发布者独立。
为何用消息中间件
从上面的描述中可以看出消息中间件是一种应用间的异步协作机制,那什么时候需要使用 MQ 呢?
针对异步任务实现
以常见的订单系统为例,用户点击【下单】按钮之后的业务逻辑可能包括:扣减库存、生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步执行,比如发放红包、发短信通知等。这种场景下就可以用 MQ ,在下单的主流程(比如扣减库存、生成相应单据)完成之后发送一条消息到 MQ 让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由 MQ 推送消息),当发现 MQ 中有发红包或发短信之类的消息时,执行相应的业务逻辑。
以上是用于业务解耦的情况,其它常见场景包括最终一致性、广播、错峰流控、缓冲等等。
关于python的队列,内置的有两种,一种是线程queue,另一种是进程queue,但是这两种queue都是只能在同一个进程下的线程间或者父进程与子进程之间进行队列通讯,并不能进行程序与程序之间的信息交换,这时候我们就需要一个中间件,来实现程序之间的通讯。
RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现,可做到热插拔(无需重启)。
AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:
可靠性(Reliability)
RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
灵活的路由(Flexible Routing)
在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
消息集群(Clustering)
多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
高可用(Highly Available Queues)
队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
多种协议(Multi-protocol)
RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
多语言客户端(Many Clients)
RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
管理界面(Management UI)
RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
跟踪机制(Tracing)
如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
插件机制(Plugin System)
RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。
RabbitMQ整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:包裹送到邮局,邮局暂存并最终通过邮递员送到收件人手上。RabbitMQ好比邮局、邮箱和邮递员组成的一个系统,从计算机层面来说,RabbitMQ 模型更像是一种交换机模型。
RabbitMQ整体模型架构:
RabbitMQ 内部结构
首先生产者将业务数据进行可能的包装,之后封装成消息,发送到Broker, 消费者订阅并接受消息,经过可能的解包处理得到原始的数据,之后再进行业务处理逻辑。这个业务处理逻辑并不一定需要和接受消息的逻辑使用同一个线程。消费者进程可以使用一个线程去接收消息,存入到内存中。这会进一步解耦,提高整个应用的处理效率。
RabbitMQ中概念的详解:
Queue
Queue(队列)是RabbitMQ的内部对象,用于存储消息,用下图表示。
RabbitMQ中的消息都只能存储在Queue中,生产者(下图中的P)生产消息并最终投递到Queue中,消费者(下图中的C)可以从Queue中获取消息并消费。
多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
Exchange
在上一节我们看到生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。实际的情况是,生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。
Exchange是按照什么逻辑将消息路由到Queue的?这个将在下面的Binding中介绍。
RabbitMQ中的Exchange有四种类型,不同的类型有着不同的路由策略,这将在下面的Exchange Types中介绍。
routing key
生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。
在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。RabbitMQ为routing key设定的长度限制为255 bytes。
Binding
RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。
Binding key
Exchange Types
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种(AMQP规范里还提到两种Exchange Type,分别为system与自定义,这里不予以描述),下面分别进行介绍。
1. fanout
2. direct
3. topic
4. headers
RPC
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。
RabbitMQ中实现RPC的机制是:
服务器端收到消息并处理;
了解了RabbitMQ架构模型及相关术语后,再回顾整个消息队列的使用过程。
最初状态下,生产者发送消息:
消费者接收消息的过程:
上面介绍运转过程中引入了:Connection 和 Channel.
无论生产者还是消费者,都需要和RabbitMQ Broker 建立连接, 这个连接就是一条TCP连接, 也就是Connection。一旦TCP连接建立起来, 客户端紧接着可以建立一个AMQP信道(Channel) ,每个信道都会被职派一个唯一的ID。 信道是建立再Connection之上的虚拟连接, RabbitMQ处理的每条AMQP命令都是通过信道完成。
完全可以使用Connection就能完成信道的工作,之所以引入信道,是应对如下场景:
一个应用程序中有多个线程需要从RabbitMQ中消费消息,或者生产信息,那么必然要建立多个Connection, 也就是多个TCP连接,对操作系统来说,建立和销毁TCP连接时非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。RabbitMQ采用类型NIO(Non-blocking I/O) 的做法,选择TCP连接复用,减少性能开销同时便于管理。
每个线程把握一个信道,所以信道复用了Connection的TCP连接,同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。
一般来说安装 RabbitMQ 之前要安装 Erlang ,可以去Erlang官网下载。接着去RabbitMQ官网下载安装包,之后解压缩即可。根据操作系统不同官网提供了相应的安装说明:Windows、Debian / Ubuntu、RPM-based Linux、Mac
Python 使用rabbitMQ需要 pika
pip install pika
or
源码
https://pypi.python.org/pypi/pika
启动rabbitmq web服务:
远程访问rabbitmq:自己增加一个用户,步骤如下:
1. 创建一个admin用户:sudo rabbitmqctl add_user admin 123123
2. 设置该用户为administrator角色:sudo rabbitmqctl set_user_tags admin administrator
3. 设置权限:sudo rabbitmqctl set_permissions -p '/' admin '.' '.' '.'
4. 重启rabbitmq服务:sudo service rabbitmq-server restart
之后就能用admin用户远程连接rabbitmq server了。
生产者:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
Channel = connection.channel()
# 声明queue
Channel.queue_declare(queue='hello')
# rabbitmq 不能直接发送至队列,需要exchange
Channel.basic_publish(exchange='',routing_key = 'hello', body = 'Hello World')
print("[x] sent 'hello world'")
connection.close()
发送后,可以查看MQ中的对列状态:
[root@localhost ~]# rabbitmqctl list_queues
Listing queues ...
hello 1
消费者:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
Channel = connection.channel()
# 或许你会讶异,不是已经声明了队列了,咋还来?
# 是的,我们假设之前声明的队列依然存在,但假设我们发送一个消费请求,但是你并不知道哪个程序先执行。
# 所以最好是两个程序都声明一下。
Channel.queue_declare(queue='hello')
def callback(ch, method, properties, body):
print('[x] Received %r' %body)
Channel.basic_consume(callback, queue='hello', no_ack=True)
print('[*] Waiting for message. To exit press CTRL+C')
# 启动后进入死循环,一直等待消息。
Channel.start_consuming()
./sbin/rabbitmq-server
启动正常的话会看到一些启动过程信息和最后的 completed with 7 plugins,这也说明启动的时候默认加载了7个插件。
正常启动
sudo rabbitmq-server -detached
./sbin/rabbitmqctl status
该命令将输出服务器的很多信息,比如 RabbitMQ 和 Erlang 的版本、OS 名称、内存等等
./sbin/rabbitmqctl stop
它会和本地节点通信并指示其干净的关闭,也可以指定关闭不同的节点,包括远程节点,只需要传入参数 -n :
./sbin/rabbitmqctl -n [email protected] stop
-n node 默认 node 名称是 rabbit@server ,如果你的主机名是 server.example.com ,那么 node 名称就是 [email protected] 。
./sbin/rabbitmqctl stop_app
这个命令在后面要讲的集群模式中将会很有用。
./sbin/rabbitmqctl start_app
./sbin/rabbitmqctl reset
该命令将清除所有的队列。
./sbin/rabbitmqctl list_queues
./sbin/rabbitmqctl list_exchanges
该命令还可以附加参数,比如列出交换器的名称、类型、是否持久化、是否自动删除:
./sbin/rabbitmqctl list_exchanges name type durable auto_delete
./sbin/rabbitmqctl list_bindings
RabbitMQ 最优秀的功能之一就是内建集群,这个功能设计的目的是允许消费者和生产者在节点崩溃的情况下继续运行,以及通过添加更多的节点来线性扩展消息通信吞吐量。RabbitMQ 内部利用 Erlang 提供的分布式通信框架 OTP 来满足上述需求,使客户端在失去一个 RabbitMQ 节点连接的情况下,还是能够重新连接到集群中的任何其他节点继续生产、消费消息。
RabbitMQ 集群中的一些概念
RabbitMQ 会始终记录以下四种类型的内部元数据:
在单一节点中,RabbitMQ 会将所有这些信息存储在内存中,同时将标记为可持久化的队列、交换器、绑定存储到硬盘上。存到硬盘上可以确保队列和交换器在节点重启后能够重建。而在集群模式下同样也提供两种选择:存到硬盘上(独立节点的默认设置),存在内存中。
如果在集群中创建队列,集群只会在单个节点而不是所有节点上创建完整的队列信息(元数据、状态、内容)。结果是只有队列的所有者节点知道有关队列的所有信息,因此当集群节点崩溃时,该节点的队列和绑定就消失了,并且任何匹配该队列的绑定的新消息也丢失了。还好RabbitMQ 2.6.0之后提供了镜像队列以避免集群节点故障导致的队列内容不可用。
RabbitMQ 集群中可以共享 user、vhost、exchange等,所有的数据和状态都是必须在所有节点上复制的,例外就是上面所说的消息队列。RabbitMQ 节点可以动态的加入到集群中。
当在集群中声明队列、交换器、绑定的时候,这些操作会直到所有集群节点都成功提交元数据变更后才返回。集群中有内存节点和磁盘节点两种类型,内存节点虽然不写入磁盘,但是它的执行比磁盘节点要好。内存节点可以提供出色的性能,磁盘节点能保障配置信息在节点重启后仍然可用,那集群中如何平衡这两者呢?
RabbitMQ 只要求集群中至少有一个磁盘节点,所有其他节点可以是内存节点,当节点加入火离开集群时,它们必须要将该变更通知到至少一个磁盘节点。如果只有一个磁盘节点,刚好又是该节点崩溃了,那么集群可以继续路由消息,但不能创建队列、创建交换器、创建绑定、添加用户、更改权限、添加或删除集群节点。换句话说集群中的唯一磁盘节点崩溃的话,集群仍然可以运行,但知道该节点恢复,否则无法更改任何东西。
RabbitMQ 集群配置和启动
如果是在一台机器上同时启动多个 RabbitMQ 节点来组建集群的话,只用上面介绍的方式启动第二、第三个节点将会因为节点名称和端口冲突导致启动失败。所以在每次调用 rabbitmq-server 命令前,设置环境变量 RABBITMQ_NODENAME 和 RABBITMQ_NODE_PORT 来明确指定唯一的节点名称和端口。下面的例子端口号从5672开始,每个新启动的节点都加1,节点也分别命名为test_rabbit_1、test_rabbit_2、test_rabbit_3。
启动第1个节点:
RABBITMQ_NODENAME=test_rabbit_1 RABBITMQ_NODE_PORT=5672 ./sbin/rabbitmq-server -detached
启动第2个节点:
RABBITMQ_NODENAME=test_rabbit_2 RABBITMQ_NODE_PORT=5673 ./sbin/rabbitmq-server -detached
启动第2个节点前建议将 RabbitMQ 默认激活的插件关掉,否则会存在使用了某个插件的端口号冲突,导致节点启动不成功。
现在第2个节点和第1个节点都是独立节点,它们并不知道其他节点的存在。集群中除第一个节点外后加入的节点需要获取集群中的元数据,所以要先停止 Erlang 节点上运行的 RabbitMQ 应用程序,并重置该节点元数据,再加入并且获取集群的元数据,最后重新启动 RabbitMQ 应用程序。
停止第2个节点的应用程序:
./sbin/rabbitmqctl -n test_rabbit_2 stop_app
重置第2个节点元数据:
./sbin/rabbitmqctl -n test_rabbit_2 reset
第2节点加入第1个节点组成的集群:
./sbin/rabbitmqctl -n test_rabbit_2 join_cluster test_rabbit_1@localhost
启动第2个节点的应用程序
./sbin/rabbitmqctl -n test_rabbit_2 start_app
第3个节点的配置过程和第2个节点类似:
RABBITMQ_NODENAME=test_rabbit_3 RABBITMQ_NODE_PORT=5674 ./sbin/rabbitmq-server -detached
./sbin/rabbitmqctl -n test_rabbit_3 stop_app
./sbin/rabbitmqctl -n test_rabbit_3 reset
./sbin/rabbitmqctl -n test_rabbit_3 join_cluster test_rabbit_1@localhost
./sbin/rabbitmqctl -n test_rabbit_3 start_app
RabbitMQ 集群运维
停止某个指定的节点,比如停止第2个节点:
RABBITMQ_NODENAME=test_rabbit_2 ./sbin/rabbitmqctl stop
查看节点3的集群状态:
./sbin/rabbitmqctl -n test_rabbit_3 cluster_status