RabbitMQ:我的消息去哪儿了呢?持久化和你的策略。

关于在Rabbit里创建队列和交换器有个不可告人的秘密:默认情况下他们无法幸免于服务器重启。没错,重启RabbitMQ服务器后,那些队列和交换器就都消失了(随同里面的消息)。原因在于每个队列和交换器的durable属性。该属性默认情况为false,他决定了RabbitMQ是否需要在崩溃或者重启之后重新创建队列(或者交换器)。将他设置为true,这样你就不需要在服务器断电后重新创建队列和交换器了。你也许会仍未把队列和交换器的durable属性设置为true就足够可以让消息幸免于重启,但是你错了。队列和交换器当然必须被设置成true,但光这些做还不够。
能从AMQP服务器崩溃中恢复的消息,我们称之为持久化消息。在消息发布前,通过把他的“投递模式”(delivery mode)选项设置为2(AMQP客户端可能会使用人性化的常量来代替数值)来把消息标记成持久化。到目前为止,消息还只是被标识为持久化的,但是他还必须被发布到持久化的交换器中并到达持久化的队列中才行。如果不是这样的话,则包含持久化消息的队列(或者交换器)会在Rabbit崩溃重启后不复存在,从而导致消息成为孤儿。因此,如果消息想要从Rabbit崩溃中恢复,那么消息必须:

  • 把他的投递模式选项设置为2(持久)
  • 发送到持久化的交换器
  • 到达持久化的队列

做到以上三点,你就不用和你的关键消息玩“躲猫猫”了。
RabbitMQ确保持久性消息能从服务器重启中恢复的方式是,将他们写入磁盘上的一个持久化日志文件。当发布一条持久性消息到持久交换器上时,Rabbit会在消息提交到日志文件后才发送响应。记住,之后这条消息如果路由到了非持久队列的话,他会自动从持久性日志中移除,并且无法从服务器重启中恢复。如果你使用持久性消息的话,则确保之前提到的持久性消息的那三点都必须做到位(我们再怎么强调也不为过)。一旦你从持久化队列中消费了一条持久性消息的话(并且确认了他),RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。在你消费持久性消息前,如果RabbitMQ重启的话,服务器会自动重建交换器和队列(以及绑定),重播持久性日志文件中的消息到合适的队列或者交换器上(取决于Rabbit服务器宕机的时候,消息处在路由过程的哪个环节)。
你可能认为自己应该为所有的消息都启用持久化消息通信。你可以这样做,但同时你也要为此付出代价:性能。写入磁盘要比存入内存中慢不止一点点,而且会极大的减少RabbitMQ服务器每秒可处理的消息总数。使用持久化机制而导致消息吞吐量降低至少10倍的情况并不少见(将RabbitMQ的消息存储于SSD上的话,就可以极大的提升持久化消息通信的性能)。另外还有一点就是,持久性消息在RabbitMQ内建集群环境下工作得并不好。虽然RabbitMQ集群允许你和集群中的任何节点的任一队列进行通信,但是事实上那些队列均匀的分布在各个节点而没有冗余(在集群中任何一个队列都没有备份的拷贝)。如果运行seed_bin队列的集群节点崩溃了,那么直到节点恢复前,这个队列也就从整个集群中消失了(如果队列是可持久化的)。更重要的是,当节点宕机时,其上的队列也都不可用了,而且持久化队列也无法重建。这就会导致消息丢失。
权衡取舍,什么情况下你应该使用持久性/持久化消息通信呢?首先,你需要分析(并测试)性能需求。你是否需要单台Rabbit服务器每秒处理100 000条消息呢?如果是这样的话,你应该寻找其他方式来保证消息投递(或者使用更快速的存储系统)。举例来说,生产者可以在单独的信道上监听应答队列。每次发送消息的时候,都包含应答队列的名称,这样的消费者就可以回发应答以确认接收到了。如果消息应答未在合理时间范围内到达,生产者就重新发送消息。也就是说,要保证消息的投递这一关键本质决定了相对于其他类型的消息(例如日志消息)会有更低的吞吐量。因此如果持久性消息通信能够满足性能需求的话,那么用这种机制确保休息投递是极佳的方式。我们更多的是为关键消息使用持久化机制。我们只是对何种类型的内容使用持久性消息通信举棋不定。举个例子,我们运行两种类型的Rabbit集群:非持久化消息通信的传统RabbitMQ集群和持久化消息通信的活动/热备非集群RabbitMQ服务器(使用负载均衡)。这样做确保了为持久化消息通信处理负载不会减慢非持久化消息的处理。这也意味着Rabbit内建集群在节点宕机时不会让持久性消息消失。请记住Rabbit能帮助确保投递,但并不是万无一失的。硬盘崩溃、充满bug的消费者或者其他极端事件都能导致持久化消息丢失。最终确保消息安全到达都将取决于你的策略。持久化消息通信是一个很好的工具,可以帮助你完成这一点。
和消息持久化相关的一个概念是AMQP事务(transaction)。到目前为止,我们讨论的是将消息、队列和交换器设置为持久化。这一切都工作得很好,并且RabbitMQ也负责保证消息的安全。但是由于发布操作不返回任何信息给生产者,那你怎么样知道服务器是否已经持久化了持久消息到硬盘呢?服务器可能会在把消息写入磁盘前就宕机了,消息因此而丢失,而你却不知道。这就是事务发挥作用的地方。

当继续处理其他任务前,你必须确保代理接收到了消息(并且已经将消息路由给所有匹配的订阅队列),你需要把这些行为包装到一个事务中。如果你有数据库背景的话,不要把AMQP事务和大多数数据库的事务概念搞混了。在AMQP中,在把信道设置成事务模式后,你通过信道发送那些想要确认的消息,之后还有多个其他AMQP命令。这些命令是执行还是忽略,取决于第一条消息发送是否成功。一旦你发送完所有命令,就可以提交事务了。如果事务中首次发布成功了,那么信道会在事务中完成其他AMQP命令。如果发送失败的话,其他AMQP命令将不会执行。事务填补了生产者发布消息以及RabbitMQ将他们提交到磁盘上这两者之间“最后1英里”的差距。不过,还有更好的方法来填补差距。
虽然事务是正式AMQP 0-9-1规范的一部分,但是他们有阿喀琉斯之踵:几乎吸干了RabbitMQ的性能。使用事务不但会降低大约2~10倍的消息吞吐量,而且会使生产者应用程序产生同步。而你使用消息通信就是想要避免同步。知晓了所有这一生产者应用程序产生同步。而你使用消息通信就是想要避免同步。知晓了所有这一切之后,RabbitMQ团队决定拿出更好的方案来保证消息投递:发送方确认模式。和事务相仿,你需要告诉Rabbit将信道设置成confirm模式,而且你只能通过重新创建信道来关闭该设置。一旦信道进入confirm模式,所有在信道上发布的消息都会被指派一个唯一的ID号(从1开始)。一旦消息被投递给所有匹配的队列后,信道会发送一个发送方确认模式给生产者应用程序(包含消息的唯一ID)。这使得生产者知晓消息已经安全到达目的队列了。如果消息和队列是可持久化的,那么确认消息只会在队列将消息写入磁盘后才会发出。发送方确认模式的最大好处是他们是异步的。一旦发布了一条消息,生产者应用程序就可以在等待确认的同时继续发送吓一跳。当确认消息最终收到的时候,生产者应用的回调方法就会被触发来处理该确认消息。如果Rabbit发生了内部错误从而导致了消息的丢失,Rabbit会发送一条nack(not acknowledged,未确认)消息。就像发送方确认消息那样,只不过这次说明的是消息已经丢失了。同时由于没有消息回滚的概念(同事务相比),因此发送方确认模式更加轻量级,同时对Rabbit代理服务器的性能影响几乎可以忽略不计。

你可能感兴趣的:(RabbitMQ)