rabbitmq可靠性

本文翻译汇总自rabbitmq的官方文档。

翻译使用谷歌翻译后简单修改,部分内容读起来仍然比较晦涩,不过意思传达到了。

可靠性指南

 

本页介绍了如何使用AMQP和RabbitMQ的各种功能来实现可靠的传送 - 确保消息始终被传递,甚至在系统的任何部分遇到故障。

 

什么可以失败?

 

网络问题可能是最常见的失败类。网络不仅可能出现故障,防火墙可以中断空闲连接,并且不会立即检测到网络故障。

 

除了连接故障之外,broker和客户端应用程序可能会随时遇到硬件故障(或软件崩溃)。此外,即使客户端应用程序持续运行,逻辑错误也可能导致通道或连接错误,迫使客户端建立新的通道或连接,并从问题中恢复。

 

连接失败

 

在连接失败的情况下,客户端将需要与broker建立新的连接。以前连接中打开的任何通道都将自动关闭,这些通道也需要重新打开。

 

一般来说,当连接失败时,客户端将被连接引发异常(或类似的语言结构)通知。官方Java和.NET客户端还提供了回调方法,让您听到其他上下文中的连接失败 - Java在Connection和Channel类上都提供了ShutdownListener回调,.NET客户端提供了IConnection.ConnectionShutdown和IModel.ModelShutdown事件目的。

 

Acknowledgements和confirm

 

当连接失败时,消息可能在客户端和服务器之间传输 - 它们可能处于被解析或生成的中间,在OS缓冲区或电线上。传输中的消息将丢失 - 它们将需要重传。Acknowledgements让服务器和客户端知道何时这样做。

 

Acknowledgements可以在两个方向使用 - 允许消费者向服务器指示它已经接收/处理了消息,并允许服务器向生产者指示相同的东西。 RabbitMQ将后一种情况称为"confirm"。

 

当然,TCP确保已经接收到数据包,并且将重新发送,直到它们 - 但这只是网络层。Acknowledgements和confirm表明已收到消息并采取行动。confirm信号表示接收到消息,并且转让所有权,接收方承担全部责任。

 

Acknowledgements因此具有语义 - 消费的应用程序不应该confirm消息,直到它完成了与它们需要的任何操作 - 将它们记录在数据库中,转发它们,将它们打印到纸张或其他任何东西上。一旦这样做,broker可以自由地忘记该消息。

 

同样,broker一旦承担责任,就会confirm消息(见这里是什么意思)。

 

confirm的使用保证至少一次Delivery。没有confirm,在发布和消费操作期间可能发生消息丢失,并且只有最多的一次Delivery才能得到保证。

 

用心跳检测死TCP连接

 

在某些类型的网络故障中,数据包丢失可能意味着中断的TCP连接需要较长时间(例如,在Linux上使用默认配置约11分钟)才能被操作系统检测到。 AMQP 0-9-1提供心跳功能,以确保应用程序层及时发现连接中断(以及完全无响应的对等体)。心跳也可以防止可能终止"空闲"TCP连接的某些网络设备。有关详细信息,请参阅心跳。

 

在broker

 

为了避免在broker中丢失消息,我们需要应对broker重新启动,broker硬件故障,甚至是甚至broker崩溃。

 

为了确保重新启动时消息和broker定义生效,我们需要确保它们在磁盘上。 AMQP标准具有交换,队列和持久消息的耐久性概念,要求持久对象或持久消息将在重新启动后生存。有关持久性和持久性的具体标志的更多详细信息,请参见"AMQP概念指南"。

群集和高可用性

 

如果我们需要确保我们的broker幸存硬件故障,我们可以使用RabbitMQ的集群。在RabbitMQ集群中,所有定义(交换,绑定,用户等)都跨整个集群镜像。队列的行为方式不同,默认情况下只驻留在单个节点上,但可以跨多个或所有节点进行镜像。队列保持可见,并且可以从所有节点访问,无论它们位于何处。

 

镜像队列在所有已配置的集群节点之间复制其内容,可以无缝地容忍节点故障,并且不会丢失消息(尽管请参阅非同步从站上的此注释)。然而,消费应用程序需要注意,当队列失败时,消费者将被取消,他们将需要重新考虑 - 有关详细信息,请参阅文档。

 

在Producer

 

当使用confirms时,从通道恢复的生产者或连接故障应重发任何尚未从broker收到confirm的消息。这里存在消息重复的可能性,因为broker可能发送了一个从未到达生产者的confirm(由于网络故障等)。因此,消费者应用程序将需要以幂等(重复执行的效果一致)方式执行重复数据删除或处理传入的消息。

 

确保消息路由

 

在某些情况下,生产者可能很重要的是确保他们的消息被路由到队列(尽管并不总是 - 在公共子系统生产者只会发布的情况下,如果没有消费者感兴趣,那么消息是正确的丢弃)。

 

为了确保消息被路由到一个已知的队列,生产者只能声明一个目标队列并直接发布给它。如果消息可能以更复杂的方式进行路由,但是生产者仍然需要知道他们是否到达了至少一个队列,则可以在basic.publish上设置mandatory标志,确保basic.return(包含回复码和一些文本解释)将被发送回客户端,如果没有队列被适当地绑定。

 

在消费者

 

在网络故障(或节点崩溃)的情况下,可能消息重复,消费者必须准备好处理它们。如果可能,最简单的方法是确保您的消费者以幂等方式处理消息,而不是明确处理重复数据消除。

 

不能处理的消息

 

如果消费者确定它不能处理消息,那么它可以使用basic.reject(或basic.nack)拒绝它,要求服务器重新启动它(在这种情况下,服务器可能被配置为死信)代替。

 

 

消费者Acknowledgements和Producerconfirm

 

介绍

 

使用消息传递broker(如RabbitMQ)的系统按照定义分布。由于发送的协议方法(消息)不能保证到达对等体或被其成功处理,所以发布者和消费者都需要一种用于传送和处理confirm的机制。 RabbitMQ支持的几种消息协议提供了这样的功能。

 

(消费者)DeliveryAcknowledgements

 

当RabbitMQ向消费者发送消息时,需要知道何时成功发送消息。什么样的逻辑优化取决于系统。因此,它主要是应用程序的决定。

 

在我们继续讨论其他主题之前,重要的是要解释Delivery是如何被识别的(而且confirm表明他们各自的Delivery)。当消费者(订阅)注册时,消息将由RabbitMQ使用basic.deliver方法传递(推送)。该方法携带Deliverytags,其唯一地标识信道上的传递。

 

Deliverytags是单调增长的正整数,并由客户端库呈现。承认Delivery的客户端库方法将Deliverytags作为参数。

 

频道预取设置(QoS)

 

由于消息以异步方式发送(推送)到客户端,因此通常在任何给定时刻通常会有多个消息"在飞行中"。此外,客户端的手动confirm本质上也是异步的。所以有一个未被confirm的Deliverytags的滑动窗口。开发人员通常会倾向于限制此窗口的大小,以避免消费者端端的无限缓冲区问题。这是通过使用basic.qos方法设置"预取计数"值来完成的。该值定义了通道上允许的未confirmDelivery的最大数量。一旦数量达到配置的计数,RabbitMQ将停止在通道上传递更多消息,除非至少有一个未confirm的消息被confirm。

 

例如,鉴于在通道Ch上未confirm的Deliverytags5,6,7和8设置为4,RabbitMQ不会再推送任何更多的Delivery,除非至少有一个未完成的Delivery被confirm。当通过delivery_tag设置为8的confirm帧到达该通道时,RabbitMQ将会注意并传递一条消息。

 

值得重申的是,Delivery流程和手动客户端confirm完全是异步的。因此,如果在飞行中已经有Delivery时改变了预取值,则出现自然竞争条件,并且可能暂时超过在通道上预取计数未confirm的消息。

 

可以为通道或消费者配置QoS设置。有关详细信息,请参阅消费者预取。

 

即使在手动confirm模式下,QoS设置也不会影响使用basic.get("pull API")获取的消息。

 

 

Producer confirm

 

使用标准AMQP 0-9-1,保证消息不丢失的唯一方法是使用事务 - 使信道事务发布,发布消息,提交。在这种情况下,交易是不必要的重量级,并将吞吐量降低250倍。为了弥补这一点,引入了confirm机制。它模仿了协议中已经存在的消费者confirm机制。

 

要启用confirm,客户端发送confirm.select方法。根据是否设置不等待,broker可以通过confirm.select-ok进行回复。一旦在通道上使用了confirm.select方法,就被认为处于confirm模式。事务通道不能进入confirm模式,一旦通道处于confirm模式,则不能进行事务处理。

 

一旦一个通道处于confirm模式,broker和客户端都会计数消息(从第一个confirm.select开始计数)。然后,broker通过在同一个频道上发送basic.ack来confirm消息。发送tags字段包含已confirm消息的序列号。broker还可以在basic.ack中设置多个字段,以指示所有到达并包含具有序列号的消息的消息已被处理。

 

下面是Java中以confirm模式向通道发布大量消息并等待confirm的示例。

// Copyright (c) 2007-Present Pivotal Software, Inc. All rights reserved.

//

// This software, the RabbitMQ Java client library, is triple-licensed under the

// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2

// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see

// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL,

// please see LICENSE-APACHE2.

//

// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,

// either express or implied. See the LICENSE file for specific language governing

// rights and limitations of this software.

//

// If you have any questions regarding licensing, please contact us at

// info@rabbitmq.com.

 

 

package com.rabbitmq.examples;

 

import java.io.IOException;

 

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import com.rabbitmq.client.MessageProperties;

import com.rabbitmq.client.QueueingConsumer;

 

public class ConfirmDontLoseMessages {

static int msgCount = 10000;

final static String QUEUE_NAME = "confirm-test";

static ConnectionFactory connectionFactory;

 

public static void main(String[] args)

throws IOException, InterruptedException

{

if (args.length > 0) {

msgCount = Integer.parseInt(args[0]);

}

 

connectionFactory = new ConnectionFactory();

 

// Consume msgCount messages.

(new Thread(new Consumer())).start();

// Publish msgCount messages and wait for confirms.

(new Thread(new Publisher())).start();

}

 

@SuppressWarnings("ThrowablePrintedToSystemOut")

static class Publisher implements Runnable {

public void run() {

try {

long startTime = System.currentTimeMillis();

 

// Setup

Connection conn = connectionFactory.newConnection();

Channel ch = conn.createChannel();

ch.queueDeclare(QUEUE_NAME, true, false, false, null);

ch.confirmSelect();

 

// Publish

for (long i = 0; i < msgCount; ++i) {

ch.basicPublish("", QUEUE_NAME,

MessageProperties.PERSISTENT_BASIC,

"nop".getBytes());

}

 

ch.waitForConfirmsOrDie();

 

// Cleanup

ch.queueDelete(QUEUE_NAME);

ch.close();

conn.close();

 

long endTime = System.currentTimeMillis();

System.out.printf("Test took %.3fs\n",

(float)(endTime - startTime)/1000);

} catch (Throwable e) {

System.out.println("foobar :(");

System.out.print(e);

}

}

}

 

static class Consumer implements Runnable {

public void run() {

try {

// Setup

Connection conn = connectionFactory.newConnection();

Channel ch = conn.createChannel();

ch.queueDeclare(QUEUE_NAME, true, false, false, null);

 

// Consume

QueueingConsumer qc = new QueueingConsumer(ch);

ch.basicConsume(QUEUE_NAME, true, qc);

for (int i = 0; i < msgCount; ++i) {

qc.nextDelivery();

}

 

// Cleanup

ch.close();

conn.close();

} catch (Throwable e) {

System.out.println("Whoosh!");

System.out.print(e);

}

}

}

}

否定confirm

 

在特殊情况下,当broker无法成功处理消息时,代替basic.ack,broker将发送一个basic.nack。在这种情况下,basic.nack的字段具有与basic.ack中相应的含义相同的含义,并且请求字段应该被忽略。broker表示无法处理消息,拒绝对其发送一则或多封消息;在这一点上,客户端可能会选择重新发布消息。

 

通道置于confirm模式后,所有后续发布的消息将被confirm或不存在一次。不能保证消息被confirm多久。没有任何消息将被confirm和否定。

 

如果在负责队列的Erlang进程中发生内部错误,则只会传递basic.nack。

 

当消息被重新排队时,如果可能,它将被置于其队列中的原始位置。如果没有(由于多个消费者共享队列时由于其他消费者的并发Delivery和confirm),该消息将被重新排列到更接近队列头的位置。

 

什么时候confirm message?

 

对于不可路由的消息,一旦交换验证消息将不会路由到任何队列(返回空列表的队列),broker将发出confirm。如果消息也被发布为强制性,则basic.return将在basic.ack之前发送给客户端。否定的confirm也是如此(basic.nack)。

 

对于可路由消息,当所有队列接受消息时,发送basic.ack。对于路由到持久队列的持久消息,这意味着持续到磁盘。对于镜像队列,这意味着所有镜像都已接受该消息。

 

持久化消息的Ack延迟

 

在将消息持续存储到磁盘后,将发送一个持久消息的basic.ack路由到持久化队列。 RabbitMQ消息存储在间隔(几百毫秒)之后分批地将消息存储到磁盘,以最小化fsync(2)调用的数量,或者当队列空闲时。这意味着在一个恒定的负载下,basic.ack的延迟可以达到几百毫秒。为了提高吞吐量,强烈建议应用程序异步处理confirm(作为流)或发布批次的消息,并等待未完成的confirm。客户端库之间的具体API有所不同。

 

Producerconfirm的订购注意事项

 

在大多数情况下,RabbitMQ将按照发布的相同顺序向Producerconfirm消息(这适用于在单个频道上发布的消息)。然而,发布者的confirm是异步发出的,可以confirm一个消息或一组消息。发出confirm的确切时刻取决于消息的传递模式(持久与瞬态)以及消息被路由到的队列的属性(见上文)。也就是说,不同的消息可以被认为是准备好在不同的时间进行confirm。这意味着与其各自的消息相比,confirm可以以不同的顺序到达。应用程序不应该依赖于confirm的顺序。

 

Producer confirm和保证Delivery

 

如果在所有消息写入磁盘之前崩溃,broker将丢失持久的消息。在某些情况下,这将导致broker以惊人的方式表现。

 

例如,考虑这种情况:

 

客户端向持久队列发布持久消息

客户端从队列中消耗消息(指出消息是持久的,队列持久的),但是还没有确定,

broker死亡并重新启动,

客户端重新连接并开始消费消息。

在这一点上,客户端可以合理地假设该消息将被再次发送。不是这样:重新启动导致broker丢失该消息。为了保证持久性,客户应该使用confirm。如果Producer的频道处于confirm模式,Producer将不会收到丢失的消息的confirm(因为该消息尚未写入磁盘)。

限制

 

最大Deliverytags

 

Deliverytags是一个64位长的值,因此其最大值为9223372036854775807.由于每个渠道的Deliverytags是范围限定的,所以Producer或消费者在实践中不太可能超过此值。

 

 

个人理解:

上面的描述非常复杂,我总结来说,有一下几种情况需要在开发中注意:

  • Producter发送消息之后,没有收到Broker的confirm:

消息可能终止在了传送的层面,如操作系统缓冲层,或者网络传输层,或者是在Broker接受之后,由于内部故障不能处理,如exchang故障,也不会发送confirm给Producter。所以,在我们的系统中,我们在producter端其实是有数据库表存储需要发送的消息的,我们一次批量发送100条消息,一旦收到confirm,就会删除这部分消息,所以没有接收到confirm的话,就不删除相应的数据。

还要保证消息的幂等。如此就可以保证在producter层面不会丢失消息。

  • broker接收到消息之后,在exchang或者queue中丢失:

设置消息和exchang和queue都为持久的。

  • 找不到消息对应的queue

我们的程序不会出现这种情况。

  • queue没有对应的consumer

这种情况下,消息会在queue中挤压,也不会丢失。

  • 消息可能会重复发送,所以需要保证消息处理的幂等性。

 

 

broker将在下面的情况中对消息进行confirm:

  • broker发现当前消息无法被路由到指定的queues中(如果设置了mandatory属性,则broker会发送basic.return) 
  • 非持久属性的消息到达了其所应该到达的所有queue中(和镜像queue中)
  • 持久消息到达了其所应该到达的所有queue中(和镜像中),并被持久化到了磁盘(fsync) 
  • 持久消息从其所在的所有queue中被consume了(如果必要则会被ack)

 

批量发送消息,并批量接收确认的例子:

// 发送持久化消息,消息内容为helloWorld for (long i = 0; i < msgCount; ++i)

{

ch.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_BASIC, "helloWorld".getBytes());

}

// 等待所有消息都被ack或者nack,如果某个消息被nack,则抛出IOException

ch.waitForConfirmsOrDie();

网上有人做的测试,使用这种批量确认的模式,和使用异步的方式,性能差的不是太多。但是如果使用单条确认,性能将差别数倍。

 

 

posted on 2017-09-03 21:36  张小贱1987  阅读(443)  评论(0编辑  收藏  举报

导航