RabbitMQ负载均衡

读RabbitMQ文档的时候奇怪为什么它不支持负载均衡。后来看过别人写的Haproxy+RabbitMQ实现负载均衡,觉得似乎这样做就完美了,直到读到这篇文章《Load Balancing a RabbitMQ Cluster》。

这篇文章里介绍了RabbitMQ在队列备份时的一些细节:假设一个cluster里有两个实例,记作rabbitA和rabbitB。如果某个队列在rabbitA上创建,随后在rabbitB上镜像备份,那么rabbitA上的队列称为该队列的主队列(master queue),其它备份均为从队列。接下来,无论client访问rabbitA或rabbitB,最终消费的队列都是主队列。换句话说,即使在连接时主动连接rabbitB,RabbitMQ的cluster会自动把连接转向rabbitA。当且仅当rabbitA服务down掉以后,在剩余的从队列中再选举一个作为继任的主队列。

如果这种机制是真的,那么负载均衡就不能简单随机化连接就能做到了。需要满足下面的条件:

  1. 队列本身的建立需要随机化,即将队列分布于各个服务器;
  2. client访问需要知道每个队列的主队列保存在哪个服务器;
  3. 如果某个服务器down了,需要知道哪个从队列被选择成为继任的主队列。
    于是,Load Balancing a RabbitMQ Cluster的作者给出了下图的结构。

负载均衡架构

这还是颇有点复杂的。首先,在建立一个新队列的时候,Randomiser会随机选择一个服务器,这样能够保证队列均匀分散在各个服务器(这里暂且不考虑负载)。建立队列后需要在Meta data里记录这个队列对应的服务器;另外,Monitor Service是关键,它用于处理某个服务器down掉的情况。一旦发生down机,它需要为之前主队列在该服务器的队列重新建立起与服务器的映射关系。

这里会遇到一个问题,即怎么判断某个队列的主队列呢?一个方法是通过rabbitmqctl,如下面的例子:

./rabbitmqctl -p production list_queues pid slave_pids
registration-email-queue        <rabbit@mq01.2.1076.0>       [<rabbit@mq00.1.285.0>]
registration-sms-queue  <rabbit@mq01.2.1067.0>       [<rabbit@mq00.1.281.0>]

可以看到pid和slave_pids分别对应主队列所在的服务器和从服务器(可能有多个)。利用这个命令就可以了解每个队列所在的主服务器了。

RabbitMQ内存管理

之前一直有个疑问:如果一个exchange关联n个queue,那么一份消息会不会需要拷贝n份呢?这个问题十分关键。如果真的会有n个拷贝,那么queue的数量就必须严格限制了。庆幸的是RabbitMQ官方文档的确完整,于是看摘抄如下(完整文章在此):

A bit of background
First of all we need to understand how Erlang manages memory. Erlang is a bit different from most garbage-collected languages in that it does not have a global heap. Instead, each process has a separate heap that’s private to it. In RabbitMQ terms process might be queues, channels, connections and so on. This means that the entire system does not have to come to a halt every time garbage collection needs to happen; instead each process collects garbage on its own schedule.

That’s fine, but as a message passes through RabbitMQ, it will pass through several different processes. We’d like to avoid doing too much copying as this happens. Therefore Erlang gives us a different memory management scheme for binaries, which are used for a number of things inside RabbitMQ, of which the most interesting is message bodies. Binaries are shared between processes and reference-counted (with references being held by processes and garbage-collected along with everything else).

How this applies to RabbitMQ
This means that memory used by message bodies is shared among processes in RabbitMQ. And this sharing also happens between queues too: if an exchange routes a message to many queues, the message body is only stored in memory once.

由此可见,RabbitMQ在内存处理上还是考虑周到的,每个消息无论在内存还是disk都只会保留一个拷贝,队列里只保存对消息的引用。因此,在设计系统时,队列数量可以作为一个相对比较廉价的开销。

RabbitMQ安装部署

安装Erlang
RabbitMQ是由Erlang编写的,所以需要先安装Erlang。RabbitMQ官方提供了一个足够RabbitMQ使用的Erlang的rpm,可以这样安装:

wget https://www.rabbitmq.com/releases/erlang/erlang-17.4-1.el6.x86_64.rpm
rpm -i erlang-17.4-1.el6.x86_64.rpm

安装RabbitMQ
RabbitMQ的安装非常简单,只需要下载tar包即可。

wget https://github.com/rabbitmq/rabbitmq-server/releases/download/rabbitmq_v3_5_3/rabbitmq-server-generic-unix-3.5.3.tar.gz
tar zxvf rabbitmq-server-generic-unix-3.5.3.tar.gz
启动/关闭RabbitMQ服务
sbin/rabbitmq-server -detached
sbin/rabbitctl stop #关闭服务

端口配置
如果需要建立RabbitMQ集群,需要打开下面的端口:
4369 (epmd), 25672 (Erlang distribution)
5672 (AMQP 0-9-1 without TLS)
一些插件还需要打开额外的端口,暂时不需要。例如rabbitmq-management需要打开15672端口(推荐打开)。

iptables -I INPUT 1 -p tcp –dport 5672 -j ACCEPT
iptables -I INPUT 1 -p tcp –dport 4369 -j ACCEPT
iptables -I INPUT 1 -p tcp –dport 25672 -j ACCEPT
iptables -I INPUT 1 -p tcp –dport 15672 -j ACCEPT
/etc/init.d/iptables save
/etc/init.d/iptables restart

建立集群
官方文档在此https://www.rabbitmq.com/clustering.html

  1. 需要在多台机器上部署RabbitMQ,并保证上面提到的端口没有被防火墙盾掉。注意,不需要每台机器去配置vhost、exchange、queue等等,待建立集群后在任意一节点上创建就好了。
  2. 将一个节点的/var/lib/rabbitmq/.erlang.cookie(如果该目录无权限则在$HOME下)拷贝到其它节点对应的位置,注意权限要保持一致400(即启动rabbitmq的用户下400)。另外,在每个节点上要保证可以通过短hostname(hostname -s来查看)来找到对应的ip地址,所以需要修通/etc/hosts文件,将所有节点的hostname和ip添加到该文件里。
  3. 以任意一个节点为主节点(例如rabbit1),在非主节点节点上执行下面操作:
rabbit2$ sbin/rabbitmqctl stop_app
Stopping node rabbit@rabbit2 ...done.
rabbit2$ sbin/rabbitmqctl join_cluster rabbit@rabbit1
Clustering node rabbit@rabbit2 with [rabbit@rabbit1] ...done.
rabbit2$ sbin/rabbitmqctl start_app
Starting node rabbit@rabbit2 ...done.

这样rabbit2上的节点就与rabbit1上的节点组成一个cluster。在其它非主节点上依次执行上面的操作即可。

查看集群状态:sbin/rabbitmqctl cluster_status
停止某个节点:在对应节点上执行sbin/rabbitmqctl stop
某个节点退出:在对应节点上执行sbin/rabbitmqctl stop_app;sbin/rabbitmqctl reset;sbin/rabbitmqctl start_app

hostname的问题
RabbitMQ直接读取hostname来写数据,如果hostname变了RabbitMQ会重新建立一个新的数据库。

单个节点异常后如何恢复
前提必须保证 /var/lib/rabbitmq/.erlang.cookie一致

rabbitmqctl stop_app;rabbitmqctl update_cluster_nodes rabbit@hostname;rabbitmqctl start_app

用户配置
RabbitMQ默认的用户名密码为guest:guest,这个用户只能在localhost环境下使用,不能远程使用。RabbitMQ大部分配置都可以用sbin/rabbitmqctl完成,可以参考https://www.rabbitmq.com/man/rabbitmqctl.1.man.html。

增加用户:sbin/rabbitmqctl add_user user-name this-is-the-password
删除用户:sbin/rabbitmqctl delete_user user-name
更改密码:sbin/rabbitmqctl change_password user-name new-password
列举所有用户:sbin/rabbitmqctl list_users
为用户打tag:sbin/rabbitmqctl set_user_tags production monitoring

virtual host
RabbitMQ设计了virtual host这个概念,类似于文件系统的不同路径。一个应用是将同一组服务器给线上、线下使用,这时候可以建立两个不同的virtual host,它们相互不影响。

增加virtual host:sbin/rabbitmqctl add_vhost vhost-path
删除virtual host:sbin/rabbitmqctl delete_vhost vhost-path
列举所有virtual host:sbin/rabbitmqctl list_vhosts

设置用户权限

sbin/rabbitmqctl set_permissions [-p vhostpath] user-name conf-regex write-regex read-regex
这个命令可以指定virtual host路径,跟前面提到的对应。需要注意的是后三个参数均为正则表达式,conf控制配置权限,write控制写权限,read控制读权限。例如:
rabbitmqctl set_permissions -p /myvhost tonyg "^tonyg-.*" ".*" ".*"
这个命令配置用户tonyg只能配置以tongyg-开头的资源,但可以读写所有资源。
清空权限:sbin/rabbitmqctl clear_permissions [-p vhostpath] user-name
列举所有用户权限:sbin/rabbitmqctl list_permissions [-p vhostpath]
列举特定用户权限:sbin/rabbitmqctl list_user_permissions user-name

设置服务规则
一个RabbitMQ cluster中所有节点都共享访问user、vhost、queue、exchange等等,其中queue默认只在创建的node上保存,虽然其他节点可以访问这个queue,但如果创建节点down掉就没法访问了。一个解决方案是让队列在多个节点上备份。关于High availability的配置参考https://www.rabbitmq.com/ha.html
一个常用的policy是

sbin/rabbitmqctl [-p vhostpath] set_policy ha-all ".*" '{"ha-mode":"all"}'
这将所有queue都设置为全节点备份,在小规模的cluster中是可行的。 **文件位置** RabbitMQ默认把所有相关文件都保存在安装目录下,一些常用的目录包括: * sbin目录保存控制服务的可执行脚本; * etc目录保存配置文件; * var目录保存服务日志文件; **PHP client库部署** RabbitMQ有多个PHP client实现,其官方tutorial对应的库是https://github.com/videlalvaro/php-amqplib。php-amqplib是一个完全用PHP实现的库。安装方法见github说明文档。 **维护** RabbitMQ自带rabbitmq_management 插件可以用来监控队列状态。https://www.rabbitmq.com/management.html
打开插件:sbin/rabbitmq-plugins enable rabbitmq_management
关闭插件:sbin/rabbitmq-plugins disable rabbitmq_management