RabbitMQ學習筆記08:Publisher Confirms — RabbitMQ

參考資料:RabbitMQ tutorial - Reliable Publishing with Publisher Confirms 

 

Overview

在這篇 tutorial 中,官方僅提供了 Java 的客戶端。

發佈者確認(Publisher Confirms)是一種 RabbitMQ 擴展用於實現可靠的發佈。當在一個channel上啓用了publisher confirm 的話,客戶端發送的消息會被 mq 服務器異步確認,這表明在服務器端已經接收到這些消息了。

我們有幾種方案(strategies)來實現 publish confirms 用來確保消息安全抵達 mq 服務器。我們會闡釋每種方案各自的優缺點。

 

Enabling Publisher Confirms on a Channel

Publish confirm 是 AMQP 0.9.1 協議的擴展,因此默認情況下它不會被啓用。Publish confirm 的啓用是在 channel 級別使用 confirmSelect 方法。

Channel channel = connection.createChannel();
channel.confirmSelect();

在每一個我們期望使用 publish confirm 功能的 channel 中都需要這麼調用該方法。我們不需要在每次發送消息的時候都調用該方法,在 channel 級別中調用一次即可。

 

Strategy #1: Publishing Message Individually

我們從最簡單的方法開始:發佈一個帶 confirm 的消息,然後等待異步的確認。

while (thereAreMessagesToPublish()) {
    byte[] body = ...;
    BasicProperties properties = ...;
    channel.basicPublish(exchange, queue, properties, body);
    // uses a 5 second timeout
    channel.waitForConfirmsOrDie(5_000);
}

在這個例子中,我們像往常一樣發佈了一個消息並且等待確認,使用了channel.waitForConfirmsOrDie(long)方法。只要消息被確認,這個方法就會被返回。如果在超時時間內沒有確認或者消息被 nack-ed(意味着 mq 服務器出於某些原因沒有辦法處理該消息),那麼這個方法就會拋出一個異常。異常的處理通常包含了錯誤日誌的記錄以及消息重發。

不同的客戶端程序有不同的用於處理 publisher confirms 的方法,請仔細閱讀相關的文檔。

這種方案很直接但是缺點也很明顯:它會顯著降低消息發佈的速率,因爲每次發佈消息之後都需要等待確認或者其他異常,後續的消息發佈必須處於阻塞的狀態。這種方案不適合吞吐量達到每秒幾百條消息的情況。但是它可能適用於某些應用程序。


Are Publisher Confirms Asynchronous?

剛開始我們提到 mq 確認消息發佈的方式是異步地,但是從第一個方案來看卻是同步地(因爲消息必須被確認纔會繼續發佈下一個消息,否則處於阻塞狀態)。客戶端實際上是異步接收確認然後相應地疏通(阻塞 block 的反義詞)針對waitForConfirmsOrDie的調用。把waitForConfirmsOrDie想象成一個同步的助手,它在底層基於異步通知的機制。


 

Strategy #2: Publishing Messages in Batches

在方案2中我們改善之前的示例,我們發送一整批消息並且等待整批消息的確認。假設每一批消息是100條。

int batchSize = 100;
int outstandingMessageCount = 0;
while (thereAreMessagesToPublish()) {
    byte[] body = ...;
    BasicProperties properties = ...;
    channel.basicPublish(exchange, queue, properties, body);
    outstandingMessageCount++;
    if (outstandingMessageCount == batchSize) {
        channel.waitForConfirmsOrDie(5_000);
        outstandingMessageCount = 0;
    }
}
if (outstandingMessageCount > 0) {
    channel.waitForConfirmsOrDie(5_000);
}

等待一整批的消息被確認極大地提高了相對於等待每個消息被確認的吞吐量(大概20-30倍,對於遠程 mq 服務器來說)。缺點是我們不知道具體哪條消息除了問題,因此我們可能需要將整批消息都放在內存中去記錄一些有意義的日誌或者重新發布整批消息。這個方案依然是同步的,所以還是會阻塞,只不過是整批阻塞。

 

Strategy #3: Handling Publisher Confirms Asychronously

要想實現 mq 異步確認消息,只需要在客戶端上註冊一個用於通知這些消息被確認的回調接口即可:

Channel channel = connection.createChannel();
channel.confirmSelect();
channel.addConfirmListener((sequenceNumber, multiple) -> {
    // code when message is confirmed
}, (sequenceNumber, multiple) -> {
    // code when message is nack-ed
});

這裏有兩個回調函數,一個用於已經 confirmed 的消息,另一個用於 nack-ed 的消息。nack-ed 指的是消息被 mq 認爲已經丟失了。每個回調函數有兩個參數:

  • sequence number: 消息的ID,用來識別 confirmed 或者 nack-ed 的消息。一會我們就會看到如何將其和發佈的消息關聯。
  • multiple: 這是一個布爾值。如果是 false 的話,只有一個消息會被 confirmed/nack-ed。如果是 true 的話,所有的帶有更低或者相同 sequence number 的消息會被confirmed/nack-ed。

sequence number 可以在消息被髮布之前使用Channel#getNextPublishSeqNo()獲取到:

int sequenceNumber = channel.getNextPublishSeqNo());
ch.basicPublish(exchange, queue, properties, body);

將消息和 sequence number 關聯起來的一種簡單的方法是使用映射(map)。假設我們想要發佈一些字符串消息,因爲它們比較容易轉換成字節數組用於發送。以下是代碼示例:

ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
// ... code for confirm callbacks will come later
String body = "...";
outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
channel.basicPublish(exchange, queue, properties, body.getBytes());

發佈代碼現在會基於映射來追蹤發出的消息。我們需要清理映射關係,當確認消息已經發布或者當消息 nack-ed 的時候記錄錯誤日誌:

ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
    if (multiple) {
        ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
          sequenceNumber, true
        );
        confirmed.clear();
    } else {
        outstandingConfirms.remove(sequenceNumber);
    }
};

channel.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
    String body = outstandingConfirms.get(sequenceNumber);
    System.err.format(
      "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
      body, sequenceNumber, multiple
    );
    cleanOutstandingConfirms.handle(sequenceNumber, multiple);
});
// ... publishing code

剛纔的代碼包含了清理映射關係的回調函數。這個回調函數在消息發佈被確認後會被使用。針對 nack-ed 消息的回調函數會檢索消息的 body 並且放出警告信息。然後它會重新使用之前的回調函數去清理與未完成的確認之間的映射關係(無論消息是否被 confirm or nack-ed,在 map 中的關係一定要被移除)。

 

How to Track Outstanding Confirms?

我們的示例使用ConcurrentNavigableMap去追蹤未完成的confirms。這種結構出於一些原因是很方便的。它允許簡單地關聯sequence number和消息(無論消息數據是什麼),取消關聯也很簡單(只要提供sequence id)。最後,它支持併發訪問,因爲消息確認的回調函數是在線程中被調用的,這個線程是被客戶端庫所擁有,它和發佈消息的線程是不同的。(可能意思是想表示不同的線程?)

存在其他的方式(而不是精妙的映射實現)用於追蹤未確認的發佈消息,比如使用簡單的併發 hash 映射和一個變量去追蹤更低的發佈序列界限,但是它們更復雜並且不屬於此 tutorial。

 

總之,異步處理髮布者確認通常要求以下步驟:

  • 提供一種方式關聯發佈序列號碼和消息。
  • 在channel上註冊一個確認監聽器,當消息 acks/nacks 抵達的時候做出響應,比如記錄日誌或者重新發送一條 nack-ed 的消息。消息和序列號之間的映射關係可能在這步中需要被解除。
  • 在發出消息之前就要追蹤需要被髮布的消息的序列號。

 

Re-publishing nack-ed Messages?

可以嘗試從關聯的回調函數重新發送一個 nack-ed 消息,但是應該儘量避免這麼做,因爲 confirm 回調函數是位於 I/O 線程中的,在這裏 channels 不被期望做操作。一個更好的解決方案是將消息推入一個內存隊列中,該隊列會被髮送消息的線程輪詢。像ConcurrentLinkedQueue類就是一個比較好的候選者用於在 confirm 回調函數和發送消息的線程之間傳輸消息。

 

 

總結

在某些應用程序中,確保消息被髮送到mq服務器是必不可少的。Publisher confirms 是RabbitMQ的特性用於幫助滿足這個要求。Publisher confirms 本質上是異步的但是可以以同步的方式處理它們。沒有最好的實現 publisher confirms 的方式,這通常要取決於程序或者系統中的約束。典型的技術有:

  • 逐一發送消息,同步等待(阻塞)已發送的每一條消息的確認信息:簡單,但是吞吐量最差。
  • 整批發送消息,同步等待(阻塞)已發送的整批消息的確認信息:簡單,合理的吞吐量,但是當消息丟失的時候很難定位具體的有問題的消息。
  • 異步處理(非阻塞,無需等待):最佳的性能和資源利用,報錯時可定位具體的消息從而做出適當的處理,但是實現的難度較大,需要一定的技術要求。

譯者注:雖然這篇 tutorial 理論不會很難,但是使用 Java 實現我看不太懂,特別是異步處理那部分的代碼,因此剩餘的代碼演示我這裏就沒有做了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章