Pulsar 入門實戰(1)--Pulsar 訊息傳遞

且行且码發表於2024-08-18

本文主要介紹 Pulsar 訊息傳遞的相關概念,對應的 pulsar 版本為 3.3.x。

1、概述

Pulsar 基於釋出-訂閱模式構建。在這種模式中,生產者將訊息釋出到主題;消費者訂閱這些主題,處理傳入的訊息,並在處理完成後向 broker 傳送確認。

當建立訂閱時,即使消費者斷開連線,Pulsar 也會保留所有訊息。只有當消費者確認所有這些訊息都已成功處理後,保留的訊息才會被丟棄。
如果訊息消費失敗並且希望重新消費該訊息,可以啟用訊息重投遞機制,讓 broker 重新傳送該訊息。

2、訊息

訊息是 Pulsar 的基本“單元”。它們是生產者釋出到主題的內容,也是消費者從主題中消費的內容。下表列出了訊息的組成部分。

元件 說明
Value / data payload 訊息的內容
Key 訊息的 key
Properties 訊息的屬性,使用者定義的鍵值對
Producer name 生產者的名稱,如果沒有指定,將自動生成
Topic name 主題名稱
Schema version 訊息所使用模式的版本號
Sequence ID 訊息的序列 ID
Message ID 訊息 ID
Publish time 訊息釋出的時間戳
Event time 由應用程式附加到訊息上的可選時間戳。例如,應用程式可以附加訊息處理的時間戳。預設為 0。

訊息的預設最大大小為 5 MB。可以修改如下配置來調整訊息的最大大小:

A、broker.conf

# The max size of a message (in bytes).
maxMessageSize=5242880

B、bookkeeper.conf

# The max size of the netty frame (in bytes). Any messages received larger than this value are rejected. The default value is 5 MB.
nettyMaxFrameSizeBytes=5253120

2.1、Acknowledgment(訊息確認)

消費者在成功消費一條訊息後,會向 broker 傳送一條訊息確認。訊息會被永久儲存,直到所有訂閱已確認該訊息後才會被刪除。確認(ack)是Pulsar判斷訊息可以從系統中刪除的一種方式。如果您想儲存已被消費者確認的訊息,需要配置訊息保留策略。

對於批次訊息,可以啟用批次索引確認以避免向消費者重新傳送已確認的訊息。

訊息可以透過以下兩種方式進行確認:

  • 單獨確認 消費者對每條訊息進行確認,向 broker 傳送確認請求。
  • 累計確認 消費者只確認它接收到的最後一條訊息。在流中,直到(包括)提供的那條訊息為止,所有訊息都不會重新投遞給該消費者。

單獨確認 API:

consumer.acknowledge(msg);

累計確認 API:

consumer.acknowledgeCumulative(msg);

注意:累計確認不能用於共享(Shared)或鍵共享(Key_Shared)訂閱型別,因為共享或鍵共享訂閱型別涉及多個消費者訪問相同的訂閱。在共享訂閱型別中,訊息是單獨確認的。

2.2、Negative acknowledgment(否定確認)

否定確認機制允許向 broker 傳送通知,指示消費者未處理某條訊息。當消費者未能成功消費一條訊息並需要重新消費時,消費者會向 broker 傳送一個否定確認(nack),觸發 broker 將這條訊息重新投遞給消費者。

根據訂閱型別不同,訊息可以以單獨或累積方式進行否定確認。
在 Exclusive 和 Failover 訂閱型別中,消費者可用累計方式進行否定確認。
在 Shared 和 Key_Shared 訂閱型別中,消費者可可用單獨方式進行否定確認。

在有序訂閱型別(如 Exclusive、Failover 和 Key_Shared)上使用否定確認可能會導致失敗的訊息按非原始順序傳送給消費者。

如果你打算對某條訊息使用否定確認,請確保在確認超時之前進行否定確認。

否定確認 API:

Consumer<byte[]> consumer = pulsarClient.newConsumer()
                .topic(topic)
                .subscriptionName("sub-negative-ack")
                .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
                .negativeAckRedeliveryDelay(2, TimeUnit.SECONDS) // the default value is 1 min
                .subscribe();

Message<byte[]> message = consumer.receive();

// call the API to send negative acknowledgment
consumer.negativeAcknowledge(message);

message = consumer.receive();
consumer.acknowledge(message);

要以不同的延遲重新投遞訊息,你可以透過設定訊息重投次數來使用重投遞退避機制。

Consumer<byte[]> consumer = pulsarClient.newConsumer()
        .topic(topic)
        .subscriptionName("sub-negative-ack")
        .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
        .negativeAckRedeliveryBackoff(MultiplierRedeliveryBackoff.builder()
            .minDelayMs(1000)
            .maxDelayMs(60 * 1000)
            .multiplier(2)
            .build())
        .subscribe();

訊息重投時延如下:

重投訊息數 重投時延
1 1 秒
2 2 秒
3 4 秒
4 8 秒
5 16 秒
6 32 秒
7 60 秒
8 60 秒

注意:如果啟用了批處理,批處理中的所有訊息都會重新投遞給消費者。

2.3、Acknowledgment timeout(確認超時)

注意:預設情況下,確認超時是禁用的,這意味著傳送給消費者的訊息不會被重新投遞,除非消費者崩潰。

確認超時機制允許你設定一個時間,用於客戶端跟蹤未確認的訊息。在達到確認超時時間(ackTimeout)後,客戶端會向 broker 傳送重新投遞未確認訊息的請求,從而使 broker 將未確認的訊息重新傳送給消費者。

你可以配置確認超時機制,在 ackTimeout 之後重新投遞訊息,定時任務會在每個 ackTimeoutTickTime 週期檢查確認超時的訊息。
你還可以使用重投遞迴退機制,透過設定訊息重投的次數,以不同的延遲時間重新投遞訊息。

重投遞迴退機制 API:

consumer.ackTimeout(10, TimeUnit.SECOND)
        .ackTimeoutRedeliveryBackoff(MultiplierRedeliveryBackoff.builder()
            .minDelayMs(1000)
            .maxDelayMs(60 * 1000)
            .multiplier(2)
            .build());

訊息重投時延如下:

重投訊息數 重投時延
1 10 + 1 秒
2 10 + 2 秒
3 10 + 4 秒
4 10 + 8 秒
5 10 + 16 秒
6 10 + 32 秒
7 10 + 60 秒
8 10 + 60 秒

注意:

如果啟用了批處理,一個批次中的所有訊息都會重新投遞給消費者。
與確認超時相比,否定確認是首選。首先,設定超時值很困難。其次,當訊息處理時間超過確認超時時,broker 會重新傳送訊息,但這些訊息可能不需要被重新消費。

確認超時 API:

Consumer<byte[]> consumer = pulsarClient.newConsumer()
                .topic(topic)
                .ackTimeout(2, TimeUnit.SECONDS) // the default value is 0
                .ackTimeoutTickTime(1, TimeUnit.SECONDS) //定時檢查確認超時訊息的時間間隔
                .subscriptionName("sub")
                .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
                .subscribe();

Message<byte[]> message = consumer.receive();

// wait at least 2 seconds
message = consumer.receive();
consumer.acknowledge(message);

2.4、Retry letter topic(重試信主題)

重試信主題允許你儲存未能被消費的訊息,並在稍後重新嘗試消費它們。透過這種方法,你可以自定義訊息重新投遞的間隔。原始主題上的消費者也會自動訂閱重試訊息主題。一旦達到最大重試次數,未被消費的訊息將被移動到一個死信主題進行手動處理。重試信主題的功能是由消費者實現的。

使用重試信主題與使用延遲訊息傳遞的意圖不同,儘管它們都旨在稍後消費訊息。重試信主題透過訊息重新投遞來處理失敗,以確保關鍵資料不會丟失,而延遲訊息傳遞則旨在在指定的延遲時間傳遞訊息。

預設情況下,重試是禁用的。你可以將 enableRetry 設定為 true,以在消費者上啟用重試功能。

可用使用以下 API 來從重試信主題消費訊息;當達到 maxRedeliverCount 的值時,未被消費的訊息將會被移動到死信主題。

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
                .topic("my-topic")
                .subscriptionName("my-subscription")
                .subscriptionType(SubscriptionType.Shared)
                .enableRetry(true)
                .deadLetterPolicy(DeadLetterPolicy.builder()
                        .maxRedeliverCount(maxRedeliveryCount)
                        .build())
                .subscribe();

預設的重試信主題格式如下:

<topicname>-<subscriptionname>-RETRY

透過程式碼指定重試信主題:

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
        .topic("my-topic")
        .subscriptionName("my-subscription")
        .subscriptionType(SubscriptionType.Shared)
        .enableRetry(true)
        .deadLetterPolicy(DeadLetterPolicy.builder()
                .maxRedeliverCount(maxRedeliveryCount)
                .retryLetterTopic("my-retry-letter-topic-name")
                .build())
        .subscribe();

重試信主題中的訊息包含一些特殊屬性,這些屬性是由客戶端自動建立的。

屬性 描述
REAL_TOPIC 實際的主題
ORIGIN_MESSAGE_ID 訊息原始 message ID
RECONSUMETIMES 消費訊息的重試次數
DELAY_TIME 訊息重試間隔,單位為毫秒

可使用以下 API 將訊息儲存在重試信主題中:

consumer.reconsumeLater(msg, 3, TimeUnit.SECONDS);

可以使用以下 API 新增自定義屬性。在下一次消費時,可以透過 message#getProperty 獲取自定義屬性。

Map<String, String> customProperties = new HashMap<String, String>();
customProperties.put("custom-key-1", "custom-value-1");
customProperties.put("custom-key-2", "custom-value-2");
consumer.reconsumeLater(msg, customProperties, 3, TimeUnit.SECONDS);

注意:目前,在共享訂閱型別中啟用了重試主題。 與否定確認相比,重試信主題更適合需要大量重試且具有可配置重試間隔的訊息。因為重試主題中的訊息被持久化到了 BookKeeper,而因否定確認需要重試的訊息則被快取在客戶端。

2.5、Dead letter topic(死信主題)

死信主題允許您在某些訊息未成功消費時繼續訊息的消費。那些未能成功消費的訊息會被儲存在一個特定的主題中,稱為死信主題。死信主題的功能由消費者實現。您可以決定如何處理死信主題中的訊息。

啟用預設的死信主題:

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
                .topic("my-topic")
                .subscriptionName("my-subscription")
                .subscriptionType(SubscriptionType.Shared)
                .deadLetterPolicy(DeadLetterPolicy.builder()
                      .maxRedeliverCount(maxRedeliveryCount)
                      .build())
                .subscribe();

預設死信主題格式如下:

<topicname>-<subscriptionname>-DLQ

死信主題的生產者名稱格式如下:

<topicname>-<subscriptionname>-<consumername>-DLQ

指定死信主題名稱:

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
                .topic("my-topic")
                .subscriptionName("my-subscription")
                .subscriptionType(SubscriptionType.Shared)
                .deadLetterPolicy(DeadLetterPolicy.builder()
                      .maxRedeliverCount(maxRedeliveryCount)
                      .deadLetterTopic("my-dead-letter-topic-name")
                      .build())
                .subscribe();

預設情況下,在建立 DLQ 主題時不會建立訂閱。如果在 DLQ 主題上沒有即時訂閱,可能會丟失訊息。為了自動建立 DLQ 的初始訂閱,您可以指定 initialSubscriptionName 引數。如果設定了這個引數,但是 Broker 的 allowAutoSubscriptionCreation 被禁用,DLQ 的生產者將無法建立。

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
                .topic("my-topic")
                .subscriptionName("my-subscription")
                .subscriptionType(SubscriptionType.Shared)
                .deadLetterPolicy(DeadLetterPolicy.builder()
                      .maxRedeliverCount(maxRedeliveryCount)
                      .deadLetterTopic("my-dead-letter-topic-name")
                      .initialSubscriptionName("init-sub")
                      .build())
                .subscribe();

initialSubscriptionName 只是為了訊息不丟失建立的訂閱,並不是本消費者訂閱了該死信主題;需要另外寫程式處理死信主題中的訊息或手工處理。

死信主題用於儲存未成功消費的訊息,觸發條件包括確認超時、否定確認或重試信主題。

注意:目前,死信主題已在共享(Shared)和鍵共享(Key_Shared)訂閱型別中啟用。

2.6、Compression(壓縮)

訊息壓縮可以透過小號一些 CPU 開銷來減小訊息大小。Pulsar 客戶端支援的壓縮型別:LZ4、ZLIB、ZSTD、SNAPPY。

壓縮型別儲存在訊息的後設資料中,因此消費者可以根據需要自動採用不同的壓縮型別。

生產者中啟用壓縮:

client.newProducer()
    .topic("topic-name")
    .compressionType(CompressionType.LZ4)
    .create();

2.7、Batching(批處理)

當啟用批處理時,生產者會累積並在單個請求中傳送一批訊息。批次大小由最大訊息數和最大發布延遲定義。因此,積壓大小表示的是批的總數,而不是訊息的總數。

在 Pulsar 中,批次被作為單個單位進行跟蹤和儲存,而不是作為單獨的訊息。消費者將一個批次解開成單獨的訊息。然而,即使啟用了批處理,透過 deliverAt 或 deliverAfter 引數配置的預定訊息始終會作為單獨的訊息傳送。

通常情況下,一個批次在所有訊息被消費者確認後才會被確認。這意味著如果批次中不是所有訊息都被確認,可能由於意外故障、否定確認(NACK)或確認超時,會導致重新投遞該批次中的所有訊息。

為了避免將已確認的批次訊息重新投遞給消費者,Pulsar 從版本 2.6.0 開始引入了批次索引確認功能。啟用批次索引確認後,消費者會過濾已確認的批次索引,並向 broker 傳送批次索引確認請求。broker 會維護批次索引的確認狀態,並跟蹤每個批次索引的確認狀態,以避免向消費者分發已確認的訊息。當批次中所有訊息的索引都被確認時,該批次會被刪除。

預設情況下,批次索引確認是禁用的(acknowledgmentAtBatchIndexLevelEnabled=false)。可以在 broker 中將 acknowledgmentAtBatchIndexLevelEnabled 引數設定為 true 來啟用批次索引確認。啟用批次索引確認會導致更多的記憶體開銷。

批次索引確認還必須在消費者端透過呼叫 .enableBatchIndexAcknowledgment(true) 來啟用:

Consumer<byte[]> consumer = pulsarClient.newConsumer()
        .topic(topicName)
        .subscriptionName(subscriptionName)
        .subscriptionType(subType)
        .enableBatchIndexAcknowledgment(true)
        .subscribe();

注意:非同步傳送訊息(sendAsync)時批處理才會生效。

2.8、Chunking(分塊)

訊息分塊允許 Pulsar 在生產者端將大訊息分割成多個塊,並在消費者端將分塊的訊息聚合起來處理。

啟用訊息分塊後,當訊息大小超過允許的最大有效載荷大小(即 broker 的 maxMessageSize 引數)時,訊息傳遞的工作流程如下:

  1. 生產者將原始訊息分割為分塊訊息(並帶有分塊後設資料),將它們按順序分別傳送到 broker。
  2. broker 將分塊訊息以與普通訊息相同的方式儲存在同一管理 ledger 中,並使用 chunkedMessageRate 引數記錄主題上的分塊訊息速率。
  3. 消費者快取分塊訊息,在接收到訊息的所有分塊後,將分塊訊息聚合到接收佇列中。
  4. 客戶端從接收佇列消費聚合後的訊息。

注意:

分塊僅適用於持久化主題。
分塊不能與批處理同時啟用。在啟用分塊之前,需要先禁用批處理。

2.8.1、使用有序消費者處理連續的分塊訊息

下圖顯示了一個主題,其中有一個生產者釋出了分塊訊息和常規非分塊訊息。生產者將訊息 M1 分為三個標記為 M1-C1、M1-C2 和 M1-C3 的分塊訊息。broker 將所有三個分塊訊息儲存在管理 ledger 中,並按相同順序將它們傳送到有序(獨佔/故障轉移)消費者。消費者在記憶體中緩衝分塊訊息,直到接收到所有分塊訊息,然後將它們聚合成一條訊息,最後將原始訊息 M1 交給客戶端。

2.8.2、使用有序消費者處理交織的分塊訊息

當多個生產者將分塊訊息釋出到單個主題時,broker 將來自不同生產者的所有分塊訊息儲存在同一個管理 ledger 中。管理 ledger 中的分塊訊息可能會交織在一起。如下所示,生產者1將訊息 M1 分為三個分塊訊息 M1-C1、M1-C2 和 M1-C3。生產者2將訊息 M2 分為三個分塊訊息 M2-C1、M2-C2 和 M2-C3。特定訊息的所有分塊訊息仍然是有序的,但在管理 ledger 中可能不是連續的。

注意:在這種情況下,交織的分塊訊息可能會給消費者帶來一些記憶體壓力,因為消費者為每個大訊息保留一個單獨的緩衝區,以將其所有分塊訊息聚合成一條訊息。你可以透過配置maxPendingChunkedMessage 引數來限制消費者同時維護的最大分塊訊息數。當達到閾值時,消費者透過靜默確認來丟棄待處理的訊息或請求 broker 稍後重新傳遞它們,以最佳化記憶體利用。

2.8.3、啟用訊息分塊

前提:將 enableBatching 引數設定為 false 來禁用批處理。

訊息分塊功能預設處於關閉狀態。要啟用訊息分塊,在建立生產者時將 chunkingEnabled 設定為 true。

注意:如果消費者在指定時間(expireTimeOfIncompleteChunkedMessage)內未能接收到訊息的所有分塊,則未完成的分塊將過期。過期時間預設值為 1 分鐘。

3、主題

Pulsar 主題是一種儲存單元,用於將訊息組織成流。與其他釋出-訂閱系統類似,Pulsar 中的主題是用於從生產者傳輸訊息到消費者的通道。主題名稱是具有明確定義結構的 URL:

{persistent|non-persistent}://tenant/namespace/topic
主題名稱元件 描述
persistent/non-persistent Pulsar 支援兩種型別的主題:持久化主題和非持久化主題。預設情況下是持久化主題,對於持久化主題,所有訊息都會持久化儲存在磁碟上(如果 broker 不是獨立的,訊息將在多個磁碟上持久化儲存),而非持久化主題的資料則不會持久化到儲存磁碟上。
tenant 主題租戶,租戶在 Pulsar 中對於多租戶架構至關重要。
namespace 主題名稱空間,每個租戶擁有一個或多個名稱空間。
topic 主題名稱

注意:在 Pulsar 中,你不需要顯式地建立主題。如果客戶端嘗試向一個尚不存在的主題寫入或接收訊息,Pulsar 會自動建立該主題。如果客戶端在建立主題時沒有指定租戶或名稱空間,那麼該主題會被建立在預設的租戶和名稱空間中。你也可以在指定的租戶和名稱空間中建立主題,例如 persistent://my-tenant/my-namespace/my-topic。

4、名稱空間

Pulsar 名稱空間是主題的邏輯分組,同時也是租戶內的邏輯機率。租戶透過管理 API 建立名稱空間。例如,具有多個應用程式的租戶可以為每個應用程式建立單獨的名稱空間。名稱空間允許應用程式建立和管理主題的層次結構。例如,主題 my-tenant/app1 是租戶 my-tenant 下應用程式 app1 的名稱空間。你可以在名稱空間下建立任意數量的主題。

5、訂閱

Pulsar 訂閱是一個命名的配置規則,確定訊息如何傳遞給消費者。它是由一組消費者在主題上建立的租約。Pulsar 有四種訂閱型別:

  • 獨佔訂閱(exclusive)
  • 共享訂閱(shared)
  • 故障轉移訂閱(failover)
  • 鍵共享(key_shared)

提示:

在 Pulsar 中,你可以靈活地使用不同的訂閱來實現釋出-訂閱或佇列的效果。
1、如果你希望在消費者之間實現傳統的“廣播式釋出-訂閱訊息”,可以為每個消費者指定一個唯一的訂閱名稱,這是一種獨佔式訂閱型別。
2、如果你希望在消費者之間實現“訊息佇列”,可以讓多個消費者共享相同的訂閱名稱(共享訂閱、故障轉移訂閱、鍵共享訂閱)。
3、如果你希望同時實現這兩種效果,可以將獨佔式訂閱型別與其他訂閱型別結合使用,為消費者建立不同的訂閱。

5.1、訂閱型別

當一個訂閱沒有消費者時,其訂閱型別是未定義的。訂閱的型別在有消費者連線時確定,並且可以透過重新啟動所有消費者並使用不同的配置來更改訂閱型別。

5.1.1、獨佔訂閱(Exclusive)

獨佔訂閱只允許單個消費者連線到該訂閱。如果多個消費者使用相同的訂閱名稱訂閱同一個主題,會發生錯誤。需要注意的是,如果主題是分割槽的,所有分割槽都將由允許連線到該訂閱的單個消費者來消費。

在下圖中,只有消費者A被允許消費訊息。

提示:獨佔訂閱是預設的訂閱型別。

5.1.2、故障轉移訂閱(Failover)

故障轉移訂閱允許多個消費者連線到同一個訂閱上。
對於非分割槽主題或分割槽主題的每個分割槽,會選擇一個主消費者來接收訊息。
當主消費者斷開連線時,所有(未確認的和隨後的)訊息將被傳遞給下一個排隊的消費者。

注意:在某些情況下,一個分割槽可能存在一個較舊的活動消費者在處理訊息,同時一個新切換的活動消費者開始接收新訊息。這可能導致訊息重複或順序錯亂的問題發生。

故障轉移 | 分割槽主題

對於分割槽主題,broker 按照消費者的優先順序和消費者名稱的詞典順序進行排序。broker 嘗試將分割槽均勻地分配給優先順序最高的消費者。消費者是透過執行一個模運算 mod(partition index, consumer index)來選擇的。

A、如果分割槽主題中的分割槽數量少於消費者數量

例如,在下圖中,這個分割槽主題有 2 個分割槽,並且有 4 個消費者。每個分割槽有 1 個活動消費者和 3 個備用消費者。
對於 P0,消費者A是主消費者,而消費者B、消費者C 和消費者D是備用消費者。
對於 P1,消費者B是主消費者,而消費者A、消費者C 和消費者D是備用消費者。
此外,如果消費者A和消費者B都斷開連線,那麼
對於 P0:消費者C是活動消費者,消費者D是備用消費者。
對於 P1:消費者D是活動消費者,消費者C是備用消費者。

B、如果分割槽主題中的分割槽數量多於消費者數量

例如,在下圖中,這個分割槽主題有 9 個分割槽和 3 個消費者。
P0、P3 和 P6 分配給消費者A。消費者A是它們的活躍消費者。消費者B和消費者C是它們的備用消費者。
P1、P4 和 P7 分配給消費者B。消費者B是它們的活躍消費者。消費者A和消費者C是它們的備用消費者。
P2、P5 和 P8 分配給消費者C。消費者C是它們的活躍消費者。消費者A和消費者B是它們的備用消費者。

故障轉移 | 非分割槽主題

A、如果是一個非分割槽主題,那麼 broker 會按照消費者訂閱非分割槽主題的順序選擇它們。

例如,在下面的圖表中,有 1 個非分割槽主題,2 個消費者。
該主題有 1 個活躍消費者和 1 個備用消費者。
消費者A是主要消費者,如果消費者A斷開連線,消費者B將成為下一個接收訊息的消費者。

B、如果存在多個非分割槽主題,消費者的選擇是基於雜湊消費者名稱和雜湊主題名稱。客戶端使用相同的訂閱名稱訂閱所有主題。

例如,在下面的圖表中,有 4 個非分割槽主題和 2 個消費者。
非分割槽主題 1 和非分割槽主題 4 分配給消費者 A。消費者 B 是它們的備用消費者。
非分割槽主題 2 和非分割槽主題 3 分配給消費者 B。消費者 A 是它們的備用消費者。

5.1.3、共享訂閱(Shared)

Pulsar 中的共享訂閱型別允許多個消費者連線到同一個訂閱。訊息以迴圈分發方式傳送到各個消費者,並且每條訊息只會傳送到一個消費者那裡。當一個消費者斷開連線時,所有已傳送但未被確認的訊息將被重新安排傳送給其餘的消費者。

在下面的圖表中,Consumer A、Consumer B 和 Consumer C 都可以訂閱該主題。

注意:共享訂閱不保證訊息順序或不支援累積確認。

5.1.4、鍵共享訂閱(Shared)

Pulsar 中的建共享訂閱型別允許多個消費者連線到同一個訂閱。但與共享型別不同,鍵共享型別是把具有相同鍵或相同排序鍵的訊息送給同一個消費者。無論訊息被重新傳遞多少次,它都會傳遞給同一個消費者。

注意:

如果有新切換的活躍消費者,它將從舊的非活躍消費者確認訊息的位置開始讀取訊息。
舉例來說,如果 P0 被分配給 Consumer A。Consumer A 是活躍消費者,而 Consumer B 是備用消費者。
如果 Consumer A 斷開連線而沒有讀取任何來自 P0 的訊息,在新增 Consumer C 並使其成為新的活躍消費者後,Consumer C 將直接開始從 P0 讀取訊息。
如果Consumer A 在從 P0 讀取訊息(0,1,2,3)後斷開連線,當新增 Consumer C 並使其成為活躍消費者後,Consumer C 將開始從 P0 讀取訊息(4,5,6,7)。

有三種對映演算法決定如何為給定的訊息鍵(或排序鍵)選擇消費者:

  • 自動分割雜湊範圍(Auto-split Hash Range)
  • 自動分割一致性雜湊(Auto-split Consistent Hashing)
  • 粘性(Sticky)

每種演算法都有其獨特的方式來將訊息分配給消費者,以確保訊息的有效處理和負載均衡。

所有對映演算法的步驟如下:
1、訊息鍵(或排序鍵)傳遞給雜湊函式(例如,Murmur3 32-bit),生成一個 32 位整數雜湊值。
2、該雜湊值傳遞給演算法,從現有的連線消費者中選擇一個消費者。

                      +--------------+                              +-----------+
Message Key ----->  / Hash Function / ----- hash (32-bit) -------> / Algorithm / ----> Consumer
                   +---------------+                               +----------+

當一個新的消費者連線並因此被新增到已連線消費者列表時,演算法會重新調整對映,使當前對映到現有消費者的一些鍵被對映到新新增的消費者。當一個消費者斷開連線並因此從已連線消費者列表中移除時,對映到該消費者的鍵將被對映到其他消費者。

Auto-split Hash Range

自動分割雜湊範圍(Auto-split Hash Range)假設每個消費者被對映到 0 到 2^16(65,536)範圍內的某個區域;所有的對映區域覆蓋整個範圍,並且沒有重疊。透過對訊息雜湊進行取模(取模的大小為65,536)運算,得到的數字(0 <= i < 65,536)包含在某個區域內;對映到該區域的消費者就是被選擇的消費者。

例子:

假設我們有4個消費者(C1、C2、C3和C4),那麼:

 0               16,384            32,768           49,152             65,536
 |------- C3 ------|------- C2 ------|------- C1 ------|------- C4 ------|

給定訊息鍵 Order-3459134,其雜湊值為 murmur32("Order-3459134") = 3112179635,它在範圍內的索引為 3112179635 mod 65536 = 6067。該索引包含在區域 [0, 16384) 內,因此消費者 C3 將被對映到此訊息鍵。

當一個新的消費者連線時,選擇範圍最大的區域,並將其一分為二——下半部分將對映到新新增的消費者,上半部分將對映到原來擁有該區域的消費者。以下是從 1 個消費者增加到 4 個消費者的情況:

C1 connected:
|---------------------------------- C1 ---------------------------------|

C2 connected:
|--------------- C2 ----------------|---------------- C1 ---------------|

C3 connected:
|------- C3 ------|------- C2 ------|---------------- C1 ---------------|

C4 connected:
|------- C3 ------|------- C2 ------|------- C4 ------|------- C1 ------|

當一個消費者斷開連線時,其區域將合併到其右側的區域中。例子:

C4 斷開連線:

|------- C3 ------|------- C2 ------|---------------- C1 ---------------|

C1 斷開連線:

|------- C3 ------|-------------------------- C2 -----------------------|

這種演算法的優點是在新增或刪除消費者時隻影響單個現有消費者,但代價是區域大小不均勻。這意味著某些消費者會獲得比其他消費者更多的鍵。下一個演算法則相反。

Auto-split Consistent Hashing

自動拆分一致性雜湊(Auto-split Consistent Hashing)假設每個消費者被對映到一個雜湊環中。雜湊環是一個從 0 到 MAX_INT(32 位整數)的數值範圍,當你遍歷這個範圍時,到達 MAX_INT 後,下一個數字將會是 0。這就好像把一條從 0 開始到 MAX_INT 結束的線彎成一個圓,使得終點與起點連線在一起:

 MAX_INT -----++--------- 0
              ||
         , - ~ ~ ~ - ,
     , '               ' ,
   ,                       ,
  ,                         ,
 ,                           ,
 ,                           ,
 ,                           ,
  ,                         ,
   ,                       ,
     ,                  , '
       ' - , _ _ _ ,  '

當新增一個消費者時,我們在那個圓上標記 100 個點,並將它們與新新增的消費者關聯。對於 1 到 100 之間的每個數字,我們將該數字與消費者名稱連線起來,然後對其執行雜湊函式,以獲取將在圓上標記的點的位置。例如,如果消費者名稱是 "orders-aggregator-pod-2345-consumer",那麼我們會在圓上標記 100 個點:

    murmur32("orders-aggregator-pod-2345-consumer1") = 1003084738
    murmur32("orders-aggregator-pod-2345-consumer2") = 373317202
    ...
    murmur32("orders-aggregator-pod-2345-consumer100") = 320276078

由於雜湊函式具有均勻分佈的屬性,這些點將在圓上均勻分佈。

透過將訊息鍵的雜湊值放置在圓上,然後順時針沿著圓繼續前進,直到到達一個標記點,來為給定的訊息鍵選擇一個消費者。該點可能會有多個消費者(雜湊函式可能會產生碰撞),因此,我們執行以下操作來獲取消費者列表中的一個消費者:hash % consumer_list_size = index。

當新增一個消費者時,我們會如前所述在圓上新增 100 個標記點。由於雜湊函式的均勻分佈,這 100 個點會使得新消費者從每個現有消費者中分出一小部分鍵。它保持了均勻分佈,但代價是影響到所有現有的消費者。

Sticky

粘性(Sticky)假定每個消費者被對映到從 0 到 2^16(65,536)的某個區域,並且區域之間沒有重疊。透過對訊息雜湊值進行模運算(除以範圍大小 65,536),得到的數字(0 <= i < 65,536),將位於某個區域內;對映到該區域的消費者即為所選的消費者。

在這個演算法中,你擁有完全的控制權。每個新新增的消費者可以透過 Consumer API 指定希望對映到的範圍。你需要確保沒有重疊,並且所有範圍都被區域覆蓋。

例子:
假設我們有兩個消費者(C1 和 C2),每個消費者都指定了他們的範圍,那麼:

C1 = [0, 16384), [32768, 49152)
C2 = [16384, 32768), [49152, 65536)

 0               16,384            32,768           49,152             65,536
 |------- C1 ------|------- C2 ------|------- C1 ------|------- C2 ------|

給定訊息鍵 Order-3459134,其雜湊值為 murmur32("Order-3459134") = 3112179635,其索引為 3112179635 mod 65536 = 6067。這個索引位於 [0, 16384) 範圍內,因此這條訊息將被路由到消費者 C1。

如果新連線的消費者沒有提供其範圍,或者其範圍與現有消費者的範圍重疊,那麼該消費者將被斷開連線,從消費者列表中移除,並且被視為從未嘗試過連線。

如何使用對映演算法?

要使用上述提到的對映演算法,可以在構建消費者時指定 keySharedPolicy。

  • AUTO_SPLIT - Auto-split Hash Range
  • STICKY - Sticky

如果 broker 中啟用了 subscriptionKeySharedUseConsistentHashing,則會使用一致性雜湊(Consistent Hashing)來分割,而不是使用雜湊範圍(Hash Range)。

保持處理順序

鍵共享訂閱型別保證任何時刻一個關鍵字只會由一個消費者處理。當新的消費者連線時,一些關鍵字的對映將從現有消費者轉移到新的消費者身上。連線建立後,broker 將記錄當前的讀取位置,並將其與新的消費者關聯。讀取位置是一個標記,表示這點及之前的訊息已分發給消費者,此後的訊息尚未被分發。只有當讀取位置及之前的訊息都被確認後,broker 才會向新的消費者傳遞訊息。這將確保特定關鍵字在任何給定時間只由一個消費者處理。然而,這樣做的代價是,如果現有的某個消費者卡住了且沒有定義確認超時,新的消費者將無法收到任何訊息,直到卡住的消費者恢復或斷開連線。

可以透過 Consumer API 啟用 allowOutOfOrderDelivery 來放寬這一要求。如果在新消費者上設定了這個選項,那麼在它連線時,broker 將允許它接收訊息,即使該關鍵字的某些訊息在其他消費者中仍在處理中,因此在新增新消費者的短時間內可能會影響順序。

鍵共享訂閱批處理

注意:當消費者使用鍵共享訂閱型別時,你需要禁用批處理或者為生產者使用基於關鍵字的批處理。

使用基於關鍵字的批處理在鍵共享訂閱型別中是必要的,原因有兩個:

1、broker 根據訊息的鍵分發訊息,但預設的批處理方法可能無法將具有相同鍵的訊息打包到同一個批次中。
2、由於是消費者而不是 broker 從批次中分發訊息,因此一個批次中第一條訊息的鍵被視為該批次中所有訊息的鍵,從而導致上下文錯誤。

基於關鍵字的批處理旨在解決上述問題。這種批處理方法確保生產者將具有相同鍵的訊息打包到同一個批次中。沒有鍵的訊息被打包到一個批次中,這個批次沒有鍵;當 broker 分發這個批次訊息時,它會使用 NON_KEY 作為鍵。基於關鍵字的批處理,生成者的 batchingMaxMessages 引數是針對的所有不同 key 訊息的總數,當訊息達到該數量時將會按鍵分別打包傳送。

以下是鍵共享訂閱型別下啟用基於鍵的批處理示例:

Producer<byte[]> producer = client.newProducer()
        .topic("my-topic")
        .batcherBuilder(BatcherBuilder.KEY_BASED)
        .create();

注意:

使用鍵共享訂閱時,需注意以下幾點:
1、你需要為訊息指定一個鍵或排序鍵。
2、不能使用累積確認機制。
3、當主題中最新訊息的位置為 X 時,新連線的鍵共享消費者將不會接收任何訊息直到 X 之前(包括)的訊息都被確認。

5.2、訂閱模式

5.2.1、什麼是訂閱模式

訂閱模式指示遊標屬於持久型別還是非持久型別。
當建立訂閱時,會建立一個關聯的遊標來記錄最後消費的位置。
當訂閱的消費者重新啟動時,它可以繼續從上次消費的位置開始消費。

訂閱模式 描述 注意
持久 游標是持久的,它會保留訊息並持久化當前位置。代理因故障重新啟動,可以從持久儲存(BookKeeper)中恢復遊標,使得可以從上次消費的位置繼續消費訊息。 預設的訂閱模式
非持久 游標是非持久的。一旦 broker 停止執行,遊標將丟失且無法恢復,因此無法繼續從上次消費的位置消費訊息。 Reader 的訂閱模式是非永續性的,並且不會阻止主題中的資料被刪除。Reader 的訂閱模式無法更改。

一個訂閱可以有一個或多個消費者。當消費者訂閱一個主題時,必須指定訂閱名稱。持久訂閱和非持久訂閱可以使用相同的名稱,它們彼此獨立。如果消費者指定了一個之前不存在的訂閱,該訂閱會自動建立。

5.2.2、什麼時候使用

預設情況下,沒有任何持久訂閱的主題的訊息會被標記為已刪除。如果你希望阻止訊息被標記為已刪除,可以為該主題建立一個持久訂閱。在這種情況下,只有被確認的訊息才會被標記為已刪除。更多資訊,可參閱訊息保留和過期

5.2.3、如何使用

建立消費者後,消費者的訂閱模式預設是持久的。可以透過更改消費者的配置將訂閱模式更改為非持久。

Consumer<byte[]> consumer = pulsarClient.newConsumer()
                .topic("my-topic")
                .subscriptionName("my-sub")
                .subscriptionMode(SubscriptionMode.Durable)
                .subscribe();

關於如何建立、檢查或刪除持久訂閱,可檢視訂閱管理

6、多主題訂閱

當消費者訂閱主題時,預設情況下它訂閱一個特定的主題,比如 persistent://public/default/my-topic。然而,從 Pulsar 1.23.0-incubating 開始,消費者可以同時訂閱多個主題。您可以透過兩種方式定義主題列表:

  • 基於正規表示式(regex)來定義,例如 persistent://public/default/finance-.*
  • 顯式地定義一個主題列表

注意:當透過正規表示式訂閱多個主題時,所有主題必須位於同一個名稱空間。

當訂閱多個主題時,Pulsar 客戶端會自動發現與正規表示式或主題列表匹配的主題,然後訂閱所有這些主題。如果其中某些主題不存在,消費者在主題被建立後會自動訂閱它們。

注意:在多個主題之間沒有順序保證。當生產者向單個主題傳送訊息時,所有訊息都保證按照傳送順序消費。然而,這些保證在多個主題之間並不適用。因此,當生產者向多個主題傳送訊息時,從這些主題讀取訊息的順序不能保證是相同的。

多主題訂閱示例:

import java.util.regex.Pattern;

import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.PulsarClient;

PulsarClient pulsarClient = // Instantiate Pulsar client object

// Subscribe to all topics in a namespace
Pattern allTopicsInNamespace = Pattern.compile("persistent://public/default/.*");
Consumer<byte[]> allTopicsConsumer = pulsarClient.newConsumer()
                .topicsPattern(allTopicsInNamespace)
                .subscriptionName("subscription-1")
                .subscribe();

// Subscribe to a subsets of topics in a namespace, based on regex
Pattern someTopicsInNamespace = Pattern.compile("persistent://public/default/foo.*");
Consumer<byte[]> someTopicsConsumer = pulsarClient.newConsumer()
                .topicsPattern(someTopicsInNamespace)
                .subscriptionName("subscription-1")
                .subscribe();

7、分割槽主題

普通主題由單個 broker 提供服務,這限制了主題的吞吐量。分割槽主題是一種特殊型別的主題,由多個 broker 處理,因此允許更高的吞吐量。

分割槽主題被實現為 N 個內部主題,其中 N 是分割槽的數量。當向分割槽主題釋出訊息時,每條訊息會被路由到多個 broker 中的一個。Pulsar 自動處理分割槽在各個 broker 之間的分佈。

Topic1 主題有五個分割槽(P0到P4),分佈在三個 broker 上。由於分割槽的數量多於 broker 的數量,其中兩個 broker 每個處理兩個分割槽,而第三個 broker 只處理一個分割槽(再次強調,Pulsar 會自動處理這些分割槽的分佈)。

該主題的訊息被廣播給兩個消費者。路由模式決定訊息釋出到哪個分割槽,而訂閱型別決定訊息傳送給哪個消費者。

在大多數情況下,可以決定路由模式和訂閱型別。通常,分割槽和路由模式應由吞吐量決定,而訂閱型別則應由應用程式語義來決定。

分割槽主題需要透過 admin API 明確建立。在建立主題時可以指定分割槽的數量。

7.1、路由模式

模式 描述
RoundRobinPartition

如果沒有提供鍵值,生產者將以輪詢方式將訊息釋出到所有分割槽,以實現最大的吞吐量。啟用批處理時,按批次訊息輪詢。如果在訊息上指定了鍵值,分割槽生產者將對鍵值進行雜湊處理,並將訊息分配給特定的分割槽。這是預設模式。

SinglePartition 如果未提供鍵值,生產者將隨機選擇一個分割槽並將所有訊息釋出到該分割槽。而如果在訊息上指定了鍵值,生產者將對鍵值進行雜湊計算,並將訊息釋出到對應的分割槽。
CustomPartition 自定義訊息路由器,可以實現 MessageRouter 介面來建立自定義的訊息路由。

7.2、順序保證

訊息的順序與訊息路由模式和訊息鍵密切相關。通常,使用者希望 Per-key-partition 的順序保證。

如果提供了鍵值,使用 RoundRobinPartition 或 SinglePartition 模式時,訊息將透過雜湊方案被路由到相應的分割槽。

順序保證 描述 路由模式和鍵值
Per-key-partition 所有具有相同鍵的訊息將按順序釋出到同一個分割槽中。 使用單分割槽模式(SinglePartition)或輪詢分割槽模式(RoundRobinPartition),並且每條訊息都提供了鍵值。
Per-producer 同一個生產者生產的訊息是有序的。 使用單分割槽模式,並且每條訊息都沒有提供鍵值。

7.3、雜湊方案

雜湊方案是在選擇訊息的分割槽時可用的標準雜湊函式集。有兩種型別的標準雜湊函式可供選擇:

  • JavaStringHash
  • Murmur3_32Hash

生產者的預設雜湊函式是 JavaStringHash。

8、非持久主題

預設情況下,Pulsar 會持久化儲存所有未被確認的訊息在多個 BookKeeper bookie(儲存節點)上。因此,持久化主題上的訊息資料可以在 broker 重啟和訂閱者故障轉移時繼續存在。

當然,Pulsar 也支援非持久化主題。非持久化主題是指訊息資料永遠不會被持久化儲存到磁碟,而是僅保留在記憶體中。當使用非持久化主題時,關閉 broker 或斷開訂閱者與主題的連線會導致該主題上的所有在途訊息丟失,這意味著客戶端可能會看到訊息丟失。

非持久化主題的名稱通常是這種形式:

non-persistent://tenant/namespace/topic

在非持久化主題中,broker 會立即將訊息傳遞給所有已連線的訂閱者,而無需將它們持久化儲存到 BookKeeper 中。如果訂閱者斷開連線,broker 將無法傳遞那些在途訊息,訂閱者也將無法再次接收到這些訊息。省略持久化儲存步驟在某些情況下使非持久化主題上的訊息傳遞速度略快於持久化主題,但需要注意的是,這樣做會喪失 Pulsar 的一些核心優勢。

注意:對於非持久化主題,訊息資料僅存在於記憶體中,沒有特定的緩衝區,這意味著資料不會在記憶體中快取。接收到的訊息會立即傳輸給所有連線的消費者。如果 broker 發生故障或者訊息資料無法從記憶體中獲取,那麼您的訊息資料可能會丟失。只有當您確定使用案例確實需要時才使用它們。

預設情況下,broker 啟用了非持久化主題,可以在 broker 的配置中禁用它們。可以使用 pulsar-admin topics 命令管理非持久化主題,詳情請參閱 pulsar-admin

目前,未分割槽的非持久化主題不會被持久化到 ZooKeeper 中。這意味著如果擁有它們的broker崩潰,它們不會被重新分配給另一個代理,因為它們只存在於原始代理的記憶體中。目前的解決方法是在 broker 的配置中將 allowAutoTopicCreation 的值設定為 true,並且將 allowAutoTopicCreationType 設定為 non-partitioned(這些是預設值)。

8.1、效能

持久化主題中,所有訊息都會持久化儲存在磁碟上,而非持久化主題中,broker 不會持久化訊息,並在訊息傳遞到 broker 後立即向生產者傳送確認資訊,因此非持久化訊息通常比持久化訊息傳遞速度更快。因此,生產者在非持久化主題中通常有較低的釋出延遲。

8.2、客戶端 API

生產者和消費者可以以與持久化主題相同的方式連線到非持久化主題,但關鍵的區別在於主題名稱必須以 non-persistent 開頭。所有訂閱型別——獨佔、共享、鍵共享和故障轉移——都支援非持久化主題。

這裡是一個非持久化主題的消費者示例:

PulsarClient client = PulsarClient.builder()
        .serviceUrl("pulsar://localhost:6650")
        .build();
String npTopic = "non-persistent://public/default/my-topic";
String subscriptionName = "my-subscription-name";

Consumer<byte[]> consumer = client.newConsumer()
        .topic(npTopic)
        .subscriptionName(subscriptionName)
        .subscribe();

這裡是同一個非持久化主題的生產者示例:

Producer<byte[]> producer = client.newProducer()
                .topic(npTopic)
                .create();

8、系統主題

系統主題是 Pulsar 內部預定義的主題,用於內部使用。它可以是持久化或非持久化主題。

系統主題用於實現某些功能,並消除對第三方元件的依賴,例如事務、心跳檢測、主題級策略和資源組服務。系統主題使這些功能的實現變得簡化、依賴性降低和靈活性增強。以心跳檢測為例,可以利用系統主題進行健康檢查,內部使用生產者/讀者在心跳名稱空間下生產/消費訊息,從而檢測當前服務是否仍然存活。

以下表格列出了每個特定名稱空間的系統主題。

名稱空間 主題名稱 是否持久主題 數量 用途
pulsar/system transaction_coordinator_assign_${id} 持久主題 預設 16 事務協調器
pulsar/system __transaction_log_${tc_id} 持久主題 預設 16 事務日誌
pulsar/system resource-usage 非持久主題 預設 4 資源組服務
host/port heartbeat 持久主題 1 心跳檢測
User-defined-ns __change_events 持久主題 預設 4 主題事件
User-defined-ns __transaction_buffer_snapshot Persistent 持久主題 每個名稱空間一個 事務緩衝區快照
User-defined-ns ${topicName}__transaction_pending_ack 持久主題 一個訂閱的事務確認一個 事務確認

注意:

1、不能建立系統主題。要列出系統主題,可以在使用管理 API 獲取主題列表時新增 --include-system-topic 選項。
2、從 2.11.0 開始,系統主題預設啟用。在早期版本中,需要修改 conf/broker.conf 或 conf/standalone.conf 檔案中的以下配置以啟用系統主題。

systemTopicEnabled=true
topicLevelPoliciesEnabled=true

9、訊息重投遞

Apache Pulsar 支援優雅的故障處理,並確保關鍵資料不會丟失。軟體總是會出現意外情況,有時訊息可能無法成功傳遞。因此,擁有處理失敗的內建機制尤為重要,特別是在非同步訊息傳遞中,如下例所示。

  • 消費者與資料庫或 HTTP 伺服器斷開連線時,會導致以下情況發生:資料庫在消費者向其寫入資料時暫時離線,消費者呼叫的外部 HTTP 伺服器暫時不可用。
  • 消費者由於消費者崩潰、連線中斷等原因與 broker 斷開連線時,會導致以下結果:未確認的訊息會傳遞給其他可用的消費者。

在 Apache Pulsar 中,訊息重新投遞透過至少投遞一次語義來避免非同步訊息傳遞中的失敗,確保 Pulsar 會多次處理訊息。

要使用訊息重新投遞功能,需要在客戶端中啟用此機制。可以透過三種方法啟用訊息重新投遞功能。

  • 否定確認
  • 確認超時
  • 重試信主題

10、訊息保留和過期

預設情況下,broker 會執行以下動作:

  • 立即刪除所有已被消費者確認的訊息
  • 將所有未被確認的訊息持久化儲存在訊息日誌中。

然而,Pulsar 有兩個特性可以覆蓋這種預設行為:

  • 訊息保留允許儲存已被消費者確認的訊息。
  • 訊息過期允許為尚未被確認的訊息設定生存時間(TTL)。

注意:所有訊息的保留和過期管理都在名稱空間級別進行。如需操作指南,請參閱訊息保留和過期

透過訊息保留,對於名稱空間中的所有主題,某些訊息即使已經確認,也會在 Pulsar 中持久儲存。未涵蓋保留策略的已確認訊息將被刪除。如果沒有保留策略,所有已確認的訊息將被刪除。

對於訊息過期,一些訊息會被刪除,即使它們尚未被確認,因為它們根據 TTL 已經過期(例如,應用了 5 分鐘的 TTL,訊息雖然未被確認,但已經存在 10 分鐘)。

11、訊息去重

訊息重複發生在 Pulsar 多次持久化同一條訊息時。訊息去重確保每條訊息只被持久化到磁碟一次,即使訊息被生產多次也是如此。訊息去重在伺服器端自動處理。

以下圖表說明了當訊息去重功能禁用和啟用時會發生的情況:

訊息去重功能禁用時,生產者在主題上釋出訊息1;訊息到達 broker 並被持久化到 BookKeeper。然後,生產者再次傳送訊息1(可能是由於某些重試邏輯),broker 接收訊息並再次儲存到 BookKeeper,這意味著發生了重複。
訊息去重功能啟用時,生產者釋出訊息1,該訊息被 broker 接收並持久化。當生產者嘗試再次釋出相同的訊息時,broker 知道它已經接受到訊息1,因此不會再次將該訊息持久化。

注意:

  • 訊息去重功能可以作用在名稱空間級別或主題級別上。要獲取更多指導,請參閱訊息去重操作手冊
  • 可以參閱 PIP-6 中關於訊息去重的設計。

11.1、生產者冪等性

訊息去重的另一種方法是生產者冪等性,這意味著每條訊息只會被生產一次,避免了資料丟失和重複。這種方法的缺點是將訊息去重的工作放到了應用程式中。在 Pulsar 中,這是在 broker 層級處理的,因此無需修改 Pulsar 客戶端程式碼,只需要進行管理上的更改。

11.2、去重和一次有效語義

訊息去重使 Pulsar 成為理想的訊息傳遞系統,可以與流處理引擎(spe)和其他尋求提供一次有效處理語義的系統一起配合使用。那些不提供自動訊息去重功能的訊息傳遞系統需要由 SPE 或其他系統來保證去重,這意味著嚴格的訊息順序需要應用程式承擔去重的責任。而在 Pulsar 中,嚴格的順序保證不會增加應用程式的成本。

12、延遲訊息投遞

延遲訊息投遞允許稍後消費訊息。在這種機制中,訊息被儲存在 BookKeeper 中。DelayedDeliveryTracker 在訊息釋出到 broker 後,在記憶體中維護時間索引(時間 -> 訊息ID)。一旦指定的延遲時間結束,這條訊息將被傳遞給消費者。

注意:只有共享訂閱和鍵共享訂閱支援延遲訊息投遞。在其他型別的訂閱中,延遲訊息會立即分發。

下圖說明了延遲訊息投遞的概念:

broker 儲存訊息而不進行任何檢查。當消費者消費訊息時,如果訊息設定為延遲,則該訊息會被新增到 DelayedDeliveryTracker 中。訂閱會檢查並從 DelayedDeliveryTracker 中獲取超時的訊息。

注意:

與保留策略一起使用:在 Pulsar 中,當 ledger 中的訊息被消費後,該 ledger 會自動刪除。Pulsar 會刪除主題前面的 ledger,但不會刪除主題中間的 ledger。這意味著如果傳送了一條延遲很長時間的訊息,該訊息直到達到延遲時間之前不會被消費。這意味著即使某些後續 ledger 已完全消費,只要延遲訊息未被消費,該主題上的所有 ledger 都無法刪除。

與積壓配額策略一起使用:在使用延遲訊息後,使用積壓配額策略要小心謹慎。這是因為延遲訊息可能會導致訊息在長時間內未被消費,觸發積壓配額策略,從而導致後續訊息傳送被拒絕。

與訊息過期策略一起使用:當時間到期時,Pulsar 會自動將訊息移動到已確認狀態(準備將其刪除),即使這些訊息是延遲訊息,也不會考慮預期的延遲時間。

12.1、Broker

延遲訊息傳遞預設啟用,可以按以下方式在 broker 配置檔案中進行更改:

# Whether to enable the delayed delivery for messages.
# If disabled, messages are immediately delivered and there is no tracking overhead.
delayedDeliveryEnabled=true

# Control the ticking time for the retry of delayed message delivery,
# affecting the accuracy of the delivery time compared to the scheduled time.
# Note that this time is used to configure the HashedWheelTimer's tick time for the
# InMemoryDelayedDeliveryTrackerFactory (the default DelayedDeliverTrackerFactory).
# Default is 1 second.
delayedDeliveryTickTimeMillis=1000

# When using the InMemoryDelayedDeliveryTrackerFactory (the default DelayedDeliverTrackerFactory), whether
# the deliverAt time is strictly followed. When false (default), messages may be sent to consumers before the deliverAt
# time by as much as the tickTimeMillis. This can reduce the overhead on the broker of maintaining the delayed index
# for a potentially very short time period. When true, messages will not be sent to consumer until the deliverAt time
# has passed, and they may be as late as the deliverAt time plus the tickTimeMillis for the topic plus the
# delayedDeliveryTickTimeMillis.
isDelayedDeliveryDeliverAtTimeStrict=false

12.2、Producer

以下是生產者延遲訊息傳遞的示例:

// message to be delivered at the configured delay interval
producer.newMessage().deliverAfter(3L, TimeUnit.Minute).value("Hello Pulsar!").send();

參考:https://pulsar.apache.org/docs/3.3.x/concepts-messaging

相關文章