《一篇文了解分布式隊列編程:從模型、實戰(zhàn)到優(yōu)化》要點:
本文介紹了一篇文了解分布式隊列編程:從模型、實戰(zhàn)到優(yōu)化,希望對您有用。如果有疑問,可以聯(lián)系我們。
本文由美團點評技術(shù)團隊出品,一篇文助你掌握分布式隊列編程的要義.從模型到實戰(zhàn)再到優(yōu)化,基本涵蓋你可能踩的坑與其解決辦法.
作為一種基礎(chǔ)的抽象數(shù)據(jù)結(jié)構(gòu),隊列被廣泛應(yīng)用在各類編程中.大數(shù)據(jù)時代對跨進程、跨機器的通訊提出了更高的要求,和以往相比,分布式隊列編程的運用幾乎已無處不在.但是,這種常見的基礎(chǔ)性的事物往往容易被忽視,使用者往往會忽視兩點:
使用分布式隊列的時候,沒有意識到它是隊列.
有具體需求的時候,忘記了分布式隊列的存在.
文章首先從最基礎(chǔ)的需求出發(fā),詳細(xì)剖析分布式隊列編程模型的需求來源、定義、結(jié)構(gòu)以及其變化多樣性.通過這一部分的講解,作者期望能在兩方面幫助讀者:一方面,提供一個系統(tǒng)性的思考方法,使讀者能夠?qū)⒕唧w需求關(guān)聯(lián)到分布式隊列編程模型,具備進行分布式隊列架構(gòu)的能力;另一方面,通過全方位的講解,讓讀者能夠快速識別工作中碰到的各種分布式隊列編程模型.
文章的第二部分實戰(zhàn)篇.根據(jù)作者在新美大實際工作經(jīng)驗,給出了隊列式編程在分布式環(huán)境下的一些具體應(yīng)用.這些例子的基礎(chǔ)模型并非首次出現(xiàn)在互聯(lián)網(wǎng)的文檔中,但是所有的例子都是按照挑戰(zhàn)、構(gòu)思、架構(gòu)三個步驟進行講解的.這種講解方式能給讀者一個“從需求出發(fā)去構(gòu)架分布式隊列編程”的旅程.
老司機介紹
劉丁,新美大廣告平臺CRM系統(tǒng)技術(shù)負(fù)責(zé)人,曾就職于Amazon、Tripadvisor.2014年加入美團,先后負(fù)責(zé)美團推薦系統(tǒng)、智能篩選系統(tǒng)架構(gòu),作為技術(shù)負(fù)責(zé)人主導(dǎo)了美團廣告系統(tǒng)的開發(fā)和上線.目前致力于推進新美大廣告運營的標(biāo)準(zhǔn)化、自動化和智能化.
新美大廣告平臺是美團、大眾點評雙平臺的營銷推廣平臺,幫助商戶推廣店鋪品牌及提升客流量.
模型篇從基礎(chǔ)的需求出發(fā),去思考何時以及如何使用分布式隊列編程模型.建模環(huán)節(jié)非常重要,因為大部分中高級工程師面臨的都是具體的需求,接到需求后的第一個步驟就是建模.通過本篇的講解,希望讀者能夠建立起從需求到分布式隊列編程模型之間的橋梁.
通信是人們最基本的需求,同樣也是計算機最基本的需求.對于工程師而言,在編程和技術(shù)選型的時候,更容易進入大腦的概念是RPC、RESTful、Ajax、Kafka.在這些具體的概念后面,最本質(zhì)的東西是“通訊”.
所以,大部分建模和架構(gòu)都需要從“通信”這個基本概念開始.當(dāng)確定系統(tǒng)之間有通訊需求的時候,工程師們需要做很多的決策和平衡,這直接影響工程師們是否會選擇分布式隊列編程模型作為架構(gòu).從這個角度出發(fā),影響建模的因素有四個:When、Who、Where、How.
通信的一個基本問題是:發(fā)出去的消息什么時候需要被接收到?這個問題引出了兩個基礎(chǔ)概念:“同步通訊”和“異步通訊”.根據(jù)理論抽象模型,同步通信和異步通信最本質(zhì)的差別來自于時鐘機制的有無.同步通信的雙方需要一個校準(zhǔn)的時鐘,異步通信的雙方不需要時鐘.
現(xiàn)實的情況是,沒有完全校準(zhǔn)的時鐘,所以沒有絕對的同步通信.同樣,絕對異步通信意味著無法控制一個發(fā)出去的消息被接收到的時間點,無期限的等待一個消息顯然毫無實際意義.
所以,實際編程中所有的通信既不是“同步通信”也不是“異步通信”;或者說,既是“同步通信”也是“異步通信”.特別是對于應(yīng)用層的通信,其底層架構(gòu)可能既包含“同步機制”也包含“異步機制”.判斷“同步”和“異步”消息的標(biāo)準(zhǔn)問題太深,而不適合繼續(xù)展開.作者這里給一些啟發(fā)式的建議:
發(fā)出去的消息是否需要確認(rèn),如果不需要確認(rèn),更像是異步通信,這種通信有時候也稱為單向通信(One-WayCommunication).
如果需要確認(rèn),可以根據(jù)需要確認(rèn)的時間長短進行判斷.時間長的更像是異步通信,時間短的更像是同步通信.當(dāng)然時間長短的概念是純粹的主觀概念,不是客觀標(biāo)準(zhǔn).
發(fā)出去的消息是否阻塞下一個指令的執(zhí)行,如果阻塞,更像是同步,否則,更像是異步.
無論如何,工程師們不能生活在混沌之中,不做決定往往是最壞的決定.當(dāng)分析一個通信需求或者進行通信構(gòu)架的時候,工程師們被迫作出“同步”還是“異步”的決定.當(dāng)決策的結(jié)論是“異步通信”的時候,分布式隊列編程模型就是一個備選項.
在進行通信需求分析的時候,需要回答的另外一個基本問題是:消息的發(fā)送方是否關(guān)心誰來接收消息,或者反過來,消息接收方是否關(guān)心誰來發(fā)送消息.如果工程師的結(jié)論是:消息的發(fā)送方和接收方不關(guān)心對方是誰、以及在哪里,分布式隊列編程模型就是一個備選項.因為在這種場景下,分布式隊列架構(gòu)所帶來的解耦能給系統(tǒng)架構(gòu)帶來這些好處:
無論是發(fā)送方還是接收方,只需要跟消息中間件通信,接口統(tǒng)一.統(tǒng)一意味著降低開發(fā)成本.
在不影響性能的前提下,同一套消息中間件部署,可以被不同業(yè)務(wù)共享.共享意味著降低運維成本.
發(fā)送方或者接收方單方面的部署拓?fù)涞淖兓挥绊憣?yīng)的另一方.解藕意味著靈活和可擴展.
在進行通信發(fā)送方設(shè)計的時候,令工程師們苦惱的問題是:如果消息無法被迅速處理掉而產(chǎn)生堆積怎么辦、能否被直接拋棄?如果根據(jù)需求分析,確認(rèn)存在消息積存,并且消息不應(yīng)該被拋棄,就應(yīng)該考慮分布式隊列編程模型構(gòu)架,因為隊列可以暫存消息.
對通信需求進行架構(gòu),一系列的基礎(chǔ)挑戰(zhàn)會迎面而來,這包括:
可用性,如何保障通信的高可用.
可靠性,如何保證消息被可靠地傳遞.
持久化,如何保證消息不會丟失.
吞吐量和響應(yīng)時間.
跨平臺兼容性.
除非工程師對造輪子有足夠的興趣,并且有充足的時間,采用一個滿足各項指標(biāo)的分布式隊列編程模型就是一個簡單的選擇.
很難給出分布式隊列編程模型的精確定義,由于本文偏重于應(yīng)用,作者并不打算完全參照某個標(biāo)準(zhǔn)的模型.總體而言:分布式隊列編程模型包含三類角色:發(fā)送者(Sender)、分布式隊列(Queue)、接收者(Receiver).發(fā)送者和接收者分別指的是生產(chǎn)消息和接收消息的應(yīng)用程序或服務(wù).
需要重點明確的概念是分布式隊列,它是提供以下功能的應(yīng)用程序或服務(wù):
接收“發(fā)送者”產(chǎn)生的消息實體;
傳輸、暫存該實體;
為“接收者”提供讀取該消息實體的功能.
特定的場景下,它當(dāng)然可以是Kafka、RabbitMQ等消息中間件.但它的展現(xiàn)形式并不限于此,例如:
隊列可以是一張數(shù)據(jù)庫的表,發(fā)送者將消息寫入表,接收者從數(shù)據(jù)表里讀消息.
如果一個程序把數(shù)據(jù)寫入Redis等內(nèi)存Cache里面,另一個程序從Cache里面讀取,緩存在這里就是一種分布式隊列.
流式編程里面的的數(shù)據(jù)流傳輸也是一種隊列.
典型的MVC(Model–view–controller)設(shè)計模式里面,如果Model的變化需要導(dǎo)致View的變化,也可以通過隊列進行傳輸.這里的分布式隊列可以是數(shù)據(jù)庫,也可以是某臺服務(wù)器上的一塊內(nèi)存.
最基礎(chǔ)的分布式隊列編程抽象模型是點對點模型,其他抽象構(gòu)架模型居于改基本模型上各角色的數(shù)量和交互變化所導(dǎo)致的不同拓?fù)鋱D.具體而言,不同數(shù)量的發(fā)送者、分布式隊列以及接收者組合形成了不同的分布式隊列編程模型.記住并理解典型的抽象模型結(jié)構(gòu)對需求分析和建模而言至關(guān)重要,同時也會有助于學(xué)習(xí)和深入理解開源框架以及別人的代碼.
基礎(chǔ)模型中,只有一個發(fā)送者、一個接收者和一個分布式隊列.如下圖所示:
如果發(fā)送者和接收者都可以有多個部署實例,甚至不同的類型;但是共用同一個隊列,這就變成了標(biāo)準(zhǔn)的生產(chǎn)者消費者模型.在該模型,三個角色一般稱為生產(chǎn)者(Producer)、分布式隊列(Queue)、消費者(Consumer).
如果只有一類發(fā)送者,發(fā)送者將產(chǎn)生的消息實體按照不同的主題(Topic)分發(fā)到不同的邏輯隊列.每種主題隊列對應(yīng)于一類接收者.這就變成了典型的發(fā)布訂閱模型.在該模型,三個角色一般稱為發(fā)布者(Publisher),分布式隊列(Queue),訂閱者(Subscriber).
MVC模型
如果發(fā)送者和接收者存在于同一個實體中,但是共享一個分布式隊列.這就很像經(jīng)典的MVC模型.
為了讓讀者更好地理解分布式隊列編程模式概念,這里將其與一些容易混淆的概念做一些對比 .
分布式隊列編程模型的通訊機制一般是采用異步機制,但是它并不等同于異步編程.
首先,并非所有的異步編程都需要引入隊列的概念,例如:大部分的操作系統(tǒng)異步I/O操作都是通過硬件中斷( Hardware Interrupts)來實現(xiàn)的.
其次,異步編程并不一定需要跨進程,所以其應(yīng)用場景并不一定是分布式環(huán)境.
最后,分布式隊列編程模型強調(diào)發(fā)送者、接收者和分布式隊列這三個角色共同組成的架構(gòu).這三種角色與異步編程沒有太多關(guān)聯(lián).
隨著Spark Streaming,Apache Storm等流式框架的廣泛應(yīng)用,流式編程成了當(dāng)前非常流行的編程模式.但是本文所闡述的分布式隊列編程模型和流式編程并非同一概念.
首先,本文的隊列編程模式不依賴于任何框架,而流式編程是在具體的流式框架內(nèi)的編程.
其次,分布式隊列編程模型是一個需求解決方案,關(guān)注如何根據(jù)實際需求進行分布式隊列編程建模.流式框架里的數(shù)據(jù)流一般都通過隊列傳遞,不過,流式編程的關(guān)注點比較聚焦,它關(guān)注如何從流式框架里獲取消息流,進行map、reduce、 join等轉(zhuǎn)型(Transformation)操作、生成新的數(shù)據(jù)流,最終進行匯總、統(tǒng)計.
這里所有的項目都是作者在新美大工作的真實案例.實戰(zhàn)篇的關(guān)注點是訓(xùn)練建模思路,所以這些例子都按照挑戰(zhàn)、構(gòu)思、架構(gòu)三個步驟進行講解.受限于保密性要求,有些細(xì)節(jié)并未給出,但這些細(xì)節(jié)并不影響講解的完整性.
另一方面,特別具體的需求容易讓人費解,為了使講解更加順暢,作者也會采用一些更通俗易懂的例子.通過本篇的講解,希望和讀者一起去實踐“如何從需求出發(fā)去構(gòu)架分布式隊列編程模型”.
需要聲明的是,這里的解決方案并不是所處場景的最優(yōu)方案.但是,任何一個稍微復(fù)雜的問題,都沒有最優(yōu)解決方案,更談不上唯一的解決方案.實際上,工程師每天所追尋的只是在滿足一定約束條件下的可行方案.當(dāng)然不同的約束會導(dǎo)致不同的方案,約束的松弛度決定了工程師的可選方案的寬廣度.
信息采集處理應(yīng)用廣泛,例如:廣告計費、用戶行為收集等.作者碰到的具體項目是為廣告系統(tǒng)設(shè)計一套高可用的采集計費系統(tǒng).
典型的廣告CPC、CPM計費原理是:收集用戶在客戶端或者網(wǎng)頁上的點擊和瀏覽行為,按照點擊和瀏覽進行計費.計費業(yè)務(wù)有如下典型特征:
計費業(yè)務(wù)的典型特征給我們帶來了如下挑戰(zhàn):
采集的高可用性意味著我們需要多臺服務(wù)器同時采集,為了避免單IDC故障,采集服務(wù)器需要部署在多IDC里面.
實現(xiàn)一個高可用、高吞吐量、高一致性的信息傳遞系統(tǒng)顯然是一個挑戰(zhàn),為了控制項目開發(fā)成本,采用開源的消息中間件進行消息傳輸就成了必然選擇.
完整性約束要求集中進行計費,所以計費系統(tǒng)發(fā)生在核心IDC.
計費服務(wù)并不關(guān)心采集點在哪里,采集服務(wù)也并不關(guān)心誰進行計費.
根據(jù)以上構(gòu)思,我們認(rèn)為采集計費符合典型的“生產(chǎn)者消費者模型”.
采集計費系統(tǒng)架構(gòu)圖如下:
采用此架構(gòu),我們可以在如下方面做進一步優(yōu)化:
緩存是一個非常寬泛的概念,幾乎存在于系統(tǒng)各個層級.典型的緩存訪問流程如下:
對于已經(jīng)存入緩存的數(shù)據(jù),其更新時機和更新頻率是一個經(jīng)典問題,即緩存更新機制(Cache Replacement Algorithms ).典型的緩存更新機制包括:近期最少使用算法(LRU)、最不經(jīng)常使用算法(LFU).
這兩種緩存更新機制的典型實現(xiàn)是:啟動一個后臺進程,定期清理最近沒有使用的,或者在一段時間內(nèi)最少使用的數(shù)據(jù).由于存在緩存驅(qū)逐機制,當(dāng)一個請求在沒有命中緩存時,業(yè)務(wù)層需要從持久層中獲取信息并更新緩存,提高一致性.
分布式緩存給緩存更新機制帶來了新的問題:
根據(jù)上面的分析,分布式緩存需要解決的問題是:在保證讀取性能的前提下,盡可能地提高老數(shù)據(jù)的一致性和新數(shù)據(jù)的可用性.如果仍然假定最近被訪問的鍵值最有可能被再次訪問(這是LRU或者LFU成立的前提),鍵值每次被訪問后觸發(fā)一次異步更新就是提高可用性和一致性最早的時機.
無論是高性能要求還是業(yè)務(wù)解耦都要求緩存讀取和緩存更新分開,所以我們應(yīng)該構(gòu)建一個單獨的集中的緩存更新服務(wù).集中進行緩存更新的另外一個好處來自于頻率控制.
由于在一段時間內(nèi),很多類型訪問鍵值的數(shù)量滿足高斯分布,短時間內(nèi)重復(fù)對同一個鍵值進行更新Cache并不會帶來明顯的好處,甚至造成緩存性能的下降.通過控制同一鍵值的更新頻率可以大大緩解該問題,同時有利于提高整體數(shù)據(jù)的一致性,參見“排重優(yōu)化”.
綜上所述,業(yè)務(wù)訪問方需要把請求鍵值快速傳輸給緩存更新方,它們之間不關(guān)心對方的業(yè)務(wù).要快速、高性能地實現(xiàn)大量請求鍵值消息的傳輸,高性能分布式消息中間件就是一個可選項.這三方一起組成了一個典型的分布式隊列編程模型.
如下圖,所有的業(yè)務(wù)請求方作為生產(chǎn)者,在返回業(yè)務(wù)代碼處理之前將請求鍵值寫入高性能隊列.Cache Updater作為消費者從隊列中讀取請求鍵值,將持久層中數(shù)據(jù)更新到緩存中.
采用此架構(gòu),我們可以在如下方面做進一步優(yōu)化:
典型的后臺任務(wù)處理應(yīng)用包括工單處理、火車票預(yù)訂系統(tǒng)、機票選座等.我們所面對的問題是為運營人員創(chuàng)建工單.一次可以為多個運營人員創(chuàng)建多個工單.這個應(yīng)用場景和火車票購買非常類似.工單相對來說更加抽象,所以,下文會結(jié)合火車票購買和運營人員工單分配這兩種場景同時講解.
典型的工單創(chuàng)建要經(jīng)歷兩個階段:數(shù)據(jù)篩選階段、工單創(chuàng)建階段.例如,在火車票預(yù)訂場景,數(shù)據(jù)篩選階段用戶選擇特定時間、特定類型的火車,而在工單創(chuàng)建階段,用戶下單購買火車票.
工單創(chuàng)建往往會面臨如下挑戰(zhàn):
如果將用戶篩選的最終規(guī)則做為消息存儲下來,并發(fā)送給工單創(chuàng)建系統(tǒng).此時,工單創(chuàng)建系統(tǒng)將具備創(chuàng)建工單所需的全局信息,具備在滿足各種約束的條件下進行統(tǒng)籌優(yōu)化的能力.如果工單創(chuàng)建階段采用單實例部署,就可以避免數(shù)據(jù)鎖定問題,同時也意味著沒有鎖沖突,所以也不會有死鎖或任務(wù)延遲問題.
居于以上思路,在多工單處理系統(tǒng)的模型中,篩選階段的規(guī)則創(chuàng)建系統(tǒng)將充當(dāng)生產(chǎn)者角色,工單創(chuàng)建系統(tǒng)將充當(dāng)消費者角色,篩選規(guī)則將作為消息在兩者之間進行傳遞.這就是典型的分布式隊列編程架構(gòu).根據(jù)工單創(chuàng)建量的不同,可以采用數(shù)據(jù)庫或開源的分布式消息中間件作為分布式隊列.
該架構(gòu)流程如下圖:
采用該架構(gòu),我們在數(shù)據(jù)鎖定、運籌優(yōu)化、原子性問題都能得到比較好成果:
接下來重點闡述工程師運用分布式隊列編程構(gòu)架的時候,在生產(chǎn)者、分布式隊列以及消費者這三個環(huán)節(jié)的注意點以及優(yōu)化建議.
確定采用分布式隊列編程模型之后,主體架構(gòu)就算完成了,但工程師的工作還遠(yuǎn)遠(yuǎn)未結(jié)束.天下事必做于細(xì),細(xì)節(jié)是一個不錯的架構(gòu)向一個優(yōu)秀的系統(tǒng)進階的關(guān)鍵因素.優(yōu)化篇選取了作者以及其同事在運用分布式隊列編程模型架構(gòu)時所碰到的典型問題和解決方案.
這里些問題出現(xiàn)的頻率較高,如果你經(jīng)驗不夠,很可能會“踩坑”.希望通過這些講解,幫助讀者降低分布式隊列編程模型的使用門檻.本文將對分布式隊列編程模型的三種角色:生產(chǎn)者(Producer),分布式隊列(Queue),消費者(Consumer)分別進行優(yōu)化討論.
在分布式隊列編程中,生產(chǎn)者往往并非真正的生產(chǎn)源頭,只是整個數(shù)據(jù)流中的一個節(jié)點,這種生產(chǎn)者的操作是處理-轉(zhuǎn)發(fā)(Process-Forward)模式.
這種模式給工程師們帶來的第一個問題是吞吐量問題.這種模式下運行的生產(chǎn)者,一邊接收上游的數(shù)據(jù),一邊將處理完的數(shù)據(jù)發(fā)送給下游.本質(zhì)上,它是一個非常經(jīng)典的數(shù)學(xué)問題,其抽象模型是一些沒有蓋子的水箱,每個水箱接收來自上一個水箱的水,進行處理之后,再將水發(fā)送到下一個水箱.
工程師需要預(yù)測水源的流量、每個環(huán)節(jié)水箱的處理能力、水龍頭的排水速度,最終目的是避免水溢出水箱,或者盡可能地減小溢出事件的概率.實際上流式編程框架以及其開發(fā)者花了大量的精力去處理和優(yōu)化這個問題.下文的緩存優(yōu)化和批量寫入優(yōu)化都是針對該問題的解決方案.
第二個需要考慮的問題是持久化.由于各種原因,系統(tǒng)總是會宕機.如果信息比較敏感,例如計費信息、火車票訂單信息等,工程師們需要考慮系統(tǒng)宕機所帶來的損失,找到讓損失最小化的解決方案.持久化優(yōu)化重點解決這一類問題.
處于“處理-轉(zhuǎn)發(fā)”模式下運行的生產(chǎn)者往往被設(shè)計成請求驅(qū)動型的服務(wù),即每個請求都會觸發(fā)一個處理線程,線程處理完后將結(jié)果寫入分布式隊列.如果由于某種原因隊列服務(wù)不可用,或者性能惡化,隨著新請求的到來,生產(chǎn)者的處理線程就會產(chǎn)生堆積.這可能會導(dǎo)致如下兩個問題:
緩解這類問題的思路來自于CAP理論,即通過降低一致性來提高可用性.生產(chǎn)者接收線程在收到請求之后第一時間不去處理,直接將請求緩存在內(nèi)存中(犧牲一致性),而在后臺啟動多個處理線程從緩存中讀取請求、進行處理并寫入分布式隊列.
與線程所占用的內(nèi)存開銷相比,大部分的請求所占內(nèi)存幾乎可以忽略.通過在接收請求和處理請求之間增加一層內(nèi)存緩存,可以大大提高系統(tǒng)的處理吞吐量和可擴展性.這個方案本質(zhì)上是一個內(nèi)存生產(chǎn)者消費者模型.
如果生產(chǎn)者的請求過大,寫分布式隊列可能成為性能瓶頸,有如下幾個因素:
如果在處理請求和寫隊列之間添加一層緩存,消息寫入程序批量將消息寫入隊列,可以大大提高系統(tǒng)的吞吐量.原因如下:
通過添加緩存,消費者服務(wù)的吞吐量和可用性都得到了提升.但緩存引入了一個新問題——內(nèi)存數(shù)據(jù)丟失.對于敏感數(shù)據(jù),工程師需要考慮如下兩個潛在問題:
所以緩存中的數(shù)據(jù)需要定期被持久化到磁盤等持久層設(shè)備中,典型的持久化觸發(fā)策略主要有兩種:
分布式隊列不等同于各種開源的或者收費的消息中間件,甚至在一些場景下完全不需要使用消息中間件.但是,消息中間件產(chǎn)生的目的就是解決消息傳遞問題,這為分布式隊列編程架構(gòu)提供了很多的便利.在實際工作中,工程師們應(yīng)該將成熟的消息中間件作為隊列的首要備選方案.
本節(jié)對消息中間件的功能、模型進行闡述,并給出一些消息中間件選型、部署的具體建議.
明白一個系統(tǒng)的每個具體功能是設(shè)計和架構(gòu)一個系統(tǒng)的基礎(chǔ).典型的消息中間件主要包含如下幾個功能:
抽象的消息中間件模型包含如下幾個角色:
要完整的描述消息中間件各個方面非常困難,大部分良好的消息中間件都有完善的文檔,這些文檔的長度遠(yuǎn)遠(yuǎn)超過本文的總長度.但如下幾個標(biāo)準(zhǔn)是工程師們在進行消息中間件選型時經(jīng)常需要考慮和權(quán)衡的.
性能主要有兩個方面需要考慮:吞吐量(Throughput)和響應(yīng)時間(Latency).
不同的消息隊列中間件的吞吐量和響應(yīng)時間相差甚遠(yuǎn),在選型時可以去網(wǎng)上查看一些性能對比報告.
對于同一種中間件,不同的配置方式也會影響性能.主要有如下幾方面的配置:
可靠性主要包含:可用性、持久化、確認(rèn)機制等.
高可用性的消息中間件應(yīng)該具備如下特征:
高可靠的消息中間件應(yīng)該確保從發(fā)送者接收到的消息不會丟失.中間件代理服務(wù)器的宕機并不是小概率事件,所以保存在內(nèi)存中的消息很容易發(fā)生丟失.大部分的消息中間件都依賴于消息的持久化去降低消息丟失損失,即將接收到的消息寫入磁盤.即使提供持久化,仍有兩個問題需要考慮:
確認(rèn)機制本質(zhì)上是通訊的握手機制(Handshaking).如果沒有該機制,消息在傳輸過程中丟失將不會被發(fā)現(xiàn).高敏感的消息要求選取具備確認(rèn)機制的消息中間件.當(dāng)然如果沒有接收到消息中間件確認(rèn)完成的指令,應(yīng)用程序需要決定如何處理.典型的做法有兩個:
采用現(xiàn)存消息中間件就意味著避免重復(fù)造輪子.如果某個消息中間件未能提供對應(yīng)語言的客戶端接口,則意味著極大的成本和兼容性問題.
投遞策略指的是一個消息會被發(fā)送幾次.主要包含三種策略:最多一次(At most Once )、最少一次(At least Once)、僅有一次(Exactly Once).
在實際應(yīng)用中,只考慮消息中間件的投遞策略并不能保證業(yè)務(wù)的投遞策略,因為接收者在確認(rèn)收到消息和處理完消息并持久化之間存在一個時間窗口.例如,即使消息中間件保證僅有一次(Exactly Once),如果接收者先確認(rèn)消息,在持久化之前宕機,則該消息并未被處理.
從應(yīng)用的角度,這就是最多一次(At most Once).反之,接收者先處理消息并完成持久化,但在確認(rèn)之前宕機,消息就要被再次發(fā)送,這就是最少一次(At least Once). 如果消息投遞策略非常重要,應(yīng)用程序自身也需要仔細(xì)設(shè)計.
消費者是分布式隊列編程中真正的數(shù)據(jù)處理方,數(shù)據(jù)處理方最常見的挑戰(zhàn)包括:有序性、串行化(Serializability)、頻次控制、完整性和一致性等.
在很多場景下,如何保證隊列信息的有序處理是一個棘手的問題.如下圖,假定分布式隊列保證請求嚴(yán)格有序,請求ri2和ri1都是針對同一數(shù)據(jù)記錄的不同狀態(tài),ri2的狀態(tài)比ri1的狀態(tài)新.T1、T2、T3和T4代表各個操作發(fā)生的時間,并且 T1 < T2 < T3 < T4(”<“代表早于).
采用多消費者架構(gòu),這兩條記錄被兩個消費者(Consumer1和Consumer2)處理后更新到數(shù)據(jù)庫里面.Consumer1雖然先讀取ri1但是卻后寫入數(shù)據(jù)庫,這就導(dǎo)致,新的狀態(tài)被老的狀態(tài)覆蓋,所以多消費者不保證數(shù)據(jù)的有序性.
很多場景下,串行化是數(shù)據(jù)處理的一個基本需求,這是保證數(shù)據(jù)完整性、可恢復(fù)性、事務(wù)原子性等的基礎(chǔ).為了在并行計算系統(tǒng)里實現(xiàn)串行化,一系列的相關(guān)理論和實踐算法被提出.對于分布式隊列編程架構(gòu),要在在多臺消費者實現(xiàn)串行化非常復(fù)雜,無異于重復(fù)造輪子.
有時候,消費者的消費頻次需要被控制,可能的原因包括:
完整性和一致性是所有多線程和多進程的代碼都面臨的問題.在多線程或者多進程的系統(tǒng)中考慮完整性和一致性往往會大大地增加代碼的復(fù)雜度和系統(tǒng)出錯的概率.
幾乎所有串行化理論真正解決的問題只有一個:性能. 所以,在性能允許的前提下,對于消費者角色,建議采用單實例部署.通過單實例部署,有序性、串行化、完整性和一致性問題自動獲得了解決.另外,單實例部署的消費者擁有全部所需信息,它可以在頻次控制上采取很多優(yōu)化策略.
天下沒有免費的午餐.同樣,單實例部署并非沒有代價,它意味著系統(tǒng)可用性的降低,很多時候,這是無法接受的.解決可用性問題的最直接的思路就是冗余(Redundancy).最常用的冗余方案是Master-slave架構(gòu),不過大部分的Master-slave架構(gòu)都是Active/active模式,即主從服務(wù)器都提供服務(wù).
例如,數(shù)據(jù)庫的Master-slave架構(gòu)就是主從服務(wù)器都提供讀服務(wù),只有主服務(wù)器提供寫服務(wù).大部分基于負(fù)載均衡設(shè)計的Master-slave集群中,主服務(wù)器和從服務(wù)器同時提供相同的服務(wù).這顯然不滿足單例服務(wù)優(yōu)化需求.
有序性和串行化需要Active/passive架構(gòu),即在某一時刻只有主實例提供服務(wù),其他的從服務(wù)等待主實例失效.這是典型的領(lǐng)導(dǎo)人選舉架構(gòu),即只有獲得領(lǐng)導(dǎo)權(quán)的實例才能充當(dāng)實際消費者,其他實例都在等待下一次選舉.采用領(lǐng)導(dǎo)人選舉的Active/passive架構(gòu)可以大大緩解純粹的單實例部署所帶來的可用性問題.
令人遺憾的是,除非工程師們自己在消費者實例里面實現(xiàn)Paxos等算法,并在每次消息處理之前都執(zhí)行領(lǐng)導(dǎo)人選舉.否則,理論上講,沒有方法可以保障在同一個時刻只有一個領(lǐng)導(dǎo)者.而對每個消息都執(zhí)行一次領(lǐng)導(dǎo)人選舉,顯然性能不可行.
實際工作中,最容易出現(xiàn)的問題時機發(fā)生在領(lǐng)導(dǎo)人交接過程中,即前任領(lǐng)導(dǎo)人實例變成輔助實例,新部署實例開始承擔(dān)領(lǐng)導(dǎo)人角色.為了平穩(wěn)過渡,這兩者之間需要有一定的通訊機制,但是,無論是網(wǎng)絡(luò)分區(qū)(Network partition)還是原領(lǐng)導(dǎo)人服務(wù)崩潰都會使這種通訊機制變的不可能.
對于完整性和一致性要求很高的系統(tǒng),我們需要在選舉制度和交接制度這兩塊進行優(yōu)化.
典型的領(lǐng)導(dǎo)人選舉算法有Paxos、ZAB( ZooKeeper Atomic Broadcast protocol).為了避免重復(fù)造輪子,建議采用ZooKeeper的分布式鎖來實現(xiàn)領(lǐng)導(dǎo)人選舉.典型的ZooKeeper實現(xiàn)算法如下:
Let ELECTION be a path of choice of the application. To volunteer to be a leader:
1.Create znode z with path “ELECTION/guid-n_” with both SEQUENCE and EPHEMERAL flags;
2.Let C be the children of “ELECTION”, and i be the sequence number of z;
3.Watch for changes on “ELECTION/guid-n_j”, where j is the largest sequence number such that j < i and n_j is a znode in C;
Upon receiving a notification of znode deletion:
1.Let C be the new set of children of ELECTION;
2.If z is the smallest node in C, then execute leader procedure;
3.Otherwise, watch for changes on “ELECTION/guid-n_j”, where j is the largest sequence number such that j < i and n_j is a znode in C;
領(lǐng)導(dǎo)人選舉的整個過程發(fā)生在ZooKeeper集群中,各個消費者實例在這場選舉中只充當(dāng)被告知者角色(Learner).領(lǐng)導(dǎo)人選舉算法,只能保證最終只有一個Leader被選舉出來,并不保障被告知者對Leader的理解是完全一致的.
本質(zhì)上,上文的架構(gòu)里,選舉的結(jié)果是作為令牌(Token)傳遞給消費者實例,消費者將自身的ID與令牌進行對比,如果相等,則開始執(zhí)行消費操作.所以當(dāng)發(fā)生領(lǐng)導(dǎo)人換屆的情況,不同的Learner獲知新Leader的時間并不同.
例如,前任Leader如果因為網(wǎng)絡(luò)問題與ZooKeeper集群斷開,前任Leader只能在超時后才能判斷自己是否不再承擔(dān)Leader角色了,而新的Leader可能在這之前已經(jīng)產(chǎn)生.另一方面,即使前任Leader和新Leader同時接收到新Leader選舉結(jié)果,某些業(yè)務(wù)的完整性要求迫使前任Leader仍然完成當(dāng)前未完成的工作.
以上的講解非常抽象,生活中卻給了一些更加具體的例子.眾所周知,美國總統(tǒng)候選人在選舉結(jié)束后并不直接擔(dān)任美國總統(tǒng),從選舉到最終承擔(dān)總統(tǒng)角色需要一個過渡期.對于新當(dāng)選Leader的候選人而言,過渡期間稱之為加冕階段(Inauguration).對于即將卸任的Leader,過渡期稱為交接階段(HandOver).
所以一個基于領(lǐng)導(dǎo)人選舉的消費者從加冕到卸任經(jīng)歷三個階段:Inauguration、Execution、HandOver.在加冕階段,新領(lǐng)導(dǎo)需要進行一些初始化操作.Execution階段是真正的隊列消息處理階段.在交接階段,前任領(lǐng)導(dǎo)需要進行一些清理操作.
類似的,為了解決領(lǐng)導(dǎo)人交接問題,所有的消費者從代碼實現(xiàn)的角度都需要實現(xiàn)類似ILeaderCareer接口.這個接口包含三個方發(fā)inaugurate(),handOver()和execute().某個部署實例(Learner)在得知自己承擔(dān)領(lǐng)導(dǎo)人角色后,需要調(diào)用inaugurate()方法,進行加冕.主要的消費邏輯通過不停的執(zhí)行execute()實現(xiàn),當(dāng)確認(rèn)自己不再承擔(dān)領(lǐng)導(dǎo)人之后,執(zhí)行handOver()進行交接.
如果承擔(dān)領(lǐng)導(dǎo)人角色的消費者,在執(zhí)行execute()階段得知自己將要下臺,根據(jù)消息處理的原子性,該領(lǐng)導(dǎo)人可以決定是否提前終止操作.如果整個消息處理是一個原子性事務(wù),直接終止該操作可以快速實現(xiàn)領(lǐng)導(dǎo)人換屆.否則,前任領(lǐng)導(dǎo)必須完成當(dāng)前消息處理后,才進入交接階段.這意味著新的領(lǐng)導(dǎo)人,在inaugurate()階段需要進行一定時間的等待.
頻次控制是一個經(jīng)典問題.對于分布式隊列編程架構(gòu),相同請求重復(fù)出現(xiàn)在隊列的情況并不少見.如果相同請求在隊列中重復(fù)太多,排重優(yōu)化就顯得很必要.分布式緩存更新是一個典型例子,所有請求都被發(fā)送到隊列中用于緩存更新.如果請求符合典型的高斯分布,在一段時間內(nèi)會出現(xiàn)大量重復(fù)的請求,而同時多線程更新同一請求緩存顯然沒有太大的意義.
排重優(yōu)化是一個算法,其本質(zhì)是基于狀態(tài)機的編程,整個講解通過模型、構(gòu)思和實施三個步驟完成.
進行排重優(yōu)化的前提是大量重復(fù)的請求.在模型這一小節(jié),我們首先闡述重復(fù)度模型、以及不同重復(fù)度所導(dǎo)致的消費模型,最后基于這兩個模型去講解排重狀態(tài)機.
首先我們給出最小重復(fù)長度的概念.同一請求最小重復(fù)長度:同一請求在隊列中的重復(fù)出現(xiàn)的最小間距.例如,請求ri第一次出現(xiàn)在位置3,第二次出現(xiàn)在10,最小重復(fù)長度等于7.
是否需要進行排重優(yōu)化取決于隊列中請求的重復(fù)度.由于不同請求之間并不存在重復(fù)的問題,不失一般性,這里的模型只考了單個請求的重復(fù)度,重復(fù)度分為三個類:無重復(fù)、稀疏重復(fù)、高重復(fù).
對于不同的重復(fù)度,會有不同的消費模型.
在整個隊列處理過程中,所有的請求都不相同,如下圖:
當(dāng)同一請求最小重復(fù)長度大于消費者隊列長度,如下圖.假定有3個消費者,Consumer1將會處理r1,Consumer2將會處理r2,Consumer3將會處理r3,如果每個請求處理的時間嚴(yán)格相等,Consumer1在處理完r1之后,接著處理r4,Consumer2將會處理r2之后會處理r1.雖然r1被再次處理,但是任何時刻,只有這一個消費者在處理r1,不會出現(xiàn)多個消費者同時處理同一請求的場景.
如下圖,仍然假定有3個消費者,隊列中前面4個請求都是r1,它會同時被3個消費者線程處理:
顯然,對于無重復(fù)和稀疏重復(fù)的分布式隊列,排重優(yōu)化并不會帶來額外的好處.排重優(yōu)化所針對的對象是高重復(fù)消費模型,特別是對于并行處理消費者比較多的情況,重復(fù)處理同一請求,資源消耗極大.
排重優(yōu)化的主要對象是高重復(fù)的隊列,多個消費者線程或進程同時處理同一個冪等請求只會浪費計算資源并延遲其他待請求處理.所以,排重狀態(tài)機的一個目標(biāo)是處理唯一性,即:同一時刻,同一個請求只有一個消費者處理.
如果消費者獲取一條請求消息,但發(fā)現(xiàn)其他消費者正在處理該消息,則當(dāng)前消費者應(yīng)該處于等待狀態(tài).如果對同一請求,有一個消費者在處理,一個消費者在等待,而同一請求再次被消費者讀取,再次等待則沒有意義.
所以,狀態(tài)機的第二個目標(biāo)是等待唯一性,即:同一時刻,同一個請求最多只有一個消費者處于等待狀態(tài).總上述,狀態(tài)機的目標(biāo)是:處理唯一性和等待唯一性.我們把正在處理的請求稱為頭部請求,正在等待的請求稱為尾部請求.
由于狀態(tài)機的處理單元是請求,所以需要針對每一個請求建立一個排重狀態(tài)機.基于以上要求,我們設(shè)計的排重狀態(tài)機包含4個狀態(tài)Init,Process,Block,Decline.各個狀態(tài)之間轉(zhuǎn)化過程如下圖:
狀態(tài)機描述的是針對單個請求操作所引起狀態(tài)變化,排重優(yōu)化需要解決隊列中所有請求的排重問題,需要對所有請求的狀態(tài)機進行管理.這里只考慮單虛擬機內(nèi)部對所有請求狀態(tài)機的管理,對于跨虛擬機的管理可以采用類似的方法.對于多狀態(tài)機管理主要包含三個方面:一致性問題、完整性問題和請求緩存驅(qū)逐問題.
一致性在這里要求同一請求的不同消費者只會操作一個狀態(tài)機.由于每個請求都產(chǎn)生一個狀態(tài)機,系統(tǒng)將會包含大量的狀態(tài)機.為了兼顧性能和一致性,我們采用ConcurrentHashMap保存所有的狀態(tài)機.用ConcurrentHashMap而不是對整個狀態(tài)機隊列進行加鎖,可以提高并行處理能力,使得系統(tǒng)可以同時操作不同狀態(tài)機.
為了避免處理同一請求的多消費者線程同時對ConcurrentHashMap進行插入所導(dǎo)致狀態(tài)機不一致問題,我們利用了ConcurrentHashMap的putIfAbsent()方法.代碼方案如下,key2Status用于存儲所有的狀態(tài)機.
消費者在處理請求之前,從狀態(tài)機隊列中讀取排重狀態(tài)機TrafficAutomate.如果沒有找到,則創(chuàng)建一個新的狀態(tài)機,并通過putIfAbsent()方法插入到狀態(tài)機隊列中.
完整性要求保障狀態(tài)機Init,Process,Block,Decline四種狀態(tài)正確、狀態(tài)之間的轉(zhuǎn)換也正確.由于狀態(tài)機的操作非常輕量級,兼顧完整性和降低代碼復(fù)雜度,我們對狀態(tài)機的所有方法進行加鎖.
如果不同請求的數(shù)量太多,內(nèi)存永久保存所有請求的狀態(tài)機的內(nèi)存開銷太大.所以,某些狀態(tài)機需要在恰當(dāng)?shù)臅r候被驅(qū)逐出內(nèi)存.這里有兩個思路:
每個請求對應(yīng)于一個狀態(tài)機,不同的狀態(tài)機采用不同的請求進行識別.
對于同一狀態(tài)機的不同消費者,在單虛擬機方案中,我們采用線程id進行標(biāo)識.
排重優(yōu)化的主要功能都是通過排重狀態(tài)機(TrafficAutomate)和狀態(tài)機隊列(QueueCoordinator)來實施的.排重狀態(tài)機描述的是針對單個請求的排重問題,狀態(tài)機隊列解決所有請求狀態(tài)機的排重問題.
根據(jù)狀態(tài)機模型,其主要操作為enQueue和deQueue,其狀態(tài)由頭部請求和尾部請求的狀態(tài)共同決定,所以需要定義兩個變量為head和tail,用于表示頭部請求和尾部請求.為了確保多線程操作下狀態(tài)機的完整性(Integraty),所有的操作都將加上鎖.
當(dāng)一個消費者執(zhí)行enQueue操作時:如果此時尾部請求不為空,根據(jù)等待唯一性要求,返回DECLINE,當(dāng)前消費者應(yīng)該拋棄該請求;如果頭部請求為空,返回ACCPET,當(dāng)前消費者應(yīng)該立刻處理該消息;否則,返回BLOCK,該消費者應(yīng)該等待,并不停的查看狀態(tài)機的狀態(tài),一直到頭部請求處理完成.enQueue代碼如下:
對于deQueue操作,首先將尾部請求賦值給頭部請求,并將尾部請求置為無效.deQueue代碼如下:
狀態(tài)機隊列集中管理所有請求的排重狀態(tài)機,所以其操作和單個狀態(tài)機一樣,即enQueue和deQueuqe接口.這兩個接口的實現(xiàn)需要識別特定請求的狀態(tài)機,所以它們的入?yún)?yīng)該是請求.為了兼容不同類型的請求消息,我們采用了Java泛型編程.接口定義如下:
enQueue操作過程如下:
首先,根據(jù)傳入的請求key值,獲取狀態(tài)機, 如果不存在則創(chuàng)建一個新的狀態(tài)機,并保存在ConcurrentHashMap中.
接下來,獲取線程id作為該消費者的唯一標(biāo)識,并對對應(yīng)狀態(tài)機進行enQueue操作.
如果狀態(tài)機返回值為ACCEPT或者DECLINE,返回業(yè)務(wù)層處理代碼,ACCEPT意味著業(yè)務(wù)層需要處理該消息,DECLINE表示業(yè)務(wù)層可以拋棄當(dāng)前消息.如果狀態(tài)機返回值為Block,則該線程保持等待狀態(tài).
在某些情況下,頭部請求線程可能由于異常,未能對狀態(tài)機進行deQueue操作(作為組件提供方,不能假定所有的規(guī)范被使用方實施).為了避免處于阻塞狀態(tài)的消費者無期限地等待,建議對狀態(tài)機設(shè)置安全超時時限.超過了一定時間后,狀態(tài)機強制清空頭部請求,返回到業(yè)務(wù)層,業(yè)務(wù)層開始處理該請求.
代碼如下:
deQueue操作首先從ConcurrentHashMap獲取改請求所對應(yīng)的狀態(tài)機,接著獲取該線程的線程id,對狀態(tài)機進行deQueue操作.
enQueue代碼如下:
完整源代碼可以在QueueCoordinator獲取.鏈接:
https://github.com/dinglau2008/QueueCoordinator/tree/master/src
文章出處:美團點評技術(shù)團隊
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.fzlkiss.com/jiaocheng/4455.html