核心組件(4個組件+消息存儲結構)
客戶端消費模式
1. MQ的使用場景
昨天在寫完之后,有些讀者在評論中提出:到底什么時候用MQ?舉幾個典型的例子。
1.最常用的就是Publish/Subscribe
從開發(fā)模式中,我們使用過JAVA Swing Button EventListener或者使用JQuery 的自定義事件,由某個動作觸發(fā)然后傳播下去,這個事件廣播就是Publish,我們監(jiān)聽的過程,就是訂閱的過程。這個是MQ最基本的功能。
舉個業(yè)務例子:訂單支付的過程,會牽扯到會員模塊、消息短信推送服務、訂單更新,最初我們還能想清楚,所有把代碼寫在一個service中,但后續(xù)逐漸加業(yè)務,例如:會員還需要有積分、會員余額要預警、會員等級也得變化、還得告訴商戶費用結清了。我舉的例子可能不是很恰當,但這確實反映問題了,系統(tǒng)過于耦合。從這里引入MQ,對周邊業(yè)務做清理,附屬業(yè)務直接訂閱消息。
2.應對大流量沖擊、削峰填谷
系統(tǒng)的前端數據采集設備數量太大,且具有業(yè)務波峰。還有就是典型的雙11,促銷活動會期間,系統(tǒng)會承受比正常流量高很多倍的沖擊。此時,采集的數據不要求實時,購物訂單也不需要實時反饋給用戶,面對這種應用場景,使用消息隊列可以實現消息的異步處理、請求的流量削峰作用。
3.異構系統(tǒng)的通信
直接上例子:一個系統(tǒng)有多個開發(fā)小組,由于功能特點分別使用了C++、JAVA、Go、python等多種語言實現,系統(tǒng)之間需要通信,使用事件驅動架構,那么這里可以引入MQ了,作為異構系統(tǒng)和事件驅動架構的backbone。
2. RocketMQ初識
首先應該看看RocketMQ的收發(fā)消息模型:
消息收發(fā)模型
上圖,你能看出來:
生產者生產消息,可以放到隊列中,多個消費者可以消費。
生產者的同一種消息,可以放到不同的隊列中,由消費者消息。
實際部署圖:
物理部署圖
如上圖所示, RocketMQ的部署結構有4部分組成:NameServer、Broker、Producer、Consumer。
這里我們抽象以下:分為客戶端(Producer、Consumer)、服務端(Broker、NameServer)兩部分。簡單來說,就是客戶端的收發(fā)消息、服務器接收消息并持久化。
2.1 客戶端的功能有什么?
client發(fā)送消息有負載均衡,因為客戶端中保存著當前所有服務器列表,每次發(fā)送都切換一臺服務器發(fā)送消息,服務均衡負載。
發(fā)送代碼為線程安全,支持高并發(fā)寫操作。
消費者端負載均衡集群消費模式下,同一個ID的所有消費者實例平均消費該Topic的所有隊列。這里要注意,廣播模式下,則一個consumer實例消費這個Topic對應的所有隊列。這點和Apache ActiveMQ的主題訂閱是一樣的,每個消費者消費Topic的所有內容(若存在消費者消費慢,容易內存溢出)。
2.2 服務端(Broker)的功能有什么?
Broker就是RocketMQ的核心了,RocketMQ高并發(fā)讀寫主要利用Linux操作系統(tǒng)的PageCache特性,通過Java的MappedByteBuffer直接操作PageCache。MappedByteBuffer能直接將文件直接映射到內存,其實就是Map把文件的內容被映像到計算機虛擬內存的一塊區(qū)域,這樣就可以直接操作內存當中的數據而無需操作的時候每次都通過I/O去物理硬盤寫文件的。
鼓勵下自己,下面開始深入了
2.3 服務端存儲的消息結構
RocketMQ高性能讀寫,得益于它的消息存儲結構:commitLog和comsume queue兩部分組成。(這部分大概了解,比別人多掌握一點)
數據存儲結構
commitLog
保存所有消息的元數據,所有消息到達Broker之后都會保存到commitLog中,所有的topic消息都會寫入一個commitLog中,通常的目錄是:
${user.home}/store/${commitlog}/${fileName}
每個commitLog上限是1G,滿了之后會自動新建一個commitLog文件保存數據。這里的Log文件會有清理機制,有兩種清理措施:
按時間清理,默認清理3天前的commitLog。
按磁盤占比,當磁盤使用到75%,開始清理最老的commmitLog文件。
consumeQueue
ConsumeQueue相當于CommitLog的索引文件,消費者消費時會從consumeQueue中查找消息在commitLog中的offset,再去commitLog中查找元數據。如果某個消息只在CommitLog中有數據,沒在ConsumerQueue中, 則消費者無法消費
consumeQueue的數據結構包含3個部分: consumeQueue中存儲單元是一個20字節(jié)定長的二進制數據,順序寫順序讀 。如圖:
ConsumeQueue存儲格式的特性,保證了寫過程的順序寫盤(寫CommitLog文件),在單臺服務器上,MQ元數據都落在單個文件上,大量數據IO都在順序寫同一個commitLog,滿1G了再寫新的,真正意義上的順序寫盤,再加上MQ默認是累計4K才強制從PageCache中刷到磁盤(緩存),所以高并發(fā)寫性能突出,至于讀取數據呢,有Queue的offset、size,讓讀盤操作是跳躍式的。在RocketMQ中讀取消息是依賴系統(tǒng)PageCache,PageCache命中率越高,讀性能越高,Linux平時也會盡量預讀數據,使得應用直接訪問磁盤的概率降低。
3. 核心組件
3.1 Name Server
用于存儲Topic、Broker的關系,簡單、穩(wěn)定。多個Name-Server之間不通信,單臺NameServer宕機不影響其他NameServer與集群;及時整個NameServer集群宕機,已經正常工作的Producer、Consumer、Broker仍然正常工作,但新加入的就無法工作。
NameServer壓力不會大,主要用戶維持心跳和提供Toptic-Broker的關系。但如過topic過多,導致Nameserver心跳數據過大,網絡不好的情況下,傳輸失敗,導致NameServer誤認為Broker心跳失敗。
3.2 Broker
Broker部署相對復雜:
Broker分為Master與Slave,一個Master可以對應多個Slave,但是一個Slave只能對應一個Master。
Master與Slave的對應關系通過指定相同的BrokerName,不同的BrokerId來定義。(BrokerId為0表示Master,非0表示Slave)
Master也可以部署多個,每個Broker與Name Server集群中的所有節(jié)點建立長連接,定時注冊Topic信息到所有Name Server。
具有高并發(fā)讀寫,負載均衡和動態(tài)伸縮的特性:
負載均衡:Broker上存Topic信息,Topic由多個隊列組成,隊列會平均分散在多個Broker上,而Producer的發(fā)送機制保證消息盡量平均分布到所有隊列中,最終效果就是所有消息都平均落在每個Broker上。
動態(tài)伸縮:Topic維度:假如一個Topic的消息量特別大,但集群水位壓力還是很低,就可以擴大該Topic的隊列數,Topic的隊列數跟發(fā)送、消費速度成正比。Broker維度:如果集群水位很高了,需要擴容,直接加機器部署B(yǎng)roker就可以。Broker起來后想Namesrv注冊,Producer、Consumer通過Namesrv發(fā)現新Broker,立即跟該Broker直連,收發(fā)消息。
高可用與可靠性
高可用:Broker提供主備,主宕機,備提供讀,不可寫。
可靠性:所有發(fā)送到Broker的消息,有同步刷盤和異步刷盤。同步刷盤時,消息寫入物理文件才會返回成功。異步刷盤時,只有機器宕機,才會產生消息丟失,broker掛掉可能會發(fā)生,但是機器宕機崩潰是很少發(fā)生的,除非突然斷電。
Broker與NameServer的心跳 ,單個Broker跟所有Namesrv保持心跳請求,心跳間隔為30秒,心跳請求中包括當前Broker所有的Topic信息。Namesrv會反查Broer的心跳信息,如果某個Broker在2分鐘之內都沒有心跳,則認為該Broker下線,調整Topic跟Broker的對應關系。但此時Namesrv不會主動通知Producer、Consumer有Broker宕機。
3.3 Producer
Producer啟動時,也需要指定Namesrv的地址,從Namesrv集群中隨機選一臺建立長連接。如果該Namesrver宕機,會自動連其他Nameserver。直到有可用的Namesrv為止。
Producer每30秒從Namesrv獲取Topic跟Broker的映射關系,更新到本地內存中。Producer會和它要發(fā)送的topic相關的master類型的broker建立TCP連接,每隔30秒發(fā)一次心跳。在Broker端也會每10秒掃描一次當前注冊的Producer,如果發(fā)現某個Producer超過2分鐘都沒有發(fā)心跳,則斷開連接。
生產者發(fā)送時,會自動輪詢當前所有可發(fā)送的broker,一條消息發(fā)送成功,下次換另外一個broker發(fā)送,以達到消息平均落到所有的broker上。
3.4 Consumer
消費者啟動時需要指定Namesrv地址,隨機與其中一個Namesrv建立長連接。消費者每隔30秒從nameserver獲取所有topic的最新隊列情況(這意味著某個broker如果宕機,客戶端最多要30秒才能感知),并向提供Topic服務的Master、Slave建立長連接,且定時向Master、Slave發(fā)送心跳。Consumer既可以從Master訂閱消息,也可以從Slave訂閱消息,訂閱規(guī)則由Broker配置決定。
Consumer跟Broker是長連接,會每隔30秒發(fā)心跳信息到Broker。Broker端每10秒檢查一次當前存活的Consumer,若發(fā)現某個Consumer 2分鐘內沒有心跳,就斷開與該Consumer的連接,并且向該消費組的其他實例發(fā)送通知,觸發(fā)該消費者集群的負載均衡。
3.5 通信關系
Producer和Name Server:每一個Producer會與Name Server集群中的一臺機器建立TCP連接,會從這臺Name Server上拉取路由信息。
Producer和broker:Producer會和它要發(fā)送的topic相關的master類型的broker建立TCP連接,用于發(fā)送消息以及定時的心跳信息。broker中會記錄該Producer的信息,供查詢使用
broker與Name Server:broker(不管是master還是slave)會和每一臺Name Server機器來建立TCP連接。broker在啟動的時候會注冊自己配置的topic信息到Name Server集群的每一臺機器中。即每一臺Name Server都有該broker的topic的配置信息。master與master之間無連接,master與slave之間有連接
Consumer和Name Server:每一個Consumer會和Name Server集群中的一臺機器建立TCP連接,會從這臺Name Server上拉取路由信息,進行負載均衡
Consumer和broker:Consumer可以與master或者slave的broker建立TCP連接來進行消費消息,Consumer也會向它所消費的broker發(fā)送心跳信息,供broker記錄。
4. 消費模式
4.1 消費者端的消費以及負載均衡
先討論消費者的消費模式,消費者有兩種模式消費:
集群消費
廣播消費。
廣播消費
每個消費者消費Topic下的所有隊列。
集群消費
一個topic可以由同一個ID下所有消費者分擔消費。具體例子:假如TopicA有6個隊列(kafka稱之為分區(qū)),某個消費者ID起了2個消費者實例,那么每個消費者負責消費3個隊列。如果再增加一個消費者ID相同消費者實例,即當前共有3個消費者同時消費6個隊列,那每個消費者負責2個隊列的消費。
消費者端的負載均衡,就是集群消費模式下,同一個ID的所有消費者實例平均消費該Topic的所有隊列。
對于負載均衡,在出現分區(qū)(kafka稱為分區(qū))或者隊列(RocketMQ稱隊列)增加或者減少的時候、Consumer增加或者減少的時候都會進行reblance操作。
對于RocketMQ:客戶端自己會定時對所有的topic的進行reblance操作,對于每個topic,會從broker獲取所有Consumer列表,從broker獲取隊列列表,按照負載均衡策略,計算各自負責哪些隊列。這種就要求進行負載均衡的時候,各個Consumer獲取的數據是一致的,不然不同的Consumer的reblance結果就不同。
遍歷Consumer下的所有topic,然后根據topic訂閱所有的消息
獲取同一topic和Consumer Group下的所有Consumer
然后根據具體的分配策略來分配消費隊列,分配的策略包含:平均分配、消費端配置等
4.2 客戶端消費是broker的Push還是客戶端的Pull?
實際上consumer的消費只有主動拉的方式,那么有沒有push的方式?實際上類似http的請求長連接,consumer發(fā)起pull請求,等待broker有數據之后,再把數據push回來,實際上還是主動拉消息。
以上我們對Rocket做了全面的理論探索,后續(xù)配合實施外加代碼實戰(zhàn),帶你玩轉RocketMQ。