消息隊列背后的設(shè)計思想
點擊上方“服務(wù)端思維”,選擇“設(shè)為星標(biāo)”
回復(fù)”669“獲取獨家整理的精選資料集
回復(fù)”加群“加入全國服務(wù)端高端社群「后端圈」
消息隊列也通常稱為消息中間件,提到消息隊列,大部分互聯(lián)網(wǎng)人或多或少都聽過該名詞。對于后端工程師而言,更是日常開發(fā)中必備的一項技能。隨著大數(shù)據(jù)時代的到來,apache 旗下的 kafka 一度成為消息隊列的代名詞,提起消息隊列大家自然而然就想到了 kafka。近而網(wǎng)上有太多太多介紹消息隊列 kafka 功能或者內(nèi)部實現(xiàn)的文章。
然而消息隊列本身是工程領(lǐng)域內(nèi)一種解決問題的通用方案。它的背后有著一些通用的設(shè)計思想和經(jīng)典模型,這些是消息隊列的精髓和靈魂。它們獨立于任何一種消息隊列的具體實現(xiàn)(例如 kafka),但每種消息隊列(除了 kafka 外,還有 rocketMQ、pulsar 等)的實現(xiàn)中到處體現(xiàn)著這些設(shè)計思想。本文主要從抽象層面來簡單談?wù)勏㈥犃斜澈蟮囊恍┰O(shè)計思想,輔助理解消息隊列這一類組件。
本文主要解決三個問題:
消息隊列適合什么場景? 消息隊列有哪些主流產(chǎn)品、各自的優(yōu)缺點? 消息隊列背后的設(shè)計思想(整體核心模型、數(shù)據(jù)存儲考量、數(shù)據(jù)獲取方案對比、消費者消費模型)
目錄如下:
消息隊列背后的設(shè)計思想
1.消息隊列適合哪些場景?
1.1 異步處理數(shù)據(jù)
1.2 系統(tǒng)應(yīng)用解耦
1.3 業(yè)務(wù)流量削峰
1.4 小結(jié)
2.有哪些消息隊列(解決方案)?
2.1 消息隊列主流產(chǎn)品
2.2 不同消息隊列對比
3. 消息隊列背后的設(shè)計思想
3.1 消息隊列核心模型
3.2 消息隊列數(shù)據(jù)組織方式
3.3 獲取數(shù)據(jù)的推、拉兩種方案對比
3.4 消息隊列消費模型
3.5 小結(jié)
4. 總結(jié)
5. 參考資料
1.消息隊列適合哪些場景?
消息隊列:它主要用來暫存生產(chǎn)者生產(chǎn)的消息,供后續(xù)其他消費者來消費。它的功能主要有兩個:a.暫存(存儲)、b.隊列(有序:先進先出)。其他大部分場景對數(shù)據(jù)的消費沒有順序要求,主要用它的暫存能力 。從目前互聯(lián)網(wǎng)應(yīng)用中使用消息隊列的場景來看,主要有以下三個:
1. 異步處理數(shù)據(jù)
2. 系統(tǒng)應(yīng)用解耦
3. 業(yè)務(wù)流量削峰
下面對上述每一種場景進行簡單描述。
1.1 異步處理數(shù)據(jù)

第一個例子我們以現(xiàn)實生活中送快遞來類比,在該例子中我們把暫存快遞的快遞柜比作暫存數(shù)據(jù)的消息隊列。我們來看一下在現(xiàn)實生活中,沒有快遞柜時,快遞員把快遞送到目的地后,一般需要把聯(lián)系收貨人來簽收快遞,如果收貨人此時有空,那一切都很順利。但如果收貨人此時不方便(開會、正在吃午飯、外出出差)。那對于快遞員而言,就很尷尬,需要一直等待(開會or吃午飯)或者將快遞拿回去(外出出差),導(dǎo)致白跑一趟。這對于快遞員而言簡直太不友好。
從這兒可以看出,當(dāng)快遞員送貨時,是一個同步狀態(tài),即需要等待收貨人簽收后才能去送下一趟單子,對快遞員而言效率太低。上述例子雖然有點牽強,大家湊合理解,意思能大概理解到位就ok。
接著我們再來看一下,當(dāng)有了快遞柜后,對于快遞員而言,每次需要送快遞時,只需要將快遞投擲到快遞柜,然后再通過短信或者電話通知收貨人具體的快遞信息即可。他就可以繼續(xù)去派送下一單。而對于收獲人而言,也可以根據(jù)具體方便的時間來取件。這樣一來,二者完全異步了,不用相互等待了。
在這個例子中,如果把快遞員比作生產(chǎn)者,收貨人比作是消費者,則快遞柜就類似于消息隊列。我們可以通過采用消息隊列來實現(xiàn)異步數(shù)據(jù)的處理。
1.2 系統(tǒng)應(yīng)用解耦

案例二我們以目前最主流的推薦系統(tǒng)中內(nèi)容的流轉(zhuǎn)來舉例。在推薦系統(tǒng)中當(dāng)創(chuàng)作者發(fā)布了一條內(nèi)容后,該內(nèi)容會首先經(jīng)過安全部分的相關(guān)審核,通過審核后的內(nèi)容,通常需要進行內(nèi)容入庫存儲、送入模型進行特征的計算和生成。
假如后期我們想提升推薦的效果,需要單獨構(gòu)建一份冷啟動的推薦池,此時也需要用到這部分內(nèi)容,那問題來了,在沒有使用消息隊列時,對于上游服務(wù)而言,需要通過擴展新的邏輯來實現(xiàn)該功能。同時在該場景里,會存在依賴三個下游服務(wù),如果其中一個下游服務(wù)失敗后,該如何處理,是重試還是返回失敗等這些細(xì)節(jié)的處理。如果后期這部分?jǐn)?shù)據(jù)還想在其他渠道分發(fā),那又該如何對接。明顯這種場景下面臨著系統(tǒng)緊耦合的問題。
我們再來看一下,如果我們一開始就引入了消息隊列,那問題又會變成怎樣的呢?當(dāng)內(nèi)容審核通過后,就直接將數(shù)據(jù)生產(chǎn)出來丟到消息隊列中,下游的多個服務(wù)再從消息隊列消費數(shù)據(jù)。當(dāng)后續(xù)這一份數(shù)據(jù)需要擴展供其他系統(tǒng)使用時,也只要通過新的消費者來接入到消息隊列消費就ok。上游生產(chǎn)消息的模塊不要做任何的改動。這樣我們就通過消息隊列進行了系統(tǒng)應(yīng)用之間的解耦。這是消息隊列的第二個用途。
1.3 業(yè)務(wù)流量削峰

消息對應(yīng)的第三個使用場景便是削峰。在現(xiàn)如今的互聯(lián)網(wǎng)世界中,電商場景中每年的618秒殺活動、雙11搶購便是最典型的案例。這種場景中系統(tǒng)的峰值流量往往集中于一小段時間內(nèi),平常的流量比較可控,所以為了防止系統(tǒng)在短時間內(nèi)的峰值流量沖垮,往往采用消息隊列來削弱峰值流量,高峰值期間產(chǎn)生的訂單消息等數(shù)據(jù)首先送入到消息隊列中暫存,然后供下游系統(tǒng)根據(jù)自己的消費能力來逐步處理。同時這類消息往往對時延的要求不是很高,比較適合采用消息隊列暫存。
1.4 小結(jié)

最后我們在對本節(jié)的內(nèi)容做一個簡單的總結(jié),上面通過三個簡單的實例介紹了消息隊列的典型的三個使用場景:異步、解耦、削峰。換個角度來理解可以看到,消息隊列主要適用于處理對消息要求不是很實時,同時一份數(shù)據(jù)可能會多處使用的場景,不同的使用方處理速率不同。更多的消息隊列的使用場景讀者可以自行找資料閱讀和總結(jié)。
2.有哪些消息隊列(解決方案)?
2.1 消息隊列主流產(chǎn)品

上圖根據(jù)時間線展示了不同時間點產(chǎn)生的消息列隊產(chǎn)品,主要的產(chǎn)品有:ActiveMQ(2003)、RabbitMQ(2006)、Kafka(2010)、RocketMQ(2011)、Pulsar(2012)。這些消息隊列中或多或少我們都聽過一些,部分也在項目中真實使用過。下面對上述幾個消息隊列做一個簡單的介紹。
ActiveMQ: ActiveMQ由Apache軟件基金會基于Java語言開發(fā)的一個開源的消息代理。能夠支持多個客戶機或服務(wù)器。計算機集群等屬性支持ActiveMQ來管理通信系統(tǒng)。
RabbitMQ: RabbitMQ是實現(xiàn)了高級消息隊列協(xié)議(AMQP)的開源消息代理軟件(亦稱面向消息的中間件)。RabbitMQ服務(wù)器是用Erlang語言編寫的,而集群和故障轉(zhuǎn)移是構(gòu)建在開放電信平臺框架上的。所有主要的編程語言均有與代理接口通訊的客戶端庫。RabbitMQ支持多種消息傳遞協(xié)議、傳遞確認(rèn)等特性。
Kafka: Apache Kafka是由Apache軟件基金會開發(fā)的一個開源消息系統(tǒng)項目,由Scala寫成。Kafka最初是由LinkedIn開發(fā),并于2011年初開源。2012年10月從Apache Incubator畢業(yè)。該項目的目標(biāo)是為處理實時數(shù)據(jù)提供一個統(tǒng)一、高通量、低等待的平臺。Kafka是一個分布式的、分區(qū)的、多復(fù)本的日志提交服務(wù)。它通過一種獨一無二的設(shè)計提供了一個消息系統(tǒng)的功能。
RocketMQ: Apache RocketMQ是一個分布式消息和流媒體平臺,具有低延遲、強一致、高性能和可靠性、萬億級容量和靈活的可擴展性。它有借鑒Kafka的設(shè)計思想,但不是kafka的拷貝。
Pulsar: Apache Pulsar 是 Apache 軟件基金會頂級項目,是下一代云原生分布式消息流平臺,集消息、存儲、輕量化函數(shù)式計算為一體,采用計算與存儲分離架構(gòu)設(shè)計,支持多租戶、持久化存儲、多機房跨區(qū)域數(shù)據(jù)復(fù)制,具有強一致性、高吞吐、低延時及高可擴展性等流數(shù)據(jù)存儲特性,被看作是云原生時代實時消息流傳輸、存儲和計算最佳解決方案。
2.2 不同消息隊列對比

上圖詳細(xì)的展示了幾種消息隊列的各自功能及優(yōu)缺點,首先,ActiveMQ和RabbitMQ二者屬于同一量級,在吞吐量上比后面三者差一個量級;其次,支持強一致性的有RocketMQ和Pulsar,在對消息一致性要求比較高的場景可以采用這些產(chǎn)品。同時kafka雖然會有數(shù)據(jù)丟失的風(fēng)險,但其吞吐量比較高同時社區(qū)非常活躍,在大數(shù)據(jù)的絕大部分場景里,都可以采用kafka;最后kafka、RocketMQ、Pulsar這幾款是基于磁盤存儲數(shù)據(jù)的,內(nèi)存加速訪問。而ActiveMQ、RabbitMQ采用內(nèi)存存儲數(shù)據(jù),也支持?jǐn)?shù)據(jù)持久化到磁盤。
3. 消息隊列背后的設(shè)計思想
在前面,第一節(jié)內(nèi)容中,主要介紹了為什么要使用消息隊列,消息隊列適合解決哪些問題?在第二節(jié)內(nèi)容中,又介紹了有哪些可選擇的消息隊列,以及他們之間各自的優(yōu)缺點。這一節(jié)是最重要的內(nèi)容,主要會介紹一下上述消息隊列背后的通用的一些設(shè)計思想。部分思想可以擴展到其他的業(yè)務(wù)模型或者領(lǐng)域內(nèi)。后面講到對應(yīng)內(nèi)容也會有所提及。
3.1 消息隊列核心模型

上圖是幾乎所有消息隊列設(shè)計的一個核心模型。對于一個消息隊列而言,從數(shù)據(jù)流向的維度,可以拆解為三大部分:生產(chǎn)者、消息隊列集群、消費者,數(shù)據(jù)是從生產(chǎn)者流向消息隊列集群,最終再從消息隊列集群流向消費者,下面對這幾個概念進行一一闡述。
生產(chǎn)者: 生產(chǎn)數(shù)據(jù)的服務(wù),通常也稱為數(shù)據(jù)的輸入提供方,這里的數(shù)據(jù)通常指我們的業(yè)務(wù)數(shù)據(jù),例如推薦場景中用戶對內(nèi)容的點擊數(shù)據(jù)、內(nèi)容曝光數(shù)據(jù)、電商中的訂單數(shù)據(jù)等等。生產(chǎn)者通常是作為客戶端的方式存在,但在支持事務(wù)消息的消息隊列中,生產(chǎn)者也被設(shè)計為服務(wù)端,實現(xiàn)事務(wù)消息這一特性。其次生產(chǎn)者通常會有多個,消息隊列集群內(nèi)部也會有多個分區(qū)隊列,所以在生產(chǎn)者發(fā)送數(shù)據(jù)時,通常會存在負(fù)載均衡的一些策略,常見的有按key hash、輪詢、隨機等方式。其本質(zhì)是一條數(shù)據(jù),被消息隊列封裝后也被稱為一條消息,該條消息只能發(fā)送到其消息隊列集群內(nèi)部的一個分區(qū)隊列中。因此只需按照一定的策略從多個隊列中選擇一個隊列即可。
消息隊列集群: 消息隊列集群是消息隊列這種組件實現(xiàn)中的核心中的核心,它的主要功能是存儲消息、過濾消息、分發(fā)消息。
其中存儲消息主要指生產(chǎn)者生產(chǎn)的數(shù)據(jù)需要存儲到消息隊列內(nèi)部;存儲消息可以說是消息隊列的核心,一個消息隊列吞吐量的高低、性能優(yōu)劣都和它的存儲模型脫不開關(guān)系。這部分內(nèi)容會在3.2節(jié)進行介紹。
過濾消息只指消息隊列可以通過一定的規(guī)則或者策略進行消息的過濾,該項能力通常也被稱為消息路由;過濾消息屬于高階的特性功能,AMQP協(xié)議對這些能力抽象的比較完備,部分消息隊列可以選擇性的實現(xiàn)該協(xié)議來達到該功能,關(guān)于AMQP協(xié)議內(nèi)容讀者可以自行搜索資料閱讀,此處不再展開。
分發(fā)消息是指消息隊列通常需要將消息分發(fā)給處理同一邏輯的多個消費者處理或者處理不同邏輯的不同消費者處理。分發(fā)消息可以說和消費者模型想掛鉤,這塊會涉及到不同的數(shù)據(jù)獲取方式,也會涉及到消費者消費消息的模型。這兩部分內(nèi)容會在3.3節(jié)和3.4節(jié)進行重點介紹。
此外絕大部分的消息隊列也都支持對消息進行分類,分類的標(biāo)簽稱為topic(主題),一個topic中存放的是同一類消息。
消費者: 最終消息隊列存儲的消息會被消費者消費使用,消費者也可以看做消息隊列中數(shù)據(jù)的輸出方。消費者通常有兩種方式從消息隊列中獲取數(shù)據(jù):推送(push)數(shù)據(jù)、拉取(pull)數(shù)據(jù),3.3節(jié)中會對這兩種方案進行詳細(xì)對比說明。其次消費者也經(jīng)常是作為客戶端的角色出現(xiàn)在在消息隊列這種組件中。
3.2 消息隊列數(shù)據(jù)組織方式
在這一節(jié)中,我們詳細(xì)看看消息隊列存儲消息這個環(huán)節(jié)的一些權(quán)衡考量,通常數(shù)據(jù)的存儲無外乎就是兩種,一種是存儲在非易失性存儲中,例如磁盤這種介質(zhì);另一種是選擇存儲在易失性存儲中,典型的就是內(nèi)存。關(guān)于二者的對比大家可以參考下表,此處就不再贅述。

通常在大部分組件設(shè)計時,往往會選擇一種主要介質(zhì)來存儲、另一種介質(zhì)作為輔助使用。就拿redis來說,它主要采用內(nèi)存存儲數(shù)據(jù),磁盤用來做輔助的持久化。拿RabbitMQ舉例,它也是主要采用內(nèi)存存儲消息,但也支持將消息持久化到磁盤。而RocketMQ、Kafka、Pulsar這種,則是數(shù)據(jù)主要存儲在磁盤,通過內(nèi)存來主力提升系統(tǒng)的性能。關(guān)系型數(shù)據(jù)庫例如mysql這種組件也是主要采用磁盤組織數(shù)據(jù),合理利用內(nèi)存提升性能。
針對采用內(nèi)存存儲數(shù)據(jù)的方案而言,難點一方面在于如何在不降低訪問效率的情況下,充分利用有限的內(nèi)存空間來存儲盡可能多的數(shù)據(jù),這個過程中少不了對數(shù)據(jù)結(jié)構(gòu)的選型、優(yōu)化;另一方面在于如何保證數(shù)據(jù)盡可能少的丟失,我們可以看到針對此問題的解決方案通常是快照+廣泛意義的wal文件來解決。此類典型的代表就是redis啦。
針對采用磁盤存儲數(shù)據(jù)的方案而言,難點一方面在于如何根據(jù)系統(tǒng)所要解決的特點場景進行合理的對磁盤布局。讀多寫少情況下采用b+樹方式存儲數(shù)據(jù);寫多讀少情況下采用lsm tree這類方案處理。另一方面在于如何盡可能減少對磁盤的頻繁訪問,一些做法是采用mmap進行內(nèi)存映射,提升讀性能;還有一些則是采用緩存機制緩存頻繁訪問的數(shù)據(jù)。還有一些則是采用巧妙的數(shù)據(jù)結(jié)構(gòu)布局,充分利用磁盤預(yù)讀特性保證系統(tǒng)性能。
總的來說,針對寫磁盤的優(yōu)化,要不采用順序?qū)懱嵘阅?、要不采用異步寫磁盤提升性能(異步寫磁盤時需要結(jié)合wal保證數(shù)據(jù)的持久化,事實上wal也主要采用順序?qū)懙奶匦?;針對讀磁盤的優(yōu)化,一方面是緩存、另一方面是mmap內(nèi)存映射加速讀。
上述這些存儲方案上權(quán)衡的選擇在kafka、RocketMQ、Pulsar中都可以看到。其實拋開消息隊列而言,這些存儲方案的選擇上無論是關(guān)系型數(shù)據(jù)庫還是kv型組件都是通用的。
下圖列舉了幾種磁盤上的數(shù)據(jù)組織方式,僅供大家參考。

3.3 獲取數(shù)據(jù)的推、拉兩種方案對比
在前面3.1節(jié)中提到,消費者在從消息隊列中獲取數(shù)據(jù)時,主要有兩種方案:
1. 等待推送數(shù)據(jù)
2. 主動拉取數(shù)據(jù)
目前的消息隊列實現(xiàn)時,都會選擇支持兩種的至少一種,關(guān)于這兩種方案的對比,大家可以參考下表。

在此處,個人想拋開消息隊列談一點關(guān)于這兩種方案的理解,其實推拉模型不僅僅只用于消息隊列這種組件中,更一般意義上,它解決的其實是數(shù)據(jù)傳送雙方的一個問題。本質(zhì)是數(shù)據(jù)需要從一方流向另一方。順著這個思路來看,下面這三個例子都是遵循這個原則。
網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù): 在IO多路復(fù)用中,以epoll為例,當(dāng)內(nèi)核檢測到監(jiān)聽的描述符有數(shù)據(jù)到來時,epoll_wait()阻塞會返回,上層用戶態(tài)程序就知道有數(shù)據(jù)就緒了,然后可以通過read()系統(tǒng)調(diào)用函數(shù)讀取數(shù)據(jù)。這個過程就緒通知,類似于推送,但推送的不是數(shù)據(jù),而是數(shù)據(jù)就緒的信號。具體的數(shù)據(jù)獲取方式還是需要通過拉取的方式來主動讀。
feeds流系統(tǒng)用戶時間線后臺實現(xiàn)方案(讀擴散、寫擴散): 讀擴散和寫擴散更是這樣一個case。對于讀擴散而言,主要采用拉取的方式獲取數(shù)據(jù)。而對于寫擴散而言,它是典型的數(shù)據(jù)推送的方式。當(dāng)然在系統(tǒng)實現(xiàn)中,更復(fù)雜的場景往往會選擇讀寫結(jié)合的思路來實現(xiàn)。
生活中的點外賣例子: 當(dāng)下單點外賣時,通常也會有兩種方式可以選擇,外賣派送、到店自取。不過通常外賣派送比較實時,我們通常就選擇這種方式了而已??梢钥闯鐾赓u派送其實就是一種推的方式,而到店自取,則是拉的方式。
3.4 消息隊列消費模型
本節(jié)我們來介紹最后一部分內(nèi)容,消息隊列中消費者的消費模型。下圖中上半部分展示了最簡單的一種消費模型。一個生產(chǎn)者、一個消費者。但往往我們的一份數(shù)據(jù)通常會被不同場景所使用。那這個時候,首先就會存在每種場景需要使用全量的數(shù)據(jù)、而且不同場景之間不會相關(guān)影響,彼此獨立。方便理解起見,我們假設(shè)有N個場景需要使用這同一份數(shù)據(jù),每個場景需要消費全量的數(shù)據(jù)。而在N個場景中的一種場景里,又會有多個消費者一起分?jǐn)傁M這些數(shù)據(jù)。我們假設(shè)一個場景里有M個消費者。由于每個場景中包含M個消費者,我們也將其采用消費者組來描述。通過上面的介紹,我們可以用下面一句話總結(jié)消息隊列中的消費模型:
消費者消費者模型其實是一個1:N:M的關(guān)系,一份數(shù)據(jù)被N個消費者組獨立使用,每個消費者組中有M個消費者進行分?jǐn)傁M
其實這種模型也稱為發(fā)布訂閱模型,對于一條消息而言,組間廣播、組內(nèi)單播。一條消息只能被一個消費者組中的一個消費者使用。在消費者組內(nèi)部也存在一些負(fù)載均衡的策略。常用的有:輪詢、隨機、hash、一致性hash等方案。

3.5 小結(jié)
第三部分內(nèi)容我們重點介紹了關(guān)于消息隊列背后的一些設(shè)計思想,其中包括:消息隊列的核心模型、數(shù)據(jù)存儲模型、推拉方案獲取數(shù)據(jù)對比、消費者消費模型。其中數(shù)據(jù)存儲模型不僅僅適用于消息隊列,也同樣適用于其他數(shù)據(jù)存儲組件的方案選擇。同理數(shù)據(jù)獲取的推拉兩種方案也不僅僅局限于消息隊列。我們可以在很多業(yè)務(wù)場景里看到同類思想的影子。
4. 總結(jié)
到此,本文也就告一段落了。本文主要從理論、抽象層面泛泛的談了下關(guān)于消息隊列的一些思想和理念。主要介紹了消息隊列的使用場景,主流的消息隊列可選方案以及他們之間的優(yōu)缺點。最后介紹了一些關(guān)于消息隊列背后的設(shè)計理念。本文只是拋磚引玉,希望上述內(nèi)容能輔助大家一起重新認(rèn)識消息隊列。后面會逐步挑選上述的幾種消息隊列(kafka、RocketMQ、Pulsar),重點分析其內(nèi)部實現(xiàn)機制,敬請期待。限于個人水平有限,理解有誤之處歡迎大家批評指正。
5. 參考資料
ActiveMQ與RabbitMQ的區(qū)別
Kafka、ActiveMQ、RabbitMQ、RocketMQ 區(qū)別以及高可用原理
Kafka、RabbitMQ、RocketMQ等消息中間件的對比
Apache Pulsar 在騰訊計費場景下的應(yīng)用
kafka Push vs. pull
消息隊列-推/拉模式學(xué)習(xí) & ActiveMQ及JMS學(xué)習(xí)
— 本文結(jié)束 —

● 漫談設(shè)計模式在 Spring 框架中的良好實踐
關(guān)注我,回復(fù) 「加群」 加入各種主題討論群。
對「服務(wù)端思維」有期待,請在文末點個在看
喜歡這篇文章,歡迎轉(zhuǎn)發(fā)、分享朋友圈


