System Design·2026年6月17日·12 分鐘閱讀

訊息佇列選型:Kafka 還是 RabbitMQ,我怎麼選

L
Louis Wu

Louis Wu,台灣資深後端工程師,主力 Go,做過交易所撮合、金流與高併發系統,專注於把架構取捨講清楚。

做後端做久了,會發現一個現象:每次團隊要導入訊息佇列,總有人第一句話就是「我們上 Kafka 吧」。彷彿 Kafka 是某種預設答案,是進步的象徵,是架構成熟的勳章。我做過交易所撮合、金流、後台這些對可靠度跟延遲都很敏感的系統,踩過的坑夠多,所以我想很誠實地講一句:Kafka 不是預設答案,RabbitMQ 也不是。它們解決的根本就不是同一類問題。

選型選錯,不會馬上爆。它會在半年後、在某個你以為穩定的環境裡,用一種你最不想面對的方式爆給你看——可能是訊息順序錯亂導致對帳對不平,可能是消費延遲累積到幾十萬條沒人發現,也可能是某天運維半夜被叫起來重啟一個你根本不需要的 ZooKeeper。

這篇我想把這兩個東西的本質差異講清楚,然後給出我自己在實戰裡用的判斷原則。不是教科書式的功能對照表,那種網路上一搜一大把。我要講的是取捨,是那些只有踩過才知道的事。

先講本質:它們根本是兩種東西

很多人把 Kafka 跟 RabbitMQ 放在一起比,這個前提本身就有點問題。它們長得像(都能傳訊息、都有 producer 跟 consumer),但骨子裡是兩種不同的設計哲學。

RabbitMQ 是一個傳統意義上的訊息 broker。 它的核心模型是:訊息進來,經過 exchange 根據 routing key 決定丟到哪些 queue,consumer 從 queue 把訊息拿走、處理、然後 ack。訊息一旦被 ack,broker 就把它刪掉了。它的世界觀是「投遞」——我有一個任務,我要確保它被某個 worker 拿去做完。

Kafka 本質上是一個分散式的 commit log。 它不是「把訊息投遞給你然後刪掉」,而是「把訊息按順序寫進一個持久化的 log,你愛從哪裡讀就從哪裡讀」。每個 partition 就是一個 append-only 的有序日誌,consumer 自己記錄讀到哪了(offset)。訊息被消費了不會消失,它就躺在那,直到 retention 時間到了才清掉。它的世界觀是「事件流」——這裡有一條持續發生的事件河流,你可以重複讀、可以回放、可以多組人各自從不同位置讀。

這個本質差異,決定了後面所有的取捨。我給你一個最直觀的判斷:

  • 如果你腦中想的是「我要派一個任務給某個 worker 去做」——那是 RabbitMQ 的場景。
  • 如果你腦中想的是「我這裡有一條事件流,可能有很多人要消費,而且我可能要回放歷史」——那是 Kafka 的場景。

吞吐量:Kafka 的看家本領,但別被數字騙了

Kafka 最常被拿出來講的優點就是吞吐量。確實,Kafka 單機隨便就能上每秒幾十萬條訊息,這是它的設計目標——它用順序寫磁碟(順序寫其實非常快,甚至比隨機寫記憶體還有競爭力)、零拷貝、批次傳輸這些手段,把吞吐做到極致。

但我要潑個冷水:你真的有那個吞吐需求嗎?

我看過太多團隊,日訊息量可能就幾百萬條,平均下來每秒幾十條,結果上了 Kafka,然後為了「Kafka 該有的樣子」配了三個 broker、一套 ZooKeeper(或現在的 KRaft)、一堆監控。為了一個每秒幾十條的需求,養了一隻每秒幾十萬條的怪獸。這不是技術選型,這是技術自我感動。

RabbitMQ 的單佇列吞吐確實比 Kafka 低,一般單佇列大概落在每秒一兩萬條這個量級(看訊息大小跟 ack 模式),但說真的,每秒一兩萬條對絕大多數業務系統來說,已經是綽綽有餘。我做過的某個金流系統,尖峰期交易量也就每秒幾百筆,RabbitMQ 連喘都不喘。

所以我的原則是:吞吐量是個門檻,不是個加分項。 你先估算你真實的尖峰吞吐(不是老闆畫的大餅,是真實數據乘上一個合理的成長係數),如果 RabbitMQ 的單佇列吞吐能輕鬆覆蓋,那吞吐量這一項在你的選型裡就根本不該有權重。把它從天平上拿掉。

路由彈性:RabbitMQ 真正的殺手鐧

如果說吞吐是 Kafka 的主場,那路由彈性就是 RabbitMQ 把 Kafka 按在地上摩擦的地方。

RabbitMQ 的 exchange 機制非常靈活。你有 direct exchange(按 routing key 精確匹配)、topic exchange(按 pattern 匹配,支援萬用字元)、fanout exchange(廣播給所有綁定的 queue)、headers exchange(按 header 匹配)。這套東西讓你可以在 broker 層面就把訊息的分發邏輯處理得很細。

舉個我實際做過的例子。後台有個通知系統,要根據事件類型、用戶等級、地區去決定發給哪些下游服務。用 RabbitMQ 的 topic exchange,我可以讓 routing key 長成「notification.vip.tw.order」這種形式,然後不同的 consumer 用不同的 binding pattern 去訂閱——做高級用戶處理的綁「notification.vip.#」,做台灣區的綁「notification.#.tw.#」。這套分發邏輯完全在 broker 裡,業務代碼很乾淨。

同樣的事情在 Kafka 要怎麼做?Kafka 沒有這種 broker 層的路由。它只有 topic 跟 partition,partition 是用來做平行跟順序的,不是用來做內容路由的。你要嘛開一堆 topic(每種組合一個 topic,很快就失控),要嘛全部丟進一個 topic 然後讓每個 consumer 自己讀進來、自己過濾掉不要的——這等於把過濾成本從 broker 推給了每個 consumer,而且白白浪費網路頻寬傳了一堆人家不要的訊息。

所以當你的需求是「複雜的、基於內容的訊息分發」,RabbitMQ 幾乎是不用想的選擇。Kafka 在這種場景下會逼你把路由邏輯散落到各個 consumer,那是一種架構上的退化。

訊息順序:這題比你想的微妙

順序這件事,是金流跟交易系統的命根子。我先講結論再講細節:Kafka 在 partition 內保證順序,RabbitMQ 在單一 queue 單一 consumer 的情況下保證順序。兩者都不是「全域順序」,而你大概率也不需要全域順序。

Kafka 的順序保證是 partition 級別的。同一個 partition 裡的訊息嚴格有序,但不同 partition 之間沒有任何順序保證。這意味著什麼?意味著你必須謹慎選擇 partition key。

我在交易系統裡的做法是:用帳戶 ID(或交易對)當 partition key。這樣同一個帳戶的所有操作一定落在同一個 partition,順序就有保證。不同帳戶之間誰先誰後無所謂,反正它們本來就是獨立的。這是 Kafka 在金流場景能用的關鍵——你要找到一個天然的分區維度,讓「需要保證順序的訊息」恰好都落在同一個 partition。

這裡有個真實的坑:一旦你按帳戶分區,某些熱門帳戶就會變成熱點 partition。 我遇過一個做市商帳戶,它一個人的交易量抵得上其他幾千個帳戶加起來,結果它那個 partition 永遠在排隊,其他 partition 都閒著。partition 數量還不能隨便加——加了之後 partition key 的雜湊分佈會變,歷史訊息的順序保證在重分區那一刻會被打破。這是設計時就要想清楚的事,不是出事了再來救。

RabbitMQ 這邊,單一 queue 本身是 FIFO 的,但只要你掛了多個 consumer 做平行消費,順序立刻就沒了——因為兩個 consumer 處理速度不一樣,先發的訊息不一定先處理完。所以如果你既要順序又要 RabbitMQ,你要嘛單 consumer(犧牲吞吐),要嘛用 consistent hash exchange 之類的方案把同一類訊息固定路由到同一個 queue(這其實就是在模仿 Kafka 的 partition 概念了)。

我的原則:順序需求一定要先界定清楚「以什麼為單位的順序」。 是全域順序?幾乎不需要,需要的話你的系統設計可能有問題。是「同一帳戶內的順序」「同一訂單內的順序」?那就找到那個單位,用它當分區/路由的維度。把順序的粒度想清楚,選型自然就明朗了。

消費者群組與擴展模型

這塊兩者的模型差很多,也是很多人沒搞清楚就踩坑的地方。

Kafka 的 consumer group 是這樣:一個 group 裡的多個 consumer 會去瓜分 partition。如果你有 10 個 partition、5 個 consumer,那平均一個 consumer 吃 2 個 partition。這裡有個硬限制:一個 partition 在同一個 group 裡只能被一個 consumer 消費。 所以你的消費平行度上限就是 partition 數量。你 partition 開 10 個,那同一個 group 裡開第 11 個 consumer 純粹是浪費,它會閒置。

這個模型的好處是:多個不同的 group 可以各自獨立消費同一個 topic,互不干擾。做即時計算的一個 group、做數據入庫的一個 group、做監控告警的一個 group,大家各讀各的 offset。這就是 Kafka 「一份數據多方消費」的威力,在事件流架構裡這點非常關鍵。

RabbitMQ 的擴展就單純得多:多個 consumer 掛在同一個 queue 上,broker 用 round-robin(搭配 prefetch 控制)把訊息分給它們。要加平行度,加 consumer 就好,沒有 partition 數量這種硬上限。這在「就是要把一堆任務盡快做完」的任務佇列場景裡非常順手——你的 worker 想開幾個開幾個。

但如果你要的是「同一份訊息給多個不同的下游各自消費一遍」,RabbitMQ 就要靠 fanout exchange 把訊息複製到多個 queue,每個下游一個 queue。這能做,但你管理的 queue 數量會隨下游數量增長,而 Kafka 那邊只是多開一個 consumer group 而已,topic 還是那一個。

簡單說:要做任務分配(work queue),RabbitMQ 的模型更自然;要做事件廣播(一份數據餵多個獨立消費方),Kafka 的模型更自然。

重複消費與冪等:這是你逃不掉的功課

我想特別講這一段,因為這是最多人栽跟頭、而且兩個 MQ 都救不了你的地方。

先把一個幻想打破:在分散式系統裡,沒有真正的 exactly-once 投遞。 你能拿到的,現實裡基本上是 at-least-once(至少一次,可能重複)或 at-most-once(至多一次,可能丟失)。Kafka 號稱有 exactly-once semantics,但那是在它自己的 Kafka-to-Kafka 串流場景下、配合事務才成立的,一旦你的消費端要寫進外部資料庫、呼叫外部 API,這個保證就斷在邊界上了。

所以實務上你必須假設訊息會重複,然後把消費端做成冪等的。這不是可選項,是必修課。

重複是怎麼來的?最常見的場景:consumer 處理完訊息了,業務也寫進資料庫了,結果在 ack(RabbitMQ)或 commit offset(Kafka)之前,這個 consumer 掛了。MQ 沒收到確認,就認為這條訊息沒被處理,於是重新投遞給另一個 consumer。同一條訊息就被處理了兩次。在金流場景,這可能意味著一筆錢被扣了兩次——這是會出大事的。

我的做法,講幾個實戰過的:

  • 業務唯一鍵 + 資料庫唯一約束。 每筆交易帶一個業務上唯一的 ID(不是 MQ 給的 message id,是業務自己生成的,比如冪等鍵)。寫入時用這個 ID 做唯一索引,重複寫入直接撞約束失敗,你就知道這條處理過了,安全跳過。這是最可靠的一招,因為它把冪等的保證下沉到了資料庫這個唯一可信的真相來源。
  • 狀態機。 訂單狀態從「待支付」到「已支付」,這個轉換本身就是冪等的——已經是「已支付」了,再來一條支付成功的訊息,狀態機發現狀態不對(不能從已支付再到已支付),直接忽略。讓狀態轉換的合法性檢查幫你擋掉重複。
  • 不要依賴 MQ 的 message id 去做去重。 因為重發的訊息在某些情況下 message id 會變,而且你的去重邏輯如果只靠一張 Redis 去重表,那張表本身的可靠性、過期策略又是新的問題。能用資料庫唯一約束就用資料庫,少引入一個會出錯的環節。

這一段的原則很簡單:消費端冪等是你的責任,不是 MQ 的責任。 選 Kafka 還是 RabbitMQ 都不會幫你免掉這個功課。把冪等做好了,at-least-once 就夠用了,你根本不用去追求那個虛幻的 exactly-once。

運維成本:那個沒人在選型會議上講的真相

選型會議上大家都在比功能、比吞吐、比 benchmark。沒人講運維成本,但這玩意兒是要跟你過日子的,半年一年之後它才會顯出真面目。

RabbitMQ 的運維相對輕。 它就是一個 Erlang 寫的 broker,裝起來、跑起來都簡單,單機或小集群都好搞,management plugin 那個網頁後台清清楚楚,你能直接看到每個 queue 堆積多少、消費速率多少。它的痛點主要在集群——鏡像佇列(或新版的 quorum queue)在網路分區時的行為要懂,腦裂的處理要小心,但只要你不是搞超大規模,這些都還在可控範圍。

Kafka 的運維是另一個重量級。 傳統上它依賴 ZooKeeper(雖然新版 KRaft 把 ZooKeeper 拿掉了,但你的存量集群很可能還在用),partition 的 rebalance、replica 的同步(ISR)、磁碟空間管理(log retention 配不好磁碟會爆)、consumer lag 的監控——這些都是要專人盯的。我看過 consumer lag 累積到幾百萬條沒人發現,因為沒人配告警,等到下游發現數據延遲了好幾個小時才回頭查。Kafka 跑得好的時候非常猛,但它要跑得好,背後是有運維成本墊著的。

所以我的真心話:如果你的團隊沒有專門的人或足夠的經驗去養 Kafka,那 Kafka 的高吞吐對你來說是一張你付不起的支票。 帳面上看它能扛每秒幾十萬,但你的團隊扛不住它出問題時的排查複雜度。RabbitMQ 在這點上對小團隊友善太多了。

什麼時候其實用不到 Kafka

講了這麼多,我把我的判斷收斂成幾條。下面這些情況,老老實實用 RabbitMQ,別碰 Kafka:

  • 你要的是任務佇列。 把工作丟給後台 worker 去做——發信、生成報表、處理上傳的圖片、異步扣款。這是經典的 work queue,RabbitMQ 的 per-message ack、prefetch、優先級佇列、死信佇列這套東西就是為這個生的。
  • 你的吞吐量根本不高。 每秒幾百幾千條,RabbitMQ 連暖機都不用。為這個量上 Kafka 是殺雞用牛刀,而且這把牛刀還要你天天磨。
  • 你需要複雜的路由。 基於內容、基於屬性的訊息分發,topic/headers exchange 是 RabbitMQ 的主場,Kafka 在這裡會逼你做架構妥協。
  • 你不需要回放歷史訊息。 訊息處理完就完了,不需要讓新的消費方從頭重讀。那 Kafka 那個「持久化 log、可重播」的核心賣點對你就是零價值,你只是在為用不到的能力付運維稅。
  • 你的團隊小,沒有 Kafka 運維經驗。 這條我放最後但它權重最高。技術選型要選團隊能駕馭的,不是選最潮的。

反過來,這些情況 Kafka 才是對的:

  • 你在做事件溯源、事件驅動架構。 事件本身就是事實,要持久化、要能被多方消費、可能要回放重建狀態。這是 Kafka 的本命。
  • 你有真實的高吞吐串流。 日誌收集、用戶行為埋點、IoT 數據、即時指標——這種源源不絕的大流量數據流,Kafka 就是為這個而生的。
  • 一份數據要餵給很多獨立的下游。 即時計算、數倉入庫、推薦系統、監控,各自一個 consumer group 從同一個 topic 讀,互不干擾。
  • 你需要重播能力。 下游算錯了要重算,新上線一個服務要從頭消費歷史數據——Kafka 的 offset 機制讓這件事很自然。

一個現實裡常見的答案:兩個都要

最後講個可能反直覺的:成熟的系統裡,這兩個常常是並存的,因為它們解決不同的問題。

我做過的一個比較完整的架構是這樣的:核心交易產生的事件(成交、撮合結果)寫進 Kafka,因為這是典型的事件流——風控要消費、清算要消費、行情推送要消費、數據分析要消費,一份數據多方各取所需,而且出問題要能回放重算。但同一個系統裡,那些「派任務」性質的工作——發送交易通知簡訊、生成對帳單 PDF、異步通知第三方——走 RabbitMQ,因為這就是任務佇列,要的是可靠投遞跟靈活路由,不需要 Kafka 那套重型機制。

這不是貪心,這是各司其職。把 Kafka 當事件流的骨幹,把 RabbitMQ 當任務調度的工具。硬要用一個去做另一個的事,最後不是架構彆扭就是運維痛苦。

我的選型原則,濃縮成幾句話

寫到這,我把心法收一收:

  • 先問你解決的是「投遞任務」還是「處理事件流」。 這個問題答對了,選型就對了一半。
  • 吞吐量是門檻不是加分。 估你的真實尖峰,RabbitMQ 能覆蓋就別把吞吐放進天平。
  • 順序需求要界定到具體粒度。 找到那個天然的分區/路由維度,別想著要全域順序。
  • 冪等是你的功課,跟選哪個 MQ 無關。 用業務唯一鍵加資料庫約束,別追求 exactly-once 幻覺。
  • 運維成本要算進總帳。 你的團隊養不養得起 Kafka,這比 benchmark 數字重要太多。
  • 不確定的時候,從 RabbitMQ 開始。 它更簡單、更靈活、對小團隊更友善。等你真的撞到吞吐天花板或真的需要事件回放,再上 Kafka 也不遲——而那時候你對自己的需求已經清楚多了。

技術選型最大的敵人從來不是選錯工具,而是還沒搞清楚自己的問題就先愛上某個工具。Kafka 很強,RabbitMQ 很實用,但它們都只是工具。先把問題想清楚,工具自然會選你。

#Kafka#RabbitMQ#Message Queue#System Design#Event-Driven#Idempotency

相關文章