一、rabbitmq 实现原理

   Rabbitmq 是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性,扩展性、高可用性等方面表现不俗。消息中间件主要用于组件之间的解耦,消息发送者无需知道消息使用的存在,反之亦然:

  AMQP当中有四个概念非常重要:虚拟主机(virtual host),交换机(exchange)、队列(node1)和绑定(binding)。一个虚拟主机持有一个交换机、队列和绑定。为什么需要那么多虚拟主机呢?很简单,rabbitmq 当中,用户只能在虚拟主机的力度进行权限控制。因此,如果需要禁止A组访问B组的交换机、队列、绑定,必须为A和B分别创建一个虚拟主机。每个RabbitMQ服务器都有一个默认的虚拟主机"/"。

 Producer要产生消息必须要创建一个Exchange,Exchange用于转发消息,但是他不会做存储,如果没有Node1 Bind到Exchange的话,他会直接丢掉Producer发送过来的消息,当然如果消息总是发送过去就被直接丢掉就没有什么意思了,一个consumer想要接受消息的话,就要创建一个node1,并把这个node1 bind 到指定的exchange上,然后exchange会把消息转发到node1那里,node1会负责存储消息,Consumer可以通过主动Pop或者是Subscribe 之后被动回调的方式来从node1中取得消息。

  使用rabbitmq Server需要:

   1 Erlang语言包

   2 RabbitMQ 安装包

二、rabbitmq 概念和特性

 2.1  交换机

     1 接收消息,转发消息到绑定的队列。四种类型: direct,topic,headersand fanout

        Direct:转发消息到routigkey 指定的队列

        Topic:按规则转发消息

        Headers:

        Fanout: 转发消息到所有绑定的队列

     2  如果没有绑定在交换机上,则发送到该交换机上的消息会丢失。

     3 一个交换机可以绑定多个队列,一个队列可以被多个交换机绑定

     4. topic类型交换器通过模式匹配分析消息的routing-key属性。它将routing-key和binding-key的字符串切分成单词。这些单词之间用点隔开。它同样也会识别两个通配符:#匹配0个或者多个单词,*匹配一个单词。例如,binding key:*.stock.#匹配routing key:usd.stcok和eur.stock.db,但是不匹配stock.nana。

还有一些其他的交换器类型,如header、failover、system等,现在在当前的RabbitMQ版本中均未实现。

5. 因为交换器是命名实体,声明一个已经存在的交换器,但是试图赋予不同类型是会导致错误。客户端需要删除这个已经存在的交换器,然后重新声明并且赋予新的类型。

6. 交换器的属性:

  •  持久性:如果启用,交换器将会在server重启前都有效。

  •  自动删除:如果启用,那么交换器将会在其绑定的队列都被删除掉之后自动删除掉自身。

  •  惰性:如果没有声明交换器,那么在执行到使用的时候会导致异常,并不会主动声明


2.2 队列(node1):

  1. 队列是RabbitMQ内部对象,存储消息。相同属性的node1可以重复定义。

  2. 临时队列。channel.node1Declare(),有时不需要指定队列的名字,并希望断开连接时删除队列。

  3. 队列的属性:

  •    持久性:如果启用,队列将会在server重启前都有效。

  •    自动删除:如果启用,那么队列将会在所有的消费者停止使用之后自动删除掉自身。

  •    惰性:如果没有声明队列,那么在执行到使用的时候会导致异常,并不会主动声明。

  • 排他性:如果启用,队列只能被声明它的消费者使用。

这些性质可以用来创建例如排他和自删除的transient或者私有队列。这种队列将会在所有链接到它的客户端断开连接之后被自动删除掉。它们只是短暂地连接到server,但是可以用于实现例如RPC或者在AMQ上的对等通信。4. RPC的使用是这样的:RPC客户端声明一个回复队列,唯一命名(例如用UUID),并且是自删除和排他的。然后它发送请求给一些交换器,在消息的reply-to字段中包含了之前声明的回复队列的名字。RPC服务器将会回答这些请求,使用消息的reply-to作为routing key(默认绑定器会绑定所有的队列到默认交换器,名称为“amp.交换器类型名”)发送到默认交换器。注意这仅仅是惯例而已,可以根据和RPC服务器的约定,它可以解释消息的任何属性(甚至数据体)来决定回复给谁。


2.3 消息传递:

  1. 消息在队列中保存,以轮询的方式将消息发送给监听消息队列的消费者,可以动态的增加消费者以提高消息的处理能力。

  2. 为了实现负载均衡,可以在消费者端通知RabbitMQ,一个消息处理完之后才会接受下一个消息。

    channel.basic_qos(prefetch_count=1)

    注意:要防止如果所有的消费者都在处理中,则队列中的消息会累积的情况。

  3. 消息有14个属性,最常用的几种:

     deliveryMode:持久化属性

     contentType:编码

     replyTo:指定一个回调队列

     correlationId:消息id

      

  4. 消息生产者可以选择是否在消息被发送到交换器并且还未投递到队列(没有绑定器存在)和/或没有消费者能够立即处理的时候得到通知。通过设置消息的mandatory和/或immediate属性为真,这些投递保障机制的能力得到了强化。

  5. 此外,一个生产者可以设置消息的persistent属性为真。这样一来,server将会尝试将这些消息存储在一个稳定的位置,直到server崩溃。当然,这些消息肯定不会被投递到非持久的队列中。

2.4 高可用性(HA):

  1. 消息ACK,通知RabbitMQ消息已被处理,可以从内存删除。如果消费者因宕机或链接失败等原因没有发送ACK(不同于ActiveMQ,在RabbitMQ里,消息没有过期的概念),则RabbitMQ会将消息重新发送给其他监听在队列的下一个消费者。

  channel.basicConsume(node1name,noAck=false, consumer);

  2. 消息和队列的持久化。定义队列时可以指定队列的持久化属性(问:持久化队列如何删除?)

channel.node1Declare(node1name,durable=true, false, false, null);

发送消息时可以指定消息持久化属性:

channel.basicPublish(exchangeName,routingKey,

            MessageProperties.PERSISTENT_TEXT_PLAIN,

            message.getBytes());

这样,即使RabbitMQ服务器重启,也不会丢失队列和消息。

  3. publisherconfirms

  4. master/slave机制,配合Mirrored Node1,这种情况下,publisher会正常发送消息和接收消息的confirm,但对于subscriber来说,需要接收Consumer Cancellation Notifications来得到主节点失败的通知,然后re-consume from the node1,此时要求client有处理重复消息的能力。注意:如果node1在一个新加入的节点上增加了一个slave,此时slave上没有此前node1的信息(目前还没有同步机制)。

(通过命令行或管理插件可以查看哪个slave是同步的:

rabbitmqctllist_node1s name slave_pids synchronised_slave_pids)

    当一个slave重新加入mirrored-node1时,如果node1是durable的,则会被清空。

2.5  集群(cluster):

  1. 不支持跨网段(如需支持,需要shovel或federation插件)

  2. 可以随意的动态增加或减少、启动或停止节点,允许节点故障

  3. 集群分为RAM节点和DISK节点,一个集群最好至少有一个DISK节点保存集群的状态。

  4. 集群的配置可以通过命令行,也可以通过配置文件,命令行优先。

三、Rabbit集群高可用集群

     Rabbitt 是用erlang开发的,集群非常方便,因为erlang 天生就是一门分布式语言,但其本身并不支持负责均衡。

     Rabbitt模式大概分为三种:单一模式、普通模式、镜像模式

     1 单一模式: 最简单的情况,并非集群模式

     2 普通模式:默认的集群模式

       对于Node1来说,消息实体只存在于其中一个节点,A、B两个节点,仅有相同的元素数据,即队列结构。

       当消息进入A节点的Node1中后,consumer从B节点拉取时,RabbitMQ会临时在A、B间进行消息传输,把A中的消息实体去出经过B发送给consumer。

       所以consumer应尽量连接每一个节点,从中取消息。即对于同一个逻辑列,要在多个节点建立物理node1。否则无论consumer连接A或B,出口总在A,会产生瓶颈。

       该模式存在一个问就是当A节点故障后,B节点无法取到A节点中还为消费的消息实体。

       如果做了消息持久化,那么A节点恢复,然后才可被消费。

      3 镜像模式: 把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案

     该模式解决了上述问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点时间同步,而不是在consumer取数据时临时拉取。

     该模式带来了副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之量大的消息进入,集群内部的网络带宽将会被这种同步通讯大大的消耗掉。所以在对可靠性要求较高的场合适用

      4 集群中的基本概念:

      RabbitMQ 的集群节点包括内存节点、磁盘节点。

        内存节点就是将数据存放在内存

        磁盘节点就是将数据存放在磁盘上。

        如果在投递消息时,打开了消息的持久化,那么即使是内存节点,数据还是安全的存放在磁盘。

        一个rabbitmq集群中可以共享user、vhost、node1、exchange等,所有的数据和状态都是必须在所有节点上复制的,一个例外是,那些当前只属于创建它的节点消息队列,尽管他们可见且可被所有节点读取。Rabbitmq节点可以是动态的加入到集群中,一个节点他可以加入到集群中,也可以从集群环集群会进行一个基本的负载均衡


集群中有两种节点:

   1 内存节点: 只保存状态到内存(一个例外的情况是:持久的node1的持久的内容将被保存到disk)

   2 磁盘节点:保存状态到内存和磁盘上,内存节点虽然不写入到磁盘,但是他比磁盘节点要好。集群中,只需要一次磁盘节点来保存状态就足够了,如果集群中只用内存节点,那么不能停止他们,否则所有的状态,消息等都会消失。


四  rabbit 安装

1 环境:

    OS Version:Centos 6.6
    Rabbitmqversion:  rabbitmq-server-3.5.3.tar.gz
    Erlang:   otp_src_17.5.tar.gz
    Simplejson: simplejson-3.6.5.tar.gz

2 安装erlang

    #tar  xf   otp_src_17.5.tar.gz
    #cd otp_src_17.5
    #./configure ---prefix=/usr/local/erlang
   *********************************************************************
   **********************  APPLICATIONS DISABLED  *****************           *********************************************************************
   odbc           : ODBC library - link check faile
   *********************************************************************
   *********************************************************************
   **********************  APPLICATIONS INFORMATION  *******************
    *********************************************************************
   wx             : wxWidgets not found, wx will NOTbe usable
   *********************************************************************
   #make  && make instal


3  安装simplejson

   #tar  xf simplejson-3.6.5.tar.gz
   #cd simplejson-3.6.5
   #python setup.py install

   

4 安装rabbitmq

# tar xf rabbitmq-server-3.5.3.tar.gz
#cd rabbitmq-server-3.5.3
#make
#export TARGET_DIR=/usr/local/rabbitmq/
#export SBIN_DIR=/usr/localrabbitmq/sbin
#export MAN_DIR=/usr/local/rabbitmq/man
#make install

5 配置rabbitmq

#cd /usr/local/rabbtmq/sbin
#cat rabbitmq-default | grep –v ^#| grep –v ^$
SYS_PREFIX=
ERL_DIR=
SASL_BOOT_FILE=start_sasl
CONFIG_FILE=${SYS_PREFIX}/conf/rabbitmq/rabbitmq
LOG_BASE=${SYS_PREFIX}/logs/rabbitmq
MNESIA_BASE=${SYS_PREFIX}/data/rabbitmq/mnesia
ENABLED_PLUGINS_FILE=${SYS_PREFIX}/conf/rabbitmq/enabled_plugins
PLUGINS_DIR="${RABBITMQ_HOME}/plugins"
CONF_ENV_FILE=${SYS_PREFIX}/apps/conf/rabbitmq/rabbitmq.conf

6 启动rabbitmq

#cd/usr/local/rabbitmq/sbin/rabbitmq-server
#创建用户
# ./rabbitmqctl add_user frank frank123
#User为用户名,tag为角色名(administration,monitoring,policymaker,management)可以给多个角色使用逗号分隔
#./rabbitmqctl  set_user_tags  vci administrator
### 设置用户权限
#./rabbitmqctl  set_permissions  -p /  vci  .* .*  .*
### 查看(指定hostpath)所有用户的权限信息
#./rabbitmqctl  list_permissions  [-p  /]
### 查看指定用户的权限信息
#: ./rabbitmqctllist_user_permissions vci
### 清除用户的权限信息
#:./rabbitmqctl  clear_permissions  [-p VHostPath]  vci
### 删除一个用户
#./rabbitmqctl delete_user Username
### 修改用户的密码
#:./rabbitmqctl  change_password  Username Newpassword
### 查看当前用户列表
#:./rabbitmqctl  list_users
### 添加用户远程访问
#: vi/etc/rabbitmq/rabbitmq.config
[ 
{rabbit,[{tcp_listeners, [5672]}, {loopback_users, ["vci"]}]} 
].

五 rabbitmq 集群安装

 1 安装rabbitmq 的管理插件

    #cd /usr/local/rabbitmq/sbin
   #./rabbitmq-plugins enablerabbitmq_management

 2 在/etc/hosts添加一下内容

192.168.1.115 node1
192.168.1.116 node2
192.168.1.200 node3

  3 设置Erlang cookie

   Rabbitmq的集群是依赖于erlang的集群来工作的,所以必须先构建起erlang的集群环境。Erlang的集群中各节点是通过一个magic cookie来实现的,这个cookie存放在/var/lib/rabbitmq/.erlang.cookie 中,文件是400的权限。所以必须保证各节点cookie保持一致,否则节点之间就无法通信。

-r--------.1 rabbitmq rabbitmq 20 3月 500:00 /var/lib/rabbitmq/.erlang.cookie

将其中一台节点上的.erlang.cookie值复制下来保存到其他节点上。或者使用scp的方法也可,但是要注意文件的权限和属主属组。

我们这里将node1中的cookie 复制到 node2、node3中,先修改下node2、node3中的.erlang.cookie权限

#chmod777  /var/lib/rabbitmq/.erlang.cookie

将node1的/var/lib/rabbitmq/.erlang.cookie这个文件,拷贝到node2、node3的同一位置(反过来亦可),该文件是集群节点进行通信的验证密钥,所有节点必须一致。拷完后重启下RabbitMQ。

复制好后别忘记还原.erlang.cookie的权限,否则可能会遇到错误

#chmod400 /var/lib/rabbitmq/.erlang.cookie

设置好cookie后先将三个节点的rabbitmq重启

#rabbitmqctl stop
#rabbitmq-server start

   4 停止所有节点RabbitMq服务,然后使用detached参数独立运行,这步很关键,尤其增加节点停止节点后再次启动遇到无法启动都可以参照这个顺序

   

  node1# rabbitmqctl stop
  node2# rabbitmqctl stop
  node3# rabbitmqctl stop
  node1# rabbitmq-server -detached
  node2# rabbitmq-server -detached
  node3# rabbitmq-server –detached

   5  分别查看下每个节点

    

node1# rabbitmqctl cluster_status
          Cluster status of node rabbit@node1...
[{nodes,[{disc,[rabbit@node1]}]},
{running_nodes,[rabbit@node1]},
{partitions,[]}]
..        .done.
node2# rabbitmqctl cluster_status
          Cluster status of node rabbit@node2...
[{nodes,[{disc,[rabbit@node2]}]},
{running_nodes,[rabbit@node2]},
{partitions,[]}]
...         done.
node3# rabbitmqctl cluster_status
          Cluster status of node rabbit@node3...
[{nodes,[{disc,[rabbit@node3]}]},
{running_nodes,[rabbit@node3]},
{partitions,[]}]
...        done.

6 将node2、node3作为内存节点与queue连接起来,在node2上,执行如下命令: 

node2# rabbitmqctl stop_app
node2#rabbitmqctl join_cluster --ram rabbit@queue  
node2#rabbitmqctl start_app
node3#rabbitmqctl stop_app
node3#rabbitmqctl join_cluster --ram rabbit@queue  (上方已经将node2与queue连接,也可以直接将node3与node2连接,同样而已加入集群中)
node3#rabbitmqctl start_app

上述命令先停掉rabbitmq应用,然后调用cluster命令,将node2连接到,使两者成为一个集群,最后重启rabbitmq应用。在这个cluster命令下,node2、node3是内存节点,queue是磁盘节点(RabbitMQ启动后,默认是磁盘节点)。

queue 如果要使node2或node3在集群里也是磁盘节点,join_cluster 命令去掉--ram参数即可

      #rabbitmqctl join_clusterrabbit@queue  

只要在节点列表里包含了自己,它就成为一个磁盘节点。在RabbitMQ集群里,必须至少有一个磁盘节点存在。

8     往任意一台集群节点里写入消息队列,会复制到另一个节点上,我们看到两个节点的消息队列数一致:(如何发送消息参见)

# rabbitmqctllist_queues -p hrsystem
Listing queues …
test_queue 10000
…done.
# rabbitmqctllist_queues -p hrsystem
Listing queues …
test_queue 10000
…done.
# rabbitmqctllist_queues -p hrsystem
Listing queues …
test_queue 10000
…done.

-p参数为vhost名称

这样RabbitMQ集群就正常工作了,

这种模式更适合非持久化队列,只有该队列是非持久的,客户端才能重新连接到集群里的其他节点,并重新创建队列。假如该队列是持久化的,那么唯一办法是将故障节点恢复起来。 

为什么RabbitMQ不将队列复制到集群里每个节点呢?这与它的集群的设计本意相冲突,集群的设计目的就是增加更多节点时,能线性的增加性能(CPU、内存)和容量(内存、磁盘)。理由如下:

1. storage space:If every cluster node had a full copy of every queue, adding nodes wouldn’tgive you more storage capacity. For example, if one node could store 1GB ofmessages, adding two more nodes would simply give you two more copies of thesame 1GB of messages.

2. performance:Publishing messages would require replicating those messages to every clusternode. For durable messages that would require triggering disk activity on allnodes for every message. Your network and disk load would increase every timeyou added a node, keeping the performance of the cluster the same (or possiblyworse).

当然RabbitMQ新版本集群也支持队列复制(有个选项可以配置)。比如在有五个节点的集群里,可以指定某个队列的内容在2个节点上进行存储,从而在性能与高可用性之间取得一个平衡。

镜像模式配置

上面配置RabbitMQ默认集群模式,但并不保证队列的高可用性,尽管交换机、绑定这些可以复制到集群里的任何一个节点,但是队列内容不会复制,虽然该模式解决一部分节点压力,但队列节点宕机直接导致该队列无法使用,只能等待重启,所以要想在队列节点宕机或故障也能正常使用,就要复制队列内容到集群里的每个节点,需要创建镜像队列。

我们看看如何镜像模式来解决复制的问题,从而提高可用性

9 设置镜像队列策略

#./rabbitmqctlset_policy ha-all "^" '{"ha-mode":"all"}'

### 将所有队列设置为镜像队列,即队列会被复制到各个节点,各个节点状态保持一直。