一文理解消息隊(duì)列如何保證高可用
之前博客《一文理解為什么需要使用消息隊(duì)列》提到過,系統(tǒng)引入消息隊(duì)列后,需要考慮如何保證消息隊(duì)列的高可用。
本篇文章將圍繞幾個(gè)常見的消息隊(duì)列中間件(RabbitMQ,RocketMQ,Kafka)進(jìn)行逐個(gè)講解。
RabbitMQ
RabbitMQ 有三種模式:單機(jī)模式、普通集群模式、鏡像集群模式。
單機(jī)模式
單機(jī)模式,一般是開發(fā)者本地啟動(dòng)調(diào)試使用,不會(huì)應(yīng)用到生產(chǎn)環(huán)境。
普通集群模式
普通集群模式下,在多臺(tái)機(jī)器上分別啟動(dòng)一個(gè)RabbitMQ實(shí)例。新創(chuàng)建的queue,只會(huì)放在其中一個(gè)RabbitMQ實(shí)例上,但是每個(gè)實(shí)例都同步queue的元數(shù)據(jù)(元數(shù)據(jù)是queue的一些配置信息,例如通過元數(shù)據(jù)可以找到queue所在實(shí)例)。在消費(fèi)者進(jìn)行消費(fèi)的時(shí)候,如果連接到的實(shí)例沒有指定的queue,那么這個(gè)實(shí)例會(huì)從queue所在實(shí)例上拉取數(shù)據(jù)過來。
這種模式下,要么消費(fèi)者每次隨機(jī)連接一個(gè)實(shí)例然后拉取數(shù)據(jù),要么固定連接指定queue所在實(shí)例消費(fèi)數(shù)據(jù),前者有數(shù)據(jù)拉取的開銷,后者可能會(huì)導(dǎo)致單實(shí)例性能瓶頸。
缺點(diǎn):
性能開銷大,不同節(jié)點(diǎn)間的數(shù)據(jù)傳輸,可能會(huì)導(dǎo)致網(wǎng)絡(luò)帶寬壓力和消耗很重。
如果某個(gè)queue實(shí)例宕機(jī),會(huì)導(dǎo)致節(jié)點(diǎn)接下來其他實(shí)例就無法從那個(gè)實(shí)例拉取消息。即時(shí)開啟了消息持久化,在該節(jié)點(diǎn)重啟的過程中,對(duì)外服務(wù)也是中斷。
普通集群模式相對(duì)單機(jī)模式,提升了消費(fèi)速度,提高了吞吐量。

鏡像集群模式
這種模式下RabbitMQ才能實(shí)現(xiàn)高可用。跟普通集群模式不一樣的是,在鏡像集群模式下,創(chuàng)建的queue,無論元數(shù)據(jù)還是queue里的消息都會(huì)完整存在于多個(gè)實(shí)例上。

這樣就可以保證任何一個(gè)節(jié)點(diǎn)宕機(jī)后,其他節(jié)點(diǎn)還包含了這個(gè)queue的完整數(shù)據(jù),consumer可以到其他的節(jié)點(diǎn)上去消費(fèi)數(shù)據(jù)。
缺點(diǎn):
性能開銷大,消息需要同步到多個(gè)節(jié)點(diǎn),導(dǎo)致網(wǎng)絡(luò)帶寬壓力和消耗很重。
節(jié)點(diǎn)沒有拓展性。如果某個(gè)queue負(fù)載過重,即使添加新的RabbitMQ節(jié)點(diǎn),也需要包含這個(gè)queue的所有數(shù)據(jù)。
注:
指定queue的消息同時(shí)存在節(jié)點(diǎn)的數(shù)量是可以通過RabbitMQ控制臺(tái)配置進(jìn)行設(shè)置。
鏡像集群模式相對(duì)普通集群模式,提升了可用性,但對(duì)吞吐量沒有改善。
RocketMQ
RocketMQ的集群方式分為:
多Master模式
多Master多Slave異步模式(一般生產(chǎn)環(huán)境使用)
多Master多Slave同步模式(對(duì)數(shù)據(jù)可靠性要求高時(shí)使用)
RocketMQ中由NameServer集群、Broker 集群、Producer 集群和Consumer集群組成。
NameServer: 提供輕量級(jí)的服務(wù)發(fā)現(xiàn)和路由。每個(gè)NameServer記錄完整的路由信息,提供等效的讀寫服務(wù),并支持快速存儲(chǔ)擴(kuò)展。
Broker: 通過提供輕量級(jí)的 Topic 和 Queue 機(jī)制來處理消息存儲(chǔ),同時(shí)支持推(push)和拉(pull)模式以及主從結(jié)構(gòu)的容錯(cuò)機(jī)制。
Producer:生產(chǎn)者,產(chǎn)生消息的實(shí)例,擁有相同Producer Group的Producer組成一個(gè)集群。
Consumer:消費(fèi)者,接收消息進(jìn)行消費(fèi)的實(shí)例,擁有相同Consumer Group的Consumer組成一個(gè)集群。
RocketMQ是通過Broker主從機(jī)制來實(shí)現(xiàn)高可用的。相同Broker名稱,不同Brokerid的機(jī)器組成一個(gè)Broker組,BrokerId=0表明這個(gè)Broker是Master,BrokerId>0表明這個(gè)Broker是Slave。
消息生產(chǎn)的高可用:創(chuàng)建Topic時(shí),把Topic的多個(gè)message queue創(chuàng)建在多個(gè)broker組上。這樣當(dāng)一個(gè)Broker組的Master不可用后,Producer仍然可以給其他組的Master發(fā)送消息。
消息消費(fèi)的高可用:Consumer并不能配置從Master讀還是Slave讀。當(dāng)Master不可用或者繁忙的時(shí)候,Consumer會(huì)被自動(dòng)切換到從Slave讀。這樣當(dāng)Master出現(xiàn)故障后,Consumer仍然可以從Slave讀,保證了消息消費(fèi)的高可用。
可以理解為:Rocketmq是通過多個(gè)Master實(shí)現(xiàn)寫入容災(zāi),通過主從實(shí)現(xiàn)讀取容災(zāi)。

上圖中,Broker Master1和Broker Slave1 是主從結(jié)構(gòu),實(shí)例之間會(huì)進(jìn)行數(shù)據(jù)同步。同時(shí)每個(gè)Broker與NameServer集群中的所有節(jié)點(diǎn)建立長連接,定時(shí)注冊(cè)Topic信息到所有NameServer中。
Producer與NameServer集群中的其中一個(gè)節(jié)點(diǎn)(隨機(jī)選擇)建立長連接,定期從NameServer獲取Topic路由信息,并向提供Topic服務(wù)的Broker Master建立長連接,且定時(shí)向Broker發(fā)送心跳。Producer只能將消息發(fā)送到Broker Master;是Consumer則不一樣,它同時(shí)和提供Topic服務(wù)的Master和Slave建立長連接,既可以從Broker Master訂閱消息,也可以從Broker Slave訂閱消息。
在RocketMQ里面,1臺(tái)機(jī)器要么是Master,要么是Slave,這在初始的機(jī)器配置里面就確定了。其中Master的Broker id = 0,Slave的Broker id > 0。有點(diǎn)類似于MySQL的主從概念,Master掛了以后,Slave仍然可以提供讀服務(wù),但是由于有多主的存在,當(dāng)一個(gè)Master掛了以后,可以寫到其他的Master上。
2.1 多Master模式
只有Master,無Slave。某個(gè)實(shí)例掛了,該實(shí)例在重啟前未被消費(fèi)的消息無法被消費(fèi)。
優(yōu)點(diǎn):配置簡單,性能最高。
缺點(diǎn):單臺(tái)機(jī)器重啟或宕機(jī)期間,該機(jī)器下未被消費(fèi)的消息在機(jī)器恢復(fù)前不可訂閱,影響消息實(shí)時(shí)性。
2.2. 多Master多Slave異步模式
每個(gè)Master配一個(gè)Slave,有多對(duì)Master-Slave,集群采用異步復(fù)制方式,主備有短暫消息延遲,毫秒級(jí)
優(yōu)點(diǎn):性能同多Master幾乎一樣,實(shí)時(shí)性高,主備間切換對(duì)應(yīng)用透明,不需人工干預(yù)。
缺點(diǎn):Master宕機(jī)或磁盤損壞時(shí)會(huì)有少量消息丟失。
2.3. 多Master多Slave同步模式
每個(gè)Master配一個(gè)Slave,有多對(duì)Master-Slave,集群采用同步雙寫方式,主備都寫成功,才向應(yīng)用返回成功
優(yōu)點(diǎn):服務(wù)可用性與數(shù)據(jù)可用性非常高。
缺點(diǎn):性能比異步集群略低(大約低10%)。
Kafka
kafka 0.8以前,是沒有HA機(jī)制的,就是任何一個(gè)Broker宕機(jī)了,那個(gè)Broker上的Partition就沒法寫也沒法讀,無法實(shí)現(xiàn)高可用性。
Kafka 0.8版本以后,增加了replica副本機(jī)制,從而實(shí)現(xiàn)了Kafka的高可用性。
基礎(chǔ)知識(shí)
如果對(duì)Kafka不了解的話,可以先看這篇博客《一文快速了解Kafka》。
消息傳遞同步策略
Producer在發(fā)布消息到某個(gè)Partition時(shí),先通過ZooKeeper找到該P(yáng)artition的Leader,然后無論該Topic的Replication Factor為多少,Producer只將該消息發(fā)送到該P(yáng)artition的Leader。Leader會(huì)將該消息寫入其本地Log。每個(gè)Follower都從Leader pull數(shù)據(jù)。這種方式上,F(xiàn)ollower存儲(chǔ)的數(shù)據(jù)順序與Leader保持一致。Follower在收到該消息并寫入其Log后,向Leader發(fā)送ACK。當(dāng)Leader收到了ISR中的所有Replica的ACK,該消息就被認(rèn)為已經(jīng)commit了,Leader將增加HW并且向Producer發(fā)送ACK。
為了提高性能,每個(gè)Follower在接收到數(shù)據(jù)后就立馬向Leader發(fā)送ACK,而非等到數(shù)據(jù)寫入Log中。因此,對(duì)于已經(jīng)commit的消息,Kafka只能保證它被存于多個(gè)Replica的內(nèi)存中,而不能保證它們被持久化到磁盤中,也就不能完全保證異常發(fā)生后該條消息一定能被Consumer消費(fèi)。
Consumer讀消息也是從Leader讀取,只有被commit過的消息才會(huì)暴露給Consumer。
Kafka Replication的數(shù)據(jù)流如下圖所示:

Kafka高可用機(jī)制
每個(gè)Partition的數(shù)據(jù)都會(huì)同步到其他機(jī)器上,形成自己的多個(gè)replica副本。然后所有replica會(huì)選舉一個(gè)leader出來,那么生產(chǎn)和消費(fèi)都跟這個(gè)leader打交道,然后其他replica就是follower。寫的時(shí)候,leader會(huì)負(fù)責(zé)把數(shù)據(jù)同步到所有follower上去,讀的時(shí)候就直接讀leader上數(shù)據(jù)即可。只能讀寫leader?很簡單,要是你可以隨意讀寫每個(gè)follower,那么就要care數(shù)據(jù)一致性的問題,系統(tǒng)復(fù)雜度太高,很容易出問題。kafka會(huì)均勻的將一個(gè)partition的所有replica分布在不同的機(jī)器上,這樣才可以提高容錯(cuò)性。
kafka的這種機(jī)制,就有所謂的高可用性了,因?yàn)槿绻硞€(gè)broker宕機(jī)了,也沒事兒,因?yàn)槟莻€(gè)broker上面的partition在其他機(jī)器上都有副本的,那么此時(shí)會(huì)重新選舉一個(gè)新的leader出來,大家繼續(xù)讀寫那個(gè)新的leader即可。這就有所謂的高可用性了。
寫過程
寫數(shù)據(jù)的時(shí)候,生產(chǎn)者就寫leader,然后leader將數(shù)據(jù)落地寫本地磁盤,接著其他follower自己主動(dòng)從leader來pull數(shù)據(jù)。一旦所有follower同步好數(shù)據(jù)了,就會(huì)發(fā)送ack給leader,leader收到所有follower的ack之后,就會(huì)返回寫成功的消息給生產(chǎn)者。(當(dāng)然,這只是其中一種模式,還可以適當(dāng)調(diào)整這個(gè)行為)
讀過程
消費(fèi)的時(shí)候,只會(huì)從Leader去讀,但是只有當(dāng)一個(gè)消息已經(jīng)被所有follower都同步成功并返回ack的時(shí)候,這個(gè)消息才能夠被消費(fèi)者讀到。
對(duì)比RocketMQ與Kafka的架構(gòu)區(qū)別
namesrv VS zk(不考慮Kafka2.8的Raft元數(shù)據(jù)模式)
Kafka通過Zookeeper來進(jìn)行協(xié)調(diào),而RocketMq通過自身的namesrv進(jìn)行協(xié)調(diào):
Kafka在具備選舉功能,在Kafka里面,Master/Slave的選舉,有2步:第1步,先通過ZK在所有機(jī)器中,選舉出一個(gè)KafkaController;第2步,再由這個(gè)Controller,決定每個(gè)partition的Master是誰,Slave是誰。因?yàn)橛辛诉x舉功能,所以kafka某個(gè)partition的master掛了,該partition對(duì)應(yīng)的某個(gè)slave會(huì)升級(jí)為主對(duì)外提供服務(wù)。
RocketMQ不具備選舉,Master/Slave的角色也是固定的。當(dāng)一個(gè)Master掛了之后,你可以寫到其他Master上,但不能讓一個(gè)Slave切換成Master。那么rocketMq是如何實(shí)現(xiàn)高可用的呢,其實(shí)很簡單,rocketMq的所有broker節(jié)點(diǎn)的角色都是一樣,上面分配的topic和對(duì)應(yīng)的queue的數(shù)量也是一樣的,Mq只能保證當(dāng)一個(gè)broker掛了,把原本寫到這個(gè)broker的請(qǐng)求遷移到其他broker上面,而并不是這個(gè)broker對(duì)應(yīng)的slave升級(jí)為主。
吞吐量的對(duì)比
Kafka在消息存儲(chǔ)過程中會(huì)根據(jù)Partition的數(shù)量創(chuàng)建物理文件,例如創(chuàng)建一個(gè)topic并指定了3個(gè)Partition,那么就會(huì)有3個(gè)物理文件。
RocketMQ使用commitLog進(jìn)行消息存儲(chǔ)(順序?qū)?,隨機(jī)讀),相當(dāng)于只有一個(gè)物理文件。
Kafka的多文件并發(fā)寫入相對(duì)RocketMQ的單文件寫入,Kafka的性能要好很多。但Kafka的大量文件存儲(chǔ)會(huì)導(dǎo)致一個(gè)問題:當(dāng)Broker中包含Partition特別多的時(shí)候,磁盤的訪問會(huì)發(fā)生很大的瓶頸,畢竟單個(gè)文件看著是append操作,但是多個(gè)文件之間必然會(huì)導(dǎo)致磁盤的尋道。
