8--RabbitMQ学习 | 青训营笔记

127 阅读10分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

简介

昨天提到了RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP,STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了Broker架构,核心思想是生产者不会将消息直接发送给队列,消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)、数据持久化都有很好的支持。多用于进行企业级的ESB整合。

可靠性

通过一些机制例如,持久化,传输确认等来确保消息传递的可靠性

拓展性

多个RabbitMQ节点可以组成集群

高可用性

队列可以在RabbitMQ集群中设置镜像,如此一来即使部分节点挂掉了,但是队列仍然可以使用

多种协议

原生的支持AMQP,也能支持STOMP,MQTT等协议

丰富的客户端

我们常用的编程语言都支持RabbitMQ

管理界面

自带提供一个WEB管理界面

插件机制

RabbitMQ 自己提供了很多插件,可以按需要进行拓展 Plugins

应用场景

对于一个大型的软件系统来说,它会有很多的组件或者说模块或者说子系统或者(subsystem or Component or submodule)。那么这些模块的如何通信?这和传统的IPC有很大的区别。传统的IPC很多都是在单一系统上的,模块耦合性很大,不适合扩展(Scalability);如果使用socket那么不同的模块的确可以部署到不同的机器上,但是还是有很多问题需要解决。比如: 1)信息的发送者和接收者如何维持这个连接,如果一方的连接中断,这期间的数据如何方式丢失? 2)如何降低发送者和接收者的耦合度? 3)如何让Priority高的接收者先接到数据? 4)如何做到load balance?有效均衡接收者的负载? 5)如何有效的将数据发送到相关的接收者?也就是说将接收者subscribe 不同的数据,如何做有效的filter。 6)如何做到可扩展,甚至将这个通信模块发到cluster上? 7)如何保证接收者接收到了完整,正确的数据? AMDQ协议解决了以上的问题,而RabbitMQ实现了AMQP

AMQP

一个提供统一消息服务的应用层标准高级消息队列协议,是一个通用的应用层协议

消息发送与接受的双方遵守这个协议可以实现异步通讯。这个协议约定了消息的格式和工作方式。

www.cnblogs.com/frankyou/p/…

安装教程

常用命令

启动监控管理器:rabbitmq-plugins enable rabbitmq_management 关闭监控管理器:rabbitmq-plugins disable rabbitmq_management 启动rabbitmq:rabbitmq-service start 关闭rabbitmq:rabbitmq-service stop 查看所有的队列:rabbitmqctl list_queues 清除所有的队列:rabbitmqctl reset 关闭应用:rabbitmqctl stop_app 启动应用:rabbitmqctl start_app

用户和权限设置

添加用户:rabbitmqctl add_user username password 分配角色:rabbitmqctl set_user_tags username administrator 新增虚拟主机:rabbitmqctl add_vhost vhost_name 将新虚拟主机授权给新用户:rabbitmqctl set_permissions -p vhost_name username “.*” “.*” “.*”(后面三个”*”代表用户拥有配置、写、读全部权限)

角色说明
  • 超级管理员(administrator) 可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
  • 监控者(monitoring) 可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
  • 策略制定者(policymaker) 可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
  • 普通管理者(management) 仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
  • 其他 无法登陆管理控制台,通常就是普通的生产者和消费者。

消息的可靠性

Message durability (消息持久化)

将保存在内存中的数据都写入磁盘,防止服务器重启后数据丢失;有哪些数据需要持久化保存呢?

元数据、消息需要持久化到磁盘;

磁盘节点:持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,只有在内存吃紧的时候才会从内存中清除;

内存节点:非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存空间;

Message acknowledgment

在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除。

如果一个Queue没被任何的Consumer Subscribe(订阅),当有数据到达时,这个数据会被cache,不会被丢弃。当有Consumer时,这个数据会被立即发送到这个Consumer。这个数据被Consumer正确收到时,这个数据就被从Queue中删除。

那么什么是正确收到呢?通过ACK。每个Message都要被acknowledged(确认,ACK)。我们可以显示的在程序中去ACK,也可以自动的ACK。如果有数据没有被ACK,那么RabbitMQ Server会把这个信息发送到下一个Consumer。

生产者消息确认机制

如何知道消息有没有正确到达exchange呢?

1、通过AMQP提供的事务机制实现:

消息中间件——RabbitMQ(三)理解RabbitMQ核心概念和AMQP协议! - 掘金 (juejin.cn)

2、通过生产者消息确认机制(publisher confirm)实现:

RabbitMQ系列(四)RabbitMQ事务和Confirm发送方消息确认--深入解读 - 掘金juejin.im/post/5b54681bf265da0f82023014

常见故障

集群状态异常
  1. rabbitmqctl cluster_status检查集群健康状态,不正常节点重新加入集群
  2. 分析是否节点挂掉,手动启动节点。
  3. 保证网络连通正常
队列阻塞、数据堆积
  1. 保证网络连通正常
  2. 保证消费者正常消费,消费速度大于生产速度
  3. 保证服务器TCP连接限制合理
脑裂[1][2]
  1. 按正确顺序重启集群
  2. 保证网络连通正常
  3. 保证磁盘空间、cpu、内存足够
内存使用量超过阀值
  =INFO REPORT==== 15-Mar-2022::03:21:13 ===
  rabbit on node 'rabbit@hb-lvs-rabbitmq-mem-1' down
  
  =INFO REPORT==== 15-Mar-2022::03:21:16 ===
  vm_memory_high_watermark set. Memory used:3194473120 allowed:3188020019
  
  =WARNING REPORT==== 15-Mar-2022::03:21:16 ===
  memory resource limit alarm set on node 'rabbit@hb-lvs-rabbitmq-disk-2'.
  
  **********************************************************
  *** Publishers will be blocked until this alarm clears ***
  **********************************************************

这时会阻塞生产者,以避免服务崩溃。

临时解决办法是调大内存阀值,默认是0.4;

  rabbitmqctl set_vmmemory_high_watermark 0.6

Golang 操作RabbitMQ

RabbitMQ 支持我们常见的编程语言,此处我们使用 Golang 来操作

Golang操作RabbitMQ的前提我们需要有个RabbitMQ的服务端,至于RabbitMQ的服务怎么搭建我们此处就不详细描述了.

Golang操作RabbitMQ的客户端包,网上已经有一个很流行的了,而且也是RabbitMQ官网比较推荐的,不需要我们再从头开始构建一个RabbitMQ的Go语言客户端包.

  go get github.com/streadway/amqp
项目目录
  ___lib
  ______commonFunc.go
  ___producer.go
  ___comsumer.go

commonFunc.go

  package lib
  
  import (
      "github.com/streadway/amqp"
      "log"
  )
  // RabbitMQ连接函数
  func RabbitMQConn() (conn *amqp.Connection,err error){
      // RabbitMQ分配的用户名称
      var user string = "admin"
      // RabbitMQ用户的密码
      var pwd string = "123456"
      // RabbitMQ Broker 的ip地址
      var host string = "192.168.230.132"
      // RabbitMQ Broker 监听的端口
      var port string = "5672"
      url := "amqp://"+user+":"+pwd+"@"+host+":"+port+"/"
      // 新建一个连接
      conn,err =amqp.Dial(url)
      // 返回连接和错误
      return
  }
  // 错误处理函数
  func ErrorHanding(err error, msg string){
      if err != nil{
          log.Fatalf("%s: %s", msg, err)
      }
  }
基础队列使用

简单队列模式是RabbitMQ的常规用法,简单理解就是消息生产者发送消息给一个队列,然后消息的消息的消费者从队列中读取消息

当多个消费者订阅同一个队列的时候,队列中的消息是平均分摊给多个消费者处理

producer.go

定义一个消息的生产者

  package main
  
  import (
      "encoding/json"
      "log"
      "myDemo/rabbitmq_demo/lib"
 ​
      "github.com/streadway/amqp"
  )
  type simpleDemo struct {
      Name string `json:"name"`
      Addr string `json:"addr"`
  }
  func main() {
      // 连接RabbitMQ服务器
      conn, err := lib.RabbitMQConn()
      lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
      // 关闭连接
      defer conn.Close()
      // 新建一个通道
      ch, err := conn.Channel()
      lib.ErrorHanding(err, "Failed to open a channel")
      // 关闭通道
      defer ch.Close()
      // 声明或者创建一个队列用来保存消息
      q, err := ch.QueueDeclare(
          // 队列名称
          "simple:queue", // name
          false,   // durable
          false,   // delete when unused
          false,   // exclusive
          false,   // no-wait
          nil,     // arguments
      )
      lib.ErrorHanding(err, "Failed to declare a queue")
      data := simpleDemo{
          Name: "Tom",
          Addr: "Beijing",
      }
      dataBytes,err := json.Marshal(data)
      if err != nil{
          lib.ErrorHanding(err,"struct to json failed")
      }
      err = ch.Publish(
          "",     // exchange
          q.Name, // routing key
          false,  // mandatory
          false,  // immediate
          amqp.Publishing{
              ContentType: "text/plain",
              Body:        dataBytes,
          })
      log.Printf(" [x] Sent %s", dataBytes)
      lib.ErrorHanding(err, "Failed to publish a message")
  }

comsumer.go

定义一个消息的消费者

  package main
  
  import (
      "log"
      "myDemo/rabbitmq_demo/lib"
  )
  
  func main() {
      conn, err := lib.RabbitMQConn()
      lib.ErrorHanding(err,"failed to connect to RabbitMQ")
      defer conn.Close()
      ch, err := conn.Channel()
      lib.ErrorHanding(err,"failed to open a channel")
      defer ch.Close()
      q, err := ch.QueueDeclare(
          "simple:queue", // name
          false,   // durable
          false,   // delete when unused
          false,   // exclusive
          false,   // no-wait
          nil,     // arguments
      )
      lib.ErrorHanding(err,"Failed to declare a queue")
      // 定义一个消费者
      msgs, err := ch.Consume(
          q.Name, // queue
          "",     // consumer
          true,   // auto-ack
          false,  // exclusive
          false,  // no-local
          false,  // no-wait
          nil,    // args
      )
      lib.ErrorHanding(err,"Failed to register a consume")
      go func() {
          for d := range msgs {
              log.Printf("Received a message: %s", d.Body)
          }
      }()
  
      log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
      select {}
  }
工作队列

工作队列也称为 任务队列 任务队列是为了避免等待执行一些耗时的任务,而是将需要执行的任务封装为消息发送给工作队列,后台运行的工作进程将任务消息取出来并执行相关任务 , 多个后台工作进程同时间进行,那么任务在他们之间共享

task.go

我们定义一个任务的生产者,用于生产任务消息

  package main
  
  import (
      "github.com/streadway/amqp"
      "log"
      "myDemo/rabbitmq_demo/lib"
      "os"
      "strings"
  )
  
  func bodyFrom(args []string) string {
      var s string
      if (len(args) < 2) || os.Args[1] == "" {
          s = "no task"
      } else {
          s = strings.Join(args[1:], " ")
      }
      return s
  }
  func main() {
      // 连接RabbitMQ服务器
      conn, err := lib.RabbitMQConn()
      lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
      // 关闭连接
      defer conn.Close()
      // 新建一个通道
      ch, err := conn.Channel()
      lib.ErrorHanding(err, "Failed to open a channel")
      // 关闭通道
      defer ch.Close()
      // 声明或者创建一个队列用来保存消息
      q, err := ch.QueueDeclare(
          // 队列名称
          "task:queue", // name
          false,          // durable
          false,          // delete when unused
          false,          // exclusive
          false,          // no-wait
          nil,            // arguments
      )
      lib.ErrorHanding(err, "Failed to declare a queue")
      body := bodyFrom(os.Args)
      err = ch.Publish(
          "",
          q.Name,
          false,
          false,
          amqp.Publishing{
              ContentType: "text/plain",
              // 将消息标记为持久消息
              DeliveryMode: amqp.Persistent,
              Body:         []byte(body),
          })
      lib.ErrorHanding(err, "Failed to publish a message")
      log.Printf("sent %s", body)
  }

worker.go

定义一个工作者,用于消费掉任务消息

  package main
  
  import (
      "log"
      "myDemo/rabbitmq_demo/lib"
  )
  
  func main() {
      conn, err := lib.RabbitMQConn()
      lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
      defer conn.Close()
  
      ch, err := conn.Channel()
      lib.ErrorHanding(err, "Failed to open a channel")
      defer ch.Close()
  
      q, err := ch.QueueDeclare(
          "task:queue", // name
          false,         // durable
          false,        // delete when unused
          false,        // exclusive
          false,        // no-wait
          nil,          // arguments
      )
      lib.ErrorHanding(err, "Failed to declare a queue")
      // 将预取计数器设置为1
      // 在并行处理中将消息分配给不同的工作进程
      err = ch.Qos(
          1,     // prefetch count
          0,     // prefetch size
          false, // global
      )
      lib.ErrorHanding(err, "Failed to set QoS")
  
      msgs, err := ch.Consume(
          q.Name, // queue
          "",     // consumer
          false,  // auto-ack
          false,  // exclusive
          false,  // no-local
          false,  // no-wait
          nil,    // args
      )
      lib.ErrorHanding(err, "Failed to register a consumer")
  
      forever := make(chan bool)
  
      go func() {
          for d := range msgs {
              log.Printf("Received a message: %s", d.Body)
              log.Printf("Done")
              d.Ack(false)
          }
      }()
  
      log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
      <-forever
  }