消费者确认和发布者确认
介绍
本指南涵盖了两个相关功能,即消费者确认和发布者确认,这些功能对于使用消息传递的应用程序中的数据安全非常重要。
根据定义,使用诸如RabbitMQ之类的消息传递代理的系统是分布式的。
由于发送的协议方法(消息)无法保证到达对等方或被其成功处理,因此发布者和消费者都需要一种交付和处理确认的机制。
RabbitMQ支持的几种消息传递协议提供了这些功能。
本指南涵盖了AMQP 0-9-1中的功能,但其他协议(STOMP,MQTT等)的想法基本相同。
消费者对RabbitMQ的交付处理确认称为AMQP 0-9-1用语中的确认;
代理对发布者的确认是一种称为发布者确认的协议扩展。
这两个功能都基于相同的想法,并受到TCP的启发。
它们对于从发布者到RabbitMQ节点以及从RabbitMQ节点到消费者的可靠传递至关重要。
消费者对RabbitMQ的交付处理确认称为AMQP 0-9-1用语中的确认;代理对发布者的确认是一种称为发布者确认的协议扩展。
这两个功能都基于相同的想法,并受到TCP的启发。它们对于从发布者到RabbitMQ节点以及从RabbitMQ节点到消费者的可靠传递至关重要。
消费者交付确认
当RabbitMQ向消费者传递消息时,它需要知道何时考虑成功发送消息。什么样的逻辑是最佳的取决于系统。因此,它主要是一个应用决策。在AMQP 0-9-1中,当使用basic.consume方法注册消费者或使用basic.get方法按需获取消息时进行。
交付标识符:交付标签
在我们继续讨论其他主题之前,重要的是解释如何识别交付(并且确认表明它们各自的交付)。
注册消费者(订阅)时,RabbitMQ将使用basic.deliver方法传递(推送)消息。该方法携带交付标签,其唯一地标识通道上的传递。因此,交付标识是在通道范围内的。
交付标签是单调增长的正整数,并由客户端库提供。确认交付的客户端库方法将交付标记作为参数。
由于交付标签的范围是每个通道,因此必须在收到的相同通道上确认交付。确认不同的通道将导致“未知的交付标签”协议异常并关闭通道。
消费者确认模式和数据安全注意事项
当节点向消费者传递消息时,它必须决定消费者是否应该考虑消息处理(或至少接收)消息。
由于多个东西(客户端连接,消费者应用程序等)可能会失败,因此该决定是数据安全问题。
消息传递协议通常提供一种确认机制,允许消费者确认交付给他们所连接的节点。
是否使用该机制是在消费者订阅时决定的。根据所使用的确认模式,RabbitMQ可以在发送消息(写入TCP套接字)后立即成功传递消息,或者在收到明确(“手动”)客户端确认时。手动发送的确认可以是正面的也可以是否定的,并使用以下协议方法之一:
basic.ack 用于正面确认
basic.nack 用于否定确认
basic.reject 用于否定确认,但和basic.nack相比有一个限制
下面将讨论这些方法如何在客户端库API中暴露。
正面确认只是指示RabbitMQ标识一条消息已交付,并且可以丢弃。basic.reject的否定确认具有相同的效果。
差异主要在于语义:正面确认假设消息已成功处理,而负面消息表明交付未处理但仍应删除。
在自动确认模式中,消息被认为在发送后立即成功传送。
这种模式可以获得高吞吐量(只要消费者可以跟上),以降低交付和消费者处理的安全性为代价。这种模式通常被称为“即发即忘”。与手动确认模型不同,如果消费者的TCP连接或通道在成功交付之前关闭,则服务器发送的消息将丢失。因此,自动消息确认应被视为不安全,并不适用于所有工作负载。
使用自动确认模式时需要考虑的另一件事是消费者过载。手动确认模式通常与有界通道预取一起使用,该预取限制了通道上未完成(“进行中”)交付的数量。但是,通过自动确认,根据定义没有这种限制。因此,消费者可能会被交付速度所淹没,可能会累积内存积压并耗尽堆或使操作系统终止其进程。某些客户端库将应用TCP反压(停止从套接字读取,直到未处理的交付积压降到低于某个限制)。因此,仅建议能够以稳定的速度有效处理交付的消费者使用自动确认模式。
一次确认多个交付
可以对手动确认进行批处理以减少网络流量。这是通过将确认方法的多个字段(见上文)设置为true来完成的。
请注意,basic.reject历史上没有该字段,这就是为什么basic.nack被RabbitMQ引入为协议扩展。当multiple字段设置为true时,RabbitMQ将确认所有未完成的交付标记,包括确认中指定的标记。与确认相关的所有其他内容一样,这是每个通道的范围。例如,假设在通道Ch上未确认传送标签5,6,7和8,当确认帧到达该通道时,delivery_tag设置为8并且multiple设置为true,则将确认从5到8的所有标签。。如果multiple设置为false,则交付5,6和7仍然是未确认的。
交付的否定确认和重新加入队列
有时,消费者无法立即处理交付,但其他实例可能会。在这种情况下,可能希望将其重新排队并让另一个消费者接收并处理它。basic.reject和basic.nack是用于此的两种协议方法。这些方法通常用于否定确认交付。代理可以丢弃此类提供或重新排队。此行为由requeue字段控制。当该字段设置为true时,代理将使用指定的交付标记重新排列交付(或多次交付,如下所述)。
当消息被重新加入队列时,如果可能的话,它将被放置在其队列中的原始位置。
如果不是(由于当多个消费者共享队列时同时传递和来自其他消费者的确认),该消息将被重新排队到更靠近队列头的位置。
重新加入队列的消息可以立即准备好重新发送,具体取决于它们在队列中的位置,即具有活动消费者的通道使用的预取值。
这意味着如果所有消费者因为由于瞬态情况而无法处理交付而重新加入队列,则他们将创建重新加入队列/重新发送循环。就网络带宽和CPU资源而言,这种循环可能是昂贵的。
消费者实现可以跟踪重新发送的数量并拒绝消息(丢弃它们)或在延迟后安排重新排队。
可以使用basic.nack方法一次拒绝或重新加入队列多条消息。这就是它与basic.reject的区别。它接受一个额外的参数,multiple。
通道预取设置(QoS)
因为消息是异步发送(推送)到客户端的,所以在任何给定时刻通常在通道上“飞行”中有多条消息。此外,客户的手动确认本质上也是异步的。因此,有一个未确认的交付标签的滑动窗口。开发人员通常更愿意限制此窗口的大小以避免消费者端的无限制缓冲区问题。这是通过使用basic.qos方法设置“预取数”值来完成的。该值定义通道上允许的最大未确认交货数。一旦数量达到配置的计数,RabbitMQ将停止在通道上传递更多消息,除非至少有一个未完成的消息被确认。
例如,假设在通道Ch上有未确认的传送标签5,6,7和8,并且通道Ch的预取数设置为4,则除非至少有一个未完成的传送被确认,否则RabbitMQ将不会再推送Ch。当确认帧到达该通道且delivery_tag设置为8时,RabbitMQ将注意到并再发送一条消息。
值得重申的是,交付流程和手动客户端确认完全是异步的。因此,如果在飞行中已经有交付的情况下改变了预取值,则会出现自然竞争条件,并且暂时可以在通道上多于预取未计数的未确认消息。
即使在手动确认模式下,QoS预取设置也不会影响使用basic.get(“pull API”)获取的消息。
消费者确认模式,预取和吞吐量
确认模式和QoS预取值对消费者吞吐量具有显着影响。通常,增加预取将提高向消费者传递消息的速率。自动确认模式可以产生最佳的交付率。但是,在这两种情况下,已传送但尚未处理的消息的数量也将增加,从而增加了消费者的RAM消耗。
应谨慎使用具有无限预取功能的自动确认模式或手动确认模式。在没有确认的情况下消耗大量消息的消费者将导致他们所连接的节点上的内存消耗增长。找到合适的预取值是一个试验和错误的问题,并且会因工作负载而异。100到300范围内的值通常可提供最佳吞吐量,并且不会面临压倒性消费者的重大风险。较高的价值往往会影响收益递减规律。
预取值1是最保守的。它将显着降低吞吐量,特别是在消费者连接延迟较高的环境中。对于许多应用来说,更高的值是合适的和最佳的。
当消费者失败或失去连接时:自动重新加入队列
使用手动确认时,任何未执行的传递(消息)将在关闭发生传递的通道(或连接)时自动重新加入队列。这包括客户端的TCP连接丢失,消费者应用程序(进程)故障和通道级协议异常(如下所述)。
请注意,检测不可用的客户端需要一段时间。
由于这种行为,消费者必须准备好处理重新发送,否则就要考虑到幂等性。Redeliveries将有一个特殊的布尔属性,即redeliver,由RabbitMQ设置为true。对于第一次交付,它将被设置为false。请注意,消费者可以接收先前传递给其他消费者的消息。
客户端错误:双重确认和未知标记
如果客户端多次确认相同的交付标记,RabbitMQ将导致通道错误,例如PRECONDITION_FAILED - 未知的交付标记100.如果使用未知的交付标记,则将抛出相同的通道异常。
代理 出现异常 “未知交付标签”的另一种情况是,在与接收交付的不同的通道上尝试确认(无论是正面还是负面)。交付必须在同一通道上确认。
发布者确认
网络可能以不太明显的方式失败,并且检测到某些故障需要时间。因此,将协议帧或一组帧(例如,已发布的消息)写入其套接字的客户端不能假定该消息已到达服务器并且已成功处理。它可能在途中丢失或其交付可能会显着延迟。
使用标准AMQP 0-9-1,保证消息不丢失的唯一方法是使用事务 - 使事务事务处理然后为每个消息或消息集发布,提交。在这种情况下,交易不必要地重量级并且将吞吐量减少250倍。为了解决这个问题,引入了确认机制。它模仿协议中已存在的消费者认可机制。
要启用确认,客户端将发送confirm.select方法。根据是否设置了无等待,代理可以使用confirm.select-ok进行响应。一旦在频道上使用confirm.select方法,就会说它处于确认模式。事务通道不能进入确认模式和一旦通道处于确认模式,就不能进行事务。
一旦通道处于确认模式,代理和客户端都会计数消息(计数在第一个confirm.select上从1开始)。然后,代理通过在同一通道上发送basic.ack来确认消息。delivery-tag字段包含已确认消息的序列号。代理还可以在basic.ack中设置多个字段,以指示已经处理了包括具有序列号的消息的所有消息。
给发布的否定确认
在特殊情况下,当代理无法成功处理消息而不是basic.ack时,代理将发送basic.nack。
在此上下文中,basic.nack的字段与basic.ack中的对应字段具有相同的含义,并且应该忽略requeue字段。通过nack一个或多个消息,代理表示它无法处理消息并拒绝对它们负责;此时,客户端可以选择重新发布消息。将通道置于确认模式后,将确认所有后续发布的消息或仅确认一次。不保证消息的确定时间。没有消息将同时被确认和否定确认。
只有在负责队列的Erlang进程中发生内部错误时,才会传递basic.nack。
什么时候发布的消息会被代理确认?
对于不可路由的消息,代理将在exchange验证消息不会路由到任何队列(发回一个空的队列列表)后发出确认。如果消息也作为必需消息发布,则basic.return将在basic.ack之前发送给客户端。负面确认(basic.nack)也是如此。对于可路由消息,当所有队列都接受消息时,将发送basic.ack。对于路由到持久队列的持久性消息,这意味着持久化到磁盘。对于镜像队列,这意味着所有镜像都已接受该消息
发布者确认的顺序考虑
在大多数情况下,RabbitMQ将以与发布时相同的顺序向发布者确认消息(这适用于在单个通道上发布的消息)。但是,发布者确认是异步发出的,可以确认单个消息或一组消息。发出确认的确切时刻取决于消息的传递模式(持久性与瞬态)以及消息路由到的队列的属性(参见上文)。也就是说,可以认为不同的消息可以在不同时间进行确认。这意味着与其各自的消息相比,确认可以以不同的顺序到达。应用程序不应该尽可能依赖于确认的顺序。
发布者确认和保证交付
如果代理在将所述消息写入磁盘之前崩溃,则代理会丢失持久消息。
在某些情况下,这会导致代理以令人惊讶的方式行事。
例如,考虑这种情况:
1 客户端将持久性消息发布到持久队列
2 客户端使用队列中的消息(注意消息是持久的,队列是持久的),但还没有消息
3 代理失效并重新启动,并且
4 客户端重新连接并开始使用消息
此时,客户端可以合理地假设该消息将再次传递。情况并非如此:重启导致代理丢失消息。为了保证持久性,客户应该使用确认。如果发布者的通道处于确认模式,则发布者将不会收到丢失消息的确认(因为该消息尚未写入磁盘)。
限制
最大交付标签
传递标记是64位长的值,因此其最大值是9223372036854775807.由于传递标记是按通道确定范围的,因此发布者或消费者在实践中不太可能运行此值。