MQ初窺門徑
全稱(message queue)消息隊列,一個用於接收消息、存儲消息並轉發消息的中間件
應用場景
用於解決的場景,總之是能接收消息並轉發消息
- 用於異步處理,比如A服務做了什么事情,異步發送一個消息給其他B服務。
- 用於削峰,有些服務(秒殺),請求量很高,服務處理不過來,那么請求先放到消息隊列里面,后面按照能力處理,相當於蓄水池。
- 應用解耦、消息通訊等等
總之MQ是可以存放消息並轉發消息的中間件,場景取決於拿這個能力去解決什么問題
MQ概念模型
MQ向別人承諾的場景是接收消息,存儲,並可以轉發消息
接收消息
接收消息,那么接收誰的消息,為了說明這個問題,那么mq需要引入一個概念,叫做生產者,也就是發送消息的服務,否則沒有辦法來區分是誰發的消息,生產者通過網絡發送消息就可以,中間的細節我們先不探討。
那么還有一個問題就是消息發送給誰?
- 我在發送消息的時候,指明我要發送給誰,就像發送短信一樣,你需要指明你要發送給誰?
這種方案在使用中是有問題的,因為在現在業務很多場景中, 發送方其實根本不知道對方是誰,他只是將自己的狀態發送出來,那么誰需要這個消息,誰就接收,第二個如果指明了接收方,那么以后增加一個接收方就要改一下配置或者代碼,將發送消息的人跟接收消息的人綁定在一起了
那么有沒有方案,解耦的最好辦法就是中間人,也叫中間層,我只發送給第三方,誰要消息,問第三方要,那么相當於我把發送的目標改為發送給第三方,這里的第三方就是mq,為了說明說明發送的地方,mq引入了topic的概念,發送方把消息發送到mq指定的一個通道中,以后誰想要這個消息,就跟mq說我想要這個通道的消息,也就是發送方發送的消息。
消費消息
消費消息,那么同理的一個問題,誰消費消息,為了說明那么mq需要引入一個概念,叫做消費者,也就是消費消息的服務,否則沒有辦法來區分是誰在接收消息,消費者通過網絡接收消息就可以了,中間的細節我們先不探討。
那么問題來了,消費者怎么說明消費誰的消息,上文已經說了,通過指明mq的topic,來決定我要哪一類消息。
至此我們總結一下最后的模型
也就是最后生產者和消費者通過MQ的topic概念來實現解耦。
存儲
說到存儲,其實效率才是最主要的,容量不是我們關心的,但是說到存儲,不只是mq,所有需要高效率的存儲其實最后利用的核心都是一樣的。
- 隨機寫轉換成順序寫
- 集中刷盤
為什么隨機寫要轉換為順序寫?
第一 現在主流的硬盤是機械硬盤
第二 機械硬盤的機械結構一次讀寫時間 = 尋道時間 + 旋轉延遲 + 讀取數據時間
那么尋道時間比較長,如果是順序寫,只需要一次尋道時間,關於機械硬盤整個過程,讀者可自行google。
為什么集中刷盤?
因為每次刷盤都會進行系統調用,第二還是跟硬盤的本身屬性有關,無論是機械硬盤還是ssd按照一定塊刷盤會比小數據刷盤效率更好
kafka
為什么先說kafka的存儲,因為kafka是第一個高性能的消息中間件,其中rocketmq也是借鑒於它,所以我們先說。
先給出最終模型變化圖。
- 為什么引入消費組概念?
上一次模型圖我們還沒有消費組,那么引入消費組,是因為現在一個服務都有很多實例在運行,消費組是對這群一群機器的一個划分,他還是一個概念而已。 - mq內部也發生了變化,一個topic后面又對應了很多partition,partition也是一個概念,他只不過是把一個topic分成了很多份,每一份叫一個partition,你高興也可以叫他xxx,那么我們來說說為什么要分成很多份,一份不行嗎?
因為現在一個服務有很多實例在運行,如果topic只有一份的話,那么所有的實例都會來消費消息,並且都是搶占我們一個topic,這不可避免引入了多實例競爭,以及他們之間怎么協調,一堆問題需要關注解決,現在我把topic分成了很多份,每一份只給一個實例,那么就不會引入各實例之間的競爭問題了,簡化了mq的問題。 - 生產組的引入也是一樣的,只不過是一組機器的一個概念,一個邏輯的划分,生產者發送消息原先是發往topic,那么現在topic分成了很多份,生產者發送消息,需要說明發往哪個partition或者隨意分配都可以,只不過最終發送的消息,會到一個topic下的一份里面。無論使用哪種映射方式都可以。
那么模型出來了,我們說說存儲的問題。
對於kafka,一個partition對應一個文件,每次消息來都是順序寫這個文件。並且是定時刷盤,而不是每次寫都刷盤,所以kafka的寫非常高效。
rocketmq
上文我們說了rocketmq借鑒於kafka,所以存儲借鑒了kafka,但是rocketmq不是僅僅把partition改成了ConsumeQueue,在這里做了變化,原先kafka,里面partition存儲的是整個消息,但是現在ConsumeQueue里面是存儲消息的存儲地址,但是不存儲消息了。
現在每個ConsumeQueue存儲的是每個消息在commitlog這個文件的地址,但是消息存在於commitlog中。
也就是所有的消息體都寫在了一個文件里面,每個ConsumeQueue只是存儲這個消息在commitlog中地址。
存儲對比
-
消息體存儲的變化
那么我們先來看看kafka,假設partition有1000個,一個partition是順序寫一個文件,總體上就是1000個文件的順序寫,是不是就變成了隨機寫,所以當partition增加到一定數目后,kafka性能就會下降。而rocketmq是把消息都寫到一個CommitLog文件中,所以相當於一個文件的順序寫。 -
為什么索引文件(ConsumeQueue)的增加對性能影響沒有那么partition大?
(kafka也有索引文件,在這里只是想說明索引文件的增加跟partition增加的區別)
雖然rocketmq是把消息都寫到一個CommitLog文件中,但是按照上面的實例會有1000個ConsumeQueue,也就是一千個文件,那么為什么就沒有把順序寫變成隨機寫,帶來性能的下降呢?首先就要介紹linux的pagecache我們平常調用write或者fwrite的時候,數據還沒有寫到磁盤上,只是寫到一個內核的緩存(pagecache),只有當我們主動調用flush的時候才會寫到硬盤中。或者需要回寫的pagecache占總內存一定比例的時候或者一個應該回寫的page超過一定時間還沒有寫磁盤的時候,內核會將這些數據通過后台進程寫到磁盤中(總結就是達到一定比例,或者多長時間還沒有回寫,會被內核自動回寫)。
然后我們現在來看看為什么大量索引文件的順序寫沒有像partition一樣導致性能明顯下降。ConsumeQueue只存儲了(CommitLog Offet + Size + Message Tag Hashcode),一共20個字節,那么當commitlog定時任務刷盤之后,應該回寫的pagecache的比例就會下降很多,那么ConsumeQueue的部分可以不用刷盤,就相當於ConsumeQueue的內容會等待比較長的時間聚合批量寫入,而kafka每個partition都是存儲的消息體,因為消息體都相對較大,基本在kb之上。
當一個partition刷盤的時候,應該回寫的pagecache的比例降低的並不多,不能阻止其他partition的刷盤,所以會大量存在多個partition同時刷盤的場景,變成隨機寫。但是rocketmq消息都會寫入一個commitLog,也就是順序寫。
所以我們總結下這個點:
1、consumerQueue消息格式大小固定(20字節),寫入pagecache之后被觸發刷盤頻率相對較低。就是因為每次寫入的消息小,造成他占用的pagecache少,主要占用方一旦被清理,那么他就可以不用清理了。
2、kafka中多partition會存在隨機寫的可能性,partition之間刷盤的沖撞率會高,但是rocketmq中commitLog都是順序寫。
歡迎關注博主公眾號,后面會持續更新mq系列知識點,一起討論。