首发于中间件

RabbitMQ - ACK

本文通过RabbitMQ官网Doc 来学习。

本文目录:

  • Consumer ack (auto ack mode, manual ack mode(prefetch count))
  • Message duable (queue, message)
  • Publisher confirm (transaction, publisher confirm(同步<单步,批量>、异步))
  • Fanout example
  • 进程模型
  • 流量控制


Message acknowledgment

参考文章 confirms

包含2个部分:Consumer AcknowledgementsPublisher confirms。前者为consumer向RabbitMQ发送ACK,后者为broker向publisher发送ACK。这2个方法都借鉴了TCP的思想,保证从publisher到rabbitMQ,rabbitMQ到consumer之间消息的可靠传送。


Consumer Acknowledgements

问题:

默认情况下,exchange将消息转发到queue后就将消息删除。若queue消费消息失败,则消息丢失。很多情况下,我们不愿意消息丢失,若queue处理不了消息,我们希望消息被发送到其他queue进行处理。

解决方法:

RabbitMQ提供了consumer acknowledgment,当消息被queue接收并处理后返回ACK。若RabbitMQ没有接收到ACK,则会重发消息直到被正确处理。

RabbitMQ使用basic.deliver方法来传送消息。该方法携带64位的delivery tag,用来唯一识别channel上的消息传送。该tag为单调递增的正整数,被Client library方法ack delivery时使用。

注意:由于delivery tag是针对指定channel,所有消息ack必须在同一个channel上。



  • Automatic acknowledgement mode

这种模式下,消息被发送后就被认为发送成功,主要用来处理高吞吐量并可以稍微牺牲安全性的情况。在消息被正确处理前,若connnection或channel关闭会导致消息处理失败。因此,这种模式不安全。

同时,这种模式不会限制consumer端的负载量,consumer会因为耗尽系统资源而down掉。该模式只在消息能够高效处理且消息频率稳定的情况下才推荐使用。


  • Manual acknowledgment mode

该模式默认打开,使用下面3个函数来ack:

  • basic.ack - 返回positive ACK,对应java中的Channel#basicAck,消息被正确处理,可以删除。
  • basic.nack - 返回negative ACK,对应Java中的Channel#basicNack。
  • basic.reject - 返回negative ACK,对应Java中的Channel#basicReject,表示消息没被处理。

下面是一个positive acknowledge的示例:

// this example assumes an existing channel instance

boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // positively acknowledge a single delivery, the message will be discarded
             channel.basicAck(deliveryTag, false);
         }
     });

这里channel.basicAck(deliveryTag, false);的第二个参数为multiple。若设置为true,则该channel上当前消息及之前的消息都被被设置为ACK. basic.reject中没有该参数,所以RabbitMQ引入basic.nack。

basic.reject和basic.nack标注的消息可以被丢弃或被重新发送,当requeue field被设置为true时,该消息被重新发送。basic.nack可以标注多条消息被丢弃或重新发送,因为该函数多了参数multiple。被重新发送的消息,RabbitMQ设置其属性redeliver为true来区分。

channel.basicReject(deliveryTag, false);  //消息被丢失
channel.basicReject(deliveryTag, true);  //消息被重新发送
channel.basicNack(deliveryTag, true, true);  //多条消息被重新发送

当消息被重新发送时,消息被放在队列中的原来位置或队列头。


-Prefetch Count

由于消息是异步处理的,所以channel上可能同时具有多条消息要被处理。存在sliding window来存放未被ack的delivery tags,防止consumer端unbounded buffer problem。

通过设置basic.qos函数中的prefetch count来限制未处理消息的数量,当达到prefetch count,RabbitMQ不再发送消息。该函数可以应用于channel上或queue上,设置该函数的global参数为false,则应用在queue上,否则,应用于channel上。

注意:大部分API中,该参数默认为false。

下面的例子中,每个consumer最多处理10条未提交的消息。

Channel channel = ...;
Consumer consumer1 = ...;
Consumer consumer2 = ...;
channel.basicQos(10); // Per consumer limit
channel.basicConsume("my-queue1", false, consumer1);
channel.basicConsume("my-queue2", false, consumer2);

下面例子中,2个consumer总共最多处理15条未提交消息,每个consumer最多处理10条未提交消息。

Channel channel = ...;
Consumer consumer1 = ...;
Consumer consumer2 = ...;
channel.basicQos(10, false); // Per consumer limit
channel.basicQos(15, true);  // Per channel limit
channel.basicConsume("my-queue1", false, consumer1);
channel.basicConsume("my-queue2", false, consumer2);


小总结:上面提到的ACK mode和Prefetch Count会影响安全性和吞吐量,需要做权衡。


Message durability

上面通过Consumer ACK,我们可以做到即使consumer die,消息仍会被处理。但我们仍不能阻止当RabbitMQ server挂掉时消息丢失。

我们可以做2件事情来保证RabbitMQ server挂掉时消息仍不丢失:设置queue和message为durable。

-queue durable

下面code设置queue task_queue为durable。应该在sender和receiver方都声明。这样,即使RabbitMQ重启,task_queue也不会丢失。

注意:RabbitMQ不允许用不同的参数来重新定义已经存在的queue,引发error。

boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);

-Message durable

下面通过MessageProperties.PERSISTENT_TEXT_PLAIN来设置message为durable。

import com.rabbitmq.client.MessageProperties;

channel.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());

注意:设置message为persistent并不能完全保证消息不丢失,在RabbitMQ将消息保存到disk之前仍可能丢失。


Publisher confirms

我们通过发送端的消息确认机制,包括事务和Publisher confirm机制来保证发送端到RabbitMQ的可靠性。

事务

RabbitMQ支持事务,通过调用tx.select方法开始事务模式,使用tx.commit方法来提交事务,医用tx.rollback来撤回事务。

ConnectionFactory factory = new ConnectionFactory();
connection = factory.newConnection();
channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
try {
    // 开启事务
    channel.txSelect();

    while(num-- > 0) {
        // 发送一个持久化消息到特定的交换机
        channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
    }

    // 提交事务
    channel.txCommit();
}catch(IOException e){
    e.printStackTrace();
    // 回滚事务
    channel.txRollback();
}finally {
    channel.close();
    connection.close();
}

事务的实现简单,能够保证消息正确到达RabbitMQ,但效率低,只有一般发送消息效率的1/250。

Publisher confirm

为了解决事务的低效性,RabbitMQ引入的Publisher Confirms机制。事务通道不能进入Publisher Confirms模式,一旦通道处于Publisher Confirms模式,不能开启事务。即事务和Publisher Confirms模式只能二选一。

生产者端可以通过confirm.select来启用方法Publisher Confirms机制,RabbitMQ服务端根据是否设置no-wait的值,返回confirm.select-ok。一旦在通道上使用confirm.select方法,就认为它处于Publisher Confirms模式。

分为同步Publisher Confirms模式异步Publisher Confirms模式。其中,同步Publisher Confirms模式有分为单个确认模式和批量确认模式。

同步Publisher Confirms模式:

单个确认模式:

ConnectionFactory factory = new ConnectionFactory();
connection = factory.newConnection();
channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 开启confirm模式:
channel.confirmSelect();
try{
	while(num-- > 0) {
		channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
		// 等待服务端返回Basic.Ack后,才执行下一个循环
		if(!channel.waitForConfirms()){
			System.out.println("message haven't arrived broker");
		}
	}
}finally {
    channel.close();
    connection.close();
}

批量确认模式:

ConnectionFactory factory = new ConnectionFactory();
connection = factory.newConnection();
channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 开启confirm模式:
channel.confirmSelect();
try{
	while(num-- > 0) {
		channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
		// 等待服务端返回Basic.Ack后,才执行下一个循环
	}
	if(!channel.waitForConfirms()){
		System.out.println("message haven't arrived broker");
	}
}finally {
    channel.close();
    connection.close();
}

异步Publisher Confirms模式:

ConnectionFactory factory = new ConnectionFactory();
try {
    connection = factory.newConnection();
    channel = connection.createChannel();
    channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

    channel.addConfirmListener(new ConfirmListener(){
        @Override
        public void handleAck(long deliveryTag, boolean multiple) throws IOException {}
        @Override
        public void handleNack(long deliveryTag, boolean multiple) throws IOException {}
    });

    // 开启confirm模式:
    channel.confirmSelect();
    while(num-- > 0) {
        channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
    }
}finally {
    channel.close();
    connection.close();
}


Fanout exchange示例

发送消息:

public class EmitLog {

  private static final String EXCHANGE_NAME = "logs";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {
        //声明一个名字为logs,类型为fanout的exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        String message = argv.length < 1 ? "info: Hello World!" :
                            String.join(" ", argv);

        //向logs exchange发送消息
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
        System.out.println(" [x] Sent '" + message + "'");
    }
  }
}

接收消息:

public class ReceiveLogs {
  private static final String EXCHANGE_NAME = "logs";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    //声明一个名字为logs,类型为fanout的exchange
    channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
    //生成一个non-durabe且自动删除的queue
    String queueName = channel.queueDeclare().getQueue();
    //Binding queue and exchange,参数3可以指定routing_key
    channel.queueBind(queueName, EXCHANGE_NAME, "");

    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

    DeliverCallback deliverCallback = (consumerTag, delivery) -> {
        String message = new String(delivery.getBody(), "UTF-8");
        System.out.println(" [x] Received '" + message + "'");
    };
    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
}

Queue Attribute

Queue在使用之前必须声明(channel.queueDeclare()),若声明的queue没有则生成,不做任何事情。

注意:若声明的queue已存在,声明时提供的queue必须跟已存在queue相同,否则抛出code为406的exception。

Durable queue保存在disk上,因此,broker重启后仍然存在。


Heartbeat Timeout

用来判断connnection不可达的时间,一般在创建connection时,Rabbit server与client进行协商,获得两者提供值的较小值;若其中一方提供的值为0,使用另一方的值;默认值为60s。

下面在创建connection之前设置heartbeat timeout。

ConnectionFactory cf = new ConnectionFactory();

// set the heartbeat timeout to 60 seconds
cf.setRequestedHeartbeat(60);


Virtual Hosts

创建virtual host有2种方式:rabbitMQ CLI和HTTP API endpoint。

删除virtual host,将会删除vhost上的all entities (queues, exchanges, bindings, policies, permissions, etc)。

使用CLI tool:

rabbitmqctl add_vhost qa1
rabbitmqctl delete_vhost qa1

使用HTTP API endpoint:

curl -u userename:pa$sw0rD -X PUT http://rabbitmq.local:15672/api/vhosts/vh1
curl -u userename:pa$sw0rD -X DELETE http://rabbitmq.local:15672/api/vhosts/vh1

RabbitMQ进程模型

参考文章RabbitMQ进程结构分析与性能调优

RabbitMQ Server实现了AMQP模型中Broker部分,将Channel和Queue设计成了Erlang进程,并用Channel进程的运算实现Exchange的功能。

RabbitMQ进程模型:

tcp_acceptor进程接收客户端连接,创建rabbit_reader、rabbit_writer、rabbit_channel进程。

rabbit_reader接收客户端连接,解析AMQP帧;

rabbit_writer向客户端返回数据;

rabbit_channel解析AMQP方法,对消息进行路由,然后发给相应队列进程。rabbit_amqqueue_process是队列进程,在RabbitMQ启动(恢复durable类型队列)或创建队列时创建。

rabbit_msg_store是负责消息持久化的进程。

在整个系统中,存在一个tcp_acceptor进程,一个rabbit_msg_store进程,有多少个队列就有多少个rabbit_amqqueue_process进程,每个客户端连接对应一个rabbit_reader和rabbit_writer进程。

RabbitMQ流控

RabbitMQ可以对内存和磁盘使用量设置阈值,当达到阈值后,生产者将被阻塞(block),然后RabbitMQ会进行page操作,将内存中的数据持久化到磁盘中。

RabbitMQ使用一种基于信用证的流控机制。消息处理进程有一个信用组{InitialCredit,MoreCreditAfter},默认值为{200, 50}。消息发送者进程A向接收者进程B发消息,每发一条消息,Credit数量减1,直到为0,A被block住;对于接收者B,每接收MoreCreditAfter条消息,会向A发送一条消息,给予A MoreCreditAfter个Credit,当A的Credit>0时,A可以继续向B发送消息。

消息队列状态

amqqueue进程提供消息的存储和队列功能。为了高效处理入队和出队的消息、避免不必要的磁盘IO,amqqueue进程为消息设计了4种状态和5个内部队列。

状态:alpha,消息的内容和索引都在内存中;beta,消息的内容在磁盘,索引在内存;gamma,消息的内容在磁盘,索引在磁盘和内存中都有;delta,消息的内容和索引都在磁盘。

5个内部队列包括:q1、q2、delta、q3、q4。q1和q4队列中只有alpha状态的消息;q2和q3包含beta和gamma状态的消息;delta队列是消息按序存盘后的一种逻辑队列,只有delta状态的消息。所以delta队列并不在内存中,其他4个队列则是由erlang queue模块实现。

内部队列消息传递顺序:

消息从q1入队,q4出队,在内部队列中传递的过程一般是经q1顺序到q4。实际执行并非必然如此:开始时所有队列都为空,消息直接进入q4(没有消息堆积时);内存紧张时将q4队尾部分消息转入q3,进而再由q3转入delta,此时新来的消息将存入q1(有消息堆积时)。

Paging就是在内存紧张时触发的,paging将大量alpha状态的消息转换为beta和gamma;如果内存依然紧张,继续将beta和gamma状态转换为delta状态。Paging是一个持续过程,涉及到大量消息的多种状态转换,所以Paging的开销较大,严重影响系统性能。

编辑于 2020-08-17 13:42