Pulsar訊息傳送、消費架構概述

帶你聊技術發表於2023-03-07


Pulsar基於釋出-訂閱模式,訊息傳送者向主題傳送訊息,而消費消費者訂閱主題,訊息從Pulsar Broker中獲取訊息,處理成功後需要向Pulsar傳送ACK,表示訊息處理成功。與RocketMQ/Kafka不同的是,Pulsar只有當消費者確認訊息都成功被處理後才能去刪除訊息。如果Consumer在處理一批訊息失敗後,可以再次請求Broker重新下發該批訊息,以便進行重試。

Pulsar訊息傳送、消費架構概述

2、訊息(Messages)

訊息(Message)是Pulsar中最基本的抽象單位,一條Pulsar訊息中的屬性如下所示:

元件名稱描述
Value / data payload訊息體,位元組陣列
Key訊息鍵或者分割槽鍵,主題的壓縮機制將依賴訊息的key
Properties訊息屬性,類比RocketMQ的訊息屬性、Kafka中的Header,通常用於存放訊息的擴充套件屬性
Producer name訊息的生產者
Topic name訊息所屬的主題
Schema version訊息生產者的Schema版本號
Sequence ID訊息在主題中所有訊息的序號,預設由傳送者產生,也可以由使用者自定義,可以用於在一次訊息傳送呼叫API中訊息去重。
如果服務端brokerDeduplicationEnabled設定為true,則服務端會進行唯一性校驗
Message ID訊息ID,訊息持久化到服務端後生成,裡面包含了訊息的特定儲存位置,並且在叢集內是全域性唯一的
Publish time訊息的傳送時間,訊息傳送者自動生成
Event time事件時間,由應用程式附加到訊息的可選時間時間戳,通常是業務發生的時間,例如訂單的下單時間等

在Pulsar中,訊息的預設最大訊息大小為5M,我們可以有如下兩種方式進行更改:

  • 在broker.conf中maxMessageSize=5242880
  • 在bookkeeper.conf中nettyMaxFrameSizeBytes=5253120

3、生產者(Producers)

Producer生產者,訊息傳送客戶端。

3.1傳送模式

Produder有兩種訊息傳送模式:

  • 同步傳送
  • 非同步傳送

3.2 訪問模式

在Pulsar中,生產者訪問Broker中主題提供了多種訪問模式,詳細如下表所示:

元件名稱描述
Shared多個生產者都可以向一個主題傳送訊息,預設模式
Exclusive獨佔模式,一個主題只能被一個生產者連線,如果另外一個生產者試圖連線,則會立即收到一個錯誤,但如果老的生產者當機,會選舉產生一個新的生產者
ExclusiveWithFencing只允許一個生產者往該主題傳送訊息,相比Exclusive,沒有備選機制。
WaitForExclusive支援多個生產者透過選舉機制成為Leader後傳送訊息,Leader當機後,重新競爭選舉出新的Leader,只有Leader可以傳送訊息

3.3 壓縮演算法

Pulsar目前支援LZ4、ZLIB、ZSTD、SNAPP四種壓縮機制,可以透過如下程式碼指定壓縮演算法:

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

3.4 批次傳送

Pulsar支援批次訊息傳送。如果開啟批次傳送,訊息傳送者會將多條訊息累積到一個批次中進行一次傳送。一個批次的訊息大小由最大的訊息條數+最大的傳送延遲兩個引數共同決定(參考kafka中的batch.size、linger.ms),如果開啟了批處理,backlogSize表示的一個批次中訊息的條數。

批處理示例圖如下:

Pulsar訊息傳送、消費架構概述

Pulsar會把一個批次作為一個整體儲存到Broker中,消費者接到一個批次後再解綁成一條一條的訊息。但即使開啟批處理,但排程類訊息(設定了deliverAt或者deliverAfter)會單獨一條訊息進行傳送。

預設情況下如果一個批次中的訊息出現部分消費失敗,消費端在消費重試時會再次收到這個批次中所有的訊息,為了避免這種情況,Pulsar在2.6.0版本中引入了批次索引確認機制。一個批次中所有訊息被確認後會刪除。那pulsar是如何支援訊息回溯的呢?[答案在介紹Consumer的時候會介紹]

預設情況下,批索引確認機制是關閉的。如果要開啟,需要在broker端配置acknowledgmentAtBatchIndexLevelEnabled=true。同樣在消費端也需要設定acknowledgmentAtBatchIndexLevelEnabled=true。

消費端開啟批索引確認示例程式碼:

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

3.5 訊息分塊

Pulsar能夠在訊息傳送端將一個大的訊息體分割成多個小塊的訊息,並且在訊息消費端聚合成一條完整訊息再消費。

如果開啟分塊機制,當需要傳送的訊息超過允許的最大訊息大小(maxMessageSize)時,其工作流程如下:

  • 生產者將原始訊息拆分成分塊訊息,並將他們與分塊後設資料按順序傳送給Broker。
  • Broker將分塊訊息當成普通訊息進行儲存,並且使用chunkedMessageRate來記錄分塊訊息的速率。
  • 訊息消費端首先將快取分塊訊息,直到一個訊息所有分庫都被接收,然後整合成一條完整的訊息,傳輸到接收Queue中,客戶端從接收佇列中獲取一條完整的訊息。

限制:

  • 分塊訊息只適用於持久化主題
  • 分塊只對 exclusive與failover subscription types 兩種訂閱模式生效
  • 分塊不能與批處理同時開啟

關於分塊的工作機制,官方文件如下所示:

Pulsar訊息傳送、消費架構概述

兩個生產者傳送多條訊息的分塊,在服務端,一條訊息的多個分塊會被順序儲存,但一條訊息的多個分塊並不是連續儲存的,然後消費者在接收時,會利用快取對分塊資訊進行聚合。

注意:一旦開啟了訊息分塊,消費時需要在消費端聚合成一條完整的訊息,必須為每一條大訊息建立獨立的緩衝區,會對消費端的記憶體帶來壓力,有記憶體溢位的風險。

故為了保護消費端,消費端採取了兩個措施:

  • 引入了一個maxPendingChunkedMessage引數,設定可以快取的最大chunk數,當快取的chunk數量達到這個值後,pulsar會drop掉部分chunk,先保證一條訊息順利合併,其他丟棄的訊息再在合適的時候重新從Broker拉取。
  • 引入了expireTimeOfIncompleteChunkedMessage引數,如果一個訊息的所有塊在指定時間內沒有全部到達,這些分塊將在消費端全部被移除,預設的過期時間為1min。

要開啟訊息分塊的一個前提條件是需要關閉批次傳送,具體做法是將生產者的enableBatching設定為false。

預設情況下訊息分塊是禁用的,如果需要開啟,需要將生產者的chunkingEnabled設定為true。

4、消費組(Consumers)

消費者,透過訂閱主題,從而從Broker端接受訊息,訊息傳送,訊息消費的核心示意圖如下:

Pulsar訊息傳送、消費架構概述

消費端會使用一個佇列來接受Broker端的訊息,這個快取可以透過receiverQueueSize來配置,預設為1000。

4.1 訊息接受模式

消費端接收訊息支援同步接收與非同步接收兩種模式:

  • Sync receive:同步接收模式,如果Broker沒有需要消費的訊息,接受執行緒將阻塞
  • An async receive:非同步接收模式,將立馬返回,使用了Feture模式,訊息真正到達後可用。

4.2 消費監聽器(Listeners)

訊息消費監聽器,當從Broker中收到訊息後,將呼叫消費監聽器,從而觸發業務程式碼的執行。

4.3 消費確認機制(Acknowledgment)

消費者在成功消費完一條訊息後需要告知Broker已成功消費,俗稱ACK確認資訊。然後這條訊息會被持久化儲存,並且在所有訂閱組都成功確認後才會刪除這條訊息。如果希望Broker繼續儲存已被所有訂閱確認的訊息,則需要設定訊息的持久策略(本文後面會詳細介紹)。

如果傳送端啟用了批處理,則Pulsar可以引入批索引確認機制,避免一個批次的訊息重複下發給消費者。

在Pulsar中,確認一條訊息有如下兩種方式:

  • 單條訊息獨立確認。消費者每一條訊息都會傳送ACK給Broker,消費端透過呼叫consumer.acknowledge(msg)對單條訊息進行確認。
  • 累積確認,消費者只確認接收到的最後一條訊息。消費端透過呼叫consumer.acknowledgeCumulative(msg)進行累積訊息確認。

4.3.1 Negative acknowledgment(取消確認)

消費者可以透過傳送neagative ack請求到broker,告知broker並未成功消費該條訊息,broker收到該請求後,會觸發broker將這條訊息重新下發給消費者進行消費。

如果消費者訂閱模式為Exclusive或者Failover subscription型別時,消費者只能否認收到的最後一條訊息。

如果消費者訂閱模式為Shared或者Key_Shared型別時,消費者可以否認單獨一條訊息。

值得注意的是,Negative acknowledgment機制將對順序性語義帶來破壞,在順序消費場景,請慎重考慮。

如果要對訊息使用否定確認,請確保在訊息確認超時之前進行發起。

我們可以使用如下API來進行否定確認,程式碼如下:

Consumer<byte[]> consumer = pulsarClient.newConsumer()
                .topic(topic)
                .subscriptionName("sub-negative-ack")
                .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
                 //設定當客戶端呼叫negativeAcknowledge方法後,服務端進行再投遞的延遲時間
                .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);

當客戶端呼叫negativeAcknowledge後,但服務端如果一直未收到這條訊息的再次ACK,會在服務端進行重推,並且可以設定階梯延遲投遞,啟用類似階梯投遞機制的程式碼如下所示:

Consumer<byte[]> consumer = pulsarClient.newConsumer()
        .topic(topic)
        .subscriptionName("sub-negative-ack")
        .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
         // 啟用階梯延遲重推
        .negativeAckRedeliveryBackoff(MultiplierRedeliveryBackoff.builder()
            .minDelayMs(1000)  // 最小時間,機第一次推送的延遲時間,然後按multiplier倍數增長,
            .maxDelayMs(60 * 1000// 最大延遲推送等待時間
            .multiplier(2// 一次延遲推送的倍數,這裡設定為,則重試時間如下:1s 2s 4s 8s 16s 32s 60s 60s
            .build())
        .subscribe();

溫馨提示:如果傳送端啟用了批處理,Broker是按批的維度重推這一批訊息。

4.3.2 (確認超時)Acknowledgment timeout

預設情況下,並不會開啟ACK超時確認,也就是意味著Broker將一條訊息傳遞給消費者後並不會再次投遞,除非消費者崩潰退出。

確認超時機制允許您設定一個時間範圍,在此期間客戶端跟蹤未確認的訊息。在設定的超時(ackTimeout)時間過期後,客戶端可以向Broker傳送redeliver unacknowledged messages 請求,然後Broker會將未確認訊息再次投遞給消費者。

客戶端在ackTimeout超時後,有兩種機制向服務端傳送redeliver unacknowledged messages:

  • 第一種是以固定頻率定時傳送,主要是透過設定消費者的ackTimeoutTickTime引數,示例如下:

    PulsarClient pulsarClient = PulsarClient.builder().build();
    Consumer<byte[]> consumer = pulsarClient.newConsumer()
             .ackTimeout(10, TimeUnit.SECONDS) // 開啟超時確認機制
             .ackTimeoutTickTime(1000, TimeUnit.SECONDS) // 設定定時傳送頻率
             // 省略其他屬性
             .subscribe();
  • 第二種是延遲梯度的方式進行傳送,具體程式碼如下:

    consumer.ackTimeout(10, TimeUnit.SECOND)
            .ackTimeoutRedeliveryBackoff(MultiplierRedeliveryBackoff.builder()
                .minDelayMs(1000// 最小延遲時間
                .maxDelayMs(60 * 1000// 最大延遲時間
                .multiplier(2// 遞增倍數
                .build());

溫馨提示:

  • 如果啟用了批處理,確認超時後,Broker會將一個批次作為一個整體重推,而不是重推這個批次中的部分訊息。
  • negative acknowledgment確認比超時確認擁有更高的優先順序。

4.4 重試主題(Retry letter topic)

Pulsar支援訊息消費重試,消費者在消費訊息的過程中如果處理失敗,可以將這些訊息儲存在消費者對應的重試主題中,以便後續再次重新消費,消費者會自動訂閱重試主題。達到最大消費重試次數後如果還是失敗,則會將訊息儲存在死信佇列,死信佇列中的訊息需要人工手動去處理。

重試主題的工作機制如下圖所示:

Pulsar訊息傳送、消費架構概述

訊息消費失敗重試機制預設是禁用的,可以透過設定enableRetry為true開啟消費消費失敗重試,可以透過maxRedeliverCount設定最大重試次數,開啟訊息消費重試機制的示例程式碼如下:

Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
                .topic("my-topic")
                .subscriptionName("dw_test_consumer_022000")
                .subscriptionType(SubscriptionType.Shared)
                .enableRetry(true// 開啟消費重試
                .deadLetterPolicy(DeadLetterPolicy.builder() 
                        .maxRedeliverCount(maxRedeliveryCount)  // 最大重試次數
                        .retryLetterTopic("my-retry-letter-topic-name")// 可以自定義重試主題
                        .build())
                .subscribe();

重試主題的預設命名規則:topicName-subscriptionname-RETRY

重試主題中的訊息包含一些特殊的屬性:

  • REAL_TOPIC 訊息原始主題
  • ORIGIN_MESSAGE_ID 訊息原始ID
  • RECONSUMETIMES 當前重試次數
  • DELAY_TIME 訊息重試間隔(毫秒)

如果使用訊息重試,客戶端需要呼叫如下API將訊息持久化到訊息佇列中:

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

//並且該方法還有一個過載方法,支援自定義訊息屬性
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);

溫馨提示:

  • 目前,只在Shared 訂閱模式中啟用了訊息消費重試機制
  • 與否定確認(Negative acknowledgment)相比,訊息消費重試機制更加適合類似需要大量重試並且重試間隔可配置的場景,因為訊息重試主題是持久到BookKeeper中,而否定確認是快取在客戶端。

4.5 死信佇列(Dead letter topic)

如果消費重試次數達到指定的最大值後還是未成功消費,Pulsar會將訊息傳送到消費者對應的死信佇列,一旦訊息進入到死信佇列,Pulsar不會主動對這些訊息進行任何處理,需要要消費者自己決定如何處理這些訊息。

死信佇列預設的主題名稱為:topicname-subscriptionname-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建立訂閱,可以透過initialSubscriptionName來設定訂閱組,但如果服務端將allowAutoSubscriptionCreation設定為false,則無法成功建立DLQ producer。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2938382/,如需轉載,請註明出處,否則將追究法律責任。

相關文章