列實(shí)戰(zhàn)指南:從異步解耦到避坑全解析)
消息隊(duì)列這件事我剛?cè)胄心菚?huì)兒把它想得挺神秘總覺得是那種大廠才用得起的中間件。后來在多個(gè)項(xiàng)目里親手寫完生產(chǎn)者、消費(fèi)者甚至搭過集群才意識(shí)到它沒那么玄乎但真正要用好需要注意的細(xì)節(jié)確實(shí)不少。這篇文章就當(dāng)一份來自實(shí)踐的產(chǎn)品介紹來讀。我會(huì)把消息隊(duì)列的核心價(jià)值、主流產(chǎn)品怎么選、上手時(shí)必須搞懂的幾個(gè)要點(diǎn)以及那些你大概率會(huì)遇到的坑一次講清楚。不管你現(xiàn)在是剛接觸后端的新人還是已經(jīng)在業(yè)務(wù)里用過一點(diǎn)但總感覺差點(diǎn)意思這篇應(yīng)該都能給你一些實(shí)打?qū)嵉膮⒖肌?. 消息隊(duì)列到底解決了什么問題1.1 異步讓主流程不再等“雜事”先從一個(gè)最典型也最容易理解的場(chǎng)景說起用戶注冊(cè)。以前很多系統(tǒng)的做法是注冊(cè)接口里先寫數(shù)據(jù)庫然后同步調(diào)用短信服務(wù)、郵件服務(wù)、積分服務(wù)。結(jié)果用戶點(diǎn)完注冊(cè)按鈕頁面轉(zhuǎn)圈兩三秒其實(shí)早就在庫里插入成功了剩下時(shí)間全花在等下游返回。一次注冊(cè)要等好多外部系統(tǒng)這個(gè)體驗(yàn)基本沒法接受。引入消息隊(duì)列之后注冊(cè)接口只做一件事把用戶注冊(cè)成功這條消息扔進(jìn)隊(duì)列馬上返回“注冊(cè)成功”。短信、郵件、積分這些服務(wù)各自訂閱這個(gè)消息后臺(tái)慢慢處理。用戶感知到的延遲從幾秒降到幾十毫秒。這就是消息隊(duì)列最基礎(chǔ)的價(jià)值異步化。但異步不是銀彈它換來的是系統(tǒng)復(fù)雜度。原來同步調(diào)用出了問題當(dāng)場(chǎng)能發(fā)現(xiàn)現(xiàn)在變成發(fā)完消息就不管了下游要是沒消費(fèi)到可能用戶一直收不到短信。所以用異步之后必須配套可靠性機(jī)制、監(jiān)控告警、甚至對(duì)賬系統(tǒng)。這也是我后面要反復(fù)強(qiáng)調(diào)的一點(diǎn)異步一時(shí)爽容災(zāi)不能省。1.2 削峰把流量洪峰緩沖到平谷第二個(gè)場(chǎng)景是削峰也是很多公司引入消息隊(duì)列的直接理由。比如一個(gè)促銷活動(dòng)假設(shè)平時(shí)每秒幾百請(qǐng)求活動(dòng)開始那一分鐘直接沖到每秒幾萬請(qǐng)求。如果這些請(qǐng)求全部直連數(shù)據(jù)庫和訂單服務(wù)數(shù)據(jù)庫基本一秒鐘就會(huì)被打滿連接甚至直接卡死。消息隊(duì)列能做的是先把海量請(qǐng)求接收下來堆在隊(duì)列里然后讓下游消費(fèi)者按照自己能夠承受的速度去處理比如固定每秒拉取五百條。這樣即使瞬時(shí)流量特別高后端系統(tǒng)也不會(huì)被一波打垮。流量過去之后隊(duì)列里的積壓消息再慢慢被消費(fèi)掉。簡單說消息隊(duì)列像一個(gè)水庫上游洪水來了先蓄著下游按需放水。不過要注意削峰只解決了抗壓問題不解決業(yè)務(wù)問題。如果隊(duì)列里的消息長時(shí)間堆積用戶可能以為訂單下了沒反應(yīng)。所以削峰場(chǎng)景一定要配堆積監(jiān)控以及及時(shí)的擴(kuò)容預(yù)案。別等堆積了幾百萬條才想起來看那就已經(jīng)變成事故了。1.3 解耦讓上下游各自演進(jìn)第三個(gè)價(jià)值是解耦。沒有消息隊(duì)列的時(shí)候訂單服務(wù)要下單成功通知庫存、通知積分、通知物流就得逐個(gè)調(diào)用對(duì)方的接口。每個(gè)接口都是硬依賴任何一個(gè)下游升級(jí)接口、臨時(shí)故障都會(huì)直接影響訂單主流程。更麻煩的是以后每新增一個(gè)需要感知訂單的服務(wù)訂單代碼都要改一遍。用了消息隊(duì)列訂單服務(wù)只負(fù)責(zé)發(fā)布“訂單創(chuàng)建成功”事件至于誰訂閱、什么時(shí)候訂閱、訂閱之后怎么處理訂單服務(wù)完全不用關(guān)心。下游系統(tǒng)按需消費(fèi)新增業(yè)務(wù)只要新起一個(gè)消費(fèi)者組訂閱同樣的事件即可訂單服務(wù)零改動(dòng)。這個(gè)模式在微服務(wù)架構(gòu)里特別實(shí)用也是事件驅(qū)動(dòng)架構(gòu)的基礎(chǔ)。我自己的體會(huì)是解耦帶來的好處是長期收益但代價(jià)是引入了消息中間件這樣一個(gè)額外的組件團(tuán)隊(duì)需要學(xué)習(xí)和運(yùn)維它。所以對(duì)于只有兩三個(gè)服務(wù)的小項(xiàng)目我不建議一上來就套消息隊(duì)列等真的出現(xiàn)跨系統(tǒng)協(xié)作復(fù)雜或者流量波動(dòng)明顯時(shí)再引入不遲。2. 主流消息隊(duì)列產(chǎn)品怎么選2.1 四大產(chǎn)品定位速覽市面上消息隊(duì)列產(chǎn)品很多見得最多的是 Kafka、RabbitMQ、RocketMQ、Pulsar 這四類。不少同學(xué)選型時(shí)喜歡查“哪個(gè)性能最強(qiáng)”結(jié)果往往變成口水仗。實(shí)際上每個(gè)產(chǎn)品都有自己的強(qiáng)項(xiàng)和代價(jià)選型首先得看業(yè)務(wù)場(chǎng)景。產(chǎn)品核心定位吞吐量延遲特點(diǎn)擅長的方向Kafka大數(shù)據(jù)與流處理極高毫秒級(jí)日志采集、數(shù)據(jù)管道、事件流RabbitMQ企業(yè)業(yè)務(wù)消息中等微秒級(jí)復(fù)雜路由、延遲/死信隊(duì)列、輕量級(jí)業(yè)務(wù)RocketMQ互聯(lián)網(wǎng)業(yè)務(wù)消息高毫秒級(jí)事務(wù)消息、定時(shí)消息、高可靠業(yè)務(wù)場(chǎng)景Pulsar云原生多租戶消息極高毫秒級(jí)多集群、跨地域復(fù)制、彈性擴(kuò)縮容Kafka 我用的時(shí)間最長。它天生是為高吞吐日志和流處理設(shè)計(jì)的配合生態(tài)里的流處理引擎特別順手但如果你只想在幾個(gè)微服務(wù)之間傳個(gè)簡單的訂單通知用 Kafka 反而顯得重Topic 里的分區(qū)也好、Offset 管理也好概念都比較硬核。RabbitMQ 最直觀路由靈活社區(qū)里相關(guān)資料也特別多適合中小型業(yè)務(wù)快速上手但吞吐一上來就容易遇到瓶頸。RocketMQ 在電商、交易這類對(duì)可靠性要求很高的場(chǎng)景很受歡迎支持事務(wù)消息和延遲消息非常契合業(yè)務(wù)消息。Pulsar 是后起之秀存儲(chǔ)和計(jì)算分離的設(shè)計(jì)讓它在彈性伸縮上很有優(yōu)勢(shì)多租戶能力也強(qiáng)很適合云原生環(huán)境不過相比其他幾個(gè)團(tuán)隊(duì)里的熟悉程度和踩坑資料都少一些。2.2 選型背后的四個(gè)關(guān)鍵指標(biāo)我建議選型的時(shí)候不問“哪個(gè)最好”而是問自己四個(gè)問題。第一是吞吐量需求。每天幾百萬條消息和每秒幾十萬條消息完全是兩個(gè)世界里。每秒十萬以上Kafka 和 Pulsar 更容易扛住如果只是幾萬甚至幾千RabbitMQ、RocketMQ 也完全夠用。第二是延遲要求。金融行情、在線協(xié)同這類場(chǎng)景對(duì)端到端延遲很敏感需要選擇延遲穩(wěn)定的產(chǎn)品異步通知、離線計(jì)算這類場(chǎng)景對(duì)延遲容忍度高可以優(yōu)先考慮吞吐和可靠性。第三是可靠性和一致性。業(yè)務(wù)消息、訂單狀態(tài)這類數(shù)據(jù)丟一條都是事故需要開啟持久化和生產(chǎn)者確認(rèn)機(jī)制RocketMQ 在這類場(chǎng)景下口碑很好日志數(shù)據(jù)丟掉個(gè)別條可以重采Kafka 默認(rèn)配置就足夠。第四是團(tuán)隊(duì)技術(shù)棧與運(yùn)維成本。如果團(tuán)隊(duì)只熟悉 Java 和 SpringRocketMQ、RabbitMQ 的接入和排查更平滑如果已經(jīng)有大數(shù)據(jù)生態(tài)Kafka 幾乎是必然選擇。還有一個(gè)很容易被忽略的點(diǎn)運(yùn)維能力。自建一套集群不只是裝個(gè)服務(wù)那么簡單還要考慮監(jiān)控、擴(kuò)縮容、故障恢復(fù)。如果公司沒有專門的中間件團(tuán)隊(duì)云廠商提供的托管型消息隊(duì)列其實(shí)是更穩(wěn)妥的選擇省下的運(yùn)維時(shí)間足夠你開發(fā)好幾個(gè)業(yè)務(wù)模塊了。3. 消息隊(duì)列的實(shí)操要點(diǎn)從入門到躲坑3.1 核心概念生產(chǎn)者、消費(fèi)者、Topic、分區(qū)和 Offset剛開始用消息隊(duì)列最難跨越的是概念門檻。我用 Kafka 的模型來講解因?yàn)檫@幾個(gè)概念在所有產(chǎn)品里基本都是相通的。Topic 可以理解成一個(gè)業(yè)務(wù)消息的信箱。比如訂單服務(wù)發(fā)消息就發(fā)到名為 order-event 的 Topic 里。這個(gè) Topic 下面會(huì)被拆成多個(gè) Partition也就是分區(qū)。分區(qū)的意義在于并行一個(gè)分區(qū)同一時(shí)刻只能被同一個(gè)消費(fèi)組里的一個(gè)消費(fèi)者處理分區(qū)越多能并行消費(fèi)的消費(fèi)者就越多。每條消息寫入某個(gè)分區(qū)后會(huì)得到一個(gè)遞增的 Offset它就像消息在分區(qū)里的游標(biāo)代表這條消息的位置。消費(fèi)者讀取消息的時(shí)候需要記錄自己消費(fèi)到哪個(gè) Offset 了下次從記錄的 Offset 繼續(xù)讀這就是消費(fèi)進(jìn)度。生產(chǎn)者通過負(fù)載均衡策略決定消息寫到哪個(gè)分區(qū)默認(rèn)是輪詢或按 key 哈希。如果想要某個(gè)訂單的消息嚴(yán)格按照順序處理就按訂單 ID 哈希讓同一個(gè)訂單的消息永遠(yuǎn)進(jìn)同一個(gè)分區(qū)。這個(gè)設(shè)計(jì)也很重要后面講順序性會(huì)再說。消費(fèi)者側(cè)使用消費(fèi)組多個(gè)消費(fèi)者實(shí)例組成一個(gè)組組內(nèi)的不同實(shí)例分別消費(fèi)不同分區(qū)合在一起消費(fèi)完整個(gè) Topic如果希望多個(gè)服務(wù)都完整消費(fèi)同樣的消息就分別用不同的消費(fèi)組組與組之間是相互獨(dú)立廣播的。3.2 消息可靠性與順序性不是所有消息都要“不丟不重不亂”很多剛接觸消息隊(duì)列的人一開始就提一個(gè)高要求消息一定要嚴(yán)格不丟失、不重復(fù)、不亂序。這個(gè)目標(biāo)聽起來很完美但在分布式系統(tǒng)里幾乎不可能同時(shí)做到。CAP 理論放這兒同樣適用關(guān)鍵是在業(yè)務(wù)需求里做取舍。先說可靠性。消息從生產(chǎn)到消費(fèi)要經(jīng)過三段鏈路生產(chǎn)者把消息發(fā)送給 BrokerBroker 把消息落盤消費(fèi)者從 Broker 拉取消息并處理。任何一段都可能出問題。生產(chǎn)階段要開啟生產(chǎn)者確認(rèn)機(jī)制比如 Kafka 的 acks 設(shè)置。acks0 就是發(fā)完就完性能最高但可能丟消息acks1 是寫入主副本就返回能夠抗住單點(diǎn)故障acksall 是所有副本都寫入成功才算成功安全性最高但性能會(huì)下降。Broker 存儲(chǔ)階段要注意刷盤策略和副本數(shù)內(nèi)存里沒來得及落盤就宕機(jī)數(shù)據(jù)可能丟。消費(fèi)階段核心是“處理完再確認(rèn)”不要在消息還沒處理完的情況下就提交 Offset。再說順序性。有些業(yè)務(wù)對(duì)消息順序有硬性要求比如支付狀態(tài)要先“創(chuàng)建”再“成功”順序反了業(yè)務(wù)就亂了。全局順序在所有消息隊(duì)列里都很難做成本也非常高一般不做。正確做法是分區(qū)有序把需要保證順序的消息通過相同的 key 路由到同一個(gè)分區(qū)這樣單分區(qū)內(nèi)的消息天然有序。這就是為什么按訂單 ID 哈希寫入分區(qū)是通用做法。最后是權(quán)重問題。我見過不少團(tuán)隊(duì)為了追求極端的可靠性所有消息都開啟強(qiáng)同步復(fù)制結(jié)果吞吐量掉了一半業(yè)務(wù)根本扛不住。我的建議是給不同 Topic 設(shè)置不同的可靠性級(jí)別核心交易消息用最高可靠性日志類消息用低可靠性高吞吐配置別用一套參數(shù)打天下。3.3 冪等消費(fèi)與消費(fèi)進(jìn)度管理防止重復(fù)和漏處理消息隊(duì)列里重復(fù)消費(fèi)是很常見的事。網(wǎng)絡(luò)抖動(dòng)導(dǎo)致消費(fèi)者處理完消息但確認(rèn)信息沒發(fā)出去Broker 超時(shí)后就會(huì)重新投遞消費(fèi)者在處理過程中宕機(jī)還沒提交 Offset重啟后也會(huì)從舊 Offset 重新拉取。所以只要用了消息隊(duì)列就必須默認(rèn)消息可能重復(fù)。應(yīng)對(duì)重復(fù)消費(fèi)的標(biāo)準(zhǔn)姿勢(shì)是“冪等”也就是不管這條消息被處理多少次最終結(jié)果保持一致。常見做法有幾種。最簡單的是在消費(fèi)者里記錄已經(jīng)處理過的唯一業(yè)務(wù) ID處理前查一下或者用 Redis SETNX 搶占如果之前處理過就直接跳過。還可以在數(shù)據(jù)庫表里建立唯一索引比如訂單流水號(hào)插入時(shí)如果重復(fù)就捕獲異常。更復(fù)雜的用狀態(tài)機(jī)校驗(yàn)比如訂單狀態(tài)只有“待支付”才能改成“已支付”如果重復(fù)消息嘗試把“已完成”改成“已支付”就說明沒有可轉(zhuǎn)移的狀態(tài)直接忽略。冪等設(shè)計(jì)屬于業(yè)務(wù)層的事但每一個(gè)用消息隊(duì)列的團(tuán)隊(duì)都必須正視這個(gè)問題。消費(fèi)進(jìn)度管理也很關(guān)鍵。Kafka 通過提交 Offset 來記錄消費(fèi)位置有自動(dòng)提交和手動(dòng)提交兩種。我強(qiáng)烈建議在關(guān)鍵業(yè)務(wù)里使用手動(dòng)提交并且先執(zhí)行業(yè)務(wù)處理再提交 Offset。自動(dòng)提交配置簡單但可能存在“消息剛拉到還沒來得及處理就提交”的風(fēng)險(xiǎn)一宕機(jī)消息就丟了。手動(dòng)提交也分兩種先處理再提交可能導(dǎo)致重復(fù)消費(fèi)先提交再處理可能導(dǎo)致漏消費(fèi)。兩者之間選前者因?yàn)橹貜?fù)好于丟失業(yè)務(wù)上可以通過冪等抵消丟失則需要補(bǔ)數(shù)據(jù)成本高得多。RabbitMQ 類似通過消息確認(rèn)機(jī)制ACK來控制消費(fèi)者處理完后主動(dòng)調(diào)用 basicAckBroker 才會(huì)刪除消息。如果消費(fèi)者一直不返回 ACK消息會(huì)重新進(jìn)入隊(duì)列這也是一種重試機(jī)制。4. 使用過程中的高頻事故與排查技巧4.1 消息堆積別等爆了才想起來看消息隊(duì)列最經(jīng)典的事故莫過于消息堆積。消費(fèi)者處理不過來生產(chǎn)速度遠(yuǎn)大于消費(fèi)速度隊(duì)列積壓越來越多帶來的直接體驗(yàn)就是業(yè)務(wù)延遲、用戶投訴、甚至數(shù)據(jù)對(duì)不上。遇到堆積第一步是看監(jiān)控確認(rèn)堆積量、消費(fèi)速率、生產(chǎn)速率先分清是生產(chǎn)端突然暴漲還是消費(fèi)端能力下降。如果消費(fèi)速度明顯偏低先看消費(fèi)者實(shí)例是不是掛了幾個(gè)最常見的問題是某個(gè)消費(fèi)者進(jìn)程內(nèi)存溢出退出但消費(fèi)組還掛著它導(dǎo)致部分分區(qū)沒有消費(fèi)者處理。這時(shí)要看分區(qū)的分配情況把掛掉的實(shí)例摘掉讓其他實(shí)例頂上。如果消費(fèi)者都在正常工作但消費(fèi)速度就是不到生產(chǎn)速度的一半大概率是業(yè)務(wù)處理邏輯太慢比如消費(fèi)消息時(shí)同步調(diào)用了好幾個(gè)外部服務(wù)一個(gè)接口慢兩三秒整體消費(fèi)速度直接拖垮。對(duì)策有幾個(gè)方向一是增加消費(fèi)者實(shí)例數(shù)注意消費(fèi)組的并發(fā)數(shù)受限于分區(qū)數(shù)實(shí)例數(shù)量超過分區(qū)數(shù)沒意義得先考慮增加分區(qū)二是對(duì)消費(fèi)鏈路做優(yōu)化把非核心的同步調(diào)用改成異步或者去掉不必要的等待三是批量消費(fèi)拉取一次處理一批減少消息拉取的開銷。還有一個(gè)臨時(shí)緩解手段就是緊急關(guān)閉下游一些不需要實(shí)時(shí)性的處理邏輯只保留核心數(shù)據(jù)落庫先把堆積削下去再回頭補(bǔ)處理。4.2 重復(fù)消費(fèi)為什么做不到“恰好一次”以及怎么兜底我遇到過很多回因?yàn)橹貜?fù)消費(fèi)引發(fā)的線上問題用戶點(diǎn)了兩次申請(qǐng)積分加了兩倍交易狀態(tài)回調(diào)處理了兩次生成了兩筆流水??梢哉f只要是分布式系統(tǒng)重復(fù)消息就是常態(tài)任何號(hào)稱“恰好一次”的方案本質(zhì)上都是在某個(gè)環(huán)節(jié)做了冪等兜底而不是真的只投遞一次。排查重復(fù)消費(fèi)第一步是看消費(fèi)端是否有空閑提交現(xiàn)象。如果使用自動(dòng)提交并且處理一條消息耗時(shí)較長消費(fèi)者可能在處理完之前就提交了 Offset這時(shí)候如果進(jìn)程崩潰重啟后會(huì)從更早的位置開始消費(fèi)之前處理過的消息就會(huì)重復(fù)。解決辦法是把自動(dòng)提交關(guān)掉改成手動(dòng)提交并且保證處理成功之后再提交。第二個(gè)常見原因是超時(shí)重投。比如網(wǎng)絡(luò)抖動(dòng)消費(fèi)者處理完了但 ACK 沒有得到 Broker 確認(rèn)Broker 重試投遞導(dǎo)致同一消息再次進(jìn)入消費(fèi)者。這種情況在代碼層面很難完全避免只能靠業(yè)務(wù)冪等兜底。對(duì)于冪等我再推薦一個(gè)比較實(shí)用的模式給每條消息生成全局唯一的消息 ID消費(fèi)者落庫時(shí)把消息 ID 作為主鍵或者唯一索引如果有重復(fù)插入就直接失敗。另一種方式在消費(fèi)之前用 Redis 寫一條唯一鍵例如 order_id event_type設(shè)置一個(gè)合理的過期時(shí)間如果寫入成功則繼續(xù)處理如果寫入失敗說明已經(jīng)處理過直接返回成功。這個(gè)方案在對(duì)抗重復(fù)消費(fèi)時(shí)效果很明顯而且實(shí)現(xiàn)成本低我基本每個(gè)項(xiàng)目都會(huì)用。4.3 消息丟失從生產(chǎn)到消費(fèi)的每一環(huán)都要堵數(shù)據(jù)丟失是消息隊(duì)列里最嚴(yán)重的事故。有一次我在壓測(cè)環(huán)境里下了上萬條消息重啟 Broker 后發(fā)現(xiàn)少了一些排查后才發(fā)現(xiàn)生產(chǎn)端沒有配置可靠的確認(rèn)機(jī)制。消息丟失通常不是單一環(huán)節(jié)的問題而是每一環(huán)節(jié)的可靠性沒做到位。規(guī)范排查看三個(gè)位置。生產(chǎn)端確認(rèn)消息是否開啟了“發(fā)送確認(rèn)”。Kafka 的 acks 配置如果等于 0生產(chǎn)者發(fā)出消息后不等待任何確認(rèn)一旦網(wǎng)絡(luò)閃斷消息就丟了。如果用的是 Kafka 生產(chǎn)客戶端建議生產(chǎn)核心消息時(shí)把 acks 設(shè)為 all同時(shí)在 send 回調(diào)里檢查異常發(fā)送失敗要做重試。Broker 端看副本數(shù)和落盤策略。單副本集群里Broker 宕機(jī)基本等于數(shù)據(jù)全丟至少要設(shè)置 2 個(gè)副本以上并開啟 in.sync.replicas落盤方面默認(rèn)的刷盤策略是后臺(tái)異步刷盤如果允許極端情況下的少量丟失可以使用默認(rèn)配置以求性能但如果是交易類數(shù)據(jù)需要根據(jù)產(chǎn)品能力開啟同步刷盤。消費(fèi)端看是否因?yàn)樽詣?dòng)提交 Offset 導(dǎo)致消息沒處理完就被標(biāo)記成已消費(fèi)。另一個(gè)容易被忽略的點(diǎn)是消費(fèi)端代碼異常比如消息拉取到了但處理時(shí)拋出異常后沒有正確捕獲也沒有重試策略消息直接被跳過。好的實(shí)踐是使用重試隊(duì)列或死信隊(duì)列處理失敗的消息先進(jìn)入重試隊(duì)列等待再次嘗試多次失敗后進(jìn)入死信隊(duì)列由人工介入處理。消息丟失的排查有時(shí)候需要“全鏈路日志”。我建議在生產(chǎn)者和消費(fèi)者的入口、出口都打印消息 ID 和關(guān)鍵業(yè)務(wù)信息萬一真丟了可以通過日志查這條消息是否被生產(chǎn)、是否被拉取、是否處理成功定位到具體斷了哪一環(huán)。沒有日志排查消息丟失會(huì)像是大海撈針。5. 運(yùn)營和維護(hù)的經(jīng)驗(yàn)從設(shè)計(jì)時(shí)就避開雷5.1 監(jiān)控把堆積量當(dāng)成水位線很多團(tuán)隊(duì)引入消息隊(duì)列是從“跑通”開始的能發(fā)能收就覺得行了。可一旦流量上來或者業(yè)務(wù)開始時(shí)間敏感第一個(gè)遇到的問題就是沒有任何監(jiān)控指標(biāo)。等用戶投訴了才登服務(wù)器看日志這種被動(dòng)方式會(huì)非常痛苦。我強(qiáng)烈建議消息隊(duì)列接入的當(dāng)天就做好基礎(chǔ)監(jiān)控。最核心的指標(biāo)有這么幾個(gè)生產(chǎn)速率、消費(fèi)速率、積壓量Lag、消費(fèi)延遲、Broker 的 CPU 和磁盤。其中積壓量是最直觀的水位線我們?nèi)粘6⒌木褪歉鱾€(gè) Topic 的積壓曲線。給積壓量設(shè)置告警閾值比如某 Topic 積壓超過一千條且持續(xù)三分鐘就發(fā)告警超過 1000 條持續(xù)五分鐘就走電話告警。閾值需要根據(jù)每個(gè) Topic 的業(yè)務(wù)量動(dòng)態(tài)調(diào)整不然要么天天誤報(bào)要么真出事故了還沒反應(yīng)。Kafka 可以通過消費(fèi)者組 Lag 來監(jiān)控RabbitMQ 可以看隊(duì)列 Ready 消息數(shù)RocketMQ 有專門的消費(fèi)進(jìn)度和堆積 Web 控制臺(tái)Pulsar 也有類似的指標(biāo)。無論哪種產(chǎn)品監(jiān)控?cái)?shù)據(jù)都要落到統(tǒng)一的監(jiān)控面板里別讓運(yùn)維或者開發(fā)去服務(wù)器上一條一條翻命令。5.2 分區(qū)規(guī)劃不是越多越好一個(gè)常見誤區(qū)是“分區(qū)數(shù)越多吞吐越高”。分區(qū)多了確實(shí)能增加并行度但每個(gè)分區(qū)在 Kafka 里對(duì)應(yīng)文件資源和內(nèi)存開銷也要參與 Leader 選舉。分區(qū)數(shù)太多會(huì)讓服務(wù)器的文件句柄飆升選舉時(shí)間變長反而影響穩(wěn)定性。關(guān)于分區(qū)數(shù)量業(yè)內(nèi)常用的經(jīng)驗(yàn)估算公式是目標(biāo)吞吐量除以單分區(qū)消費(fèi)能力再留出一定余量。假設(shè)單個(gè)分區(qū)每秒能穩(wěn)定消費(fèi) 500 條消息業(yè)務(wù)目標(biāo)每秒需要消費(fèi) 5000 條那至少需要 10 個(gè)分區(qū)考慮到突發(fā)流量和未來的增長再翻一倍設(shè)為 20 個(gè)比較穩(wěn)。分區(qū)數(shù)的調(diào)整成本也要提前算清楚。Kafka 支持新增分區(qū)但分區(qū)一旦增加key 與分區(qū)的映射關(guān)系可能變化原本保證的同一個(gè) key 進(jìn)同一個(gè)分區(qū)會(huì)被打破順序性可能受影響。所以盡量在業(yè)務(wù)上線前就把分區(qū)數(shù)規(guī)劃好上線后不到萬不得已不要改。5.3 消息隊(duì)列不適合什么場(chǎng)景最后想潑一點(diǎn)冷水。消息隊(duì)列雖然好用但有些場(chǎng)景真的別硬上。首先是強(qiáng)一致性的分布式事務(wù)。消息隊(duì)列本質(zhì)上是最終一致你不可能靠它實(shí)現(xiàn)多個(gè)數(shù)據(jù)庫的原子提交如果業(yè)務(wù)要求實(shí)時(shí)強(qiáng)一致應(yīng)該考慮本地消息表加事務(wù)性消息等更嚴(yán)謹(jǐn)?shù)姆桨付皇前严㈥?duì)列當(dāng)兜底。其次是請(qǐng)求-響應(yīng)這種需要同步返回值給調(diào)用方的場(chǎng)景。消息隊(duì)列是單向異步通信不適合需要立刻拿到業(yè)務(wù)結(jié)果的接口比如用戶登錄校驗(yàn)硬要繞一圈消息隊(duì)列只會(huì)增加無謂的延遲。另外消息隊(duì)列也不適合解決數(shù)據(jù)庫自身瓶頸比如某張表的慢查詢靠發(fā)消息并不能讓數(shù)據(jù)庫查詢變快最多只是把請(qǐng)求挪到了后臺(tái)改了表象沒改本質(zhì)。我覺得選型和使用消息隊(duì)列本質(zhì)上是在跟系統(tǒng)的復(fù)雜度做博弈。用好了它是架構(gòu)里的重要緩沖器能幫你扛住流量洪峰、讓團(tuán)隊(duì)協(xié)作更清爽用不好它就是系統(tǒng)里最隱蔽的雷平時(shí)看不出來關(guān)鍵時(shí)刻給你來一下大的。最后再分享一個(gè)我個(gè)人的習(xí)慣每次設(shè)計(jì)消息隊(duì)列方案我會(huì)先寫清楚“如果這條消息丟了會(huì)怎樣”再?zèng)Q定可靠性投資先寫清楚“如果這條消息重復(fù)了會(huì)怎樣”再?zèng)Q定冪等方案。把這兩件事想明白消息隊(duì)列就成功了一半。希望這篇產(chǎn)品介紹對(duì)你有點(diǎn)幫助也歡迎在實(shí)際使用中驗(yàn)證這些經(jīng)驗(yàn)再形成你自己的判斷。