版權(quán)歸本人所有,如果轉(zhuǎn)載請(qǐng)聯(lián)系本人
這篇大致說一下消息從寫入到讀取在Apache Pulsar服務(wù)端是怎么串起來的。
( 這篇不會(huì)詳細(xì)說明每個(gè)邏輯怎么走的,不過會(huì)給讀者一個(gè)整體的俯瞰印象。)
首先說一下表示業(yè)務(wù)邏輯的幾個(gè)對(duì)象。(都在org.apache.pulsar.broker.service
這個(gè)包里面)
Pulsar服務(wù)端的主要邏輯對(duì)象
Topic
: 這個(gè)對(duì)象在服務(wù)端就表示一個(gè)topic。這個(gè)類是最上層的,所有邏輯都被組織到這個(gè)對(duì)象里面。
- 負(fù)責(zé)管理
org.apache.pulsar.broker.service.Producer
(這里是服務(wù)端跟蹤狀態(tài)的對(duì)象) - 負(fù)責(zé)管理
Subscription
- 負(fù)責(zé)數(shù)據(jù)寫入
Subscription
這個(gè)對(duì)象表示一份數(shù)據(jù)的消費(fèi)進(jìn)度。
- 負(fù)責(zé)管理
Consumer
(這里是服務(wù)端跟蹤狀態(tài)的對(duì)象) - 負(fù)責(zé)維護(hù)一份數(shù)據(jù)的消費(fèi)進(jìn)度(單獨(dú)ack,累積ack,哪些已經(jīng)消費(fèi)哪些還沒有消費(fèi))
- 讀取消息,負(fù)責(zé)消息的分發(fā)(哪個(gè)消費(fèi)分給哪個(gè)consumer)
- 消息重發(fā),延遲投遞。
上面這2個(gè)對(duì)象,在存儲(chǔ)層對(duì)應(yīng)的就是Topic
-> ManagedLedger
,Subscription
-> ManagedCursor
消息寫入
通過找到服務(wù)端記錄的Producer
對(duì)象,經(jīng)過一些邏輯處理(消息去重,加密,狀態(tài)檢查等)
確定Topic
最后寫入到ManagedLedger
里面。
消息讀取
則是客戶端通過CommandFlow
告知自己當(dāng)前可以處理消息的狀態(tài),
觸發(fā)消息的分發(fā)流程。通過從Consumer
拿到 Subscription
觸發(fā)消息的讀取和Dispatch
的過程。
寫入邏輯相對(duì)讀取邏輯來說比較直觀。寫入到ManagedLedger
即可。主要說下Subscription
。
Pulsar的消息派發(fā)流程(Dispatcher)
我們先單獨(dú)看一下一個(gè)分區(qū)的topic
- 有多個(gè)producer
- 有一個(gè)subscription按照
Shared
方式消費(fèi)。 - 這個(gè)subscription里面有多個(gè)consumer(比如說3個(gè)consumer)
- 每個(gè)consumer可能會(huì)單獨(dú)ack某一個(gè)消息。
假設(shè)這個(gè)topic的數(shù)據(jù)是一個(gè)紙帶,如果確認(rèn)消費(fèi)好了一個(gè)消息就涂黑一段。
寫入的話就是不斷給紙帶增加長度。
我們把那些已經(jīng)分發(fā)出去的消息但是還沒有ack的消息認(rèn)為是灰色的。
服務(wù)端會(huì)記錄每個(gè)consumer當(dāng)前的一個(gè)隊(duì)列容量。
如果consumer的隊(duì)列可以接受更多消息的時(shí)候會(huì)主動(dòng)發(fā)送CommandFlow
請(qǐng)求給服務(wù)端,來標(biāo)識(shí)自己能接受更多的消息。
C1 -> 10
C2 -> 4
C3 -> 8
有新消息寫入的時(shí)候,如果為了減少延遲,最好馬上能通知每個(gè)Subscription
有新的消息到來了。
這樣的話,比如說新寫入了20條消息,則會(huì)觸發(fā)一個(gè)消息分發(fā)的動(dòng)作來把這20條消息讀取出來(很可能走的cache)之后按照客戶端和消息狀態(tài)分給這3個(gè)consumer。
Dispatcher
這個(gè)類是Subscription
對(duì)象里面的一個(gè)成員變量。
當(dāng)消息從ManagedLedger
里面成功讀取之后,這個(gè)類需要按照consumer的狀態(tài)和消息的一些屬性
把消息推送給consumer(客戶端)。
根據(jù)訂閱方式的不同分發(fā)的邏輯也有區(qū)別
-
Exclusive
: 這個(gè)模式比較容易,所有消息都會(huì)分給唯一的一個(gè)Active Consumer -
Failover
: 這個(gè)模式在Exclusive
的基礎(chǔ)上,增加了一個(gè)切換consumer的邏輯。不過還是分發(fā)消息給一個(gè)consumer -
Shared
: 這個(gè)模式是按照輪訓(xùn)的方式將這個(gè)topic的消息分發(fā)給多個(gè)consumer,同時(shí)consumer發(fā)生變化的時(shí)候也需要做一些邏輯來調(diào)整分發(fā)邏輯。 -
Key_Shared
:這個(gè)模式也是分發(fā)給多個(gè)consumer,不過可以一定程度上保證一個(gè)key上面的消息是有順序的消費(fèi)的(不一定嚴(yán)格保證)
這樣一個(gè)Dispatcher
需要有以下功能
- 跟蹤consumer狀態(tài):consumer隊(duì)列容量,consumer個(gè)數(shù)變化。(因?yàn)闀?huì)影響消息派發(fā)的邏輯)
- 消息分發(fā):根據(jù)當(dāng)前狀態(tài)確定讀取成功的消息要分配到哪些consumer上面,并把消息推送給consumer
- 消息重新投遞的邏輯(replay):有的消息被consumer標(biāo)記需要重新投遞了,這樣的話需要重新讀取并分發(fā)。
消費(fèi)進(jìn)度跟蹤
1. 消息確認(rèn)方式
消息確認(rèn)可以是累積確認(rèn)或者是單獨(dú)確認(rèn)的。
累積確認(rèn)比較容易。
如果一個(gè)位置被確認(rèn)那么之前的消息都認(rèn)為是成功消費(fèi)了的(kafka是這樣的方式)
不過這樣靈活性比較差,如果一個(gè)消費(fèi)不成功的話,可能就卡在這個(gè)位置了,影響其他消息的處理。單獨(dú)確認(rèn)的話,某條消息可以直接ack,而不會(huì)影響其他消息ack的狀態(tài)被覆蓋。
2. 單條消息ack狀態(tài)的維護(hù)(需要持久化)
單獨(dú)確認(rèn)的話,相當(dāng)于紙帶上有一些位置之前全是黑色的,這種是某個(gè)點(diǎn)位之前全都消費(fèi)完的。
我們稱這個(gè)點(diǎn)位是deletePoint。
有一些位置是黑色和灰色相間的,這種就是某些消息已經(jīng)被標(biāo)記ack了,
有些已經(jīng)被投遞到客戶端但是還沒有ack。
隨著單獨(dú)確認(rèn)逐漸累積,這樣黑色的部分會(huì)慢慢連接起來。
這樣的話這個(gè)deletePoint就可以推進(jìn)了。
我們?cè)倏匆幌耣atch的情況,上面說的消息都是針對(duì)單條消息的(一個(gè)Entry)
如果這個(gè)Entry里面的消息是做了batch發(fā)送的,里面會(huì)包含多條信息。
客戶端可能會(huì)單獨(dú)ack這個(gè)batch里面的某個(gè)消息。
這樣的話我們需要記錄這個(gè)Entry的batch里面哪些消息是已經(jīng)被ack的。
對(duì)于這種消息,Dispatcher在投遞消息的時(shí)候會(huì)帶著一個(gè)位圖來標(biāo)記這個(gè)Entry里面哪些消息是已經(jīng)處理的。
這樣consumer會(huì)按照這個(gè)bitmap來進(jìn)行過濾。
3. 消息重發(fā)狀態(tài)跟蹤(非持久化)
灰色的位置我們稱為當(dāng)前的readPoint,標(biāo)記的是當(dāng)前讀取結(jié)尾的位置。如果有新的數(shù)據(jù)寫入的話,dispatcher會(huì)從這個(gè)位置嘗試讀取新寫入的數(shù)據(jù),并推進(jìn)這個(gè)readPoint。
如果有的消息已經(jīng)被發(fā)送給consumer了,但是這個(gè)消息consumer又通知服務(wù)端說需要重新投遞。
則這個(gè)時(shí)候就需要標(biāo)記readPoint之前的某個(gè)消息需要重新讀取,這個(gè)重新讀取的話不會(huì)更改readPoint。
同時(shí)因?yàn)檫@個(gè)消息沒有被ack。所以這個(gè)redeliver的消息不需要被持久化。
(重新加載之后就可以根據(jù)ack的狀態(tài)推斷出來)
4. 消費(fèi)進(jìn)度持久化狀態(tài)
那么如果這個(gè)topic被unload到其他的broker。對(duì)于一個(gè)Subscription
需要加載哪些狀態(tài)呢?
主要狀態(tài)就是哪些消息已經(jīng)ack了。同時(shí)確認(rèn)下一次讀取的位置是什么。
需要知道記錄單獨(dú)ack消息的狀態(tài)和batch ack信息的狀態(tài)。
當(dāng)ManagedCursor
從 ledger(狀態(tài)被寫到bookkeeper里面)或者zk(寫入一直失敗的話會(huì)fallback到zk上)
讀取到這個(gè)狀態(tài)的話,按照相應(yīng)邏輯恢復(fù)即可。
// MLDataFormats.proto
message ManagedCursorInfo {
// If the ledger id is -1, then the mark-delete position is
// the one from the (ledgerId, entryId) snapshot below
required int64 cursorsLedgerId = 1;
// Last snapshot of the mark-delete position
optional int64 markDeleteLedgerId = 2;
optional int64 markDeleteEntryId = 3;
// 記錄了單獨(dú)某個(gè)消息被ack的狀態(tài)
repeated MessageRange individualDeletedMessages = 4;
// Additional custom properties associated with
// the current cursor position
repeated LongProperty properties = 5;
// 時(shí)間戳
optional int64 lastActive = 6;
// Store which index in the batch message has been deleted
// batch 的 ack狀態(tài)記錄
repeated BatchedEntryDeletionIndexInfo batchedEntryDeletionIndexInfo = 7;
}