RabbitMQ笔记

RabbitMQ 主要处理的问题

  • 限流(削峰)
  • 解耦
  • 异步

RabbitMq 常用的交换器类型

  • fanout (分列)
  • direct (直接)
  • topic (主题)

fanout 分列

它会把所有发送到该交换器的消息路由到所有与该交换器绑定的对垒

direct 直接

direct类型的交换器路由规则也很简单, 它会把消息路由到那些BindingKeyRoutingKey完全匹配的队列中

一个队列可有多个routeKey

direct类型的交换器路由规则是完全匹配BindingKeyRoutingKey

topic (主题)

前面讲到direct类型的交换器路由规则是完全匹配BindingKeyRoutingKey,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。topic类型的交换器在匹配规则上进行了扩展, 它与direct类型的交换器相似,也是将消息路由到BindingKeyRoutingKey想匹配的队列中,但这里的匹配规则有些不同。

它约定 :

  • RoutingKey 为一个点号"."分割的字符串。如"com.rabbitmq.client"、"java.util.concurrent"、"com.hidden.client";
  • BindingKey 也跟 RoutingKey一样也是点号 "."分割的字符串
  • BindingKey中可以存在两种特殊字符串"*""#",用于做模糊匹配,其中"#"用于匹配一个单词,"#" 用于匹配多规格单词(可以是零个)。

headers

headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。headers类型的交换器性能很差,而且也不使用,基本上不会看到它的存在。

Connection 和 Channel

Connection (连接)

Connection使用的是TCP协议

Channel (信道)

Channel使用的是AMQP协议,每个信道都会被分配一个唯一的ID,信道是建立在Connection之上的虚拟连接,RabbitMQ处理每条AMQP指令都是通过信道完成的。

AMQP协议

  • Module Layer:位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。例如,客户端可以使用Queue.Declare命令声明一个队列或者使用Basic.Consume订阅消费一个队列中的消息。
  • Session Layer:位于协议中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性的同步机制和错误处理。
  • Transport Layer:位于最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。

AMQP 说到底还是一个通信协议,通信协议都会涉及报文交互,从low-level举例来说 AMQP本身是应用层协议,其填充于TCP协议层的数据部分,而从high-level来说,AMQP是通过协议命令进行交互的。AMQP协议可以看作一系列结构化命令的集合,这里的命令代表一种操作,类似于HTTP中的方法(GET、POST、PUT、DELETE等)。

为了形象的说明AMQP协议命令的流转过程,请看以下代码

//连接RabbitMQ
conn, e :=amqp.Dail("amqp://localhost:5672")
if e != nil {
    log.Fatalf("Failed to connect to RebbitMQ: %s",e)
}
defer conn.Close()

//打开信道
ch, e := conn.Channel()
if e != nil {
    log.Fatalf("Failed to op channel: %s",e)
}
defer ch.Close()

//声明队列
q, e := ch.QueueDeclare("hello",false, false, false, false, nil)
if e != nil {
    log.Fatalf("Failed to declare a queue: %s",e)
}

message := "hello world"

e = ch.Publish(
    "",
    q.Name,
    false,
    false,
    amqp.Publishing{
        ContentType: "text/plain",
        Body: []byte(body),
    },
)
if e != nil {
    log.Fatalf("Failed to Publish Message: %s",e)
}

当客户端与Broker建立连接的时候,会调用amqp.Dail方法,这个方法进一步封装成Protocol Header 0-9-1的报文头 发送给Broker,以此通知Broker本次交互采用的是AMQP 0-9-1协议,紧接着Broker返回Connection.Start来建立连接,在连接的过程中涉及Connection.Start/.Start-OKConnection.Tune/Tune-OkConnectionOpen/.Open-OK这六个命令交互

实战指南

连接RabbitMQ

conn,e := amqp.Dail(
    "amqp://<user>:<password>@<IPAddress>:<port>[/virtualHost]")

一个Connection 可以用来创建多个Channel实例,但是Channel实例不能在线程间共享,应用程序应该为每一个线程开辟一个Channel。某些情况下Channel的操作可以并发运行,但是在其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响发送方确认机制的运行,所以多线程间共享Channel实例是非线程安全的。

打开信道

ch , e := conn.Channel()

ExChange(交换器)、Queue(队列)

ch, e := conn.Channel()
    failOrError(e, "Failed to open channel")
    defer ch.Close()

ch.ExchangeDeclare(
    "",     //交换器名称
    "" ,    //交换器类型
    false, //持久化
    false, //自动删除
    false , //内置
    false , //非等待
    nil)
//声明队列
q, e := ch.QueueDeclare(
    "hello", //队列名称
    false,   //持久化
    false,   //自动删除
    false,   //排他
    false,   //非等待
    nil,     //参数
)

ch.QueueBind(
    "queueName","routingKey","exchange",false,nil)

ch.ExchangeDecalre 声明交换器

参数说明 :

  • Name string 交换器名称
  • Kind string 交换器类别 [fanout、direct、topic、header、sys、自定义]
  • durable bool 持久化 持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
  • autoDelete bool 自动删除 如果设置为true表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。 !!注意:不要把这个参数理解为“当与此交换器连接的客户端都断开时,RabbitMQ会自动删除本交换器
  • internal bool 设置是否内置。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
  • noWait bool 不需要服务器返回任何值,[如果没有特殊缘由,不建议开启]
  • args Table->map[string]interface{} 参数比如alternate-exchange

ch.ExchangeDeclarePassive 被动声明交换器

参数同上

这个方法实际应用中还是非常有用的,他主要用来检测相应的交换器是否存在

ch.ExchangeDelete 删除交换器

参数说明 :

  • Name string 交换器名称
  • ifUnused bool 用来设置是否在交换器没有被使用的情况下删除。如果ifUnused设置为true,则只有在此交换器没有被使用的情况下才会被删除;如果设置为false,则无论如何这个交换器都要被删除
  • noWait bool 同上没有特殊缘由,不建议开启

ch.QueueDeclare 声明队列

  • Name string 队列名称
  • Durable bool 持久化
  • autoDelete bool 自动删除
  • exclusive bool 排他 ,设置为true则设置队列为排他的。如果一个队列被声明为排他队列,则该队列仅对首次声明它的连接课件, 并在连接断开时自动删除。这里需要注意三点:1.排他队列是基于连接(Connection)可见的,同一个连接的不同信道(Channel)是可以同时访问统一连接创建的排他队列的2.首次是指如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同3.即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景
  • noWait bool
  • args : map[string]interface{} 比如x-message-ttlx-expiresx-max-lengthx-max-length-bytesx-dead-letter-exchangex-dead-letter-routing-key、x-max-priority`等。

生产者和消费者都能够使用QueueDeclare 来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法在声明队列了。必须先取消订阅,然后将信道设置为“传输”模式,之后才能声明队列。

ch.queueBind 将队列与交换器绑定

  • name string 队列名称
  • key string RoutingKey
  • exchange string 交换器名称
  • noWait bool 不等待
  • args map[string]interface{}

ch.queueBind 将队列与交换器解除绑定

  • name string 队列名称
  • key string RoutingKey
  • exchange string 交换器名称
  • args map[string]interface{}

ch.exchangeBind 将交换器与另一个交换器绑定

  • destination string 目标交换器
  • key string RoutingKey
  • source string 源交换器
  • noWait bool
  • args map[string]interface{}

ch.exchangeBind 将交换器与另一个交换器解除绑定

  • destination string 目标交换器
  • key string RoutingKey
  • source string 源交换器
  • noWait bool
  • args map[string]interface{}

ch.publish 发布消息

  • exchange string 交换器
  • queuename string 队列名称
  • mandatory bool 强制
  • immediate bool 立即发送
  • Publishing Publishing 这个需要详细说明一下

Publishing 源代码

type Publishing struct {
    Header Table //map[string]interface{}

    ContentType     string      //消息类型MIME
    ContentEncoding string      //消息序列化
    Priority        uint8       //优先级
    CorrelationId   string      //关联ID | 相关标识符
    ReplyTo         string      //回复地址 (例如RPC)
    Expiration      string      //消息过期规则
    MessageId       string      //消息Id | 消息标识符
    Timstamp        time.Time   //时间戳
    Type            string      //消息类型名称
    UserId          string      //创建用户ID 例如"guest"
    AppId           string      //创建应用ID

    Body []byte
}

ch.Consume 消费信息 推模式 ,持续订阅的方式来消费信息

  • queue string 队列名称
  • consumer string 消费者标签,用来区分多个消费者
  • autoAck bool 自动确认应答,建议设为false
  • exclusive bool 排他
  • noLocal bool 非本地化,设置为true则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者;
  • noWait bool
  • args map[string]interface{}

todo 3.4.2 消费消息 拉模式 ,单条拉取

  • queue string 队列名称
  • aotuAck bool 自动确认应答,不建议开启

消息的何去何从

mandatory 参数 (强制)

mandatory参数设置为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参数设置为false,出现上述情况,则消息直接被丢弃。

那么生产者如何获得到没有被正确路由到合适队列的消息呢?这时候可以通过调用channel.AddReturnListener来添加ReturnListener监听器实现。 #todo GOlang客户端没有提供监听函数??

immediate 参数 (立即执行)

immediate设置为true时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic.Return返回至生产者

概括来说

mandatory 参数告诉服务器至少将该消息路由到一个队列中,否则将消息返回给生产者。 immediate 参数告诉服务器如果该消息关联的队列上有消费者,则立即投递;如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者,不用讲消息存入队列而等待消费者了。RabbitMQ 3.0 将去掉immediate 的支持!建议采用TTL 和DLX的方法替代。

备份交换器

备份交换器,英文名为Alternate EXchange,简称AE

生产者在发送消息的时候如果不设置mandatory,那么消息在未被路由的情况下将会丢失;如果设置了mandatory参数,那么需要添加ReturnListener的编程逻辑, 生产者的代码将变得负责。如果既不想负责话生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在RabbitMQ中,再在需要的时候去处理这些消息。

可以通过在声明交换器(调用channel.exchangeDeclare方法)的时候添加alternate-exchange参数来实现, 也可以通过策略(Policy)的方式实现。如果两者同时使用,则前者优先级更高,会覆盖Policy的设置

var args amqp.Table
args["alternate-exchange"] = "myAe"

ch.ExchangeDeclare(
    "",     //交换器名称
    "" ,    //交换器类型
    false, //持久化
    false, //自动删除
    false , //排他
    false , //非等到
    args)

ch.ExchangeDeclare(
    "myAe",     //交换器名称
    "" ,    //交换器类型
    false, //持久化
    false, //自动删除
    false , //排他
    false , //非等到
    args)

备份交换器其实和普通的交换器没太大的区别,为了方便实用,建议设置为fanout类型,如果想设置为direct或者topic的类型也没有什么不妥。只是需要注意的是,消息被重新发送到备份交换器时的路由键和从生产者发出的路由键是一样的。

考虑一种情况, 如果备份交换器的类型是direct,并且有一个与其绑定的队列,假设绑定的路由键是Key1,当某条携带路由键为key2的消息被转发到这个备份交换器的时候,备份交换器没有匹配到合适的队列,则消息将丢失。如果消息携带的路由键为key1,则可以存储到队列中。

对于交换器,总结以下几种特殊情况:

  • 如果设置的备份交换器不存在,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器没有绑定任何队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器没有任何匹配的队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器和mandatory参数一起使用,那么mandatory参数无效

过期时间 (TTL)

TTL,Time to Live的简称,即过期时间。RabbitMQ可以对消息和队列设置TTL。

设置消息的TTL

通过队列属性设置消息TTL的方法是在channel.queueDeclare方法中加入x-message-ttl参数实现的,这个参数的单位是毫秒。

var args amqp.Table
args["x-message-ttl"] = 60000

//声明队列
q, e := ch.QueueDeclare(
    "hello", //队列名称
    false,   //持久化
    false,   //自动删除
    false,   //互斥排斥 ?
    false,   //不等待
    args,     //参数
)

同时也可以通过Policy的方式来设置TTL

rabbitmqctl set_policy TTL ".*" '{"message-ttl":60000}' --apply-to queues

还可以通过调用HTTP API接口设置:

$ curl -i -u root:root -H "context-type:application/json" -X PUT \
-d '{"auto_delete":false,"durable":true,"arguments":{"x-message-ttl":60000}}' \
http://localhost:15672/api/queues/{vhost}/{queuename}

如果不设置TTL,则表示此消息不会过期;如果将TTL设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃,这个特性可以部分替代RabbitMQ 3.0版本之前的immediate参数,之所以部分代替,是因为immediate参数在投递失败是会用Basic.Return将消息返回(这个功能可以用死信队列来实现)

针对每条消息设置TTL的方法是在channel.BasicPulish方法中的Publishing中的Expiration属性设置的 #todo 以下代码并未验证

e = ch.Publish(
    "",
    q.Name,
    true,
    false,
    amqp.Publishing{
        ContentType: "text/plain",
        Body: []byte(body),
        Expiration:"60000",
    },
)

设置队列的TTL

通过channel.queueDeclare方法中的x-expires参数可以控制队列被自动删除前处于未使用状态的时间。未使用的意思是队列上没有任何消费者,队列也没有被重新声明,并且在过期时间段内也未调用过Baisc.Get命令。

设置队列里的TTL可以应用于类似RPC方式的回复队列,在RPC中,许多队列会被创建出来,但是却是未被使用。

RabbitMQ会确保在过期时间到达后将队列删除,但是不保障删除动作有多及时。在RabbitMQ重启后,持久化的队列的过期时间会被重新计算。

用于表示过期时间的x-expires参数以毫秒为单位,并且服从和x-message-ttl一样的约束条件,不过不能设置为0.比如该参数设置为1000,则表示该队列如果在1秒钟内未使用则被删除。

var args amqp.Table
args["x-message-ttl"] = 60000

//声明队列
q, e := ch.QueueDeclare(
    "hello", //队列名称
    false,   //持久化
    false,   //自动删除
    false,   //互斥排斥 ?
    false,   //不等待
    args,     //参数
)

死信队列

DLX,全称为 Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个消息队列中编程死信(dead message)之后,他能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列被称之为死信队列。

消息编程死信一般是由于以下几种情况:

  • 消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false;
  • 消息过期;
  • 队列达到最大长度;

DLX也是一个正常的交换器,和一般的交换器没有区别,他能在任何的队列上被指定,实际上就是设置某个队列属性。当这个队列中存在死信时,RabbitMQ就会自动的将这个消息重新发布到设置的DLX上去,进而被路由到另一个队里,即死信队列。可以监听这个队列中的消息以进行相应的处理,这个特性与将消息的TTL设置为0配合使用可以弥补immediate参数的功能

通过channel.queueDeclare方法中设置x-dead-letter-exchange参数来为这个队列添加DLX

queue, e := ch.QueueDeclare(
    "queue",
    true,
    false,
    false,
    false,
    map[string]interface{}{
        "x-dead-letter-exchange": "dlx_exchange",
    },
)

下面创建一个队列, 为期设置TTL、DLX等

    //connect RabbitMq
    conn, e := amqp.Dial("amqp://localhost:5672")
    if e != nil {
        log.Fatalf("Failed to connect RabbitMQ: %s", e)
    }
    defer conn.Close()

    // open channel
    ch, e := conn.Channel()
    if e != nil {
        log.Fatalf("Failed to open Channel: %s", e)
    }
    defer ch.Close()

    //声明一个死信交换器
    if e := ch.ExchangeDeclare(
        "exchange.dlx",
        "direct",
        true,
        false,
        false,
        false,
        map[string]interface{}{

        },
    ); e != nil {
        log.Fatalf("Failed to declare exchange.dlx: %s", e)
    }

    //声明一个正常交换器
    if e := ch.ExchangeDeclare(
        "exchange.normal",
        "fanout",
        true,
        false,
        false,
        false,
        map[string]interface{}{

        },
    ); e != nil {
        log.Fatalf("Failed to declare exchange.normal: %s", e)
    }

    //声明正常队列 并设置死信交换器,和超时时间
    _, e = ch.QueueDeclare(
        "queue.normal",
        true,
        false,
        false,
        false,
        map[string]interface{}{
            "x-message-ttl":             10000,
            "x-dead-letter-exchange":    "exchange.dlx",
            "x-dead-letter-routing-key": "routingkey",
        },
    )
    if e != nil {
        log.Fatalf("Failed to Declare Queue: %s", e)
    }
    //将正常的队列绑定到正常的交换器上
    e = ch.QueueBind("queue.normal", "", "exchange.normal", false, nil)
    if e != nil {
        log.Fatalf("Failed to Bind Queue: %s", e)
    }

    //声明死信队列
    _, e = ch.QueueDeclare("queue.dlx", true, false, false, false, nil)
    if e != nil {
        log.Fatalf("Failed to Declare Queue: %s", e)
    }
    //将死信队列绑定到死信交换器上
    e = ch.QueueBind("queue.dlx", "routingkey", "exchange.dlx", false, nil)
    if e != nil {
        log.Fatalf("Failed to Bind Queue: %s", e)
    }

    //发布消息
    _ = ch.Publish("exchange.normal", "rk", false, false, amqp.Publishing{
        ContentType: "text/plain",
        Body:        []byte("hello world"),
    })

这里创建了两个交换器exchange.normalexchange.dlx,分别绑定两个队列queue.normalqueue.dlx

延迟队列

延迟队列存储的对象是对应的延迟消息,所谓“延迟消息”是指当消息被发送以后,并不让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

延迟队列的使用场景有很多,例如

  • 在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延迟队列来处理这些订单。
  • 用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将用户指令发送到延迟队列,当指令设定的时间到了再将指令推送到智能设备。

AMQP协议中或者RabbitMQ本身没有直接支持延迟队列的功能,但是可以通过前面所介绍的DLX和TTL模拟出延迟队列的功能。

延迟队列的时间是通过DLX跟TTL实现的,只是最后消费的是DLX队列

在真实应用中,对于延迟队列可以根据延迟时间的长短分为多个等级,一般分为5秒、30秒、1分钟、5分钟、10分钟、30分钟、1小时这几个维度,当然也可以再细化一下。

优先级队列

优先级队列,具有高优先级的队列具有高的优先级,优先级高的消息具备优先被消费的特权。

可以通过设置队列x-max-priority参数来实现。例如设置优先级代码片段:

ch.QueueDeclare(
    "queue.normal",
    true,
    false,
    false,
    false,
    map[string]interface{}{
        //设置队列优先级
        "x-max-priority" : 10,
    },
)

以上代码演示了如何设置队列优先级,之后还需要在发送消息的时候设置消息的优先级。

ch.Publish("exchange", "", false, false, amqp.Publishing{
    ContentType: "text/plain",
    Body:        []byte("hello world"),
    //设置消息优先级为5
    Priority: 5,
})

上面的代码中设置了消息的优先级为5。默认优先级为0,最高为队列的最大优先级。优先级高的消息可以被优先消费到。这个其实也是有前提的:如果在消费者的消费速度大于生产者的速度且Broker没有消息堆积的情况下,对发送的消息设置优先级也就没有什么实际意义。因为生产者刚发送完一条消息就被消费者消费了,那么就相当于Broker中最多只有一条消息,对于单独一条消息来说优先级并没有什么意义

RPC实现

RPCRemote Procedure Call的简称,即远程过程调用。它是一种通过网络从远程计算机上请求服务,而不需要了解底层网络的技术。RPC的主要功能是让构建分布式计算更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。

通俗点来说,假设有两台服务器A和B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数或者方法,由于不在同一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

RPC的协议有很多,比如最早的CORBA、Java RMI、WebService的RPC风格、Hessian、Thrift、Restful API。

一般在RabbitMQ中进行RPC是很简单的。客户端发送请求消息,服务端响应消息。为了接受响应的消息,我们需要在请求消息中发送一个回调队列(Publich设置ReplyTo属性)如以下代码

#TODO 验证一下RPC的用法

_ = ch.Publish("exchange.normal", "rk", false, false, amqp.Publishing{
    ContentType: "text/plain",
    Body:        []byte("hello world"),
    Priority: 5,
    //设置回调队列名称
    ReplyTo: "callback.queue.Name",
    CorrelationId: "rpc_queue"
})

  • ReplyTo 通常用来设置一个回调队列名称
  • CorrelationId 用来关联请求(request)和其调用RPC之后的回复(response)

如果像上面的代码一样,为每个RPC请求创建一个回调队列,则是非常低效的。但是幸运的是这里有一个通用的解决方案 可以为每个客户端创建一个单一的回调队列

这样就产生了一个新新问题,对于回调队列而言,在其接受到一条消息之后,它并不知道这条消息应该和哪一个请求匹配。这里就用到correlationId这个属性了,我们应该为每个请求设置一个唯一的correlationId.之后在回调队列收到回复的消息时,可以根据这个属性匹配到相应的请求。如果回调队列接收到一条未知的correlationId的回复消息,可以简单的将其丢弃。

代码请查询 官方GitHub RPC代码示例 https://github.com/rabbitmq/rabbitmq-tutorials/tree/master/go

持久化

持久化这个词汇在前面的篇幅中有多次提及,持久化可以提高RabbitMQ的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。本届针对这个概念做一个总结。RabbitMQ的持久化分为三个部分:

  • 交换器持久化:交换器持久化是通过在声明队列时将durable参数设置为true实现的。如果交换器不设置持久化,那么在RabbitMQ服务重启之后,相关的交换器元数据将会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器中。对一个长期使用的交换器来说建议将其设置为持久化
  • 队列持久化:队列持久化是通过在声明队列时将durable参数设置为true实现的,如果队列不设置持久化,那么在RabbitMQ服务重启之后,相关队列的元数据会丢失,此时数据也会丢失。队列没了,消息也就没了。队列的持久化能保证其本身的元数据不会因为异常情况而丢失,但是并不能保证内部所存储的小七不会丢失。要确保消息不会丢失,需要将消息也设置为持久化。
  • 消息持久化:消息的持久化是通过将消息投递模式设置为2实现的

设置了队列和消息的持久化,当RabbitMQ服务重启之后,消息以后存在。单单只设置队列持久化,重启之后消息会丢失;单单设置消息持久化,重启后队列消失,相继的消息也会丢失。单单设置消息持久化而不设置队列持久化显得毫无意义。队列和消息的持久化应该是成对出现的,至少消息持久化必须在队列持久化的基础上。

可以将所有的消息都设置为持久化,但是这样会严重影响RabbitMQ的性能(随机)。写入磁盘的数据比写入内存速度慢的不只一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吞吐量之间做一个权衡。

事物机制

RabbitMQ 客户端中与事务机制相关的方法有三个: `channel.txSelect、channel.txCommit、channel.txRollback。

  • channel.txSelect: 用于将当前的信道设置成事务模式
  • channel.txCommit:用于提交事务
  • channel.txRollback:用于事务回滚

消息的传输保障

在一般的消息中间件中传输保障分为三个层级:

  • At most once:最多一次。消息可能会丢失,但绝不会重复传输
  • At least once: 最少一次。消息绝不会丢失,但可能会重复传输
  • Exactly once: 恰好一次。每条消息肯定会被传输一次且仅传输一次

最少一次 投递实现需要考虑以下几个方面

  1. 消息生产者需要开启事务机制或者消息确认机制,以确保消息可以可靠的传递到RabbitMQ中。
  2. 消息生产者需要配合使用mandatory参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能够保存下来而不会被丢弃。
  3. 消息和队列都需要进行持久化,以确保RabbitMQ服务器在遇到异常情况时不会造成消息丢失。
  4. 消费者在消费消息的同时需要将autoAck设置为false,然后通过手动确认方式去确认已经正确消费的消息,一面在消费端引起不必要的消息丢失

最多一次

生产者随意发送,消费者随意消费,不过这样很难确保消息不会丢失

RabbitMQ管理

建立RabbitMQ 虚拟机

$ rabbitmqctl add_vhost {vhost}

注:RabbitMQ的虚拟机是完全隔离的。即vhost1上的交换器无法绑定vhost2上的任何交换器、队列

查看虚拟机列表

$ rabbitmqctl list_vhosts [vhostinfoitem...]

删除虚拟机

$ rabbitmqctl delete_vhost {vhost}

授权

$ rabbitmqctl set_permissions [-p vhost] {username} {conf} {write} {read}

  • vhost:授予用户访问权限的vhost名称 默认的vhost为"/"
  • user:用户名
  • conf:一个用于匹配用户在哪些资源上拥有可配置权限的正则表达式
  • write:一个用于匹配用户在哪些资源上拥有可写权限的正则表达式
  • read:一个用于匹配用户在哪些资源上拥有可读权限的正则表达式

可配置是指创建、删除队列或者交换器之类的操作;可写是指发布消息;可读是指与消息相关的操作,包括读取消息、清空整个队列等。

AMQP协议中并没有指定权限在vhost级别还是在服务器级别实现,由具体的应用自定义。 权限控制则以vhost为单位的。当创建一个用户时,用户通常会被指派给至少一个vhost,并且只能访问被指派的vhost内的队列、交换器和绑定关系。因此RabbitMQ中的授权是指vhost级别对用户而言的权限授予。

清楚权限

$ rabbitmqctl clear permissions [-p vhost] {username}

查看权限列表

$ rabbitmqctl list_permissions [-p vhost|username]

rabbitmqctl工具的标准语法如下

$ rabbitmqctl [-n node] [-t timeout] [-q] {command} [command options...]

  • [-n node] 默认节点是“rabbit@hostname”,此处的hostname是主机名称。在一个名为“node.hidden.com”的主机上,RabbitMQ节点的名称通常是 rabbit@node(除非RABBIT_NODENAME参数在启动时被设置成非默认值。)hostname -s命令输出的通常是“@”标志后的东西。
  • [-q] 使用-q标志来启动quiet模式,这样可以屏蔽一些消息的输出。默认不开启
  • [-t timeout] 操作超时时间(单位:秒),只能用于"list_xxx"类型的命令。默认是不限制

用户管理

添加用户

$ rabbitmqctl add_user {username} {password}

修改用户密码

$ rabbitmqctl change_password {username} {poassword}

清理用户密码,用户可以不使用密码登录

$ rabbitmqctl clear_password {username}

通过密码来验证用户

$ rabbitmqctl authenticate_user {username} {password}

删除用户

$ rabbitmqctl delete_user {username}

查看用户列表

$ rabbitmqctl list_users

设置用户角色

$ rabbitmqctl set_user_tags {username} {tag...}

用户角色分为5种类型

  • none:无任何角色。新创建的用户默认为none
  • management:可以访问Web管理页面。
  • policymaker:包含management的所有权限,并且可以管理策略(policy)和参数(Parmeter)。
  • monitoring:包含management的所有权限,并且可以看到所有连接、信道、节点相关信息
  • administrator:包含moitoring,并且可以管理用户、虚拟机、权限、策略、参数等。是最高权限角色

插件

查看插件使用情况

$ rabbitmq-plugins list

应用与集群管理

应用管理

停止虚拟机、RabbitMQ应用

$ rabbitmqctl stop [pid_file]

  • pid_file: 默认情况下生成在Mnesia目录中,可以通过RABBIT_PID_FILE这个环境变量来改变存放路径。注意如果使用rabbitmq-server -detach 来启动服务就不会生成pid_file

不通过pid_file,阻塞等待指定进程的关闭,(发送指令等它自己关闭就行了)

$ rabbitmqctl shutdown

停止服务应用

$ rabbitmqctl stop_app

启动服务应用

$ rabbitmqctl start_app

等待应用的启动

$ rabbitmqctl wait {pid_file}

重置还原到最初状态

$ rabbitmqctl reset

这个命令会从原来所在的集群中删除此节点,从管理数据库中删除所有配置数据,如已配置的用户、vhost等,以及删除所有持久化消息。执行此命令前必须停止RabbitMQ应用(比如先执行 rabbitmqctl stop_app)

强制重置

$ rabbitmqctl force_reset

转换日志文件

$ rabbitmqctl rotate_logs {suffix}

?? HiPE 编译 TODO

$ rabbitmqctl hipe_compile {directory}

集群管理

将节点加入指定集群中

$ rabbitmqctl join_cluster {cluster} [--ram]

显示集群的状态

$ rabbitmqctl cluster_status

修改集群节点的类型

$ rabbitmq change_cluster_node_type {disc|ram}

删除集群节点

$ rabbitmq forget_cluster_node [--offline]

节点启动前咨询clusternode节点的最新信息,并更新响应的集群信息

$ rabbitmqctl update_cluster_nodes {clusternode}

确保节点可以启动(无条件启动节点)

$ rabbitmqctl force_boot

当集群遇到突然断电,所有节点都不认为自己是最后一个关闭的节点的时候可以调用rabbitmqctl force_boot 告诉节点可以无条件启动。在此节点关闭后,集群的任何变化,他都会丢失。如果最后一个关闭的节点永久丢失了,那么需要优先使用rabbitmqctl forget_cluster_node --offline命令,因为它可以确保镜像的正常运转

队列同步镜像

$ rabbitmqctl sync_queue [-p vhost] {queue}

指示未同步队列queue的slave镜像可以同步master镜像行的内容。同步期间次队列会被阻塞(所有次队列的生产者、消费者都会被阻塞),直到同步完成。词条命令执行成功的前提是队列queue配置了镜像。注意未同步队列中的消息被耗尽后,最后也会变成同步,此命令主要用于未耗尽的队列

取消队列同步镜像

$ rabbitmqctl cancel_sync_queue [-p host] {queue}

设置集群名称

$ rabbitmqctl set_cluster_name {name}

服务端状态

队列详细信息

$ rabbitmqctl list_queues [-p vhost] [queueinfoitem ...]

queueinfoitem 的值:

  • name:队列名称。
  • durable:队列是否持久化
  • auto_delete:队列是否自动删除
  • arguments:队列的参数
  • policy:应用到队列上的策略名称
  • pid:队列关联的Erlang进程的ID
  • owner_pid:处理排他队列连接的Erlang进程ID。如果此队列是非排他的,此值将为空。
  • exclusive:队列是否是排他的
  • exclusive_consumer_pid:订阅到此排他队列的消费者相关的信道关联erlang进程ID。如果此队列是非排他的,此值将为空。
  • exclusive_consumer_tag:订阅到此排他队列的消费者的consumerTag。如果此队列是非排他的,此值为空
  • messages_ready:准备发送给客户端的消息个数
  • messages_unacknowledged:发送给客户端但尚未应答的消息个数
  • messages:准备发送给客户端和为应答消息的总和。
  • messages_ready_ram:驻留在内存中的message_ready的个数。
  • messages_unacknowledged_ram:驻留在内存中messages_unacknowledged的消息个数。
  • messages_ram:驻留在内存中的消息总数。
  • messages_persistent:队列中持久化消息的个数。对于非持久队列来说总是0
  • messages_bytes:队列中所有消息的大小总和。这里不包括消息属性或者任何其他开销。
  • messages_bytes_ready:准备发送给客户端的消息的大小总和。
  • messages_bytes_unacknowledged:发送给客户端但尚未应答的消息的大小总和
  • messages_bytes_ram:驻留在内存的 messages_bytes。
  • messages_bytes_persistent:队列中持久化的messages_bytes。
  • disk_reads:从队列启动开始,已从磁盘中读取该队列的消息总次数。
  • disk_writes:从队列启动开始,已向磁盘队列写消息的总次数。
  • consumer:消费者数量。
  • consumer_utilisation:队列中的消息能够立刻投递给消费者的比率,介于0和1之间。这个受网络拥塞或basic.Qos的影响而小于1。
  • memory:与队列相关的Erlang进程小号的字节数,包括堆、栈、内部结构。
  • slave_pids:如果队列是镜像的,列出所有slave镜像的pid。
  • synchronised_slave_pids:如果队列是镜像的,列出所有已经同步的slave镜像的pid
  • state:队列状态。正常情况下是running;如果队列正常同步数据可能会有“{syncing,MsgCount}”的状态;如果队列在所在节点掉线了,则队列显示状态为 down(此时大多数的queueinfoitems也将不可用)

交换器详细信息

$ rabbitmqctl list_exchanges [-p host] [exchangeinfoitem...]

绑定关系细节

$ rabbitmqctl list_bindings [-p host] [bindinginfoitem...]

TCP/IP连接的统计信息

$ rabbitmqctl list_connections [connenctionsinfoitem...]

查看当前所有信道信息

$ rabbitmqctl list_channels [channelinfoitem...]

列举消费者信息

$ rabbitmqctl list_consumers [-p vhost]

显示Broker的状态

$ rabbitmqctl status

查看节点健康状态

$ rabbitmqctl node_health_check

显示每个运行程序环境中的环境变量

$ rabbitmqctl environment

为所有服务器状态生成一个服务器状态报告,并输入到一个文件内

$ rabbitmqctl report > report.txt

执行任意Erlang表达式

$ rabbitmqctl eval 'node().'

RabbitMQ配置

一般情况下,可以使用内建配置来有效的运行RabbitMQ,并且大多数情况下并不需要修改任何RabbitMQ的配置。当然为了更加有效的操作RabbitMQ,也可以利用调节系统范围内的参数来达到定制化需求。

RabbitMQ提供三种方式来定制化服务:

  • 环境变量:RabbitMQ服务端参数可以通过环境变量进行配置,例如节点名称、RabbitMQ配置文件的地址、加点内部通信端口等。
  • 配置文件:可以定义RabbitMQ服务和插件设置,例如TCP监听端口,以及其他网络相关的设置、内存限制、磁盘限制等。
  • 运行时参数和策略:可以在运行时定义集群层面的服务设置。

环境变量

RabbitMQ的环境变量都是以 RABBITMQ_开头的,可以在Shell环境中配置,也可以也在rabbitmq-env.conf这个环境变量定义的文件中设置

设置文件说明:

# Rabbit环境变量定义文件
# 定义节点名称
NODENAME=rabbit@node1 
# 对外开放的端口
NODE_PORT=5672
# 定义RABBITMQ配置文件目录
CONFIG_FILE=xxx

RABBITMQ_NODE_IP_ADDRESS

绑定某个特定的网络接口。默认值是空字符串,即绑定到所有网络接口上。如果要绑定两个甚至更多网络接口,需要配置tcp_listeners使用

RABBITMQ_NODE_PORT

监听客户端连接的端口

RABBITMQ_DIST_PORT

RabbitMQ节点内部通信的端口,默认为RABBITMQ_NODE_PORT+20000。如果设置了kernel.inet_dist_listen_min或者kernel.inect_dist_listen_max时此环境变量忽略

RABBITMQ_NODENAME

节点名称。默认为rabbit@$HOSTNAME。在每个Erlang节点和机器组合时,节点名称必须唯一

RABBITMQ_CONF_ENV_FILE

环境变量的配置文件的地址

RABBITMQ_USE_LONGNAME

如果当前hostname为node1.longname那么默认情况下创建的节点名称为rabbit@node1,将此参数设置为true时,创建的节点名称为rabbit@node1.longname

RABBITMQ_MNESIA_BASE

RABBITMQ_MNESIA_BASE 的父沐浴露。除非明确设置了RABBITMQ_MNESIA_BASE目录,柔则每个节点都应该设置这个环境变量,默认值为$RABBITMQ_HOME/var/lib/rabbitmq/mnesia注意对于RabbitMQ的操作用户来说,需要对当前目录有可以读写创建文件的权限

RABBITMQ_MNESIA_DIR

包含RabbitMQ服务节点的数据库、数据存储以及集群状态等目录,默认值为RABBITMQ_MNESIA_BASE/RABBITMQ_NODENAME

RABBITMQ_LOG_BASE

RabbitMQ服务日志所在基础目录

RABBITMQ_LOGS

RabbitMQ与Erlang相关的日志 默认为:RABBITMQ_LOG_BASE/$RABBITMQ_NODENAME.log

RABBITMQ_SASL_LOGS

RabbitMQ服务于Erlang的SASL(system application support libraries)相关的日志。 默认值为:BABBITMQ_LOG_BASE/RABBITMQ_NODENAME-sasl.log

RABBITMQ_PLUGINS_DIR

插件所在目录

注意:没有特殊需求,不建议修改RabbitMQ的环境变量。如果在实际生产环境中,对配置和日志的目录有着特殊的管理目录那么可以参考以下配置:

#配置文件的地址
CONFIG_FILE=/app/conf/rabbitmq/rabbitmq
#环境变量的配置文件地址
CONF_ENV_FILE=/app/conf/rabbitmq/rabbitmq-env.conf
#服务日志的地址
LOG_BASE=/app/logs/rabbitmq
#Mnesia的路径
MNESIA_BASE=/app/dbdat/rabbitmq/mnesia

配置文件

tcp_listeners 用来监听AMQP连接。

可以配置为端口号或者主机+端口号二元组

[
    {
        rabbit,
        [
            {
                tcp_listeners,[{"192.168.0.2",5672}]
            }
        ]
    }
]

num_tcp_acceptors

用来处理TCP连接的Erlang进程数目,默认为10

handshake_timeout

AMQP 0-8/0-9/0-9-1 握手(在socker连接和ssl握手之后)的超时时间,单位为毫秒。默认值为10000

ssl_listeners

同tcp_listener,用于ssl连接。默认值为[]

num_ssl_acceptors

用来处理SSL连接的Erlang进程数目,默认值为1

ssl_options

SSL配置。默认值为[]

ssl_handshake_timeout

SSL的握手超时时间。默认值为5000

vm_memory_high_watermark

触发流量控制的内存阈值。默认值为0.4

vm_memory_calculation_strategy

内存的报告方式。一共两种

  • rss:采用操作系统的RSS的内存报告
  • erlang:采用Erlang的内存报告 默认为rss

vm_memory_high_watermark_paging_ratio

内存高水位的百分比阈值,当达到阈值时,队列开始将消息持久化到磁盘以释放内存。这个需要配合vm_memory_high_watermark这个参数一起使用。默认值为0.5

disk_free_limit

RabbitMQ存储数据分区的可用磁盘空间限制。当可用空间低于阈值时,流程控制将被触发。辞职可以根据RAM的相对大小来设置(如{mem_ralative,1.0})。此值也可以设置为正数(单位B),或者使用数字+单位(如“50MB”)。默认情况下可用磁盘空间必须超过50MB。默认值为50000000

log_levels

控制日志的粒度。该值为日志时间类别(category)和日志级别(level)的而援助列表 目前定义了4种日志类别

  • channel:所有与AMQP信道相关的日志
  • connection:所有与连接相关的日志
  • faderation:所有与federation相关的日志
  • mirroring:所有与镜像相关的日志 其他未分类的日志也会被记录下来

日志级别有5种

  • none 不会记录日志事件
  • error 只记录错误
  • warning 记录错误和警告
  • info 记录错误、警告和消息
  • debug 记录错误、警告、消息和调试信息 默认值为[{connenction,info}]

frame_max

与客户端协商的允许最大帧大小,单位为B。设置为0表示无限制,在某些QPID客户端会引发BUG。设置较大的值可以提高吞吐量;设置一个较小的值可能会提高延迟。默认值为131072

channel_max

与客户端协商允许最大的信道数。设置为0表示无限制。该值越大则broker的内存使用越高。默认值为0

channel_operation_timeout

信道运行的超时时间,单位为毫秒(内部使用,因为消息协议的区别和现在不暴露给客户端)默认为15000

heartbeat

服务器和客户端连接的心跳延迟,单位为秒。如果设置为0表示禁用心跳。在有大量连接的情况下,禁用心跳可以提高性能,但可能会导致一些异常。默认值为60。3.5.5版本之前为580

default_vhost

设置默认的vhost。交换器amq.rabbitmq.log就在这个vhost上默认值为/

default_user

设置默认的用户。

default_pass

设置默认的密码。

default_user_tags

设置默认的用户角色

default_permissions

设置默认的权限

loopback_users

设置智能通过本地网络(如localhost)来访问Broker用户列表。如果希望通过默认的guest用户能够通过远程网络访问Broker,那么需要将这个项设置为[]。默认值为“guest”

cluster_nodes

用来配置集群。这个值是一个二元组,二元组的第一个匀速是想要与其建立集群关系的节点,第二个元素是节点的类型,要么是disc,要么是ram。默认值为{[],disc}

server_properties

连接时向客户端生命的键值对列表。默认值为[]

collect_statistics

统计数据的收集模式,主要与RabbitMQ Management插件相关,共有3个值可选:

  • none:不发布统计事件
  • coarse:发布每个队列、信道、连接的统计事件
  • fine:同时还发布每个消息的统计事件 默认值为none

collect_statistics_intervel

统计数据的收集时间间隔,主要与RabbitMQ Management插件相关。默认值为5000

management_db_cache_multiplier

设置管理插件缓存代价较高的查询的查询的时间。缓存将把最后一个查询的运行时间乘以这个值,并在此时间内缓存结果。默认为5

delegate_count

内部集群通信中,委派进程的数目。在拥有很多个内核并且是集群中的一个节点机器上可以增加此值。默认值为16

tcp_listen_options

默认的socket选项。 默认值为

[
    {backlog,128},
    {nodelay,true},
    {linger,{true,0}},
    {exit_on_close,false}
]

hipe_compolie

将此配置项设置为true就可以开启HiPE功能,即Erlang的即时编译器。虽然在启动时会增加延迟,但能够有20%~50%的性能提升,当然这个数字高度依赖负载和机器配置。在你的Erlang安装包中可能没有包含HiPE的支持,如果没有,则在开启这一项,并且在RabbitMQ启动时会有相应的警告信息。HiPE并非在所有平台都可以,尤其是Windows操作系统。在Erlang/OTP17.5版本之前,HiPE有明显的问题。如果要使用HiPE推荐使用最新版的Erlang/OTP版本。默认值为false

cluster_partition_handling

如何处理网络分区。有4种取值:

  • ignore
  • pause_minority
  • {pause_if_all_down,[nodes],ignore|autoheal}
  • autoheal 默认值为false

cluster_keepalive_interval

向其他节点发存活消息的频率。单位为毫秒。这个参数和net_ticktime参数不同, 丢失存活消息并不会导致节点被认为失效。默认值为10000

queue_index_embed_msgs_below

消息的大小小于此值时会直接嵌入队列的索引中。单位为B。默认值为4096

msg_store_index_module

队列索引的实现模块。 默认值为rabbit_msg_store_ets_index

backing_queue_module

队列内容的实现模块。默认值为rabbit_variable_queue。

不建议修改此项

mnesia_table_loading_retry_limit

等待集群中Mnesia数据表可用时最大的重试次数。默认值为10

mnesia_table_loading_retry_timeout

每次重试时,等待集群中Mnesia数据表可用时的超时时间默认值为30000

queue_master_locator

队列的定位策略,即创建队列时以什么策略判断坐落的Broker节点。如果配置了镜像,则这里指master镜像定位策略 可用策略有:

  • min-masters
  • client-local
  • random 默认值为 client-local

lazy_queue_explicit_gc_run_operation_threshold

在使用惰性队列(lazy_queue)时进行内存回收动作的阈值。一个低的值会降低性能,一个高的值可以提高性能,但是会导致更高的内存消耗。默认值为1000

queue_explicit_gc_run_operation_threshold

在使用正常队列时进行内存回收动作的阈值,一个低的值会降低性能,一个高的值可以提高性能,但是会导致更高的内存消耗。默认值为1000

rabbitMQ 配置加密

配置文件中有一些敏感的配置项可以被加密,然后在RabbitMQ启动时可以对这些项进行解密。对这些项进行加密并不意味着系统的安全性增强了,而是遵循一些必要的规范,让一些敏感的数据不会出现在文本形式的配置文件中。在配置文件中将加密之后的值以{encrypted,加密的值}的形式包裹,比如以下实例中使用口令“zzhpasssphrase”将密码“guest”加密。

[
    {
        rabbit, [
            {default_user, <<"guest">>},
            {
                {
                    encrypted,<<"xxxcode">>
                }
            },
            {loopback_users,[]},
            {
                config_entry_decoder,[
                    {
                        passphrase,<<"zzhpassphrase">>
                    }
                ]
            }

        ]
    }
]

优化网络配置

优化网络配置的一个重要目标就是提高吞吐量,比如禁用Nagle算法、增大TCP缓存区的大小。每个TCP连接都分配了缓冲区。一般来说说,缓冲区越大,吞吐量也会越高,但每个连接上消耗的内存也就越多,从而使整体服务的内存增大,这是一个权衡的问题。在linux操作系统中,默认会自动调节TCP缓冲区的大小,通常会设置为80KB到120KB之间。要提高吞吐量可以使用rabbit.tcp_listen_options来加大配置。下面的示例中将TCP缓冲区大小设置为192KB:

[
    {
        rabbit,[
            {
                tcp_listen_options,[
                    {backlog,   128},
                    {nodelay,   true},
                    {linger,    {true,0}},
                    {exit_on_close, false},
                    {sndbuf,    196608},
                    {recbuf,    196608},
                ]
            }
        ]
    }
]

Erlang在运行时使用线程池来异步执行 I/O操作。线程池的大小可以通过`RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS这个环境变量来调节。 示例:

RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS="A 128"

目前3.6.x版本默认值为128。当机器内核个数大于等于8时,建议将此值设置为大于等于96,这样可以确保每个内核上运行大于等于12个I/O线程。注意这个值并不是越高越能提高吞吐量

大部分操作系统都受限制与同一时间可以打开的文件句柄数。在优化并发连接数的时候,需要确保系统有足够的文件句柄数来支撑客户端和Broker的交互。可以用每个节点上连接数目乘以1.5来粗略的估计限制。例如,要支撑10万个TCP连接,需要设置句柄数为15万。当然略微增加文件句柄数可以增加空闲机器内存的使用量,这个需要合理的权衡。

如上所述,增大TCP缓冲区的大小可以提高吞吐量,如果减小TCP缓冲区的大小,这样就可以较小每个连接上的内存使用量。如果并发量比吞吐量更重要,可以修改此值

前面提到禁用Nagle算法可以提高吞吐量,但是其主要还是用于减少延迟。RabbitMQ内部节点交互可以在kernel.inet_default_connect_optionskernel.inet_default_listen_options配置项中配置{nodelay,true}来禁用Nagle算法。rabbit.tcp_listen_options也需要包含同样的配置,并且默认都是这样的配置。

示例:

[
    {kernel , [
        {inet_default_connect_options, [{nodelay, true}]},
        {inet_default_listen_options, [{nodelay,true}]}
    ]},
    {rabbit, [
        {tcp_listen_options,[
            {backlog,   4096},
            {nodelay,   true},
            {linger,    {true,0}},
            {exit_on_close, false}
        ]}
    ]}
]

当优化并发连接数时,恰当的Erlang虚拟机的I/O线程池的大小也很重要,具体可以参考前面的内容

当只有少量的客户端时,新建立的连接分布式非常不均匀的,但是由于数量足够小,所以没太大的差异。当连接数量达到数万或者更高的时候,重要的是确保服务器能够接受入站连接。未接受的TCP连接会放在有长度限制的队列中。这个通过rabbit.tcp_listen_options.backlog参数来设置,详细内容可以参考前一个示例。默认值为128,当挂起的连接队列长度超过此值时,连接将被操作系统拒绝。 以下为TCP套接字的几个通用的选项

  • rabbit.tcp_listen_options.nodelay:当设置为true,可以禁用Nagle算法。默认为true 对于大多数用户而言,推荐设置为true
  • rabbit.tcp_listen_options.sndbuf:TCP缓存区。一般取值范围在88KB至128KB之间。增大缓冲区可以提高消费者的吞吐量,同时也会加大每个连接上的内存使用量。减小则有相反的效果。
  • rabbit.tcp_listen_options.recbuf:TCP缓存区。一般取值范围在88KB至128KB之间。一般针对发送者或者协议操作。
  • rabbit.tcp_listen_options.backlog:队列中未接受的最大的数目。当达到此值时,新链接会被拒绝。对于成千上万的并发连接环境及可能存在大量客户重新连接的场景可以设置4096或者更高
  • rabbit.tcp_listen_options.linger:当套接字关闭时,设置为{true,N},用于设置刷新未发送数据的超时时间,单位为秒
  • rabbit.tcp_listen_options.keepalive:当设置为true时,启用TCP的存活时间。默认为false。对于长时间空闲的连接(至少10分钟)是有意义的,虽然更推荐使用heartbeat的选项。

系统相关的网络配置参数

操作系统有关的网络设置也会影响到RabbitMQ的运行,理解这些设置选项同样至关重要。系统内核参数在/etc/sysctl.conf文件中配置。

  • fs.file-max:内核分配的最大文件句柄数。极限值和当前值可以通过/proc/sys/fs/file-nr来查看`cat /proc/sys/fs/file-nr
  • net.ipv4.ip_local_port_range:本地IP端口范围,定义为一对值。该范围必须为并发连接提供足够的条目
  • net.ipv4.tcp_tw_reuse:当启用时,允许内核重用TIME_WAIT状态的套接字。当用在NAT时,此选项是很危险的
  • net.ipv4.tcp_fin_timeout:降低此值到5~10可减少连接关闭的时间,之后会停留在TIME_WAIT状态,建议用在有大量并发连接的场景
  • net.core.somaxconn:监听队列的大小(同一时间建立过程中有多少个连接)。默认为128.增大到4096甚至更高,可以支持入站连接的爆发,如client集体重连
  • net.ipv4.tcp_max_syn_backlog:尚未收到连接客户端确认的连接请求的最大数量。默认为128,最大值为65535.优化吞吐量时,4096和8192是最推荐的起始值
  • net.ipv4.tcp_keepalive_*:time、intvl、probes用于配置TCP存活时间
  • net.ipv4.conf.default.rp_filter:启动反向地址过滤。如果系统不关心IP欺骗,那么就禁用它

参数及策略

参数

也称之为运行时参数,比如在RabbitMQ某项配置需要在运行过程中改变,就可以使用它

Parameter 可以通过rabbitmqctl工具或者RabbitMQ management插件提供的HTTP API接口来设置。RabbitMQ中一共有两种类型的Parameter:vhost级别的Parameter和global级别的Parameter。

  • vhost级别的Parameter由一个组件名称(component name)、名称(name)和值(value)组成,
  • global级别的Parameter有一个名称和值组成。

不管vhost级别还是global级别的参数其所对应的值都是json类型的。

vhost级别的参数对应的rabbitmqctl相关命令有三种:set_parameterlist_parametersclear_parameter

set_parameter

$ rabbitmqctl set_parameter [-p vhost] {component_name} {name} {value}

HTTP_API : PUT /api/parameters/{component_name}/vhost/name

list_parameters

$ rabbitmqctl list_parameters [-p vhost]

HTTP_API : GET /api/parameters

clear_parameter

$ rabbitmqctl clear_parameter [-p vhost] {component_name} {name}

HTTP_API : DELETE /api/parameters/{component_name}/vhost/name

global级别Parameter的set、clear和list功能所对应的rabbitmqctl工具与HTTP API接口如下

set_parameter

$ rabbitmqctl set_global_parameter {name} {value}

HTTP_API : PUT /api/global-parameters/name

list_parameters

$ rabbitmqctl list_global_parameters [-p vhost]

HTTP_API : GET /api/global-parameters

clear_parameter

$ rabbitmqctl clear_global_parameter {name}

HTTP_API : DELETE /api/global-parameters/name

策略(Policy)

策略是vhost级别的。一个Policy可以匹配一个队列或者多个队列(或者交换器,或者两者都有),这样便于批量管理。与此同时,Policy也可以支持动态的修改一些属性参数,大大的提高了应用的灵活度。一般来说,Policy用来配置Federation、镜像、备份交换器、死信等。

rabbitmq_management插件本身就提供了Policy的支持,可以在admin->policies->add/updata a policy中添加一个policy 参数说明:

  • Virtual host:表示设置当前Policy所在的vhost
  • Name:策略名称
  • Pattern:正则表达式用来匹配相关的队列或者交换器
  • Apply to:用来指定当前Policy作用于哪一方。一共有三个选项Exchanges and queues 表示Pattern匹配的交换器和队列;Exchanges:表示Pattern匹配的交换器;Queues:表示Pattern匹配的队列
  • Priority:设置优先级。如果多个Policy作用于同一个交换器或者队列,那么Priority最大的那个Policy才会有用。
  • Definition:定义一组或者多组键值对,为匹配的交换器或者队列附加相应的功能。

作为一个Parameter,Policy也可以通过rabbitmqctl工具或者HTTP API接口来操作。与前面所说的Parameter对应,rabbitmqctl工具或者Http Api接口各种都有set、clear和list功能。

$ rabbitmqctl set_policy [-p host] [--priority priority] [--apply-to apply-to] {name} {pattern} {definition}

用来设置一个Policy。其中的参数name、patten和definition是必填项。 例如:

rabbitmqctl set_policy --apply-to exchanges --priority 1 p1 "^amq." '{"federation-upstream":"f1"}'

集群搭建

多机多节点配置

第一步、配置各节点的hosts文件

$ vi /etc/hosts
192.168.0.10 node1
192.168.0.11 node2
192.168.0.12 node3

第二步、编辑RabbitMQ的cookie文件

确保各节点的cookie文件使用的是同一个值。可以读取节点node1的cookie值,然后复制到node2、node3节点中。cookie文件默认路径/var/lib/rabbitmq/.erlang.cookie或者$HOME/.erlang.cookie

cookie相当于密钥令牌,RabbitMQ急群众需要通过交换密钥令牌以获取相互认证。

第三步、配置集群

配置集群有三种方式:

  • 通过rabbitmqctl工具配置
  • 通过rabbitmq.config配置文件配置
  • 通过rabbitmq-autocluster插件配置

这里主要使用rabbitmqctl工具进行配置(这也是最常用的方式)。

  1. 首先启动3个节点的rabbitmq服务
$ rabbit-server -detached
$ rabbit-server -detached
$ rabbit-server -detached

暂时使用docker分别建立是三个节点服务

# 创建rabbit网络
$ docker network create rabbit-net

# 创建节点nodo1
$ docker run  -d --hostname rabbit1 --name rabbit1 -p5671:5672 -p15671:15672 --network rabbit-net rabbitmq:management

# 创建节点nodo2
$ docker run  -d --hostname rabbit2 --name rabbit2 -p5672:5672 -p15672:15672 --network rabbit-net rabbitmq:management

# 创建节点nodo3
$ docker run  -d --hostname rabbit3 --name rabbit3 -p5673:5672 -p15673:15672 --network rabbit-net rabbitmq:management

通过rabbitmqctl cluster_status命令来查看各个节点的状态

接下来将3个节点组成一个集群,需要以node1节点为基准,将node2node3节点加入node1节点的集群中。这三个节点是平等的,如果想要调换彼此的加入顺序也未尝不可。首先将node2加点加入node1节点的集群中

# 1.停止node2的服务应用
$ rabbitmqctl stop_app

# 2.重置服务
$ rabbitmqctl reset

# 3.加入node1节点的集群中
$ rabbitmqctl join_cluster rabbit@node1

# 4.启动node2的服务应用
$ rabbitmqctl start_app

如此node1和node2就处于同一个集群中,可以使用rabbitmqctl cluster_status命令查看、验证

node3同上步骤

这里可能会遇到权限错误问题。原因是erlang.cookie不一致失败 解决办法,手动修改三个erlang.cookie 重启node2、node3

如果关闭了集群中的所有节点,则需要确保在启动的时候最后关闭的节点是第一个启动的,如果第一个启动的不是最后关闭的节点,那么这个节点会等待最后关闭的节点启动,这个等待时间是30秒,如果没等到,那么这个先启动的节点也会失败。在最新版本中会有重试10次30秒以等待最后关闭的节点启动。

单点故障

当一个节点出现硬件问题(不可恢复的问题)其他节点执行rabbitmqctl forget_cluster_node {nodename} 来剔除故障节点。

集群监控

通过HTTP API接口提供检测数据

假设集群中一共有4个节点node1、node2、node3和node4,有一个交换器exchange通过同一个路由键rk 绑定了3个队列queue1、queue2和queue3。

获取节点信息

首先收集节点信息,集群节点的信息可以通过/api/nodes接口来获取。其中包含了很多数据统计项,可以挑选需要的内容进行数据收集。

获取交换器信息

/api/exchanges/{vhost}/{name}

例如:http://localhost:15672/api/exchanges/%2F/exchange

获取队列信息

/api/queues/{vhost}/{name}

检查RabbitMQ服务是否健康

/api/aliveness-test/vhost

跨越集群的界限

RabbitMQ可以通过3种方式实现分布式部署: 集群、Federation和Shovel。这三种方式不是互斥的,可以根据需要选择其中的一种或者几种方式组合来打到分布式部署的目的。Federation和Shovel可以为RabbitMQ的分布式部署提供更高的灵活性,但同时也提高了部署的复杂度。

Federation

Federation 插件的设计目标是使RabbitMQ在不同的Broker节点之间进行消息传递而无需建立集群,该功能在很多场景下都非常有用

  • Federation插件能够在不同的管理域(可能设置了不同的用户和vhost,也可能运行在不同版本的RabbitMQ和Erlang上)中的Broker或者集群之间传递消息。
  • Federation插件基于AMQP 0-9-1协议在不同的Broker之间进行通信,并设计成能够容忍不稳定网络连接情况。
  • 一个Broker节点中可以同时存在联邦交换器(或队列)或者本地交换器(或队列),只需要对特定的交换器(或者队列)创建Federation连接(Federation Link)
  • Federation不需要在N个Broker节点之间创建O(N2)个连接(尽管这是最简单的使用方法),这也意味着Federation使用时更容易扩展。

Federation 插件可以让多个交换器或者多个队列进行联邦。一个联邦交换器或者一个联邦队列接收上游(upstream)的消息,这里的上游是指位于其他Broker上的交换器或者队列。联邦交换器能够将原本发送给上游交换器的消息路由到本地的某个队列中;联邦交换器则允许一个本地消费者接收到来自上游队列的消息。

存储机制

不管是持久化的消息还是费持久化的消息都可以被写入磁盘。持久化的消息在到达队列时就被写入磁盘,并且如果可以,持久化的消息也会在内存中保存一分备份,这样就可以提高一定的性能,当内存吃紧的时候会从内存中清除。非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入磁盘中,以节省内存空间。这两种类型的消息的落盘处理都在RabbitMQ的持久层中完成。

持久层是一个逻辑上的概念,实际包含两个部分:队列索引(rabbit_queue_index)和消息存储(rabbit_msg_store)。rabbit_queue_index负责维护队列中落盘消息的信息,包括消息的存储地点、是否已被交付给消费者、是否已被消费者ack等。每个队列都有与之对应的一个rabbit_queue_indexrabbit_msg_store以键值对的形式存储消息,他被所有队列共享,在每个节点中且只有一个。从技术层面上来说,rabbit_msg_store具体还可以分为msg_store_persistentmsg_store_transientmsg_store_persistent负责持久化消息的持久化,重启后消息不会丢失;msg_stote_transient负责非持久化消息的持久化,重启后消息会丢失。通常情况下,习惯性地将msg_store_persistentmsg_stone_transient看成rabbit_msg_store这样的一个整体。

消息(包括消息体、属性和headers)可以直接存储在rabbit_queue_index中,也可以被保存在rabbit_msg_store中。默认在RABBITMQ_HOME/var/lib/mnesia/rabbit@HOMENAME/路径下包含queues、msg_store_persistent、msg_store_transient这3个文件夹(下面信息下加粗的部分),其分别存储对应的信息。

惰性队列

惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到对应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机或者由于维护而关闭等)致使长时间内不能消费消息而造成积压时,惰性队列就很有必要了。

默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同事也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页到磁盘中,这个操作会消耗较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然RabbitMQ的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

惰性队列会将接收到的消息直接存入文件系统中,而不管是持久化的或者是非持久化的,这样可以减少内存的消耗,但是会增加I/O的使用,如果消息时持久化的,那么这样的I/O操作不可避免,惰性队列和持久化的消息可谓是最佳拍档。注意如果队形队列中存储的是非持久化的消息,内存使用率一直很稳定,但是重启后消息一样会丢失。

队列具备两种模式

  • default
  • lazy (惰性)

默认为default模式。lazy模式可以通过调用channel.queueDeclare方法的时候在参数设置,也可以通过Policy的方式设置,如果一个队列同事使用这两种方式,那么Policy的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式,那么只能先删除队列,然后重新声明一个新的。

在队列声明的时候可以通过x-queue-mode参数来设置队列的模式,取值为default和lazy

queue, e := channel.QueueDeclare(
    "",     //名称
    true,   //持久化
    false,  //自动删除
    false,  //排他的
    false,  //nowait
    map[string]interface{}{
        "x-queue-mode": "lazy",
    },
)

策略设置的方式为:

$ rabbitmqctl set_policy Lazy "^myqueue$" '{"queue-mode":"lazy"}' --apply-to-queues

惰性队列和普通队列相比,只有很小的内存开销。这里很难对每种情况给出一个具体的数值,但是我们可以类比一下:发送一千万条消息,每条消息大小1KB,并且此时没有任何的消费者,那么普通队列会消耗1.2GB的内存,而惰性队列只消耗1.5MB内存。

流控 (流量控制)

内存跟磁盘设置阈值相当于全局流控,当内存、硬盘使用率达到设定的阈值后,RabbitMQ会block阻塞生产者发送消息,直至对应项恢复正常。这里说的流控是针对单个连接的。

打破瓶颈 封装自己Rabbit类,将多个queue通过多个rk绑定到exchange上

镜像队列

如果RabbitMQ集群中只有一个Broker 节点,那么该节点如果失效将导致整体服务的临时性不可用。但是在集群中是有多个Broker节点组成的,如果出现单点故障,尽管交换器和绑定关系是可以保障的,但是队列和队列上的存储则不行。因为队列进程以及其内容维持在单个节点上而不是集群上。所以单个节点出现问题,其上的队列、以及队列内容会暂时不可用,可能会出现某些问题。

镜像队列可以解决这个问题, 可以将队列镜像到集群中的其他Broker节点之上,如果集群中一个节点失效了,队列能自动的切换到镜像中的另一个节点上,以保证服务的可用性。

通常的做法,针对每个配置镜像的队列都包含一个主节点和若干个从节点。 slave会准确的按照master执行命令的顺序进行动作,故slave和master上维护的状态应该是相同的。如果master由于某种原因失效,那么“资历最老”的slave就会晋升为master。

镜像队列的配置主要是通过添加相应的Policy来完成的.

$ rabbitmqctl set_policy [-p host] [--priority priority] [--apply-to apply-to] {name} {pattern} {definition}

命令中的definition部分,对于镜像队列的配置来说,definition中需要包含3个部分:ha-mode、ha-params和ha-sync-mode。

  • ha-mode:指明镜像队列模式,有效值为allexactlynodes,默认为all。all表示在集群中所有节点上进行镜像;exactly表示在指定个数的节点上进行镜像,节点个数有ha-params指定;nodes表示在指定节点上进行镜像,节点名称托工ha-params执行,节点上的名称通常类似于rabbit@hostname,可以通过rabbitmqctl cluster_status命令查看到
  • ha-params:不同的ha-mode配置中需要用到的参数
  • ha-sync-mode:队列中的消息同步方式,有效值为automatic和manual。

TODO未完待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • http://liuxing.info/2017/06/30/Spring%20AMQP%E4%B8%AD%E6%...
    sherlock_6981阅读 15,873评论 2 11
  • 关于消息队列,从前年开始断断续续看了些资料,想写很久了,但一直没腾出空,近来分别碰到几个朋友聊这块的技术选型,是时...
    预流阅读 584,424评论 51 785
  • 什么叫消息队列? 消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复...
    Agile_dev阅读 2,369评论 0 24
  • 关于消息队列,从前年开始断断续续看了些资料,想写很久了,但一直没腾出空,近来分别碰到几个朋友聊这块的技术选型,是时...
    Johnson_zx阅读 1,107评论 0 5
  • 一转眼就2017年了,时间过得真快,回想最初公众号的样子,感慨良多。 开始接触公众号是因为一个朋友,日更也只是因为...
    年轻的杨胖阅读 984评论 17 11