1. 程式人生 > >RabbitMQ VS Apache Kafka (九)—— RabbitMQ叢集的分割槽容錯性與高可用性

RabbitMQ VS Apache Kafka (九)—— RabbitMQ叢集的分割槽容錯性與高可用性

本章,我們討論有關RabbitMQ的容錯性,訊息一致性及高可用性。RabbitMQ可以作為叢集節點來執行,因此RabbitMQ通常被歸為分散式訊息系統,對於分散式訊息系統,我們的關注點通常是一致性與可用性。

我們為什麼要討論分散式系統的一致性與可用性,本質在於兩者描述的是系統在失敗的情況下表現如何。在實際應用中,網路連線失敗、伺服器宕機,硬碟損壞,伺服器由於GC暫時不可用,網路連線丟失或速度慢,所有這些異常都會導致資料中斷、丟失或衝突等問題。事實證明,在所有的這些故障模式下,我們是無法同時兼顧最終一致性(無資料丟失,無資料差異)和可用性。系統一致性與可用性就像光的兩端,你必須選擇其中一種作為首要關注點。

本章,我們將深入討論什麼樣的配置會導致確認寫場景下的資料丟失。之前篇幅中我們有討論,在生產者、代理節點和消費者之間存在一個訊息傳遞的責任鏈關係,一旦訊息被傳遞到代理節點,那麼代理節點就要負責訊息的安全性。當代理節點發送確認訊息給生產者之後,我們期待的是訊息不再丟失,但事實上,即便生產者收到了訊息確認,訊息依然存在丟失的可能性,這依賴於代理與生產者的實際配置如何。

單節點持久化原語

持久化訊息佇列/交換器

RabbitMQ支援兩種型別的訊息佇列:持久化佇列和非持久化佇列,所有的佇列都是將訊息儲存到Mnesia資料庫中,區別在於在RabbitMQ服務節點啟動時,持久化佇列會重新宣告,因此當節點重啟、系統宕機或者系統異常失敗時,只要資料仍在,那麼佇列仍然存在。相反的,非持久化佇列和交換器在節點啟動時會被刪除。

持久化訊息

聲明瞭持久化佇列並不意味著當節點重啟時訊息仍舊可以正常儲存除非生產者將訊息宣告為持久化的。儘管持久化訊息會加重訊息代理負擔,但如果實際業務場景無法接受訊息丟失,那麼,持久化訊息也是不二之選。

在這裡插入圖片描述

服務叢集與佇列映象

為了避免單個訊息代理異常出現的訊息丟失,我們可以冗餘處理。我們可以在一個服務叢集中新增多個RabbitMQ節點,並通過跨多個服務節點複製佇列實現訊息冗餘。在這種架構下,即便出現單個節點失敗的情況也不會導致資料丟失的問題發生。

一個映象佇列包含以下內容:

  • 一個主佇列負責接收所有讀和寫

  • 一個或多個佇列映象,映象負責從主佇列中接收所有的訊息和元資料,映象並不是為了擴充套件訊息佇列的讀取效能,只是單純的資料冗餘而存在。
    在這裡插入圖片描述


    我們可以簡單的通過設定策略即可實現佇列映象。比如,可以選擇複製要素,甚至指定映象佇列所在節點:

  • ha-mode: all

  • ha-mode: exactly, ha-params: 2(one master and one mirror)

  • ha-mode: nodes, ha-params:[email protected], [email protected]

生產者確認

為了達到一致性寫的目的,我們需要生產者確認,否則可能會導致訊息丟失。一旦訊息被寫到磁碟中,訊息確認就會發送給生產者。注意,RabbitMQ並不是收到訊息就執行訊息寫操作,而是基於定時(每幾百毫秒之內)的訊息寫邏輯。對於映象佇列來說,只有所有的映象都完成了訊息寫入時,才會傳送確認訊息,這也就是說,使用生產者確認機制會導致訊息延遲,但如果業務場景關注的是訊息的安全性,那麼使用生產者確認也是非常必須的。

訊息佇列故障轉移

當某個訊息代理關閉或者宕機,位於當前節點上的所有主佇列都會隨之消失。接下來,叢集會從剩下的映象中選取新的佇列,並將其提升為主佇列。
在這裡插入圖片描述

訊息代理Broker 3宕機之後,Queue C在代理Broker 2上的映象被提升為了主佇列,同時在代理Broker 1建立了新的訊息映象,RabbitMQ會負責維護這種複製要素。

在這裡插入圖片描述
接下來,Broker 1宕機,我們只剩下Broker 2,Queue B映象被提升為主佇列。
在這裡插入圖片描述

我們重啟Broker 1,此時,無論當前節點上的資料是否得以恢復保留,所有的映象佇列訊息都將在節點啟動時丟棄。Broker 1作為叢集節點成員重新加入叢集,叢集本身也會根據之前設定的複製策略重新在Broker 1上建立對應的佇列映象。本例中,Broker 1上的訊息徹底丟失,因為佇列Queue D沒有映象佇列,因此,Queue D徹底丟失。
在這裡插入圖片描述
接下來,重啟Broker3,對應的Queue A 和Queue B的映象也會被建立,但,注意,此刻所有的主佇列都是在一個服務節點上的,這當然不是理想狀態,我們期待的是能將所有的主佇列平均分佈到所有節點上,但不幸的是,我們並沒有很好的方法來實現重新分佈主節點。關於這一點,我們在討論佇列同步時在討論這個問題。
在這裡插入圖片描述

同步

當新的佇列映象建立後,所有的新訊息都會被複制到映象中來。至於主佇列中的已有資料,我們可以選擇複製,這樣,新建映象就是主佇列的一個完全拷貝。也可以選擇不復制,這樣主佇列和新建映象隨著時間的流轉自動一致,因為新的訊息會不斷的加入到隊尾,而隊首已有的訊息則會被不斷處理移除。

佇列與映象間的訊息同步既可以是自動同步的,也可以通過主動觸發的,主要通過佇列策略來控制實現。

我們有兩個映象佇列,Queue A設定了訊息自動同步策略, Queue B設定的是主動觸發同步策略,兩個佇列均有10條訊息。
在這裡插入圖片描述
首先,我們讓Broker 3下線。
在這裡插入圖片描述
Broker 3重新上線,叢集會在新的節點上為每一個佇列重新建立一個訊息映象,對於Queue A映象來說,訊息自動同步。但Queue B映象就是空佇列。對於Queue A來說,自動同步帶來的是訊息全冗餘,而對於Queue B來說,我們僅有一個映象冗餘。
在這裡插入圖片描述

接下來,兩個訊息佇列都收到了另外10條訊息,此時,Broker 2下線,Queue A故障轉移到Broker 1上,訊息沒有出現丟失,而對於Queue B來說,主佇列有20條訊息,而映象卻只有10條訊息,並且永遠無法從主佇列複製到最初的10條訊息。
在這裡插入圖片描述

又有10條訊息到來,Broker 1下線,對於Queue A來說故障轉移並不會導致訊息丟失,但對於Queue B來說,就出現了訊息丟失的問題,因此,在這一點上,我們需要選擇可用性或者是一致性。

如果我們側重於可用性,那麼我們需要設定ha-promote-on-failurealways,事實上,我們根本需要設定策略,因為預設情況下,RabbitMQ叢集採取的就是這種策略。這種策略允許故障轉移的情況下未同步映象存在,可能存在訊息丟失的情況但保留了佇列的讀寫可用性。
在這裡插入圖片描述
也可以設定ha-promote-on-failurewhen-synced,這種策略將阻止主佇列在Borker 1節點的佇列進行故障轉移,一直等待Broker 1重新資料無損上線,在此期間,佇列將不可用。策略考慮了資料的安全性但犧牲了可用性。
在這裡插入圖片描述

這裡有一個問題:為什麼不採用自動同步策略?原因在於同步操作是阻塞操作,同步期間主佇列無法執行任何讀寫操作。

我們看一個簡單的例子,假設我們有一個非常大的佇列。至於佇列為什麼這麼大,可能有多個原因:

  • 消費者未能有效處理

  • 佇列容量本身較大,且消費者處理效率不高

  • 佇列容量本身較大,期間發生中斷,消費者正加緊處理中
    在這裡插入圖片描述
    假設,Broker 3下線
    在這裡插入圖片描述
    Broker 3重新上線,新的映象被建立,Queue A主佇列開始複製同步訊息到新建映象,在此期間,佇列將不可用,假設同步耗時需要兩個小時,那麼就會導致兩個小時的佇列下線。但對於Queue B來說,其通過犧牲了部分冗餘實現了可用性。
    在這裡插入圖片描述

滾動升級

同步期間的阻塞行為使得具有大容量佇列的叢集的滾動升級成為問題。比如,主佇列的宿主伺服器需要重啟,要麼叢集故障轉移到映象佇列上,要麼在升級期間佇列不可用。如果我們選擇故障轉移,可能我們會丟失訊息(映象未同步),預設情況,在Broker下線期間,叢集不會故障轉移到未同步映象(只剩一個映象的除外),這也意味著當代理節點重新上線後,我們並不會丟失任何訊息,唯一影響是佇列的下線時間,我們可以通過一定的策略ha-promote-on-shutdown來控制下線行為:

  • always: 允許故障轉移到未同步映象

  • when-synced: 只有同步映象存在才會執行故障轉移,否則佇列不可用,當代理重新上線之後,佇列重新可用。

策略ha-promote-on-failure=when-synced的問題

ha-promote-on-failure = when-synced 策略通過避免故障轉移至未同步映象而避免了資料丟失,但,如果主佇列宿主Broker丟失資料,那麼就會有大問題,訊息佇列就會丟失所有的資料,即便有映象已經基本和主佇列同步,那些映象也會被丟棄。所以,總結來講,使用這種策略非常危險,其存在的意義在於資料安全性但本質上是一把雙刃劍

主佇列的再平衡操作

之前我們有談到,當所有的主佇列都分佈在某個單一代理節點或者一組節點上會可能有問題。理想狀態是主佇列平均分佈在各個節點上。

但,對主佇列進行再平衡操作非常困難:

  • 無有效的適合工具

  • 佇列同步

有第三方外掛支援主佇列的再平衡操作,但外掛本身不受RabbitMQ官方支援,使用風險由自己承擔。這裡還有一個通過HA策略實現的移動主佇列的技巧,具體可參考:https://github.com/rabbitmq/support-tools/blob/master/scripts/rebalance-queue-masters

網路分割槽(網路中斷問題)

在分散式系統中,各個節點通過網路進行連線,說到網路,那麼必然免不了斷線,這依賴於實際內部架構或者所選雲的可靠性,對於分散式系統來說,必須要能處理網路斷線或者中斷問題,當然這裡又涉及到兩個問題的選擇:可用性與一致性。

對於RabbitMQ來說,我們主要有兩個選擇:

  • 允許split-brain:這種模式下,可用性有保證但可能會帶來資料丟失。

  • 不允許split-brain:可能帶來短暫的佇列不可用。

這裡解釋下split-brain的含義:因為網路連線的原因,叢集被一份為二,在每個分割槽中,都有映象被提升為主佇列,這也就意味著對於每一個佇列我們可能有多個主佇列存在。如果生產者將訊息成功寫入兩個主佇列中,那麼我們就會有兩個不同的佇列拷貝。
在這裡插入圖片描述
RabbitMQ提供了不同分割槽模式來處理split-brain場景,不同的模式側重點不同

Ignore Model:預設模式

這種模式強調的是可用性,當網路分割槽產生時,隨之帶來split-brain,當分割槽消失後,管理者必須決定啟用哪個分割槽,對應的另一個分割槽的節點則需要重啟,對應的所有資料則將丟失。
在這裡插入圖片描述
網路分割槽發生,Broker 3 從叢集中剝離,Broker 3無法探測到其他節點,將自己的映象佇列提升為主佇列
在這裡插入圖片描述

分割槽消除,但split-brain 仍舊存在,管理者必須通過選擇丟棄某個分割槽來主動消除split-brain的發生,下圖選擇了放棄Broker 3,在這種情況下,任何在Broker 3上的尚未被處理的訊息會隨著Broker 3的重新加入叢集而丟失。

在這裡插入圖片描述

在這裡插入圖片描述

Autoheal Mode

與Ignore Model一致,除了叢集會自動選擇哪個分割槽,未選擇分割槽會重新加入到叢集中,所有僅傳送到未選擇分割槽且未處理的訊息則被丟棄。

Pause Minority

如果我們不允許split-brain場景,我們可以在分割槽的少數側拒絕讀寫操作,當代理判斷出其位於分割槽的少數側,則執行暫停操作,關閉當前連線,並拒絕新的連線請求。然後,代理會每隔一秒中執行一次檢測,確認分割槽是否已經消除,一旦分割槽消除,那麼代理會自行啟動自己並重新加入到叢集中來。
在這裡插入圖片描述

網路分割槽導致Broker 3與Broker 1,2 分開,Broker 3並沒有將自己提升為主佇列,而是中止自己為不可用
在這裡插入圖片描述

分割槽消除,Broker 3重新加入到叢集中來

讓我們看下另一個例子,主佇列位於Broker 3上的場景。

在這裡插入圖片描述

同樣的網路分割槽發生,Broker 3上的佇列中止,主要側節點Broker 1 ,2上最早的佇列映象被提升為主佇列。
在這裡插入圖片描述

分割槽消除,Broker 3重新加入到叢集中來

在這裡插入圖片描述

客戶端連線保證

對於客戶端來說,我們可以有一些方式來設定客戶端連線到分割槽的主要一側,或者連線到那些存活的節點。注意,對於一個給定的佇列來說,其肯定位於某個指定的代理節點上,但交換器和策略則是跨節點複製的。客戶端可以連結到任意節點上,內部路由策略可以確保客戶端連線到正確的服務節點上。但當一個節點中止,它就會拒絕連線,因此客戶端也必須連線到其他節點上。當服務節點主動斷開客戶端連線,對於客戶端來說也是無可奈何的。

因此,對於客戶端來說,這裡有一些操作實踐:

  • 通過負載均衡連線叢集節點,當某個節點出現無法連線之後(網路中斷或者節點宕機),負載均衡器會一直嘗試可以正常連線的節點,直到獲取到正確的服務連線,並且連接獲取之後不會再去嘗試其他服務節點,這種策略對於短期網路中斷或者宕機節點迅速恢復的場景非常適用。

  • 通過負載均衡連線叢集節點,一旦檢測到有中止節點或者宕機節點,則立即將該節點從節點列表中刪除。如果我們可以迅速做出重試操作,那麼就可以獲取連貫的節點可用性。

  • 由每一個客戶端維護一個節點列表,客戶端隨機選擇連線目標,直至可以正常獲取連線。

  • 使用DNS避免訪問中止或者宕機節點,這需要一個小的TTL

結論

對於需要使用RabbitMQ叢集來說,以下兩個問題不容忽視:

  • 重新加入叢集的節點會丟棄之前的資料

  • 訊息同步是阻塞操作,從而引起一定時間內的佇列不可用。

當有以下場景時,我們不建議使用RabbitMQ叢集:

  • 網路狀態較差

  • 儲存不理想

  • 訊息佇列過大

考慮到RabbitMQ叢集的高可用性,我們可以考慮如下RabbitMQ設定:

  • ha-promote-on-failure=always

  • ha-sync-mode=manual

  • cluster_partition_handling=ignore or autoheal

  • 持久化訊息

  • 當叢集中某個節點宕機,確保客戶端可以正確的連線到剩餘存活節點

考慮到訊息一致性、可靠性,我們可以考慮如下設定:

  • 使用生產者確認或者消費者一側的主動確認

  • ha-promote-on-failure=when-synced,前提是訊息儲存可靠,並且生產者具備稍後重試的能力,否則的話,使用always選項

  • ha-sync-mode=automatic ,但對於非活躍的大容量佇列來說,建議考慮主動模式,且需要考慮不可用是否會導致訊息丟失

  • Pause Minority mode

  • 持久化訊息

原文連結
https://jack-vanlightly.com/blog/2018/8/31/rabbitmq-vs-kafka-part-5-fault-tolerance-and-high-availability-with-rabbitmq