RabbitMQ和Kafka都提供持久的消息保證。二者都提供至少一次和至多一次的保證,另外,Kafka在某些限定狀況下能夠提供精確的一次(exactly-once)保證。數據庫
讓咱們首先理解一下上述術語的含義:緩存
至多一次投遞:消息絕對不會被重複投遞,可是消息可能丟失安全
至少一次投遞:消息絕對不會被丟失,可是有可能重複被消費服務器
精確的一次投遞:消息系統的聖盃。全部的消息精確的被投遞一次。網絡
「投遞」貌似不是準確的語言描述,「處理」纔是。不管怎麼描述,咱們關心的是,消費者可否處理消息,以及處理的次數。可是使用「處理」會使問題變得複雜。好比說,消息必須投遞兩次才能被處理一次。再好比,若是消費者在處理的過程當中宕機,消息必須被第二次投遞(給另外一個消費者)。架構
其次,使用「處理」來表達會使得部分失敗(partial failure)變得頭疼。處理消息通常包括多個步驟。處理的開始到結束包括應用的邏輯以及應用與消息系統的通訊。應用邏輯的部分失敗由應用來處理。若是應用處理的邏輯是事務的,結果是all or nothing, 那麼應用邏輯能夠避免部分失敗。可是實際上,多個步驟每每涉及不一樣的系統,使得事務性變得不可能。若是咱們考慮到通訊,應用,緩存,數據庫,咱們沒法達到精確的一次處理(exactly-once processing).ide
因此,精確地一次只出如今以下狀況中:消息的處理只包括消息系統自己,而且消息系統自己的處理是事務的。在該限定場景下,咱們能夠處理消息,寫消息,發送消息被處理的ACK, 一切都在事務中。而這正是Kafka流能提供的。性能
可是,若是消息處理是冪等(idempotent)的,咱們就能夠繞過基於事務的精確一次保證。若是消息處理是冪等的,咱們能夠安全的處理重複的消息。固然,並非全部的消息處理都是冪等的。設計
責任鏈日誌
本質上講,生產者不能知道消息是否被消費。他們能知道的是,消息系統是否接收了消息,是否把消息安全的存儲起來以便投遞。這裏存在一條責任鏈,開始於生產者,移動到消息系統,最後到達消費者。每一個環節都要正確執行,環節間的交接也要正確執行。這意味着,做爲一個應用開發者,你要正確的寫程序,防止丟失消息,或者濫用消息。
消息順序
這篇文章主要關注RabbitMQ和Kafka如何提供至少一次和至多一次的投遞。可是,也包括消息的順序。簡單來說,二者都支持FIFO順序。RabbitMQ在隊列這個層次,Kafka在話題的分區層次。
RabbitMQ
投遞保證依賴於:
消息的持久性——一旦存儲下來,就不會丟失
消息的ACK——RabbitMQ與生產者、消費者之間的信號
隊列鏡像
隊列能夠在節點間被鏡像(複製)。對於每一個隊列,存在一個主隊列,在單獨一個節點上。假設咱們有3個節點,10 個隊列,每一個隊列2個鏡像。那麼10個主隊列和20個鏡像將分佈在3個節點間。主隊列如何分佈是能夠被配置的。當一個節點宕機後,
在宕機的節點上的每個主隊列,在另外一個節點上的鏡像隊列會被提高爲主隊列
在其餘節點上的鏡像隊列會被建立出來,以代替宕機的節點上的鏡像隊列,從而維護複製因子(replication factor)
持久的隊列
RabbitMQ有兩種隊列:持久的和非持久的。持久的隊列會被存儲在磁盤上,節點重啓後會從新構建出來。
持久的消息
持久的隊列不能保證消息能夠在宕機時被保留下來。只有被設定爲持久的消息纔會在宕機重啓後恢復。
對於RabbitMQ,越多的消息是持久的,隊列的吞吐率就越差。所以若是你有實時流,並且輕微的丟數據不會有大問題,那麼你不應考慮隊列鏡像,而且消息應該設定爲非持久的。然而,若是你必須不能在節點宕機時丟失數據,那麼應該使用隊列表鏡像,持久的隊列和持久的消息。
消息的ACK
消息發佈
消息發佈時,可能會被丟失或重複。這取決於生產者的行爲。
Fire and Forget 發佈者能夠選擇不使用生產者ACK,簡單的發動消息棄之不顧。消息不會被複制,可是可能被丟失(至多一次投遞)
發佈確認:當發佈者與中間人(broker)創建頻道後,能夠 設置該頻道使用確認消息。則中間人會回覆發佈者的消息以下:
basic.ack:正ACK.消息已經收到,如今消息在RabbitMQ這邊了。
basic.nack:負ACK.發生錯誤,消息未被處理。責任還在發佈者。發佈者可能須要重發。
除了以上兩種,還有一種回覆basic.return。有時發佈者不只須要知道中間人收到了消息,並且須要知道消息已經在若干隊列中持久化了。好比,有時發佈者發佈了一條消息給交換機,可是交換機上沒有綁定任何匹配的隊列,那麼中間人會簡單的丟棄消息。大多數狀況下,這沒有問題,可是有時,發佈者須要知道消息是被丟棄了仍是被處理了。能夠對每一個消息設定mandatory標記,如此一來,若是消息沒有被處理而是被丟棄,那麼會返回一個basic.return
發佈者能夠選擇發送每一條消息都等待ACK,可是會嚴重影響吞吐率。因此,發佈者通常發佈消息流,可是會限制未ACK的消息的數目。一旦達到了message in flight 的數目限制,發佈者會暫停,等待ACK的到來。
如今,咱們有了多條在途中的消息(在發佈者與RabbitMQ之間),爲了提升吞吐率,RabbitMQ使用multiple標記位來將ACK組成一組。如此一來,全部的消息會被分配一個單調遞增的序列號(Sequence Number)。消息的ACK中會包含對應的序列號。當組合使用Multiple標記位時,發佈者須要維護髮送出去消息的序列號,以便它知道哪些消息被ACK。
因此,利用ACK,咱們能夠經過如下方法避免消息丟失:
當收到nack,從新發布消息。
當收到nack或者basic.return,將消息持久化到某地。
事務:在RabbitMQ中,並不經常使用事務。由於
不明確的保證:若是消息被路由到多個隊列,或者起用了mandatory標記,那麼事務的原子性是不可靠的。
性能比較差。
坦率的講,我從未使用過事務,它增長了額外的保證,提升了不肯定性。
鏈接/頻道異常:除了消息的ACK外,發佈者還須要考慮鏈接斷開或者中間人出錯,二者都會致使頻道丟失。頻道丟失會致使沒法接收消息的ACK.在這個問題上,發佈者能夠考慮妥協,一種是冒消息丟失的風險一種是冒消息重複的風險。
若是中間人宕機,可能此時消息還在OS的buffer中,或者正在被解析,所以被丟失。又或者,這條消息已經持久化,正當中間人發送ACK時,宕機了,在這種狀況下,其實消息已經成功投遞了。
鏈接斷開一樣如此。咱們沒法得知宕機的具體時機,因此只能選擇:
不從新發布,冒消息丟失的風險
從新發布,冒消息重複的風險
若是發佈者有不少在途的消息,問題會惡化。一種方式是發佈者提供提示,告訴消費者消息是重發的,讓消費者嘗試去重。
消費者
對於ACK,消費者有兩種選擇
無ACK模式
手動ACK模式。
無ACK模式:或者稱爲自動ACK模式,是危險的。首先,只要消息投遞給應用層,就會被從隊列中刪除。這會致使消息丟失:
消息還在內部buffer中,可是應用層宕機
消息處理失敗
其次,咱們沒法控制消息傳遞的速度。使用手動ACK,咱們能夠設定預取(QoS)值,來限制應用得到的未ACK的消息的數目。若是沒有這個功能,RabbitMQ會很快的傳遞消息,超出消費者能夠處理的訥訥管理,致使內部buffer溢出或內存問題。
手動ACK模式:消費者必須手動給出消息的ACK.消費者能夠設定預取值大於一,即可以並行的處理多條數據。消費者能夠選擇單條消息的發送ACK,也能夠設定multiple標記位,一次ACK多條消息。批處理會提升性能。
當消費者打開一個頻道,被投遞的消息會收到一個單調上升的整數值Delivery Tag。這個信息會包括在ACK當中做爲消息的標識。
ACK有以下幾種:
basic.ack.RabbitMQ會從隊列中刪除該條消息。可使用multiple標記。
basic.nack。消費者須要告訴RabbitMQ是否須要從新將消息壓入隊列。重入隊列意味着消息會被放在隊列頭,再次投遞給消費者。也支持multiple標誌位。
basic.reject.與basic.nack相似,可是不支持multiple標記位。
因此從語義上級講,basic.ack與(basic.nack&requeue==false)是等價的。都會致使消息從隊列中刪除。
下一個問題是,何時發送ACK?若是消息處理很快,能夠選擇消息處理完再發送ACK.可是,若是消息處理須要幾分鐘,那麼處理完再發送ACK是有問題的。若是頻道宕機,全部未ACK的消息會重入隊列,致使消息重複。
通訊/頻道 故障
若是通訊故障,或者中間人故障致使頻道宕機,那麼全部的未ACK的消息都會從新入隊列再次投遞,這不會致使消息丟失,可是會致使消息重複。
消費者保持未ACK的消息越久,消息被從新投遞的風險越高。當消息是被重投遞時,消息會設置redelivered標誌位。因此最壞狀況下,至少消費者是能夠知道消息是一條重發的消息。
冪等性
若是你須要冪等而且保證消息不會丟失,那麼意味着你須要實現消息去重或其餘冪等模式。若是消息去重很是耗時,那麼你可讓發佈者對重發的消息添加頭數據,讓消費者檢查頭數據和redelivered 標誌位。
結論
RabbitMQ提供提供強大的,可靠地,持久的消息保證,可是,你有不少辦法把它弄糟。
如下是一些注意事項
若是想要保證至少一次投遞,使用隊列鏡像,持久的隊列,持久的消息,發佈者ACK,mandatory標誌位,手動消費者ACK;
使用最少一次投遞,你或許須要增長去重邏輯或者使用冪等範式
若是你不關心消息丟失,而更關注低延時和高度可擴展,那麼你不須要使用隊列鏡像,持久的消息和發佈者ACK.固然,我本身會保留使用手動消費者ACK,經過設定預取2值來控制消息投遞的速度,固然,你須要設定multiple標誌位並批量ACK.
Kafka
Kafka的投遞經過以下保證:
消息持久性:一旦存入話題,消息不會丟失
消息ACK:kafka(或者包括Zookeeper)與生產者、消費者信號
關於批處理
Kaka和RabbitMQ有在消息批量發送、消費方面不一樣。RabbitMQ能夠實現以下:
每發送x條消息就暫停,直到全部消息的ACK被收到。RabbitMQ一般將多條ACK組成一組,使用multiple標誌位
消費者設定一個預取值,將消息的ACK組成一組
可是消息自己不是批量發送的,它更多的是指容許一組消息在途,使用multiple 標誌位。這一點跟TCP很像。
而Kafka則有明確的消息批量處理。批處理能夠提升性能,同時也須要權衡,正如RabbitMQ權衡在途的未ACK消息同樣。越多的在途消息,會致使越嚴重的消息重複(當故障發生時)。
Kafka能夠更高效的在消費者端進行批處理,由於kafka有分區的概念。每一個分區對應一個消費者,因此及時一個很大的批處理也不會營子昂負載的分佈。然而,對於RabbitMQ而言,若是使用已經被廢棄的拉取API拉取批量的消息,會致使很是嚴重的負載不均衡。以及很長的處理延時。RabbitMQ在設計時就不適合批處理。
持久性
日誌複製
爲了容錯,Kafka在分區層面有一個主從架構,主分區成爲master,複製分區成爲slave或者follower.每一個master能夠有不少follower.當主分區的服務器宕機後,follower中會有一份被提高爲主分區,因此只會致使短暫的服務中止,可是不會致使數據丟失。
Kafka有一個概念,叫作In Sync Replicas(同步的複製)。每個複製均可以是同步的,或是非同步的。同步意味着跟主分區相比,擁有相同的消息。複製可能會變成非同步的,若是它落後了。這多是由於網絡延遲,宿主機故障等。消息丟失只會發生在以下狀況:主分區服務器宕機,全部的複製都是非同步的。
消息ACK與偏移追蹤
取決於Kafka如何存儲消息以及消費者如何消費消息,Kafka依賴於消息ACK來進行偏移追蹤。
生產者的消息ACK
當生產者發送消息時,會告訴中間人何種期待ACK:
不須要ACK:fire and forget, 對應於acks = 0
主分區已經將消息持久化。 對應於acks=1
主分區以及全部同步的複製都將消息持久化, 對應於acks=ALL
消息能夠在發佈時被複制,正如RabbitMQ同樣。若是中間人宕機或者網絡故障,發佈者會把沒有收到ACK的消息重發。固然,大多數狀況下,消息應該是被主分區持久化並複製了。
然而,Kafka有一個很好的去重的特性,可是必須以下設置:
enable.idempotence 設置成true
max.in.flight.requests.per.connection 低於5
retries設置1或更高
acks設置成ALL
在這種配置下,若是你爲了吞吐率,批處理的單位設置成6或者acks設置成0/1,那麼你就沒辦法得到去重。
消費者偏移追蹤
消費者須要存儲他們的偏移以備宕機,讓另外一個消費者接替。偏移存儲在zookeeper上或者kafka的話題中。
一旦消費者從分區中讀取一批量的消息,它有多種選擇去更新偏移:
當即更新:在開始處理消息前。這對應於最多一次投遞。不管消費者是否宕機,消息都不會被重複。好比10條正在被處理,此時消費者在第五條消息處理時宕機,那麼只有前4條消息被處理,其他被跳過,接替的消費者從下一個批次開始。
最後更新。當全部消息都被處理後。這對應於至少一次投遞。不管消費者是否宕機,沒有消息會被丟失,儘管消息會被處理兩次。好比10條消息正在被處理,當消費者在消費第五條消息時宕機,則整個10條消息會被接替的消費者再次處理。
精確地一次語義只有在使用Java Library Kafka Stream時被保證。若是你使用Java,我強烈推薦使用。精確一次語義的只要問題在於消息的處理和偏移的更新須要哎事務中完成。例如,若是消息處理是發送一條郵件的話,那麼咱們就沒法完成精確的一次。例如咱們發送玩郵件後,消費者宕機,咱們能夠更新偏移,可是會致使郵件再次被髮送。
Kafka Stream 的Java 應用,將消息處理後生成新的消息不一樣的話題,那麼這個應用將是知足精確一次語義的。由於咱們可使用Kafka的事務功能與寫消息並更新偏移。
關於事務和隔離層次
Kafka中事務的應用主要是讀-處理-寫模式。事務能夠跨越多個話題和分區。一個生產者打開一個事務,寫一個批量的消息,而後提交事務。
當消費者使用默認的read uncommited 隔離級別時,消費者能夠看到全部的消息,不管是提交的,未提交的,仍是終止的。當消費者使用read committed隔離級別時,消費者不會看到未提交的或者終止的消息。
你可能比較疑惑,隔離級別如何影響消息順序。答案是,不影響。消費者依舊按序讀取消息。Last Stable Offset(LSO)以前的消息都會被讀取。
總結
RabbitMQ和Kafka都提供可靠的,持久的消息系統,因此若是可靠性對你來講很重要,那麼你大可放心,二者都是可靠的。當時,Kafka更勝一籌,由於提供冪等的發佈,而且,及時錯誤的操做偏移,消息也不會丟失。
顯然,沒有十全十美的產品,可是隻要應用正確的使用ACK,管理員正確的配置複製,而且你的數據中心沒有轟然倒塌,你就能夠放心,消息不會丟失。至於容錯和可用性,也須要另外討論。
下面是一些簡單結論:
二者都提供至多一次和至少一次語義
二者都提供複製
二者對消息重複和吞吐率有相同的取捨。儘管kafka提供冪等的發佈,可是僅限於必定的體量。
二者均可以控制在途的未ACK消息數量
二者都保證順序
Kafka提供真正的事務操做,主要用於讀-處理-寫。儘管你須要注意吞吐率。
使用Kafka,及時消費者錯誤處理,可是可使用偏移進行回退。RabbitMQ則不行。
Kafka基於分區的概念,可使用批處理提升性能。而RabbitMQ不適合批處理,由於它基於推送模型,而且使用競爭的消費者。