消息索引

前文我們梳理了消息在Commit Log文件的存儲過程,討論了消息的落盤策略,然而僅僅通過Commit Log存儲消息是遠遠不夠的,例如當我們需要消費某個topic的消息時,通過對Commit Log整體遍歷尋找消息的方式無疑非常的低效。所以本文將引出2個很重要的概念:消費隊列、IndexFile

消費隊列

什么是消費隊列呢?其實在上一章的消息協議格式中,就有消息隊列的體現,簡單回顧一下協議的前6個字段:

msg total len:消息總長度
msg magic :魔法值,標記當前數據是一條消息
msg CRC :消息內容的CRC值
queue id :隊列id
msg flag :消息標記位
queue offset :隊列的偏移量,從0開始累加

其中第4個字段為消費隊列的id,第6個字段為當前隊列的偏移量;所以在消息產生的時候,消息所屬的隊列就已經確定

那么究竟該如何理解“消費隊列”的概念呢?我們舉例來說:假定某個RocketMQ集群部署了3個broker(brokerA、brokerB、brokerC),主題topicTest的消息分別存儲在這3個broker中,而每個broker又對應一個commit log文件。我們把broker中的主題topicTest中的消息劃分為多個隊列,每個隊列便是這個topic在當前broker的消費隊列

數據結構

當然消費隊列不會將消息體進行冗余存儲,數據結構如下:

image.png

即在消費隊列的文件中,需要存儲20個字節的索引內容。RocketMQ中默認指定每個消費隊列的文件存儲30萬條消息的索引,而一個索引占用20個字節,這樣每個文件的大小便是固定值300000*20/1024/1024≈5.72M,而文件命名采用與commit log相似的方式,即總長度20位,高位補0

store/consumequeue/topicXX/0/00000000000000000000 第一個文件
store/consumequeue/topicXX/1/00000000000006000000 第二個文件
store/consumequeue/topicXX/2/00000000000012000000 第三個文件
store/consumequeue/topicXX/3/00000000000018000000 第四個文件

與commitLog只有一個文件不同,consumeQueue是每個topic的每個消費隊列都會生產多個文件

為什么消費隊列文件存儲消息的個數要設置成30萬呢?一個文件還不到6M,為何不能像commit log那樣設置為1G呢?鄙人沒有在源碼及網上找到相關資料,猜測可能是個經驗值。首先該值不宜設置的過大,因為消息總是有失效期的,例如3天失效,如果消費隊列的文件設置過大的話,有可能一個文件中包含了過去一個月的消息,時間跨度過大,這樣不利于及時刪除已經過期的消息;其次該值也不宜過小,太小的話會產生大量的小文件,在管理維護上制造負擔。最理想情況是一個消費隊列文件對應一個commit log,這樣commit log過期時,消費隊列文件也跟著及時失效

消費隊列之commit log視角

image.png

某個commit log會存儲多個topic消息,而每個topic有可能會將消息劃分至多個隊列中;如上圖所示,commit log按順序依次存儲消息,而某個topic的消息在commit log中大概率也是不連續的,而consume queue的作用便是將某個topic下同一個隊列的消息依次標識,便于消費時順序消費

消費隊列之topic視角

image.png

上圖描述了Topic A的消費隊列分配情況,所有的消息相對均勻的分散在3個broker中,每個broker的消息又分為隊列0及隊列1,所以一共有6個消費隊列,所以Topic A的consumer端也是建議開辟6個進程去消費數據

這里簡單提一下消費端,我們知道一個消費隊列同時只能被一個消費實例消費,所以消費實例的數量建議值為 <= consumeQueue 數量,理想情況是消費實例的個數完全等于consumeQueue個數,這樣吞吐能達到最佳

consumerNum < consumeQueue 消費實例小于消費隊列個數。例如某個topic的消費隊列一共有6個,但是只有3個消費實例,RocketMQ會盡量均衡每個消費實例分配到的消費隊列,所以每個消費實例實際會消費2個隊列的內容。這種情況可能增加消費實例可以提高整體吞吐
consumerNum > consumeQueue 消費實例大于消費隊列個數。比如某個topic的消費隊列有6個,但是有8個消費實例注冊,因為一個消費實例只能對應一個消費隊列,所以勢必導致有2個消費實例處于空閑狀態,不會拿到任何數據
consumerNum == consumeQueue 消費實例等于消費隊列個數。這比較理想的狀態,不會有過載或饑餓產生
基于同一個消費隊列只能被一個消費實例消費的特性,我們可以將某類消息均發送給一個隊列,這樣消費的時候能夠嚴格保序。例如我們希望訂單的流程是保序的,可以通過orderId % consumeQueue來決定當前訂單的消費發送給哪個隊列,從而達到保序的目的

寫入流程

消費隊列的寫入跟commit log的寫入是同步進行的嗎?答案是否定的,RocketMQ會啟動一個獨立的線程來異步構建消費隊列(構建索引文件也是這個線程)

image.png

簡單描述下流程:構建索引的線程為ReputMessageService,跟寫入commitLog的線程是異步關系,該線程會不斷地將沒有構建索引的消息從commit log中取出,將物理偏移量、消息長度、tag寫入文件。值得一提的是,消息隊列文件的寫入跟commit log不同,commit log的寫入有很多刷盤策略,而consumeQueue每條消息解析完畢都會刷盤,而且采用的是FileChannel。

借此,我們拋出幾個問題

問題1:為什么消費隊列寫入文件要用FileChannel?批次多,數據量小的場景用Mapp不香嗎?

關于這個問題,我咨詢了RocketMQ開源社區比較有影響力的大佬,給出的答復是:的確是這樣,RocketMQ這樣設計考慮欠佳,寫文件這塊應該向kafka學習,即消息寫入用FileChannel,索引寫入用Mapp

問題2:為什么RocketMQ中很少有用到堆外內存?文件寫入的話,使用堆外內存少一次內存拷貝,不是可以提高性能嗎?

是這樣的,類似這樣的場景首選還是堆外內存;RocketMQ的確還有很多可優化的空間,在將來的某個版本,我們一定可以看到針對此處的優化

問題3:如果消息已經寫入commit log,但還未寫入消費隊列,consumer端能正常消費到這條消息嗎?

拋出這個問題,大家可以思考一下,在消息產生、消費的章節再回答

IndexFile

ReputMessageService線程除了構建消費隊列的索引外,還同時根據消息key構建了索引

image.png

除了正常的生產、消費消息外,RocketMQ還提供了根據msg key進行查詢的功能,將消息key相同的消息一并查出;我們當然可以通過掃描全量的commit log將相同msg key類型的消息過濾出來,但性能堪憂,而且涉及大量的IO運算;IndexFile便是為了實現快速查找目標消息而衍生的索引文件

IndexFile的命名規范也有別于消費隊列,IndexFile是按照創建時間來命名的,因為根據消息key進行匹配查詢的時候,都要帶上時間參數,文件名起到了快速定位索引數據位置的作用,下面列舉一組IndexFile的文件名

rocketMQ/store/20211204094647480
rocketMQ/store/20211205094647480
rocketMQ/store/20211206094647480
rocketMQ/store/20211207094647480

IndexFile結構

我們具體看一下此文件的結構,與消費隊列文件相同,IndexFile是定長的

image.png

由三部分組成:

文件頭,占用 40 byte
slot,hash槽兒,占用500w4= 20000000 byte
索引內容 占用2000w
20= 400000000 byte
所以文件總大小為: 40+50000004+2000000020=420000040byte ≈ 400M

存儲原理

簡單剖析一下各部分的字段

文件頭 共 20 byte

開始時間(8 byte)存儲前索引文件內,所有消息的最小時間
結束時間(8 byte)存儲前索引文件內,所有消息的最大時間,因為根據key查詢的時候 ,時間是必填選項 ,開始與結束時間用來快速定位消息是否命中
最小物理偏移量(8 byte)存儲前索引文件內,所有消息的最小物理偏移量
最大物理偏移量(8 byte)存儲前索引文件內,所有消息的最大物理偏移量;框定最小、最大物理偏移量,是為了給通過物理地址查詢時快速索引
有效hash slot數量(4 byte)因為存儲hash沖突的情況,所以最壞情況是,hash slot只有1個,最理想情況是有500萬個
index索引數量(4 byte)如果當前索引文件已經構建完畢,那么該值是固定值2000萬

slot 4 byte

當前槽兒內的最近一次index的位置(4 byte)
索引內容 20 byte

hash值(4 byte)
消息的物理地址(8 byte)
時間差(4 byte)當前消息與最早消息的時間差
索引(4 byte)當前槽兒內,上一條索引的位置

存儲方式如下

image.png

當一條新的消息索引進來時,首先定位當前消息命中的slot,該slot存儲著最近一條消息的存儲位置。將消息的索引數據append至文件尾部的同時,將最新索引數據的next指向上一條索引位置,這樣便形成了一條當前slot按照時間存入的倒序的鏈表

消息查詢

根據前文的鋪墊,同一個槽兒內的數據,已經被一個隱式的鏈兒串連在了一起,當我們根據topic+key進行數據查詢時,直接通過topic + # + key的hash值,定位到某個槽兒,進而依次尋找消息即可;當然同一個槽兒內的數據可能出現hash沖突,我們需要將不符合條件的數據過濾掉

當我閱讀這部分源碼的時候,發現了其內部存在的一個bug,其做消息過濾時,僅僅判斷消息的hash字段是否相等,如果相等的話,繼而認定為要尋找的數據從而返回

class : org.apache.rocketmq.store.index.IndexFile

if (keyHash == keyHashRead && timeMatched) {
    phyOffsets.add(phyOffsetRead);
}

進而帶來的一些問題,例如:

新建topic AaTopic,并向topic中發送一條消息,message key為Aa
新建topic BBTopic,并向topic中發送一條消息,message key為BB

當我們通過 topic=AaTopic && key=BB查詢時,預期應該返回空數據,但實際卻返回了一條數據

image.png

其主要是因為Aa與BB擁有相同的HashCode2080

message id
消息id,在RocketMQ中又定義為msg unique id,組成形式是“ip+物理偏移量”(ip非定長字段,會因ipv4與ipv6的不同而有所區別),其中ip及物理偏移量在消息的協議格式中均有體現;當我們拿到消息所屬的broker地址,以及該消息的物理存儲偏移量時,也就唯一定位了該條消息,所以使用“ip+物理偏移量”的方式作為消息id

在某些場景下,msg unique id也會存儲在indexFile中,

索引查詢

查詢這塊,我們將放在消息發送、消費的章節來闡述,此處僅僅討論索引結構設計中page cache所承擔的角色

其實在整個流程中,RocketMQ是極度依賴page cache的,尤其是消費隊列,數據查詢是通過如下流程來查詢消息的:

1、broker接受請求 -> 2、查詢ConsumeQueue文件(20byte) -> 3、拿到消息的physicOffset -> 4、查詢commitLog文件(msg size)

我們發現第二步及第四步都只是查詢很小的數據量,如果沒有page cache擋在磁盤前,整體的性能必將是斷崖式下降。我有朋友做過禁掉page cache后,RocketMQ前后的性能的對比相差好幾個量級

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容