摘要
RabbitMQ關於數據安全有兩個特性:發佈者確認(Publisher Confirms)和消費者確認(Consumer Acknowledegements)。前者用於MQ服務器(broker)告訴發佈者傳遞消息的結果,它是消息協議的拓展內容;後者用於消費者告訴MQ服務器消息傳遞的結果,是消息協議包含的定義。本文主要介紹RabbitMQ對消費者確認的實現。
一條消息傳遞的標識:傳遞標籤(delivery tags)
消息的傳遞是如何被標識的呢?
當一個消費者被註冊後,RabbitMQ通過basic.delivery方法傳遞消息,該方法會攜帶一個傳遞標籤,它唯一的標識了通道上某條消息的傳遞。因此傳遞標籤是按通道分的。
傳遞標籤是單調遞增的正整數,消費者客戶端確認消息的方法將會將傳遞標籤作爲參數。因爲傳遞標籤是按照通道分的,如果消息A是從通道1傳遞到消費者服務器的,那麼消息A的確認也必須通過通道1,如果錯誤的通過通道2確認,RabbitMQ會拋出協議異常並關閉通道2。
消費者確認的模式
當一個MQ服務器節點傳遞一條消息到消費者服務器,它需要決定什麼時候認爲這條消息已經被消費者成功處理了。消息協議通常會提供一個確認機制,允許消費者向他們連接的MQ服務器發送確認。確認機制是否啓用一般在消費者訂閱的時候決定。
取決於使用的確認模式,RabbitMQ可以在消息從MQ服務器發送出(寫入TCP Socket)後立刻就將其當做已成功處理,或者當收到來自消費者顯示的(手工的)的確認後。
手動確認模式
消費者手工發送的確認可以是以下一種協議方法:
basic.ack(deliveryTag,multiple)
用來確認成功消息(positive acknowledgements)basic.nack(deliveryTag,requeue,multiple)
用來確認失敗的消息(negative acknowledgements)basic.reject(deliveryTa,requeue)
用來確認失敗的消息
確認成功簡單的令rabbitMQ將消息記錄爲已發送並丟棄。使用basic.reject
方法令RabbitMQ記錄消息爲發送失敗,但仍然需要丟棄。
自動確認模式
在自動確認模式(automatic acknowledgement)中,消息在發出去後就被認爲是已成功處理。這種模式損失數據安全性來換取消息的高吞吐量,如果與消費者的TCP連接或者消息通道在成功發送前關閉了,則MQ服務器發送出的消息就丟失了。因此自動確認模式應該被認爲是非數據安全的,應謹慎使用。
如果要使用自動確認模式,還有個要注意的地方是消費者的負載。手動確認模式典型的使用會在消費者端定義一個有限大小的隊列(prefetch)用於存放未處理狀態的消息。然而自動確認模式根本無此限制,因此有可能造成消費者服務器超出負荷,導致堆內存不夠用而被操作系統終止進程。因此,只有在消費者能夠已穩定並高效的處理消息的前提下,才建議使用確認模式。
RabbitMQ手動確認示例
1,確認一條消息傳遞成功
JAVA庫使用 Channel#basicAck
和 Channel#basicNack
方法分別來實現協議中定義的 basic.ack
和 basic.nack
方法。示例如下:
// 假設已存在channel實例
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();
// 確認一條消息成功傳遞,消息將會被RabbitMQ丟棄
channel.basicAck(deliveryTag, false);
}
});
2,一次確認多條消息傳遞成功
手工確認模式可以通過批次確認來減少網絡流程,通過將 確認方法的的multiple
參數設置爲true
。注意,basic.reject
是沒有這個參數的,RabbitMQ引入basic.nack
方法帶這個參數,該方法作爲拓展協議的一部分。
當multiple
參數被設爲true
。RabbitMQ將會確認所有傳遞標籤小於給定數值的消息。比如通道Ch
上有未確認消息,它們的傳遞標籤是5,6,7,8,如果有確認帶的傳遞標籤是8,且multiple
參數被設爲true
,則5-8消息都會被確認;如果multiple
參數被設爲false
,則5,6,7消息仍未被確認。示例如下:
// 假設已存在channel實例
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();
//確認所有傳遞標籤小於deliveryTag的消息已成功傳遞,並丟棄它們
channel.basicAck(deliveryTag, true);
}
});
3, 確認消息失敗並重新入隊消息
有時消費者無法立即處理傳遞來的消息,但其他消費者有能力處理。在這種情況下,可能需要讓消息重新入隊並讓另一個消費者接收和處理它。basic.reject
和basic.nack
是用於此的兩種協議方法。
上面兩個方法通常被用於確認消息傳遞失敗,MQ服務器可以丟棄這些消息或重新入隊。可通過requeue
參數來控制,當設爲true
時MQ服務器會將指定傳遞標籤的消息重新入隊。示例如下:
// 假設已存在channel實例
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();
// 確認傳遞標籤爲deliveryTag的消息傳遞失敗,並丟棄它
channel.basicReject(deliveryTag, false);
}
});
// 假設已存在channel實例
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();
// 傳遞失敗,重新入隊
channel.basicReject(deliveryTag, true);
}
});
如果可能RabbitMQ會將重新入隊的消息還放在它原來的位置,否則就放到儘可能離隊首近的位置。假設某一瞬間出現,所有消費者的預取隊列(prefetch)都已經滿了(無法再接收消息),則會出現一個重新入隊/重新傳遞的循環,造成網絡帶寬和內存資源的消耗。消費者需要追蹤重新傳遞的數量,丟棄確認失敗的消息,或經過一定時延後再重新入隊。
使用 basic.nack
可以同時入隊多條消息,它比basic.reject
方法多了一個multiple
參數,示例如下:
// 假設已存在channel實例
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();
// 確認所有傳遞標籤小於deliveryTag的消息傳遞失敗,並重新入隊它們
channel.basicNack(deliveryTag, true, true);
}
});
在手動確認模式下,如果傳遞的通道關閉或失去連接,則通道上未收到確認的消息會自動重新入隊。這包括客戶端的TCP連接丟失,消費者應用程序(進程)故障以及通道級協議異常。
考慮到消息會重新入隊,消費者需要保證對消息操作的冪等性。被重發的消息,攜帶的redeliver
會被RabbitMQ設置爲true
,這個參數在消息首次發送時是false
。注意,一個消費者可能會收到上一次被其他消費者收到過的消息。
消費者常見錯誤:重複確認和未知標籤
如果消費者對某個傳遞標籤重複確認,會導致RabbitMQ報通道異常:PRECONDITION_FAILED - unknown delivery tag 100
。如果使用不存在的傳遞標籤頁會報相同異常。
其他會報unknown delivery tag
的場景是,確認消息的通道與接收消息的通道不同。謹記,消息傳遞確認和消息傳遞必須是同一通道。
通道預取設置(QoS)
因爲消息是異步發送(推送)到客戶端的,所以在任何給定時刻,通常有一個以上的消息“正在運行”。另外,來自客戶端的手動確認本質上也本質上是異步的。因此,存在一個未確認的傳遞標籤滑動窗口。開發人員通常會希望限制此窗口的大小,以避免在用戶端出現無限制的緩衝區問題。消息協議通過使用basic.qos
方法設置“預取計數”值來實現 。該值定義通道上允許的未確認交付的最大數量。一旦數量達到配置的數量,RabbitMQ將停止在通道上傳遞更多消息,除非已確認至少一個未處理的消息。
例如,假設在通道Ch
上有未確認的傳遞標籤5、6、7和8,並且通道 Ch
的預取計數設置爲4,RabbitMQ將不會在Ch
上傳遞任何消息,除非至少有一個未完成的傳遞被確認。當消費者在通道Ch
上發送確認,deliveryTag
設置爲5,RabbitMQ會注意到並再傳遞一條消息。
預取計數對消息吞吐量的影響
通常,增加預取將提高向消費者傳遞消息的速度,但傳遞但尚未處理的消息的數量也會增加,從而增加了消費者的RAM消耗。找到合適的預取值是一個反覆試驗的問題,並且會因工作負載而異。100到300範圍內的值通常可提供最佳吞吐量,並且不會帶來壓倒消費者的巨大風險。更高的取值經常會碰到收益遞減的規律。