消息隊列已經(jīng)逐漸成為企業(yè)IT系統(tǒng)內(nèi)部通信的核心手段爹殊。它具有低耦合、可靠投遞监右、廣播边灭、流量控制、最終一致性等一系列功能健盒,成為異步RPC的主要手段之一绒瘦。
當(dāng)今市面上有很多主流的消息中間件,如老牌的ActiveMQ扣癣、RabbitMQ惰帽,炙手可熱的Kafka,阿里巴巴自主開發(fā)的Notify父虑、MetaQ该酗、RocketMQ等。
本文不會一一介紹這些消息隊列的所有特性,而是探討一下自主開發(fā)設(shè)計一個消息隊列時呜魄,你需要思考和設(shè)計的重要方面悔叽。過程中我們會參考這些成熟消息隊列的很多重要思想。
本文首先會闡述什么時候你需要一個消息隊列爵嗅,然后以Push模型為主娇澎,從零開始分析設(shè)計一個消息隊列時需要考慮到的問題,如RPC睹晒、高可用趟庄、順序和重復(fù)消息、可靠投遞伪很、消費(fèi)關(guān)系解析等戚啥。
也會分析以Kafka為代表的pull模型所具備的優(yōu)點(diǎn)。最后是一些高級主題锉试,如用批量/異步提高性能猫十、pull模型的系統(tǒng)設(shè)計理念、存儲子系統(tǒng)的設(shè)計呆盖、流量控制的設(shè)計炫彩、公平調(diào)度的實(shí)現(xiàn)等。其中最后四個方面會放在下篇講解絮短。
你何時需要消息隊列
當(dāng)你需要使用消息隊列時江兢,首先需要考慮它的必要性《∑担可以使用mq的場景有很多杉允,最常用的幾種,是做業(yè)務(wù)解耦/最終一致性/廣播/錯峰流控等席里。反之叔磷,如果需要強(qiáng)一致性,關(guān)注業(yè)務(wù)邏輯的處理結(jié)果奖磁,則RPC顯得更為合適改基。
解耦
解耦是消息隊列要解決的最本質(zhì)問題。所謂解耦咖为,簡單點(diǎn)講就是一個事務(wù)秕狰,只關(guān)心核心的流程。而需要依賴其他系統(tǒng)但不那么重要的事情躁染,有通知即可鸣哀,無需等待結(jié)果。換句話說吞彤,基于消息的模型我衬,關(guān)心的是“通知”派近,而非“處理”甸怕。
比如在美團(tuán)旅游寥枝,我們有一個產(chǎn)品中心恶守,產(chǎn)品中心上游對接的是主站、移動后臺破加、旅游供應(yīng)鏈等各個數(shù)據(jù)源糕档;下游對接的是篩選系統(tǒng)、API系統(tǒng)等展示系統(tǒng)拌喉。當(dāng)上游的數(shù)據(jù)發(fā)生變更的時候,如果不使用消息系統(tǒng)俐银,勢必要調(diào)用我們的接口來更新數(shù)據(jù)尿背,就特別依賴產(chǎn)品中心接口的穩(wěn)定性和處理能力。但其實(shí)捶惜,作為旅游的產(chǎn)品中心田藐,也許只有對于旅游自建供應(yīng)鏈,產(chǎn)品中心更新成功才是他們關(guān)心的事情吱七。而對于團(tuán)購等外部系統(tǒng)汽久,產(chǎn)品中心更新成功也好、失敗也罷踊餐,并不是他們的職責(zé)所在景醇。他們只需要保證在信息變更的時候通知到我們就好了。
而我們的下游吝岭,可能有更新索引三痰、刷新緩存等一系列需求。對于產(chǎn)品中心來說窜管,這也不是我們的職責(zé)所在散劫。說白了,如果他們定時來拉取數(shù)據(jù)幕帆,也能保證數(shù)據(jù)的更新获搏,只是實(shí)時性沒有那么強(qiáng)。但使用接口方式去更新他們的數(shù)據(jù)失乾,顯然對于產(chǎn)品中心來說太過于“重量級”了常熙,只需要發(fā)布一個產(chǎn)品ID變更的通知,由下游系統(tǒng)來處理碱茁,可能更為合理症概。
再舉一個例子,對于我們的訂單系統(tǒng)早芭,訂單最終支付成功之后可能需要給用戶發(fā)送短信積分什么的彼城,但其實(shí)這已經(jīng)不是我們系統(tǒng)的核心流程了。如果外部系統(tǒng)速度偏慢(比如短信網(wǎng)關(guān)速度不好),那么主流程的時間會加長很多募壕,用戶肯定不希望點(diǎn)擊支付過好幾分鐘才看到結(jié)果调炬。那么我們只需要通知短信系統(tǒng)“我們支付成功了”,不一定非要等待它處理完成舱馅。
最終一致性
最終一致性指的是兩個系統(tǒng)的狀態(tài)保持一致缰泡,要么都成功,要么都失敗代嗤。當(dāng)然有個時間限制棘钞,理論上越快越好,但實(shí)際上在各種異常的情況下干毅,可能會有一定延遲達(dá)到最終一致狀態(tài)宜猜,但最后兩個系統(tǒng)的狀態(tài)是一樣的。
業(yè)界有一些為“最終一致性”而生的消息隊列硝逢,如Notify(阿里)姨拥、QMQ(去哪兒)等,其設(shè)計初衷渠鸽,就是為了交易系統(tǒng)中的高可靠通知叫乌。
以一個銀行的轉(zhuǎn)賬過程來理解最終一致性,轉(zhuǎn)賬的需求很簡單徽缚,如果A系統(tǒng)扣錢成功憨奸,則B系統(tǒng)加錢一定成功。反之則一起回滾凿试,像什么都沒發(fā)生一樣膀藐。
然而,這個過程中存在很多可能的意外:
A扣錢成功红省,調(diào)用B加錢接口失敗额各。
A扣錢成功,調(diào)用B加錢接口雖然成功吧恃,但獲取最終結(jié)果時網(wǎng)絡(luò)異常引起超時虾啦。
A扣錢成功,B加錢失敗痕寓,A想回滾扣的錢傲醉,但A機(jī)器down機(jī)。
可見呻率,想把這件看似簡單的事真正做成硬毕,真的不那么容易。所有跨VM的一致性問題礼仗,從技術(shù)的角度講通用的解決方案是:
強(qiáng)一致性吐咳,分布式事務(wù)逻悠,但落地太難且成本太高,后文會具體提到韭脊。
最終一致性童谒,主要是用“記錄”和“補(bǔ)償”的方式。在做所有的不確定的事情之前沪羔,先把事情記錄下來饥伊,然后去做不確定的事情,結(jié)果可能是:成功蔫饰、失敗或是不確定琅豆,“不確定”(例如超時等)可以等價為失敗。成功就可以把記錄的東西清理掉了篓吁,對于失敗和不確定茫因,可以依靠定時任務(wù)等方式把所有失敗的事情重新搞一遍,直到成功為止越除。
回到剛才的例子,系統(tǒng)在A扣錢成功的情況下外盯,把要給B“通知”這件事記錄在庫里(為了保證最高的可靠性可以把通知B系統(tǒng)加錢和扣錢成功這兩件事維護(hù)在一個本地事務(wù)里)摘盆,通知成功則刪除這條記錄,通知失敗或不確定則依靠定時任務(wù)補(bǔ)償性地通知我們饱苟,直到我們把狀態(tài)更新成正確的為止孩擂。
整個這個模型依然可以基于RPC來做,但可以抽象成一個統(tǒng)一的模型箱熬,基于消息隊列來做一個“企業(yè)總線”类垦。
具體來說,本地事務(wù)維護(hù)業(yè)務(wù)變化和通知消息城须,一起落地(失敗則一起回滾)蚤认,然后RPC到達(dá)broker,在broker成功落地后糕伐,RPC返回成功砰琢,本地消息可以刪除。否則本地消息一直靠定時任務(wù)輪詢不斷重發(fā)良瞧,這樣就保證了消息可靠落地broker陪汽。
broker往consumer發(fā)送消息的過程類似,一直發(fā)送消息褥蚯,直到consumer發(fā)送消費(fèi)成功確認(rèn)挚冤。
我們先不理會重復(fù)消息的問題,通過兩次消息落地加補(bǔ)償赞庶,下游是一定可以收到消息的训挡。然后依賴狀態(tài)機(jī)版本號等方式做判重澳骤,更新自己的業(yè)務(wù),就實(shí)現(xiàn)了最終一致性舍哄。
最終一致性不是消息隊列的必備特性宴凉,但確實(shí)可以依靠消息隊列來做最終一致性的事情。另外表悬,所有不保證100%不丟消息的消息隊列弥锄,理論上無法實(shí)現(xiàn)最終一致性。好吧蟆沫,應(yīng)該說理論上的100%籽暇,排除系統(tǒng)嚴(yán)重故障和bug。
像Kafka一類的設(shè)計饭庞,在設(shè)計層面上就有丟消息的可能(比如定時刷盤戒悠,如果掉電就會丟消息)。哪怕只丟千分之一的消息舟山,業(yè)務(wù)也必須用其他的手段來保證結(jié)果正確绸狐。
廣播
消息隊列的基本功能之一是進(jìn)行廣播。如果沒有消息隊列累盗,每當(dāng)一個新的業(yè)務(wù)方接入寒矿,我們都要聯(lián)調(diào)一次新接口。有了消息隊列若债,我們只需要關(guān)心消息是否送達(dá)了隊列符相,至于誰希望訂閱,是下游的事情蠢琳,無疑極大地減少了開發(fā)和聯(lián)調(diào)的工作量啊终。
比如本文開始提到的產(chǎn)品中心發(fā)布產(chǎn)品變更的消息,以及景點(diǎn)庫很多去重更新的消息傲须,可能“關(guān)心”方有很多個蓝牲,但產(chǎn)品中心和景點(diǎn)庫只需要發(fā)布變更消息即可,誰關(guān)心誰接入泰讽。
錯峰與流控
試想上下游對于事情的處理能力是不同的搞旭。比如,Web前端每秒承受上千萬的請求菇绵,并不是什么神奇的事情肄渗,只需要加多一點(diǎn)機(jī)器,再搭建一些LVS負(fù)載均衡設(shè)備和Nginx等即可咬最。但數(shù)據(jù)庫的處理能力卻十分有限翎嫡,即使使用SSD加分庫分表,單機(jī)的處理能力仍然在萬級永乌。由于成本的考慮惑申,我們不能奢求數(shù)據(jù)庫的機(jī)器數(shù)量追上前端具伍。
這種問題同樣存在于系統(tǒng)和系統(tǒng)之間,如短信系統(tǒng)可能由于短板效應(yīng)圈驼,速度卡在網(wǎng)關(guān)上(每秒幾百次請求)人芽,跟前端的并發(fā)量不是一個數(shù)量級。但用戶晚上個半分鐘左右收到短信绩脆,一般是不會有太大問題的萤厅。如果沒有消息隊列,兩個系統(tǒng)之間通過協(xié)商靴迫、滑動窗口等復(fù)雜的方案也不是說不能實(shí)現(xiàn)。但系統(tǒng)復(fù)雜性指數(shù)級增長名挥,勢必在上游或者下游做存儲主守,并且要處理定時、擁塞等一系列問題救湖。而且每當(dāng)有處理能力有差距的時候,都需要單獨(dú)開發(fā)一套邏輯來維護(hù)這套邏輯黄刚。所以捎谨,利用中間系統(tǒng)轉(zhuǎn)儲兩個系統(tǒng)的通信內(nèi)容民效,并在下游系統(tǒng)有能力處理這些消息的時候憔维,再處理這些消息,是一套相對較通用的方式畏邢。
總而言之业扒,消息隊列不是萬能的。對于需要強(qiáng)事務(wù)保證而且延遲敏感的舒萎,RPC是優(yōu)于消息隊列的程储。
對于一些無關(guān)痛癢,或者對于別人非常重要但是對于自己不是那么關(guān)心的事情臂寝,可以利用消息隊列去做章鲤。
支持最終一致性的消息隊列,能夠用來處理延遲不那么敏感的“分布式事務(wù)”場景咆贬,而且相對于笨重的分布式事務(wù)败徊,可能是更優(yōu)的處理方式。
當(dāng)上下游系統(tǒng)處理能力存在差距的時候掏缎,利用消息隊列做一個通用的“漏斗”皱蹦。在下游有能力處理的時候煤杀,再進(jìn)行分發(fā)。
如果下游有很多系統(tǒng)關(guān)心你的系統(tǒng)發(fā)出的通知的時候沪哺,果斷地使用消息隊列吧沈自。
如何設(shè)計一個消息隊列
綜述
我們現(xiàn)在明確了消息隊列的使用場景枯途,下一步就是如何設(shè)計實(shí)現(xiàn)一個消息隊列了柔袁。
基于消息的系統(tǒng)模型,不一定需要broker(消息隊列服務(wù)端)腥例。市面上的的Akka(actor模型)燎竖、ZeroMQ等,其實(shí)都是基于消息的系統(tǒng)設(shè)計范式纤掸,但是沒有broker借跪。
我們之所以要設(shè)計一個消息隊列掏愁,并且配備broker,無外乎要做兩件事情:
消息的轉(zhuǎn)儲辛掠,在更合適的時間點(diǎn)投遞公浪,或者通過一系列手段輔助消息最終能送達(dá)消費(fèi)機(jī)厅各。
規(guī)范一種范式和通用的模式队塘,以滿足解耦憔古、最終一致性、錯峰等需求焰情。
掰開了揉碎了看内舟,最簡單的消息隊列可以做成一個消息轉(zhuǎn)發(fā)器验游,把一次RPC做成兩次RPC。發(fā)送者把消息投遞到服務(wù)端(以下簡稱broker)赔硫,服務(wù)端再將消息轉(zhuǎn)發(fā)一手到接收端,就是這么簡單权悟。
一般來講谦铃,設(shè)計消息隊列的整體思路是先build一個整體的數(shù)據(jù)流,例如producer發(fā)送給broker,broker發(fā)送給consumer,consumer回復(fù)消費(fèi)確認(rèn)驹闰,broker刪除/備份消息等师妙。
利用RPC將數(shù)據(jù)流串起來默穴。然后考慮RPC的高可用性蓄诽,盡量做到無狀態(tài),方便水平擴(kuò)展调衰。
之后考慮如何承載消息堆積嚎莉,然后在合適的時機(jī)投遞消息,而處理堆積的最佳方式加派,就是存儲竹勉,存儲的選型需要綜合考慮性能/可靠性和開發(fā)維護(hù)成本等諸多因素次乓。
為了實(shí)現(xiàn)廣播功能,我們必須要維護(hù)消費(fèi)關(guān)系女气,可以利用zk/config server等保存消費(fèi)關(guān)系缘滥。
在完成了上述幾個功能后软吐,消息隊列基本就實(shí)現(xiàn)了凹耙。然后我們可以考慮一些高級特性,如可靠投遞意述,事務(wù)特性荤崇,性能優(yōu)化等术荤。
下面我們會以設(shè)計消息隊列時重點(diǎn)考慮的模塊為主線,穿插灌輸一些消息隊列的特性實(shí)現(xiàn)方法子库,來具體分析設(shè)計實(shí)現(xiàn)一個消息隊列時的方方面面仑嗅。
實(shí)現(xiàn)隊列基本功能
RPC通信協(xié)議
剛才講到仓技,所謂消息隊列恭理,無外乎兩次RPC加一次轉(zhuǎn)儲,當(dāng)然需要消費(fèi)端最終做消費(fèi)確認(rèn)的情況是三次RPC诉濒。既然是RPC,就必然牽扯出一系列話題片排,什么負(fù)載均衡啊率寡、服務(wù)發(fā)現(xiàn)啊冶共、通信協(xié)議啊、序列化協(xié)議啊庙楚,等等醋奠。在這一塊窜司,我的強(qiáng)烈建議是不要重復(fù)造輪子。利用公司現(xiàn)有的RPC框架:Thrift也好议薪,Dubbo也好斯议,或者是其他自定義的框架也好坯临。因為消息隊列的RPC看靠,和普通的RPC沒有本質(zhì)區(qū)別。當(dāng)然了谤祖,自主利用Memchached或者Redis協(xié)議重新寫一套RPC框架并非不可(如MetaQ使用了自己封裝的Gecko NIO框架泊脐,卡夫卡也用了類似的協(xié)議)容客。但實(shí)現(xiàn)成本和難度無疑倍增。排除對效率的極端要求供置,都可以使用現(xiàn)成的RPC框架芥丧。
簡單來講续担,服務(wù)端提供兩個RPC服務(wù),一個用來接收消息询兴,一個用來確認(rèn)消息收到诗舰。并且做到不管哪個server收到消息和確認(rèn)消息蜀铲,結(jié)果一致即可蝙茶。當(dāng)然這中間可能還涉及跨IDC的服務(wù)的問題。這里和RPC的原則是一致的,盡量優(yōu)先選擇本機(jī)房投遞厘肮。你可能會問类茂,如果producer和consumer本身就在兩個機(jī)房了,怎么辦兢哭?首先迟螺,broker必須保證感知的到所有consumer的存在。其次浙垫,producer盡量選擇就近的機(jī)房就好了夹姥。
高可用
其實(shí)所有的高可用,是依賴于RPC和存儲的高可用來做的旦部。先來看RPC的高可用士八,美團(tuán)的基于MTThrift的RPC框架蘸秘,阿里的Dubbo等醋虏,其本身就具有服務(wù)自動發(fā)現(xiàn)颈嚼,負(fù)載均衡等功能。而消息隊列的高可用限煞,只要保證broker接受消息和確認(rèn)消息的接口是冪等的晰骑,并且consumer的幾臺機(jī)器處理消息是冪等的,這樣就把消息隊列的可用性抚官,轉(zhuǎn)交給RPC框架來處理了凌节。
那么怎么保證冪等呢?最簡單的方式莫過于共享存儲卒煞。broker多機(jī)器共享一個DB或者一個分布式文件/kv系統(tǒng)畔裕,則處理消息自然是冪等的具练。就算有單點(diǎn)故障扛点,其他節(jié)點(diǎn)可以立刻頂上占键。另外failover可以依賴定時任務(wù)的補(bǔ)償,這是消息隊列本身天然就可以支持的功能翩概。存儲系統(tǒng)本身的可用性我們不需要操太多心,放心大膽的交給DBA們吧评姨!
對于不共享存儲的隊列吐句,如Kafka使用分區(qū)加主備模式,就略微麻煩一些文虏。需要保證每一個分區(qū)內(nèi)的高可用性氧秘,也就是每一個分區(qū)至少要有一個主備且需要做數(shù)據(jù)的同步,關(guān)于這塊HA的細(xì)節(jié)已添,可以參考下篇pull模型消息系統(tǒng)設(shè)計更舞。
服務(wù)端承載消息堆積的能力
消息到達(dá)服務(wù)端如果不經(jīng)過任何處理就到接收者了宇葱,broker就失去了它的意義黍瞧。為了滿足我們錯峰/流控/最終可達(dá)等一系列需求印颤,把消息存儲下來,然后選擇時機(jī)投遞就顯得是順理成章的了矢否。
只是這個存儲可以做成很多方式僵朗。比如存儲在內(nèi)存里,存儲在分布式KV里壶谒,存儲在磁盤里汗菜,存儲在數(shù)據(jù)庫里等等。但歸結(jié)起來菌瘪,主要有持久化和非持久化兩種糜工。
持久化的形式能更大程度地保證消息的可靠性(如斷電等不可抗外力)捌木,并且理論上能承載更大限度的消息堆積(外存的空間遠(yuǎn)大于內(nèi)存)。
但并不是每種消息都需要持久化存儲帆啃。很多消息對于投遞性能的要求大于可靠性的要求努潘,且數(shù)量極大(如日志)渤刃。這時候卖子,消息不落地直接暫存內(nèi)存洋闽,嘗試幾次failover羽利,最終投遞出去也未嘗不可这弧。
市面上的消息隊列普遍兩種形式都支持。當(dāng)然具體的場景還要具體結(jié)合公司的業(yè)務(wù)來看蛋辈。
存儲子系統(tǒng)的選擇
我們來看看如果需要數(shù)據(jù)落地的情況下各種存儲子系統(tǒng)的選擇冷溶。理論上礼预,從速度來看托酸,文件系統(tǒng)>分布式KV(持久化)>分布式文件系統(tǒng)>數(shù)據(jù)庫,而可靠性卻截然相反应结。還是要從支持的業(yè)務(wù)場景出發(fā)作出最合理的選擇鹅龄,如果你們的消息隊列是用來支持支付/交易等對可靠性要求非常高,但對性能和量的要求沒有這么高玷坠,而且沒有時間精力專門做文件存儲系統(tǒng)的研究八堡,DB是最好的選擇兄渺。
但是DB受制于IOPS,如果要求單broker 5位數(shù)以上的QPS性能凳兵,基于文件的存儲是比較好的解決方案庐扫。整體上可以采用數(shù)據(jù)文件+索引文件的方式處理形庭,具體這塊的設(shè)計比較復(fù)雜斟珊,可以參考下篇的存儲子系統(tǒng)設(shè)計。
分布式KV(如MongoDB晓褪,HBase)等勤庐,或者持久化的Redis愉镰,由于其編程接口較友好,性能也比較可觀类嗤,如果在可靠性要求不是那么高的場景,也不失為一個不錯的選擇嗤形。
消費(fèi)關(guān)系解析
現(xiàn)在我們的消息隊列初步具備了轉(zhuǎn)儲消息的能力。下面一個重要的事情就是解析發(fā)送接收關(guān)系霹期,進(jìn)行正確的消息投遞了历造。
市面上的消息隊列定義了一堆讓人暈頭轉(zhuǎn)向的名詞侣监,如JMS 規(guī)范中的Topic/Queue橄霉,Kafka里面的Topic/Partition/ConsumerGroup,RabbitMQ里面的Exchange等等覆糟。拋開現(xiàn)象看本質(zhì)滩字,無外乎是單播與廣播的區(qū)別麦箍。所謂單播挟裂,就是點(diǎn)到點(diǎn);而廣播渠啤,是一點(diǎn)對多點(diǎn)。當(dāng)然碟联,對于互聯(lián)網(wǎng)的大部分應(yīng)用來說壶栋,組間廣播琉兜、組內(nèi)單播是最常見的情形。
消息需要通知到多個業(yè)務(wù)集群夺饲,而一個業(yè)務(wù)集群內(nèi)有很多臺機(jī)器往声,只要一臺機(jī)器消費(fèi)這個消息就可以了。
當(dāng)然這不是絕對的慢洋,很多時候組內(nèi)的廣播也是有適用場景的,如本地緩存的更新等等太防。另外蜒车,消費(fèi)關(guān)系除了組內(nèi)組間,可能會有多級樹狀關(guān)系寓娩。這種情況太過于復(fù)雜寞埠,一般不列入考慮范圍蓝角。所以使鹅,一般比較通用的設(shè)計是支持組間廣播鲁僚,不同的組注冊不同的訂閱。組內(nèi)的不同機(jī)器裁厅,如果注冊一個相同的ID冰沙,則單播;如果注冊不同的ID(如IP地址+端口)执虹,則廣播拓挥。
至于廣播關(guān)系的維護(hù),一般由于消息隊列本身都是集群袋励,所以都維護(hù)在公共存儲上盖灸,如config server甘邀、zookeeper等。維護(hù)廣播關(guān)系所要做的事情基本是一致的:
發(fā)送關(guān)系的維護(hù)。
發(fā)送關(guān)系變更時的通知加勤。
隊列高級特性設(shè)計
上面都是些消息隊列基本功能的實(shí)現(xiàn),下面來看一些關(guān)于消息隊列特性相關(guān)的內(nèi)容,不管可靠投遞/消息丟失與重復(fù)以及事務(wù)乃至于性能沾乘,不是每個消息隊列都會照顧到滥崩,所以要依照業(yè)務(wù)的需求,來仔細(xì)衡量各種特性實(shí)現(xiàn)的成本赋访,利弊步悠,最終做出最為合理的設(shè)計踢涌。
可靠投遞(最終一致性)
這是個激動人心的話題秕噪,完全不丟消息,究竟可不可能?答案是撕贞,完全可能,前提是消息可能會重復(fù),并且,在異常情況下慢宗,要接受消息的延遲统阿。
方案說簡單也簡單,就是每當(dāng)要發(fā)生不可靠的事情(RPC等)之前齿桃,先將消息落地千绪,然后發(fā)送妈嘹。當(dāng)失敗或者不知道成功失敗(比如超時)時序愚,消息狀態(tài)是待發(fā)送,定時任務(wù)不停輪詢所有待發(fā)送消息,最終一定可以送達(dá)吟策。
具體來說:
producer往broker發(fā)送消息之前,需要做一次落地担租。
請求到server后,server確保數(shù)據(jù)落地后再告訴客戶端發(fā)送成功。
支持廣播的消息隊列需要對每個待發(fā)送的endpoint,持久化一個發(fā)送狀態(tài),直到所有endpoint狀態(tài)都OK才可刪除消息。
對于各種不確定(超時、down機(jī)、消息沒有送達(dá)票唆、送達(dá)后數(shù)據(jù)沒落地豆励、數(shù)據(jù)落地了回復(fù)沒收到),其實(shí)對于發(fā)送方來說,都是一件事情,就是消息沒有送達(dá)潮酒。
重推消息所面臨的問題就是消息重復(fù)。重復(fù)和丟失就像兩個噩夢咒劲,你必須要面對一個育叁。好在消息重復(fù)還有處理的機(jī)會蒂窒,消息丟失再想找回就難了。
Anyway,作為一個成熟的消息隊列,應(yīng)該盡量在各個環(huán)節(jié)減少重復(fù)投遞的可能性悍赢,不能因為重復(fù)有解決方案就放縱的亂投遞玉转。
最后說一句,不是所有的系統(tǒng)都要求最終一致性或者可靠投遞,比如一個論壇系統(tǒng)、一個招聘系統(tǒng)有送。一個重復(fù)的簡歷或話題被發(fā)布,可能比丟失了一個發(fā)布顯得更讓用戶無法接受穷娱。不斷重復(fù)一句話,任何基礎(chǔ)組件要服務(wù)于業(yè)務(wù)場景缸托。
消費(fèi)確認(rèn)
當(dāng)broker把消息投遞給消費(fèi)者后,消費(fèi)者可以立即響應(yīng)我收到了這個消息经窖。但收到了這個消息只是第一步,我能不能處理這個消息卻不一定兽掰。或許因為消費(fèi)能力的問題徒役,系統(tǒng)的負(fù)荷已經(jīng)不能處理這個消息孽尽;或者是剛才狀態(tài)機(jī)里面提到的消息不是我想要接收的消息,主動要求重發(fā)忧勿。
把消息的送達(dá)和消息的處理分開杉女,這樣才真正的實(shí)現(xiàn)了消息隊列的本質(zhì)-解耦瞻讽。所以,允許消費(fèi)者主動進(jìn)行消費(fèi)確認(rèn)是必要的熏挎。當(dāng)然速勇,對于沒有特殊邏輯的消息,默認(rèn)Auto Ack也是可以的坎拐,但一定要允許消費(fèi)方主動ack烦磁。
對于正確消費(fèi)ack的,沒什么特殊的哼勇。但是對于reject和error都伪,需要特別說明。reject這件事情积担,往往業(yè)務(wù)方是無法感知到的陨晶,系統(tǒng)的流量和健康狀況的評估,以及處理能力的評估是一件非常復(fù)雜的事情帝璧。舉個極端的例子先誉,收到一個消息開始build索引,可能這個消息要處理半個小時的烁,但消息量卻是非常的小褐耳。所以reject這塊建議做成滑動窗口/線程池類似的模型來控制,消費(fèi)能力不匹配的時候撮躁,直接拒絕漱病,過一段時間重發(fā),減少業(yè)務(wù)的負(fù)擔(dān)把曼。
但業(yè)務(wù)出錯這件事情是只有業(yè)務(wù)方自己知道的杨帽,就像上文提到的狀態(tài)機(jī)等等。這時應(yīng)該允許業(yè)務(wù)方主動ack error嗤军,并可以與broker約定下次投遞的時間注盈。
重復(fù)消息和順序消息
上文談到重復(fù)消息是不可能100%避免的,除非可以允許丟失叙赚,那么老客,順序消息能否100%滿足呢? 答案是可以,但條件更為苛刻:
允許消息丟失震叮。
從發(fā)送方到服務(wù)方到接受者都是單點(diǎn)單線程胧砰。
所以絕對的順序消息基本上是不能實(shí)現(xiàn)的,當(dāng)然在METAQ/Kafka等pull模型的消息隊列中苇瓣,單線程生產(chǎn)/消費(fèi)尉间,排除消息丟失,也是一種順序消息的解決方案。
一般來講哲嘲,一個主流消息隊列的設(shè)計范式里贪薪,應(yīng)該是不丟消息的前提下,盡量減少重復(fù)消息眠副,不保證消息的投遞順序画切。
談到重復(fù)消息,主要是兩個話題:
如何鑒別消息重復(fù)囱怕,并冪等的處理重復(fù)消息霍弹。
一個消息隊列如何盡量減少重復(fù)消息的投遞。
先來看看第一個話題光涂,每一個消息應(yīng)該有它的唯一身份庞萍。不管是業(yè)務(wù)方自定義的,還是根據(jù)IP/PID/時間戳生成的MessageId忘闻,如果有地方記錄這個MessageId钝计,消息到來是能夠進(jìn)行比對就能完成重復(fù)的鑒定。數(shù)據(jù)庫的唯一鍵/bloom filter/分布式KV中的key齐佳,都是不錯的選擇私恬。由于消息不能被永久存儲,所以理論上都存在消息從持久化存儲移除的瞬間上游還在投遞的可能(上游因種種原因投遞失敗炼吴,不停重試本鸣,都到了下游清理消息的時間)。這種事情都是異常情況下才會發(fā)生的硅蹦,畢竟是小眾情況荣德。兩分鐘消息都還沒送達(dá),多送一次又能怎樣呢童芹?冪等的處理消息是一門藝術(shù)涮瞻,因為種種原因重復(fù)消息或者錯亂的消息還是來到了,說兩種通用的解決方案:
版本號假褪。
狀態(tài)機(jī)署咽。
版本號
舉個簡單的例子,一個產(chǎn)品的狀態(tài)有上線/下線狀態(tài)生音。如果消息1是下線宁否,消息2是上線。不巧消息1判重失敗缀遍,被投遞了兩次慕匠,且第二次發(fā)生在2之后,如果不做重復(fù)性判斷域醇,顯然最終狀態(tài)是錯誤的台谊。
但是冤寿,如果每個消息自帶一個版本號。上游發(fā)送的時候青伤,標(biāo)記消息1版本號是1,消息2版本號是2殴瘦。如果再發(fā)送下線消息狠角,則版本號標(biāo)記為3。下游對于每次消息的處理蚪腋,同時維護(hù)一個版本號丰歌。
每次只接受比當(dāng)前版本號大的消息。初始版本為0屉凯,當(dāng)消息1到達(dá)時立帖,將版本號更新為1。消息2到來時悠砚,因為版本號>1.可以接收晓勇,同時更新版本號為2.當(dāng)另一條下線消息到來時,如果版本號是3.則是真實(shí)的下線消息灌旧。如果是1绑咱,則是重復(fù)投遞的消息。
如果業(yè)務(wù)方只關(guān)心消息重復(fù)不重復(fù)枢泰,那么問題就已經(jīng)解決了描融。但很多時候另一個頭疼的問題來了,就是消息順序如果和想象的順序不一致衡蚂。比如應(yīng)該的順序是12窿克,到來的順序是21。則最后會發(fā)生狀態(tài)錯誤毛甲。
參考TCP/IP協(xié)議年叮,如果想讓亂序的消息最后能夠正確的被組織,那么就應(yīng)該只接收比當(dāng)前版本號大一的消息丽啡。并且在一個session周期內(nèi)要一直保存各個消息的版本號谋右。
如果到來的順序是21,則先把2存起來补箍,待2到來后改执,再處理1,這樣重復(fù)性和順序性要求就都達(dá)到了坑雅。
狀態(tài)機(jī)
基于版本號來處理重復(fù)和順序消息聽起來是個不錯的主意辈挂,但凡事總有瑕疵。使用版本號的最大問題是:
對發(fā)送方必須要求消息帶業(yè)務(wù)版本號裹粤。
下游必須存儲消息的版本號终蒂,對于要嚴(yán)格保證順序的。
還不能只存儲最新的版本號的消息,要把亂序到來的消息都存儲起來拇泣。而且必須要對此做出處理噪叙。試想一個永不過期的"session",比如一個物品的狀態(tài)霉翔,會不停流轉(zhuǎn)于上下線睁蕾。那么中間環(huán)節(jié)的所有存儲就必須保留,直到在某個版本號之前的版本一個不丟的到來债朵,成本太高子眶。
就剛才的場景看,如果消息沒有版本號序芦,該怎么解決呢臭杰?業(yè)務(wù)方只需要自己維護(hù)一個狀態(tài)機(jī),定義各種狀態(tài)的流轉(zhuǎn)關(guān)系谚中。例如渴杆,"下線"狀態(tài)只允許接收"上線"消息,“上線”狀態(tài)只能接收“下線消息”藏杖,如果上線收到上線消息将塑,或者下線收到下線消息,在消息不丟失和上游業(yè)務(wù)正確的前提下蝌麸。要么是消息發(fā)重了点寥,要么是順序到達(dá)反了。這時消費(fèi)者只需要把“我不能處理這個消息”告訴投遞者来吩,要求投遞者過一段時間重發(fā)即可敢辩。而且重發(fā)一定要有次數(shù)限制,比如5次弟疆,避免死循環(huán)戚长,就解決了。
舉例子說明怠苔,假設(shè)產(chǎn)品本身狀態(tài)是下線同廉,1是上線消息,2是下線消息柑司,3是上線消息迫肖,正常情況下,消息應(yīng)該的到來順序是123攒驰,但實(shí)際情況下收到的消息狀態(tài)變成了3123蟆湖。
那么下游收到3消息的時候,判斷狀態(tài)機(jī)流轉(zhuǎn)是下線->上線玻粪,可以接收消息隅津。然后收到消息1诬垂,發(fā)現(xiàn)是上線->上線,拒絕接收伦仍,要求重發(fā)结窘。然后收到消息2,狀態(tài)是上線->下線充蓝,于是接收這個消息晦鞋。
此時無論重發(fā)的消息1或者3到來,還是可以接收棺克。另外的重發(fā),在一定次數(shù)拒絕后停止重發(fā)线定,業(yè)務(wù)正確娜谊。
中間件對于重復(fù)消息的處理
回歸到消息隊列的話題來講。上述通用的版本號/狀態(tài)機(jī)/ID判重解決方案里斤讥,哪些是消息隊列該做的纱皆、哪些是消息隊列不該做業(yè)務(wù)方處理的呢?其實(shí)這里沒有一個完全嚴(yán)格的定義芭商,但回到我們的出發(fā)點(diǎn)派草,我們保證不丟失消息的情況下盡量少重復(fù)消息,消費(fèi)順序不保證铛楣。那么重復(fù)消息下和亂序消息下業(yè)務(wù)的正確近迁,應(yīng)該是由消費(fèi)方保證的,我們要做的是減少消息發(fā)送的重復(fù)簸州。
我們無法定義業(yè)務(wù)方的業(yè)務(wù)版本號/狀態(tài)機(jī)鉴竭,如果API里強(qiáng)制需要指定版本號,則顯得過于綁架客戶了岸浑。況且搏存,在消費(fèi)方維護(hù)這么多狀態(tài),就涉及到一個消費(fèi)方的消息落地/多機(jī)間的同步消費(fèi)狀態(tài)問題矢洲,復(fù)雜度指數(shù)級上升璧眠,而且只能解決部分問題。
減少重復(fù)消息的關(guān)鍵步驟:
broker記錄MessageId读虏,直到投遞成功后清除责静,重復(fù)的ID到來不做處理,這樣只要發(fā)送者在清除周期內(nèi)能夠感知到消息投遞成功掘譬,就基本不會在server端產(chǎn)生重復(fù)消息泰演。
對于server投遞到consumer的消息,由于不確定對端是在處理過程中還是消息發(fā)送丟失的情況下睦焕,有必要記錄下投遞的IP地址猾普。決定重發(fā)之前詢問這個IP溜在,消息處理成功了嗎赏参?如果詢問無果纫溃,再重發(fā)郎楼。
事務(wù)
持久性是事務(wù)的一個特性简珠,然而只滿足持久性卻不一定能滿足事務(wù)的特性祭玉。還是拿扣錢/加錢的例子講律姨。滿足事務(wù)的一致性特征烫堤,則必須要么都不進(jìn)行,要么都能成功。
解決方案從大方向上有兩種:
兩階段提交帐萎,分布式事務(wù)葛躏。
本地事務(wù)摩窃,本地落地蒂秘,補(bǔ)償發(fā)送嘁捷。
分布式事務(wù)存在的最大問題是成本太高雄嚣,兩階段提交協(xié)議晒屎,對于仲裁down機(jī)或者單點(diǎn)故障,幾乎是一個無解的黑洞缓升。對于交易密集型或者I/O密集型的應(yīng)用鼓鲁,沒有辦法承受這么高的網(wǎng)絡(luò)延遲,系統(tǒng)復(fù)雜性港谊。
并且成熟的分布式事務(wù)一定構(gòu)建與比較靠譜的商用DB和商用中間件上骇吭,成本也太高。
那如何使用本地事務(wù)解決分布式事務(wù)的問題呢歧寺?以本地和業(yè)務(wù)在一個數(shù)據(jù)庫實(shí)例中建表為例子燥狰,與扣錢的業(yè)務(wù)操作同一個事務(wù)里,將消息插入本地數(shù)據(jù)庫斜筐。
如果消息入庫失敗龙致,則業(yè)務(wù)回滾;如果消息入庫成功顷链,事務(wù)提交目代。
然后發(fā)送消息(注意這里可以實(shí)時發(fā)送,不需要等定時任務(wù)檢出嗤练,以提高消息實(shí)時性)榛了。以后的問題就是前文的最終一致性問題所提到的了,只要消息沒有發(fā)送成功煞抬,就一直靠定時任務(wù)重試霜大。
這里有一個關(guān)鍵的點(diǎn),本地事務(wù)做的革答,是業(yè)務(wù)落地和消息落地的事務(wù)僧诚,而不是業(yè)務(wù)落地和RPC成功的事務(wù)。這里很多人容易混淆蝗碎,如果是后者湖笨,無疑是事務(wù)嵌套RPC,是大忌蹦骑,會有長事務(wù)死鎖等各種風(fēng)險慈省。
而消息只要成功落地,很大程度上就沒有丟失的風(fēng)險(磁盤物理損壞除外)。
而消息只要投遞到服務(wù)端確認(rèn)后本地才做刪除边败,就完成了producer->broker的可靠投遞袱衷,并且當(dāng)消息存儲異常時,業(yè)務(wù)也是可以回滾的笑窜。
本地事務(wù)存在兩個最大的使用障礙:
配置較為復(fù)雜致燥,“綁架”業(yè)務(wù)方,必須本地數(shù)據(jù)庫實(shí)例提供一個庫表排截。
對于消息延遲高敏感的業(yè)務(wù)不適用嫌蚤。
話說回來,不是每個業(yè)務(wù)都需要強(qiáng)事務(wù)的断傲⊥阎ǎ扣錢和加錢需要事務(wù)保證,但下單和生成短信卻不需要事務(wù)认罩,不能因為要求發(fā)短信的消息存儲投遞失敗而要求下單業(yè)務(wù)回滾箱蝠。所以,一個完整的消息隊列應(yīng)該定義清楚自己可以投遞的消息類型垦垂,如事務(wù)型消息宦搬,本地非持久型消息,以及服務(wù)端不落地的非可靠消息等劫拗。對不同的業(yè)務(wù)場景做不同的選擇间校。另外事務(wù)的使用應(yīng)該盡量低成本、透明化杨幼,可以依托于現(xiàn)有的成熟框架,如Spring的聲明式事務(wù)做擴(kuò)展聂渊。業(yè)務(wù)方只需要使用@Transactional標(biāo)簽即可差购。
性能相關(guān)
異步/同步
首先澄清一個概念,異步汉嗽,同步和oneway是三件事欲逃。異步,歸根結(jié)底你還是需要關(guān)心結(jié)果的饼暑,但可能不是當(dāng)時的時間點(diǎn)關(guān)心稳析,可以用輪詢或者回調(diào)等方式處理結(jié)果;同步是需要當(dāng)時關(guān)心的結(jié)果的弓叛;而oneway是發(fā)出去就不管死活的方式彰居,這種對于某些完全對可靠性沒有要求的場景還是適用的,但不是我們重點(diǎn)討論的范疇撰筷。
回歸來看陈惰,任何的RPC都是存在客戶端異步與服務(wù)端異步的,而且是可以任意組合的:客戶端同步對服務(wù)端異步毕籽,客戶端異步對服務(wù)端異步抬闯,客戶端同步對服務(wù)端同步井辆,客戶端異步對服務(wù)端同步。
對于客戶端來說溶握,同步與異步主要是拿到一個Result杯缺,還是Future(Listenable)的區(qū)別。實(shí)現(xiàn)方式可以是線程池睡榆,NIO或者其他事件機(jī)制萍肆,這里先不展開講。
服務(wù)端異步可能稍微難理解一點(diǎn)肉微,這個是需要RPC協(xié)議支持的匾鸥。參考servlet 3.0規(guī)范,服務(wù)端可以吐一個future給客戶端碉纳,并且在future done的時候通知客戶端勿负。
整個過程可以參考下面的代碼:
客戶端同步服務(wù)端異步。
客戶端同步服務(wù)端同步劳曹。
客戶端異步服務(wù)端同步(這里用線程池的方式)奴愉。
客戶端異步服務(wù)端異步。
上面說了這么多铁孵,其實(shí)是想讓大家脫離兩個誤區(qū):
RPC只有客戶端能做異步锭硼,服務(wù)端不能。
異步只能通過線程池蜕劝。
那么檀头,服務(wù)端使用異步最大的好處是什么呢?說到底岖沛,是解放了線程和I/O暑始。試想服務(wù)端有一堆I/O等待處理,如果每個請求都需要同步響應(yīng)婴削,每條消息都需要結(jié)果立刻返回廊镜,那么就幾乎沒法做I/O合并(當(dāng)然接口可以設(shè)計成batch的,但可能batch發(fā)過來的仍然數(shù)量較少)唉俗。而如果用異步的方式返回給客戶端future嗤朴,就可以有機(jī)會進(jìn)行I/O的合并,把幾個批次發(fā)過來的消息一起落地(這種合并對于MySQL等允許batch insert的數(shù)據(jù)庫效果尤其明顯)虫溜,并且徹底釋放了線程雹姊。不至于說來多少請求開多少線程,能夠支持的并發(fā)量直線提高衡楞。
來看第二個誤區(qū)容为,返回future的方式不一定只有線程池。換句話說,可以在線程池里面進(jìn)行同步操作坎背,也可以進(jìn)行異步操作替劈,也可以不使用線程池使用異步操作(NIO、事件)得滤。
回到消息隊列的議題上陨献,我們當(dāng)然不希望消息的發(fā)送阻塞主流程(前面提到了,server端如果使用異步模型懂更,則可能因消息合并帶來一定程度上的消息延遲)眨业,所以可以先使用線程池提交一個發(fā)送請求,主流程繼續(xù)往下走沮协。
但是線程池中的請求關(guān)心結(jié)果嗎龄捡?Of course,必須等待服務(wù)端消息成功落地慷暂,才算是消息發(fā)送成功聘殖。所以這里的模型,準(zhǔn)確地說事客戶端半同步半異步(使用線程池不阻塞主流程行瑞,但線程池中的任務(wù)需要等待server端的返回)奸腺,server端是純異步⊙茫客戶端的線程池wait在server端吐回的future上突照,直到server端處理完畢,才解除阻塞繼續(xù)進(jìn)行氧吐。
總結(jié)一句讹蘑,同步能夠保證結(jié)果,異步能夠保證效率筑舅,要合理的結(jié)合才能做到最好的效率座慰。
批量
談到批量就不得不提生產(chǎn)者消費(fèi)者模型。但生產(chǎn)者消費(fèi)者模型中最大的痛點(diǎn)是:消費(fèi)者到底應(yīng)該何時進(jìn)行消費(fèi)豁翎。大處著眼來看角骤,消費(fèi)動作都是事件驅(qū)動的隅忿。主要事件包括:
攢夠了一定數(shù)量心剥。
到達(dá)了一定時間。
隊列里有新的數(shù)據(jù)到來背桐。
對于及時性要求高的數(shù)據(jù)优烧,可用采用方式3來完成,比如客戶端向服務(wù)端投遞數(shù)據(jù)链峭。只要隊列有數(shù)據(jù)畦娄,就把隊列中的所有數(shù)據(jù)刷出,否則將自己掛起,等待新數(shù)據(jù)的到來熙卡。
在第一次把隊列數(shù)據(jù)往外刷的過程中杖刷,又積攢了一部分?jǐn)?shù)據(jù),第二次又可以形成一個批量驳癌。偽代碼如下:
這種方式是消息延遲和批量的一個比較好的平衡滑燃,但優(yōu)先響應(yīng)低延遲。延遲的最高程度由上一次發(fā)送的等待時間決定颓鲜。但可能造成的問題是發(fā)送過快的話批量的大小不夠滿足性能的極致表窘。
相反對于可以用適量的延遲來換取高性能的場景來說,用定時/定量二選一的方式可能會更為理想甜滨,既到達(dá)一定數(shù)量才發(fā)送乐严,但如果數(shù)量一直達(dá)不到,也不能干等衣摩,有一個時間上限昂验。
具體說來,在上文的submit之前昭娩,多判斷一個時間和數(shù)量凛篙,并且Runnable內(nèi)部維護(hù)一個定時器,避免沒有新任務(wù)到來時舊的任務(wù)永遠(yuǎn)沒有機(jī)會觸發(fā)發(fā)送條件栏渺。對于server端的數(shù)據(jù)落地呛梆,使用這種方式就非常方便。
最后啰嗦幾句磕诊,曾經(jīng)有人問我填物,為什么網(wǎng)絡(luò)請求小包合并成大包會提高性能?主要原因有兩個:
減少無謂的請求頭霎终,如果你每個請求只有幾字節(jié)滞磺,而頭卻有幾十字節(jié),無疑效率非常低下莱褒。
減少回復(fù)的ack包個數(shù)击困。把請求合并后,ack包數(shù)量必然減少广凸,確認(rèn)和重發(fā)的成本就會降低阅茶。
push還是pull
上文提到的消息隊列,大多是針對push模型的設(shè)計×潞#現(xiàn)在市面上有很多經(jīng)典的也比較成熟的pull模型的消息隊列脸哀,如Kafka、MetaQ等扭吁。這跟JMS中傳統(tǒng)的push方式有很大的區(qū)別撞蜂,可謂另辟蹊徑盲镶。
我們簡要分析下push和pull模型各自存在的利弊。
慢消費(fèi)
慢消費(fèi)無疑是push模型最大的致命傷蝌诡,穿成流水線來看溉贿,如果消費(fèi)者的速度比發(fā)送者的速度慢很多,勢必造成消息在broker的堆積浦旱。假設(shè)這些消息都是有用的無法丟棄的顽照,消息就要一直在broker端保存。當(dāng)然這還不是最致命的闽寡,最致命的是broker給consumer推送一堆consumer無法處理的消息代兵,consumer不是reject就是error,然后來回踢皮球爷狈。
反觀pull模式植影,consumer可以按需消費(fèi),不用擔(dān)心自己處理不了的消息來騷擾自己涎永,而broker堆積消息也會相對簡單思币,無需記錄每一個要發(fā)送消息的狀態(tài),只需要維護(hù)所有消息的隊列和偏移量就可以了羡微。所以對于建立索引等慢消費(fèi)谷饿,消息量有限且到來的速度不均勻的情況,pull模式比較合適妈倔。
消息延遲與忙等
這是pull模式最大的短板博投。由于主動權(quán)在消費(fèi)方,消費(fèi)方無法準(zhǔn)確地決定何時去拉取最新的消息盯蝴。如果一次pull取到消息了還可以繼續(xù)去pull毅哗,如果沒有pull取到則需要等待一段時間重新pull。
但等待多久就很難判定了捧挺。你可能會說虑绵,我可以有xx動態(tài)pull取時間調(diào)整算法,但問題的本質(zhì)在于闽烙,有沒有消息到來這件事情決定權(quán)不在消費(fèi)方翅睛。也許1分鐘內(nèi)連續(xù)來了1000條消息,然后半個小時沒有新消息產(chǎn)生黑竞,可能你的算法算出下次最有可能到來的時間點(diǎn)是31分鐘之后捕发,或者60分鐘之后,結(jié)果下條消息10分鐘后到了摊溶,是不是很讓人沮喪爬骤?
當(dāng)然也不是說延遲就沒有解決方案了充石,業(yè)界較成熟的做法是從短時間開始(不會對broker有太大負(fù)擔(dān))莫换,然后指數(shù)級增長等待霞玄。比如開始等5ms,然后10ms拉岁,然后20ms坷剧,然后40ms……直到有消息到來,然后再回到5ms喊暖。
即使這樣惫企,依然存在延遲問題:假設(shè)40ms到80ms之間的50ms消息到來,消息就延遲了30ms陵叽,而且對于半個小時來一次的消息狞尔,這些開銷就是白白浪費(fèi)的。
在阿里的RocketMq里巩掺,有一種優(yōu)化的做法-長輪詢偏序,來平衡推拉模型各自的缺點(diǎn)∨痔妫基本思路是:消費(fèi)者如果嘗試?yán)∈。皇侵苯觬eturn,而是把連接掛在那里wait,服務(wù)端如果有新的消息到來,把連接notify起來玖姑,這也是不錯的思路社付。但海量的長連接block對系統(tǒng)的開銷還是不容小覷的,還是要合理的評估時間間隔燃箭,給wait加一個時間上限比較好~
順序消息
如果push模式的消息隊列冲呢,支持分區(qū),單分區(qū)只支持一個消費(fèi)者消費(fèi)招狸,并且消費(fèi)者只有確認(rèn)一個消息消費(fèi)后才能push送另外一個消息碗硬,還要發(fā)送者保證全局順序唯一,聽起來也能做順序消息瓢颅,但成本太高了恩尾,尤其是必須每個消息消費(fèi)確認(rèn)后才能發(fā)下一條消息,這對于本身堆積能力和慢消費(fèi)就是瓶頸的push模式的消息隊列挽懦,簡直是一場災(zāi)難翰意。
反觀pull模式,如果想做到全局順序消息信柿,就相對容易很多:
producer對應(yīng)partition冀偶,并且單線程。
consumer對應(yīng)partition渔嚷,消費(fèi)確認(rèn)(或批量確認(rèn))进鸠,繼續(xù)消費(fèi)即可。
所以對于日志push送這種最好全局有序形病,但允許出現(xiàn)小誤差的場景客年,pull模式非常合適霞幅。如果你不想看到通篇亂套的日志~~Anyway,需要順序消息的場景還是比較有限的而且成本太高量瓜,請慎重考慮司恳。
總結(jié)
本文從為何使用消息隊列開始講起,然后主要介紹了如何從零開始設(shè)計一個消息隊列绍傲,包括RPC扔傅、事務(wù)、最終一致性烫饼、廣播猎塞、消息確認(rèn)等關(guān)鍵問題。并對消息隊列的push杠纵、pull模型做了簡要分析邢享,最后從批量和異步角度,分析了消息隊列性能優(yōu)化的思路淡诗。下篇會著重介紹一些高級話題骇塘,如存儲系統(tǒng)的設(shè)計、流控和錯峰的設(shè)計韩容、公平調(diào)度等款违。希望通過這些,讓大家對消息隊列有個提綱挈領(lǐng)的整體認(rèn)識群凶,并給自主開發(fā)消息隊列提供思路插爹。另外,本文主要是源自自己在開發(fā)消息隊列中的思考和讀源碼時的體會请梢,比較不"官方"赠尾,也難免會存在一些漏洞,歡迎大家多多交流毅弧。
后續(xù)我們還會推出消息隊列設(shè)計高級篇气嫁,內(nèi)容會涵蓋以下方面:
pull模型消息系統(tǒng)設(shè)計理念
存儲子系統(tǒng)設(shè)計
流量控制
公平調(diào)度
敬請期待哦~
作者簡介
王燁,現(xiàn)在是美團(tuán)旅游后臺研發(fā)組的程序猿够坐,之前曾經(jīng)在百度寸宵、去哪和優(yōu)酷工作過,專注Java后臺開發(fā)元咙。對于網(wǎng)絡(luò)編程和并發(fā)編程具有濃厚的興趣梯影,曾經(jīng)做過一些基礎(chǔ)組件,也翻過一些源碼庶香,屬于比較典型的宅男技術(shù)控甲棍。期待能夠與更多知己,在coding的路上并肩前行~