RabbitMQ 高级指南:从配置、使用到高可用集群搭建

博主说:在项目中,通过 RabbitMQ,咱们可以将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。

正文

3

1 RabbitMQ 简介

1.1 介绍

  RabbitMQ 是一个由 erlang 开发的基于 AMQP(Advanced Message Queue)协议的开源实现。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面都非常的优秀,是当前最主流的消息中间件之一。RabbitMQ 官网:http://www.rabbitmq.com

1.2 AMQP

  AMQP 是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,同样,消息使用者也不用知道发送者的存在。AMQP 的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。

1.3 系统架构

4

消息队列的使用过程大概如下:

  • 客户端连接到消息队列服务器,打开一个 channel;
  • 客户端声明一个 exchange,并设置相关属性;
  • 客户端声明一个 queue,并设置相关属性;
  • 客户端使用 routing key,在 exchange 和 queue 之间建立好绑定关系;
  • 客户端投递消息到 exchange,exchange 接收到消息后,就根据消息的 key 和已经设置的 binding,进行消息路由,将消息投递到一个或多个队列里。

如上图所示:AMQP 里主要说两个组件,Exchange 和 Queue。绿色的X就是 Exchange ,红色的是 Queue ,这两者都在 Server 端,又称作 Broker,这部分是 RabbitMQ 实现的,而蓝色的则是客户端,通常有 Producer 和 Consumer 两种类型。

1.4 几个概念

  • P: 为 Producer,数据的发送方;
  • C:为 Consumer,数据的接收方;
  • Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列;
  • Queue:消息队列载体,每个消息都会被投入到一个或多个队列;
  • Binding:绑定,它的作用就是把 exchange 和 queue 按照路由规则绑定起来;
  • Routing Key:路由关键字,exchange 根据这个关键字进行消息投递;
  • vhost:虚拟主机,一个 broker 里可以开设多个 vhost,用作不同用户的权限分离;
  • channel:消息通道,在客户端的每个连接里,可建立多个 channel,每个 channel 代表一个会话任务。

2 RabbitMQ 安装与配置

2.1 安装

RabbitMQ 是建立在强大的 Erlang OTP 平台上,因此安装 RabbitMQ 之前要先安装 Erlang.

注意

  • 现在先别装最新的 3.6.3 ,本人在安装完最新的版本,queue 队列有问题,降到 3.6.2 则问题解决。
  • 默认安装的RabbitMQ监听端口是:5672

2.2 配置

(1)安装完以后 erlang 需要手动设置ERLANG_HOME的系统变量。

输入:set ERLANG_HOME=C:\Program Files\erl8.0

1

(2)激活 RabbitMQ’s Management Plugin

使用 Rabbit MQ 管理插件,可以更好的可视化方式查看 Rabbit MQ 服务器实例的状态,咱们可以在命令行中使用下面的命令激活。

输入rabbitmq-plugins.bat enable rabbitmq_management

2

同时,也使用 rabbitmqctl 控制台命令(位于 rabbitmq_server-3.6.3\sbin>)来创建用户、密码、绑定权限等。

(3)创建管理用户

输入:rabbitmqctl.bat add_user zhangweizhong weizhong1988

4

(4)设置管理员

输入:rabbitmqctl.bat set_user_tags zhangweizhong administrator

5

(5)设置权限

输入:rabbitmqctl.bat set_permissions -p / zhangweizhong ".*" ".*" ".*"

6

(6)其它命令

  • 查询用户: rabbitmqctl.bat list_users
  • 查询 vhosts: rabbitmqctl.bat list_vhosts
  • 启动 RabbitMQ 服务: net stop RabbitMQ && net start RabbitMQ

以上这些,账号、vhost、权限、作用域等基本就设置完啦!

2.3 Rabbit MQ管理后台

使用浏览器打开http://localhost:15672访问 RabbitMQ 的管理控制台,使用刚才创建的账号登陆系统即可。RabbitMQ 管理后台,可以更好的可视化方式查看 RabbitMQ 服务器实例的状态。

7

2.4 创建 vhosts

创建 vhosts,在 admin 页面,点击右侧 Virtual Hosts:

8

将刚创建的 OrderQueue 分配给相关用户:

9

其它创建 exchange 、queue 大家自己在后台创建吧,这里不再赘述。

3 C# 如何使用 RabbitMQ

3.1 客户端

RabbitMQ.Client 是 RabbitMQ 官方提供的的客户端,net 版本地址 为:http://www.rabbitmq.com/dotnet.html

EasyNetQ 是基于 RabbitMQ.Client 基础上封装的开源客户端,使用非常方便,地址为:http://easynetq.com/

本篇使用示例代码下载地址:

demo 示例下载http://files.cnblogs.com/files/zhangweizhong/Weiz.RabbitMQ.RPC.rar

RabbitMQ 还有很多其它客户端 API,都非常好用。我们一直用的都是 EasyNetQ,所以这里的 demo 只介绍 EasyNetQ 客户端实现。

3.2 项目结构

10

说明:前面咱们提到过,RabbitMQ 由 Producer(生成者) 和 Consumer(消费者)两部分组成。Weiz.Consumer 就是 Consumer(消费者),Weiz. Producer 为 Producer(生成者),Weiz.MQ 为消息队列的通用处理类库。

3.3 项目搭建

(1)Weiz.MQ 项目,消息队列的通用处理类库,用于正在的订阅和发布消息。

  • 通过 nuget 安装项目 EasyNetQ 相关组件;
  • 增加 BusBuilder.cs 管道创建类,主要负责链接 RabbitMQ.
using System;
using System.Configuration;
using EasyNetQ;

namespace Weiz.MQ
{
    /// <summary>
    /// 消息服务器连接器
    /// </summary>
    public class BusBuilder
    {
        public static IBus CreateMessageBus()
        {
            // 消息服务器连接字符串
            // var connectionString = ConfigurationManager.ConnectionStrings["RabbitMQ"];
            string connString = "host=192.168.98.107:5672;virtualHost=OrderQueue;username=zhangweizhong;password=weizhong1988";
            if (connString == null || connString == string.Empty)
            {
                throw new Exception("messageserver connection string is missing or empty");
            }

            return RabbitHutch.CreateBus(connString);
        }
    }
}
  • 增加 IProcessMessage 类,定义了一个消息方法,用于消息传递
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Weiz.MQ
{
    public interface IProcessMessage
    {
        void ProcessMsg(Message msg);
    }
}
  • 增加 Message 类,定义了消息传递的实体属性字段等信息
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Weiz.MQ
{
    public class Message
    {
        public string MessageID { get; set; }

        public string MessageTitle { get; set; }

        public string MessageBody { get; set; }

        public string MessageRouter { get; set; }
    }
}
  • 增加 MQHelper 类,用于正在的订阅和发布消息
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;

using EasyNetQ;

namespace Weiz.MQ
{
    public class MQHelper
    {
        /// <summary>
        /// 发送消息
        /// </summary>
        public static void Publish(Message msg)
        {
            //// 创建消息bus
            IBus bus = BusBuilder.CreateMessageBus();

            try
            {
                bus.Publish(msg, x => x.WithTopic(msg.MessageRouter));
            }
            catch (EasyNetQException ex)
            {
                //处理连接消息服务器异常 
            }

            bus.Dispose();//与数据库connection类似,使用后记得销毁bus对象
        }

        /// <summary>
        /// 接收消息
        /// </summary>
        /// <param name="msg"></param>
        public static void Subscribe(Message msg, IProcessMessage ipro)
        {
            //// 创建消息bus
            IBus bus = BusBuilder.CreateMessageBus();

            try
            {
                bus.Subscribe<Message>(msg.MessageRouter, message => ipro.ProcessMsg(message), x => x.WithTopic(msg.MessageRouter));

            }
            catch (EasyNetQException ex)
            {
                //处理连接消息服务器异常 
            }
        }
    }
}

(2)RabbitMQ 由 Producer(生成者)

  • 创建一个 aspx 页面,增加如下代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using Weiz.MQ;

namespace Weiz.Producer
{
    public partial class TestMQ : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            Message msg = new Message();
            msg.MessageID = "1";
            msg.MessageBody = DateTime.Now.ToString();
            msg.MessageTitle = "1";
            msg.MessageRouter = "pcm.notice.zhangsan";
            MQHelper.Publish(msg);

        }
    }
}

(3)Weiz.Consumer 就是 Consumer(消费者)

  • 新增 OrderProcessMessage.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Weiz.Consumer
{
    public class OrderProcessMessage:MQ.IProcessMessage
    {
        public void ProcessMsg(MQ.Message msg)
        {
            Console.WriteLine(msg.MessageBody);
        }
    }
}
  • Program 增加如下代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Weiz.Consumer
{
    class Program
    {
        static void Main(string[] args)
        {
            OrderProcessMessage order = new OrderProcessMessage();
            MQ.Message msg = new MQ.Message();
            msg.MessageID = "1";
            msg.MessageRouter = "pcm.notice.zhangsan";

            MQ.MQHelper.Subscribe(msg, order);
        }
    }
}

3.4 运行

(1)启动 Weiz.Consumer(消费者),启动消费者,会自动在 RabbitMQ 服务器上创建相关的 exchange 和 queue.

11

Consumer 消费者,使用的是 Subscribe(订阅)的模式,所以,Weiz.Consumer 客户端启动后,会自动创建 connection,生成相关的 exchange 和 queue.

(2)启动 Weiz. Producer 里的 TestMQ.aspx 页面,往队列里面写一条消息,订阅的消费者立马就能拿到这条消息。

12

至此,C# 向 RabbitMQ 消息队列发送消息已经简单完成。

4 四种 Exchange 模式

  AMQP 协议中的核心思想就是:生产者和消费者隔离,生产者从不直接将消息发送给队列。生产者通常不知道是否一个消息会被发送到队列中,只是将消息发送到一个交换机。先由 Exchange 来接收,然后 Exchange 按照特定的策略转发到 Queue 进行存储。同理,消费者也是如此。Exchange 就类似于一个交换机,转发各个消息分发到相应的队列中。

  RabbitMQ 提供了四种 Exchange 模式:fanout、direct、topic、header. 由于 header 模式在实际使用中较少,因此本节只对前三种模式进行比较。

4.1 Fanout Exchange

13

所有发送到 Fanout Exchange 的消息都会被转发到与该 Exchange 绑定(Binding)的所有 Queue 上。

Fanout Exchange 不需要处理 RouteKey,只需要简单的将队列绑定到 exchange 上,这样发送到 exchange 的消息都会被转发到与该交换机绑定的所有队列上。类似子网广播,每台子网内的主机都获得了一份复制的消息。

所以,Fanout Exchange 转发消息是最快的。

/// <summary>
/// 生产者
/// </summary>
/// <param name="change"></param>
private static void ProducerMessage(MyMessage msg)
{
    var advancedBus = CreateAdvancedBus();

    if (advancedBus.IsConnected)
    {
        var exchange = advancedBus.ExchangeDeclare("user", ExchangeType.Fanout);

        advancedBus.Publish(exchange, "", false, new Message<MyMessage>(msg));
    } else {
         Console.WriteLine("Can't connect");
    }
}

/// <summary>
/// 消费者
/// </summary>
private static void ConsumeMessage()
{
    var advancedBus = CreateAdvancedBus();
    var exchange = advancedBus.ExchangeDeclare("user", ExchangeType.Fanout);

    var queue = advancedBus.QueueDeclare("user.notice.wangwu");
    advancedBus.Bind(exchange, queue, "user.notice.wangwu");
    advancedBus.Consume(queue, registration =>
    {
        registration.Add<MyMessage>((message, info) => { Console.WriteLine("Body: {0}", message.Body); });
     });
}

4.2 Direct Exchange

14

所有发送到 Direct Exchange 的消息被转发到 RouteKey 中指定的 Queue.

Direct 模式可以使用 RabbitMQ 自带的 Exchange:default Exchange ,因此不需要将 Exchange 进行任何绑定(binding)操作 。消息传递时,RouteKey 必须完全匹配,才会被队列接收,否则该消息会被抛弃。

/// <summary>
/// 生产者
/// </summary>
/// <param name="change"></param>
private static void ProducerMessage(MyMessage msg)
{
    var advancedBus = CreateAdvancedBus();

    if (advancedBus.IsConnected)
    {
        var queue = advancedBus.QueueDeclare("user.notice.zhangsan");

        advancedBus.Publish(Exchange.GetDefault(), queue.Name, false, new Message<MyMessage>(msg));
     } else {
          Console.WriteLine("Can't connect");
     }

}

/// <summary>
/// 消费者
/// </summary>
private static void ConsumeMessage()
{
    var advancedBus = CreateAdvancedBus();

    var exchange = advancedBus.ExchangeDeclare("user", ExchangeType.Direct);

    var queue = advancedBus.QueueDeclare("user.notice.lisi");

    advancedBus.Bind(exchange, queue, "user.notice.lisi");

    advancedBus.Consume(queue, registration =>
    {
         registration.Add<MyMessage>((message, info) =>
         {
             Console.WriteLine("Body: {0}", message.Body);
          });
     });
}

4.3 Topic Exchange

15

所有发送到 Topic Exchange 的消息被转发到所有关心 RouteKey 中指定 Topic 的 Queue 上,Exchange 将 RouteKey 和某 Topic 进行模糊匹配。此时队列需要绑定一个 Topic,可以使用通配符进行模糊匹配,符号#匹配一个或多个词,符号*匹配不多不少一个词。因此log.#能够匹配到log.info.oa,但是log.* 只会匹配到log.error.

所以,Topic Exchange 使用非常灵活。

/// <summary>
/// 生产者
/// </summary>
/// <param name="change"></param>
private static void ProducerMessage(MyMessage msg)
{
    //// 创建消息bus
    IBus bus = CreateBus();

    try {
        bus.Publish(msg, x => x.WithTopic(msg.MessageRouter));
    } catch (EasyNetQException ex) {
          //处理连接消息服务器异常 
    }

    bus.Dispose();//与数据库connection类似,使用后记得销毁bus对象
}

/// <summary>
/// 消费者
/// </summary>
private static void ConsumeMessage(MyMessage msg)
{
    //// 创建消息bus
    IBus bus = CreateBus();

    try {
        bus.Subscribe<MyMessage>(msg.MessageRouter, message => Console.WriteLine(msg.MessageBody), x => x.WithTopic("user.notice.#"));
     } catch (EasyNetQException ex) {
         //处理连接消息服务器异常 
     }
}

这个是 RabbitMQ 的实际使用的几个场景,熟悉了这个,基本上 RabbitMQ 也就了解啦!官方教程:http://www.rabbitmq.com/tutorials/tutorial-one-dotnet.html

至此,RabbitMQ 的几种 Exchange 模式已经介绍完了,实际使用过程中,咱们应该根据不同的场景,来使用不同的 exchange 模式。

5 RPC 远程过程调用

  在这一节中,主要讲述 RabbitMQ RPC. 其实,RabbitMQ RPC 就是通过消息队列(Message Queue)来实现 RPC 的功能,就是客户端向服务端发送定义好的 Queue 消息,其中携带的消息就应该是服务端将要调用的方法的参数 ,并使用 Propertis 告诉服务端将结果返回到指定的 Queue.

5.1 RabbitMQ RPC 的特点

  • Message Queue 把所有的请求消息存储起来,然后处理,和客户端解耦;
  • Message Queue 引入新的结点,系统的可靠性会受 Message Queue 结点的影响;
  • Message Queue 是异步单向的消息。发送消息设计成是不需要等待消息处理的完成。

所以对于有同步返回需求的,Message Queue 是个不错的方向。

5.2 普通 PRC 的特点

  • 同步调用,对于要等待返回结果/处理结果的场景,RPC 是可以非常自然直觉的使用方式,当然 RPC 也可以是异步调用;
  • 由于等待结果,客户端会有线程消耗。

如果以异步 RPC 的方式使用,客户端线程消耗可以去掉,但不能做到像消息一样暂存消息请求,压力会直接传导到服务端。

5.3 适用场合说明

  • 希望同步得到结果的场合,RPC 合适。
  • 希望使用简单,则 RPC;RPC 操作基于接口,使用简单,使用方式模拟本地调用。异步的方式编程比较复杂。
  • 不希望客户端受限于服务端的速度等,可以使用 Message Queue.

5.4 RabbitMQ RPC工作流程

16

基本概念

  Callback queue 回调队列,客户端向服务器发送请求,服务器端处理请求后,将其处理结果保存在一个存储体中。而客户端为了获得处理结果,那么客户在向服务器发送请求时,同时发送一个回调队列地址reply_to.

  Correlation id 关联标识,客户端可能会发送多个请求给服务器,当服务器处理完后,客户端无法辨别在回调队列中的响应具体和那个请求时对应的。为了处理这种情况,客户端在发送每个请求时,同时会附带一个独有correlation_id属性,这样客户端在回调队列中根据correlation_id字段的值就可以分辨此响应属于哪个请求。

流程说明

  • 当客户端启动的时候,它创建一个匿名独享的回调队列;
  • 在 RPC 请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的correlation_id属性;
  • 将请求发送到一个 rpc_queue 队列中;
  • 服务器等待请求发送到这个队列中来,当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给 reply_to 字段指定的队列;
  • 客户端等待回调队列里的数据。当有消息出现的时候,它会检查correlation_id属性,如果此属性的值与请求匹配,将它返回给应用。

5.5 完整代码

(1)创建两个控制台程序,作为 RPC Server 和 RPC Client,引用 RabbitMQ.Client

(2) RPC Server

class Program {
    static void Main(string[] args) {
        var factory = new ConnectionFactory() {
            HostName ="localhost",VirtualHost ="OrderQueue",UserName ="zhangweizhong",Password ="weizhong1988",Port =5672
        };
        using(var connection = factory.CreateConnection())
        using(var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue:"rpc_queue",
                durable:false,
                exclusive:false,
                autoDelete:false,
                arguments:null);
            channel.BasicQos(0, 1, false);
            var consumer = new QueueingBasicConsumer(channel);
            channel.BasicConsume(queue:"rpc_queue",
                noAck:false,
                consumer:consumer);
            Console.WriteLine(" [x] Awaiting RPC requests");

            while (true) {
                string response = null;
                var ea = (BasicDeliverEventArgs) consumer.Queue.Dequeue();

                var body = ea.Body;
                var props = ea.BasicProperties;
                var replyProps = channel.CreateBasicProperties();
                replyProps.CorrelationId = props.CorrelationId;

                try {
                    var message = Encoding.UTF8.GetString(body);
                    int n = int.Parse(message);
                    Console.WriteLine(" [.] fib({0})", message);
                    response = fib(n).ToString();
                } catch (Exception e) {
                    Console.WriteLine(" [.] " + e.Message);
                    response = "";
                } finally {
                    var responseBytes = Encoding.UTF8.GetBytes(response);
                    channel.BasicPublish(exchange:"",
                            routingKey:props.ReplyTo,
                            basicProperties:replyProps,
                            body:responseBytes);
                    channel.BasicAck(deliveryTag:ea.DeliveryTag,
                            multiple:false);
                }
            }
        }
    }

    /// <summary>
    /// Assumes only valid positive integer input.
    /// Don't expect this one to work for big numbers,
    /// and it's probably the slowest recursive implementation possible.
    /// </summary>
    private static int fib(int n) {
        if (n == 0 || n == 1) {
            return n;
        }

        Thread.Sleep(1000 * 10);

        return n;
    }
}

(3)RPC Client

class Program {
    static void Main(string[] args) {
        for (int i = 0; i < 10; i++) {
            Stopwatch watch = new Stopwatch();

            watch.Start();

            var rpcClient = new RPCClient();

            Console.WriteLine(string.Format(" [x] Requesting fib({0})", i));

            var response = rpcClient.Call(i.ToString());

            Console.WriteLine(" [.] Got '{0}'", response);

            rpcClient.Close();

            watch.Stop();

            Console.WriteLine(string.Format(" [x] Requesting complete {0} ,cost {1} ms", i, watch.Elapsed.TotalMilliseconds));
        }

        Console.WriteLine(" complete!!!! ");


        Console.ReadLine();
    }
}

class RPCClient {
    private IConnection connection;
    private IModel channel;
    private string replyQueueName;
    private QueueingBasicConsumer consumer;

    public RPCClient() {
        var factory = new ConnectionFactory() {
            HostName ="localhost",VirtualHost ="OrderQueue",UserName ="zhangweizhong",Password ="weizhong1988",Port =5672
        };
        connection = factory.CreateConnection();
        channel = connection.CreateModel();
        replyQueueName = channel.QueueDeclare().QueueName;
        consumer = new QueueingBasicConsumer(channel);
        channel.BasicConsume(queue:replyQueueName,
                noAck:true,
                consumer:consumer);
    }

    public string Call(string message) {
        var corrId = Guid.NewGuid().ToString();
        var props = channel.CreateBasicProperties();
        props.ReplyTo = replyQueueName;
        props.CorrelationId = corrId;

        var messageBytes = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish(exchange:"",
                routingKey:"rpc_queue",
                basicProperties:props,
                body:messageBytes);

        while (true) {
            var ea = (BasicDeliverEventArgs) consumer.Queue.Dequeue();
            if (ea.BasicProperties.CorrelationId == corrId) {
                return Encoding.UTF8.GetString(ea.Body);
            }
        }
    }

    public void Close() {
        connection.Close();
    }
}

(4)分别运行 Server 和 Client

5

6

5.6 官方教程及源码下载地址

  • 参照 RabbitMQ 官方教程的 RPC,地址:http://www.rabbitmq.com/tutorials/tutorial-six-dotnet.html
  • 本文源代码下载地址为:http://files.cnblogs.com/files/zhangweizhong/Weiz.RabbitMQ.RPC.rar

6 RabbitMQ 高可用集群

  RabbitMQ 是用 erlang 开发的,集群非常方便,因为 erlang 天生就是一门分布式语言,但其本身并不支持负载均衡。Rabbit 模式大概分为以下三种:单一模式、普通模式和镜像模式。

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

  没什么好说的。

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

  对于 Queue 来说,消息实体只存在于其中一个节点,A、B 两个节点仅有相同的元数据,即队列结构。当消息进入 A 节点的 Queue 中后,consumer 从 B 节点拉取时,RabbitMQ 会临时在 A、B 间进行消息传输,把 A 中的消息实体取出并经过 B 发送给 consumer. 所以 consumer 应尽量连接每一个节点,从中取消息。即对于同一个逻辑队列,要在多个节点建立物理 Queue。否则无论 consumer 连 A 或 B,出口总在 A,会产生瓶颈。该模式存在一个问题就是当 A 节点故障后,B 节点无法取到 A 节点中还未消费的消息实体。如果做了消息持久化,那么得等 A 节点恢复,然后才可被消费;如果没有持久化的话,然后就没有然后了……

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

  该模式解决了上述问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在 consumer 取数据时临时拉取。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉,所,在对可靠性要求较高的场合中适用。

6.1 集群中的基本概念

  RabbitMQ 的集群节点包括内存节点、磁盘节点。顾名思义内存节点就是将所有数据放在内存,磁盘节点将数据放在磁盘。不过,如前文所述,如果在投递消息时,打开了消息的持久化,那即使是内存节点,数据还是安全的放在磁盘。

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

集群中有两种节点

  • 内存节点:只保存状态到内存(一个例外的情况是:持久的 queue 的持久内容将被保存到 disk);
  • 磁盘节点:保存状态到内存和磁盘,内存节点虽然不写入磁盘,但是它执行比磁盘节点要好,集群中,只需要一个磁盘节点来保存状态 就足够了,如果集群中只有内存节点,那么不能停止它们,否则所有的状态,消息等都会丢失。

思路

  那么具体如何实现 RabbitMQ 高可用,咱们先搭建一个普通集群模式,在这个模式基础上再配置镜像模式实现高可用,Rabbit 集群前增加一个反向代理,生产者、消费者通过反向代理访问 RabbitMQ 集群。

架构图

7

上述图里是 3 个 RabbitMQ 运行在同一主机上,分别用不同的服务端口。当然咱们的实际生产中,多个 RabbitMQ 肯定是运行在不同的物理服务器上,否则就失去了高可用的意义。

6.2 集群模式配置

设计架构可以如下:在一个集群里,有 4 台机器,其中 1 台使用磁盘模式,另 2 台使用内存模式。2 台内存模式的节点,无疑速度更快,因此客户端(consumer、producer)连接访问它们。而磁盘模式的节点,由于磁盘 IO 相对较慢,因此仅作数据备份使用,另外一台作为反向代理。

四台服务器 hostname 分别为:queue 、panyuntao1、panyuntao2、panyuntao3(ip:172.16.3.110)

配置 RabbitMQ 集群非常简单,只需要几个命令,配置步骤如下:

Step 1:queue、panyuntao1、panyuntao2 做为 RabbitMQ 集群节点,分别安装 RabbitMq-Server ,安装后分别启动 RabbitMq-server。

启动命令# Rabbit-Server start,安装过程及启动命令参见:http://www.cnblogs.com/flat_peach/archive/2013/03/04/2943574.html

Step 2:在安装好的 3 台节点服务器中,分别修改/etc/hosts文件,指定queue、panyuntao1、panyuntao2 的 hosts,如

172.16.3.32 queue

172.16.3.107 panyuntao1

172.16.3.108 panyuntao2

还有 hostname 文件也要正确,分别是 queue、panyuntao1、panyuntao2,如果修改 hostname 建议在安装 rabbitmq 前修改。请注意 RabbitMQ 集群节点必须在同一个网段里,如果是跨广域网效果就差。

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

-r--------. 1 rabbitmq rabbitmq 20 35 00:00 /var/lib/rabbitmq/.erlang.cookie

将其中一台节点上的.erlang.cookie值复制下来保存到其他节点上,或者使用scp的方法也可,但是要注意文件的权限和属主属组。咱们这里将 queue 中的 cookie 复制到 panyuntao1、panyuntao2 中,先修改下 panyuntao1、panyuntao2 中的.erlang.cookie权限:

#chmod 777  /var/lib/rabbitmq/.erlang.cookie 

将 queue 的/var/lib/rabbitmq/.erlang.cookie这个文件,拷贝到 panyuntao1、panyuntao2 的同一位置(反过来亦可),该文件是集群节点进行通信的验证密钥,所有节点必须一致,拷完后重启下 RabbitMQ. 复制好后别忘记还原.erlang.cookie的权限,否则可能会遇到错误:

#chmod 400 /var/lib/rabbitmq/.erlang.cookie 

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

# rabbitmqctl stop
# rabbitmq-server start

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

queue# rabbitmqctl stop
panyuntao1# rabbitmqctl stop
panyuntao2# rabbitmqctl stop

queue# rabbitmq-server -detached
panyuntao1# rabbitmq-server -detached
panyuntao2# rabbitmq-server -detached

分别查看下每个节点:

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

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

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

Step 5:将 panyuntao1、panyuntao2 作为内存节点与 queue 连接起来,在 panyuntao1 上,执行如下命令

panyuntao1# rabbitmqctl stop_app
panyuntao1# rabbitmqctl join_cluster --ram rabbit@queue   
panyuntao1# rabbitmqctl start_app

panyuntao2# rabbitmqctl stop_app
panyuntao2# rabbitmqctl join_cluster --ram rabbit@queue
panyuntao2# rabbitmqctl start_app

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

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

 #rabbitmqctl join_cluster rabbit@queue 

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

Step 6:在 queue、panyuntao1、panyuntao2 上,运行cluster_status命令查看集群状态。

[root@queue ~]# rabbitmqctl cluster_status
Cluster status of node rabbit@queue ...
[{nodes,[{disc,[rabbit@queue]},{ram,[rabbit@panyuntao2,rabbit@panyuntao1]}]},
{running_nodes,[rabbit@panyuntao2,rabbit@panyuntao1,rabbit@queue]},
{partitions,[]}]
...done.

[root@panyuntao1 rabbitmq]# rabbitmqctl cluster_status
Cluster status of node rabbit@panyuntao1 ...
[{nodes,[{disc,[rabbit@queue]},{ram,[rabbit@panyuntao2,rabbit@panyuntao1]}]},
{running_nodes,[rabbit@panyuntao2,rabbit@queue,rabbit@panyuntao1]},
{partitions,[]}]
...done.

[root@panyuntao2 rabbitmq]# rabbitmqctl cluster_status
Cluster status of node rabbit@panyuntao2 ...
[{nodes,[{disc,[rabbit@queue]},{ram,[rabbit@panyuntao2,rabbit@panyuntao1]}]},
{running_nodes,[rabbit@panyuntao1,rabbit@queue,rabbit@panyuntao2]},
{partitions,[]}]
...done.

这时咱们可以看到每个节点的集群信息,分别有两个内存节点和一个磁盘节点。

Step 7:往任意一台集群节点里写入消息队列,会复制到另一个节点上,咱们会看到两个节点的消息队列数一致。

root@panyuntao2 :~# rabbitmqctl list_queues -p hrsystem

Listing queues …
test_queue 10000
…done.

root@panyuntao1 :~# rabbitmqctl list_queues -p hrsystem
Listing queues …
test_queue 10000
…done.

root@queue:~# rabbitmqctl list_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’t give you more storage capacity. For example, if one node could store 1GB of messages, adding two more nodes would simply give you two more copies of the same 1GB of messages.

  2. performance: Publishing messages would require replicating those messages to every cluster node. For durable messages that would require triggering disk activity on all nodes for every message. Your network and disk load would increase every time you added a node, keeping the performance of the cluster the same (or possibly worse).

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

6.3 镜像模式配置

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

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

Step 1:增加负载均衡器

关于负载均衡器,商业的比如 F5 的 BIG-IP,Radware 的 AppDirector 是硬件架构的产品,可以实现很高的处理能力。但这些产品昂贵的价格会让人止步,所以咱们还有软件负载均衡方案。互联网公司常用的软件 LB 一般有 LVS、HAProxy、Nginx 等。LVS 是一个内核层的产品,主要在第四层负责数据包转发,使用较复杂。HAProxy 和 Nginx 是应用层的产品,但 Nginx 主要用于处理 HTTP,所以这里选择 HAProxy 作为 RabbitMQ 前端的 LB.

HAProxy 的安装使用非常简单,在 CentOS 下直接yum install haproxy,然后更改/etc/haproxy/haproxy.cfg文件即可,文件内容大概如下:

13

负载均衡器会监听 5672 端口,轮询咱们的两个内存节点172.16.3.107172.16.3.108的 5672 端口,172.16.3.32为磁盘节点,只做备份不提供给生产者、消费者使用,当然如果我们服务器资源充足情况也可以配置多个磁盘节点,这样磁盘节点除了故障也不会影响,除非同时出故障。

Step 2:配置策略

使用 Rabbit 镜像功能,需要基于 RabbitMQ 策略来实现,政策是用来控制和修改群集范围的某个 vhost 队列行为和 Exchange 行为。在 cluster 中任意节点启用策略,策略会自动同步到集群节点:

# rabbitmqctl set_policy -p hrsystem ha-allqueue"^" '{"ha-mode":"all"}'

这行命令在 vhost 名称为 hrsystem 创建了一个策略,策略名称为 ha-allqueue,策略模式为 all 即复制到所有节点,包含新增节点,策略正则表达式为“^”表示所有匹配所有队列名称。例如:

rabbitmqctl set_policy -p hrsystem ha-allqueue "^message" '{"ha-mode":"all"}'

注意"^message"这个规则要根据自己修改,这个是指同步"message"开头的队列名称,咱们配置时使用的应用于所有队列,所以表达式为"^",官方set_policy说明参见:

set_policy [-p vhostpath] {name} {pattern} {definition} [priority]

ha-mode

14

step3

创建队列时需要指定ha参数,如果不指定x-ha-prolicy的话将无法复制。下面C#代码片段:

using ( var bus = RabbitHutch.CreateBus(ConfigurationManager .ConnectionStrings["RabbitMQ"].ToString())) {
    bus.Subscribe< TestMessage>("word_subscriber" , message => RunTable(message),x=>x.WithArgument("x-ha-policy" , "all"));
    Console.WriteLine("Subscription Started. Hit any key quit" );
    Console.ReadKey();
}

客户端使用负载服务器172.16.3.110(panyuntao3)发送消息,队列会被复制到所有节点,当然策略也可以配置制定某几个节点,这时任何节点故障 、或者重启将不会影响我们正常使用某个队列。到这里,咱们就完成了 RabbitMQ 的高可用配置(所有节点都宕机那没有办法了)。使用 RabbitMQ 管理端可以看到集群镜像模式中对列状态:

15


温馨提示:本文略有修改,如果大家对原文感兴趣的话,可以进入博客「章小鱼」阅读原文。


转载声明:本文转自微信公众账号「DBAplus社群」,RabbitMQ 高级指南:从配置、使用到高可用集群搭建

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值