RabbitMQ三四事

数据的持久化

对于非常健壮稳定的后台系统,我们必须得考虑到各种宕机的情况:物理宕机,应用自身出错崩溃等,而这个时候我们的应用需要做到重启后数据依旧不丢失,这个问题就是数据持久化,也就是说数据持久化到了磁盘。
在RabbitMQ中,如果要保证消息发送到broker,我们首先需要做到三点

  1. 持久化的exchange(交换器):声明时开启durable选项
  2. 持久化的queue(队列):声明时开启durable选项
  3. 持久化的messagedelivery_mode设置为2(php,python之类的库,2可以换成更友好的常量),在node的amqp.node库中是设置persistenttrue

需要注意的一点是,持久化会造成性能损耗(写磁盘操作),但为了保证生产环境的数据一致性,我们必须这么做。

发送消息的confirm机制

其实光光做到以上三点,数据依旧有丢失的可能,因为在客户端成功调用api存入消息之后,RabbitMQ还需要一段时间(很短,但不可忽略)才能落盘,RabbitMQ并不是为每条消息都做fsync的处理,可能仅仅保存到cache中而不是物理磁盘上,而在这段时间内RabbitMQ broker发生crash, 消息保存到cache但是还没来得及落盘,那么这些消息将会丢失。
为了解决以上问题,我们需要使用RabbitMQ的生产者确认模式
为了开启确认模式,需要生产者将channel设置成confirm模式,一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号。

confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息 (来自参考1)

简单confirm示例

示例代码使用NodeJS实现,RabbitMQ服务可以使用上一篇RabbitMQ二三事docker-compose.yml快速启动

const QUEUE_NAME = 'test_queue'
const config = require("./config")
const amqp = require('amqplib')

async function getMQConnection() {
    return await amqp.connect({
        protocol: 'amqp',
        hostname: config.host,
        port: config.port,
        username: config.user,
        password: config.pass,
        locale: 'en_US',
        frameMax: 0,
        heartbeat: 5, // 心跳
        vhost: config.vhost,
    })
}

async function run(rmqConn, msgArr) {
    try {
        const channel = await rmqConn.createConfirmChannel() // 开启confirm
        const exchangeName = `${QUEUE_NAME}_exchange`
        await channel.assertExchange(exchangeName, 'direct', { durable: true, autoDelete: false }) // 不存在exchange就新建exchange
        await channel.assertQueue(QUEUE_NAME, {durable: true, autoDelete: false}) // 不存在queue就新建
        await channel.bindQueue(QUEUE_NAME, exchangeName, QUEUE_NAME) // 绑定交换器

        // queue name当routing key
        msgArr.forEach(str => {
            channel.publish(exchangeName, QUEUE_NAME, Buffer.from(str), { persistent: true, mandatory: true })
        })
        await channel.waitForConfirms()
        console.log('发送批量数据成功')
        await channel.close()
    } catch(err) {
        // do something with err
        console.log('发送批量数据失败:' + err.message)
    }
}

async function testSendBatchMsg() {
    const conn = await getMQConnection()
    await run(conn, [
        'cat',
        'dog',
        'pig',
        'mouse',
        'mouse',
        'penguin'
    ])
    await conn.close()
}
testSendBatchMsg()

说明

assertExchangeassertQueue是保证交换器和队列一定存在,这里的exchange是简单的direct交换器
ConfirmChannel#publish方法不返回promise

消费消息的ack机制

现在我们需要考虑我们的消费者了,消费者也会遇到程序出错或者物理宕机问题,RabbitMQ官方也给出了一套解决方案,和confirm机制类似,就是ack机制(Message acknowledgment).
在ack机制中,消费者在自己处理完业务逻辑后,需要发送一个ack消息,然后broker才认为这条消息被正确消费,然后从内存和磁盘中移除掉它,只要没收到消费者的acknowledgment,broker就会一直保存着这条消息.如果一个消费者崩溃(断开了连接)却没有发送ack,broker会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。在这样的机制下,即使有一个消费者崩溃也不会丢失任何消息。

简单ack示例

const QUEUE_NAME = 'test_queue'
const config = require("./config")
const amqp = require('amqplib')

async function getMQConnection() {
    return await amqp.connect({
        protocol: 'amqp',
        hostname: config.MQ.host,
        port: config.MQ.port,
        username: config.MQ.user,
        password: config.MQ.pass,
        locale: 'en_US',
        frameMax: 0,
        heartbeat: 5, // 心跳
        vhost: config.MQ.vhost,
    })
}

async function sleep(ms) {
    return new Promise(resolve => 
        setTimeout(resolve, ms))
}

async function start() {
    const mqConn = await getMQConnection()
    console.log('connecting RabbitMQ successfully!')
    const channel = await mqConn.createChannel()
    const exchangeName = `${QUEUE_NAME}_exchange`
    await channel.assertExchange(exchangeName, 'direct', { durable: true, autoDelete: false })
    await channel.assertQueue(QUEUE_NAME, {durable: true, autoDelete: false})
    await channel.bindQueue(QUEUE_NAME, exchangeName, QUEUE_NAME)

    channel.consume(QUEUE_NAME, async function(msg) {
        console.log("Received msg: %s from %s", QUEUE_NAME, msg.content.toString())
        console.log('consuming message...')
        try {
            await sleep(500) // 模拟消费消息
            console.log('consuming ends')
            channel.ack(msg) // 消费成功,发送ack
        } catch(e) {
            console.log('consuming failed: ' + e.message)
            channel.nack(msg) // 消费失败,发送nack
        }
    }, {noAck: false}) // ack
}

start()

注意

自动ack是默认打开的,也就是说消息发送到消费者的时候就被自动ack了,而很多情况下,我们想要手动ack,所以我们需要显式设置autoAsk=false关闭这种机制(在示例中是noAck: false)

ack没有任何超时限制;只有当消费者断开时,broker才会重新投递。即使处理一条消息会花费很长的时间。

一些问题

amqp.node这个库提供了心跳检测的功能(heartbeat选项),但是没有做自动重连的。
对于heartbeat的值,RabbitMQ官网有说明

Several years worth of feedback from the users and client library
maintainers suggest that values lower than 5 seconds are fairly likely
to cause false positives, and values of 1 second or lower are very
likely to do so. Values within the 5 to 20 seconds range are optimal
for most environments.

所以心跳不宜设置的太低(因为短暂的网络拥塞或者流控制),太低容易导致误报,根据经验5s-20s是比较合理的。

参考文章:

  1. 深入学习RabbitMQ(四):channel的confirm模式
  2. when-publishes-are-confirmed
  3. Channel-oriented API reference

Salamander
上帝在我很小的时候送给我了两个苹果,一个红苹果,一个蓝苹果。红苹果代表疯狂,蓝苹果代表思考
6.7k 声望
407 粉丝
0 条评论
推荐阅读
Java AtomicInteger类使用
这个问题发生的原因是++counter不是一个原子性操作。当要对一个变量进行计算的时候,CPU需要先从内存中将该变量的值读取到高速缓存中,再去计算,计算完毕后再将变量同步到主内存中。这在多线程环境中就会遇到问...

pigLoveRabbit2阅读 2.3k

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木66阅读 6.2k评论 16

如何使用 PHPStorm 进行优雅的项目开发?
PHP Storm 这个开发工具,很多 phper 应该有所耳闻,甚至也有不少人使用其作为生产工具,但是很多人都没有最大限度的使用它,本文就来总结一些优雅开发的小技巧。

唯一丶45阅读 4.8k评论 7

从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木44阅读 7.5k评论 6

从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木33阅读 6.3k评论 9

从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
回顾 从零搭建 Node.js 企业级 Web 服务器(一):接口与分层,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,控制层与服务层实现了业务处理过程,模型层定义了业务实体并以 对象-关系...

乌柏木34阅读 4.6k评论 9

怎样用 PHP 来实现枚举?
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,...

唯一丶25阅读 6.4k评论 4

6.7k 声望
407 粉丝
宣传栏