容錯和高可用性是重要主題,因此我們將專門撰寫文章來介紹 RabbitMQ 和 Kafka。 這篇文章是關於RabbitMQ的,下一篇是關於Kafka的,與RabbitMQ比較。 這是一篇很長的文章,所以請放心。
讓我們看看容錯、一致性和高可用性 (HA) 策略以及每種策略所做的權衡。 RabbitMQ 可以在節點叢集上運作 - 因此被歸類為分散式系統。 說到分散式系統,我們經常談論一致性和可用性。
這些概念描述了系統發生故障時的行為。 網路連線故障、伺服器故障、硬碟故障、因垃圾收集、資料包遺失或網路連線速度減慢而導致伺服器暫時無法使用。 所有這些都可能導致資料遺失或衝突。 事實證明,建立一個對於所有故障場景都完全一致(無資料遺失、無資料分歧)且可用(將接受讀取和寫入)的系統幾乎是不可能的。
我們將看到一致性和可用性處於相反的兩端,您需要選擇優化哪種方式。 好消息是,使用 RabbitMQ,這個選擇是可能的。 您可以使用這些「書呆子」槓桿來將平衡轉向更高的一致性或更高的可訪問性。
我們會特別關注哪些配置會導致確認記錄導致資料遺失。 出版商、經紀人和消費者之間存在著一條責任鏈。 一旦訊息被傳送到經紀人,他的工作就是不遺失訊息。 當代理確認發布者收到訊息時,我們不希望訊息遺失。 但我們會看到這實際上可能會發生,具體取決於您的代理商和發布商配置。
單節點彈性原語
彈性排隊/路由
RabbitMQ 中有兩種類型的佇列:持久性佇列和非持久性佇列。 所有佇列都保存在 Mnesia 資料庫中。 持久隊列在節點啟動時重新通告,因此在重新啟動、系統崩潰或伺服器崩潰時仍能倖存(只要資料被持久化)。 這意味著只要您聲明路由(交換)和佇列具有彈性,排隊/路由基礎架構就會恢復上線。
當節點重新啟動時,易失性佇列和路由將被刪除。
持久訊息
僅僅因為隊列是持久的並不意味著它的所有訊息都將在節點重新啟動後繼續存在。 僅由發布者設定的消息 持續 (執著的)。 持久性訊息確實會為代理帶來額外的負載,但如果訊息遺失是不可接受的,那麼就沒有其他選擇。
米。 1. 可持續發展矩陣
使用隊列鏡像進行集群
為了承受失去經紀人的影響,我們需要冗餘。 我們可以將多個 RabbitMQ 節點組合成一個集群,然後透過在多個節點之間複製隊列來添加額外的冗餘。 這樣,如果一個節點發生故障,我們就不會丟失資料並保持可用。
隊列鏡像:
- 一個主隊列(master),接收所有寫入和讀取命令
- 從主佇列接收所有訊息和元資料的一個或多個鏡像。 這些鏡子不是為了縮放而純粹是為了冗餘。
米。 2.隊列鏡像
鏡像由適當的策略設定。 您可以在其中選擇複製係數,甚至可以選擇佇列應位於的節點。 例子:
ha-mode: all
ha-mode: exactly, ha-params: 2
(一主一鏡)ha-mode: nodes, ha-params: rabbit@node1, rabbit@node2
出版商確認
為了實現一致的記錄,需要發布者確認。 如果沒有它們,訊息就有遺失的風險。 訊息寫入磁碟後,將向發布者發送確認訊息。 RabbitMQ 不是在收到訊息時才將訊息寫入磁碟,而是定期(大約幾百毫秒)將訊息寫入磁碟。 當佇列被鏡像時,只有在所有鏡像也將其訊息副本寫入磁碟後才會發送確認。 這意味著使用確認會增加延遲,但如果資料安全很重要,那麼它們是必要的。
故障轉移佇列
當代理程式退出或崩潰時,該節點上的所有佇列領導者(主節點)都會隨之崩潰。 然後,叢集選擇每個主伺服器中最舊的鏡像並將其提升為新主伺服器。
米。 3. 多個鏡像佇列及其策略
經紀人 3 宕機了。 請注意,Broker 2 上的佇列 C 鏡像正在升級為主鏡像。 另請注意,已為 Broker 1 上的佇列 C 建立了一個新映像。RabbitMQ 始終嘗試維護策略中指定的複製因子。
米。 4. Broker 3故障,導致佇列C故障
下一個經紀人1號隕落! 我們只剩下一名經紀人了。 隊列B鏡像被提升為主鏡像。
圖。 5
我們已經返回 Broker 1。無論資料在 Broker 遺失和恢復過程中保存得如何,所有鏡像佇列訊息在重新啟動時都會被丟棄。 值得注意這一點很重要,因為這會產生後果。 我們很快就會看看這些影響。 因此,Broker 1 現在再次成為叢集的成員,並且叢集嘗試遵守策略,因此在 Broker 1 上建立鏡像。
在這種情況下,Broker 1 完全遺失,資料也完全遺失,因此未鏡像的佇列 B 也完全遺失。
米。 6. 經紀商 1 恢復服務
Broker 3 重新上線,因此佇列 A 和 B 取回在其上建立的鏡像以滿足其 HA 策略。 但現在所有的主隊列都在一個節點上! 這並不理想,節點之間均勻分佈較好。 不幸的是,這裡沒有太多用於重新平衡大師的選項。 我們稍後會再討論這個問題,因為我們需要先考慮隊列同步。
米。 7. 經紀商 3 恢復服務。 所有主隊列都在一個節點上!
所以現在你應該知道鏡像是如何提供冗餘和容錯的。 這可以確保單一節點發生故障時的可用性並防止資料遺失。 但我們還沒完成,因為實際上情況要複雜得多。
同步
建立新鏡像時,所有新訊息將始終複製到該鏡像和任何其他鏡像。 至於master隊列中現有的數據,我們可以將其複製到新的鏡像中,鏡像成為了master的完整副本。 我們也可以選擇不複製現有訊息,讓主佇列和新鏡像及時匯聚,新訊息到達尾部,現有訊息離開主佇列頭部。
此同步是自動或手動執行的,並使用佇列策略進行管理。 讓我們來看一個例子。
我們有兩個鏡像隊列。 隊列A自動同步,隊列B手動同步。 兩個隊列都包含十條訊息。
米。 8. 兩個不同同步模式的佇列
現在我們正在失去 Broker 3。
米。 9. 經紀人3倒下
經紀商 3 恢復服務。 叢集為新節點上的每個佇列建立一個鏡像,並自動將新的佇列A與master同步。 然而,新隊列B的鏡像仍然是空的。 這樣,我們就可以在佇列 A 上實現完全冗餘,並且只有一個鏡像來儲存現有的佇列 B 訊息。
米。 10. 佇列 A 的新鏡像接收所有現有訊息,但佇列 B 的新鏡像不接收。
兩個隊列中又有十條訊息到達。 然後,Broker 2 崩潰,佇列 A 回滾到位於 Broker 1 上的最舊的鏡像。失敗時不會遺失資料。 在佇列 B 中,主佇列中有 XNUMX 個訊息,鏡像中只有 XNUMX 個訊息,因為該佇列從未複製原始的 XNUMX 個訊息。
米。 11. Queue A回滾到Broker 1而不遺失訊息
兩個隊列中又有十條訊息到達。 現在Broker 1崩潰了,Queue A輕鬆切換到鏡像,並且不會丟失訊息。 但是,隊列 B 遇到了問題。 此時我們可以優化可用性或一致性。
如果我們想優化可訪問性,那麼策略 ha-失敗時升級 應該安裝在 時刻。 這是預設值,因此您可以根本不指定策略。 在這種情況下,我們本質上是允許不同步鏡像出現故障。 這會導致訊息遺失,但佇列將保持可讀可寫。
米。 12. 佇列A回滾到Broker 3,訊息不遺失。 佇列 B 回滾到 Broker 3,遺失 XNUMX 則訊息
我們還可以安裝 ha-promote-on-failure
進入意義 when-synced
。 在這種情況下,佇列將等待 Broker 1 及其資料返回到線上模式,而不是回滾到鏡像。 返回後,主隊列返回 Broker 1,沒有任何資料遺失。 為了資料安全而犧牲了可用性。 但這是一種有風險的模式,甚至可能導致資料完全遺失,我們很快就會討論這一點。
米。 13. 失去 Broker 1 後隊列 B 仍然不可用
您可能會問,“不使用自動同步是不是更好?” 答案是同步是一個阻塞操作。 同步期間,主隊列不能執行任何讀取或寫入操作!
讓我們來看一個例子。 現在我們排了很長的隊。 它們怎麼會長到這麼大呢? 有幾個原因:
- 隊列未積極使用
- 這些是高速隊列,而現在消費者速度很慢
- 這是高速排隊,出現了故障,消費者正在迎頭趕上
米。 14.兩個同步方式不同的大隊列
現在經紀人3倒下了。
米。 15. Broker 3 宕機,每個隊列中留下一個 master 和一個mirror
Broker 3 重新上線並建立新的鏡像。 主隊列A開始將現有訊息複製到新鏡像,在此期間隊列不可用。 複製資料需要兩個小時,導致該佇列停機兩個小時!
然而,隊列 B 在整個期間保持可用。 她為了可訪問性犧牲了一些冗餘。
米。 16. 同步期間佇列保持不可用
兩個小時後,佇列 A 也變得可用,並且可以再次開始接受讀取和寫入。
更新
同步過程中的這種阻塞行為使得更新具有非常大隊列的叢集變得困難。 在某些時候,主節點需要重新啟動,這意味著在伺服器升級時切換到鏡像或停用佇列。 如果我們選擇轉換,如果鏡像不同步,我們將會遺失訊息。 預設情況下,在代理程式中斷期間,不會執行到不同步鏡像的故障轉移。 這意味著一旦代理返回,我們就不會丟失任何訊息,唯一的損壞是一個簡單的隊列。 代理斷開連線時的行為規則由策略設定 ha-promote-on-shutdown
。 您可以設定兩個值之一:
always
= 啟用到不同步鏡像的轉換when-synced
= 僅轉換為同步鏡像,否則佇列將變得不可讀且不可寫。 代理返回後隊列立即恢復服務
不管怎樣,對於大型佇列,您必須在資料遺失和不可用之間做出選擇。
當可用性提高資料安全性時
在做出決定之前,還有一個複雜的問題需要考慮。 雖然自動同步更有利於冗餘,但它如何影響資料安全? 當然,有了更好的冗餘,RabbitMQ 失去現有訊息的可能性較小,但是來自發布者的新消息呢?
這裡你需要考慮以下幾點:
- 發布者是否可以簡單地返回錯誤並讓上游服務或用戶稍後重試?
- 發布者可以將訊息保存在本機或資料庫中以便稍後重試嗎?
如果發布者只能丟棄訊息,那麼實際上提高可訪問性也提高了資料安全性。
因此,必須尋求一個平衡點,解決方案要根據具體情況而定。
ha-promote-on-failure=when-synced 的問題
想法 ha-失敗時升級= 何時同步 是我們防止切換到不同步的鏡像,從而避免資料遺失。 隊列仍然不可讀取或不可寫入。 相反,我們嘗試恢復崩潰的代理並保持其資料完整,以便它可以恢復作為主伺服器的功能而不會丟失資料。
但是(這是一個很大的但是)如果經紀人丟失了他的數據,那麼我們就會遇到一個大問題:隊列丟失了! 所有數據都消失了! 即使您的鏡像大部分趕上主隊列,這些鏡像也會被丟棄。
要重新加入同名節點,我們告訴叢集忘記遺失的節點(使用命令 rabbitmqctlforget_cluster_node)並啟動一個具有相同主機名稱的新代理程式。 當叢集記住遺失的節點時,它會記住舊的佇列和不同步的鏡像。 當叢集被告知忘記一個孤立節點時,該佇列也會被忘記。 現在我們需要重新聲明它。 儘管我們有包含部分資料集的鏡像,但我們遺失了所有資料。 換成非同步鏡像會更好!
因此,手動同步(和同步失敗)結合 ha-promote-on-failure=when-synced
在我看來,風險很大。 文件說這個選項的存在是為了資料安全,但它是一把雙面刃。
掌握再平衡
如同所承諾的,我們回到所有主節點在一個或多個節點上的累積問題。 這甚至可能由於滾動叢集更新而發生。 在三節點叢集中,所有主隊列將累積在一個或兩個節點上。
重新平衡主機可能會出現問題,原因有二:
- 沒有好的工具來執行重新平衡
- 隊列同步
有第三方進行再平衡
還有另一個技巧可以透過 HA 策略來移動主隊列。 說明書上提到
- 使用優先權高於現有 HA 策略的臨時策略刪除所有鏡像。
- 將 HA 暫存策略變更為使用節點模式,指定主佇列應傳送到的節點。
- 同步推送遷移的佇列。
- 遷移完成後,刪除臨時策略。 初始HA策略生效,並建立所需數量的鏡像。
缺點是,如果您有較大的佇列或嚴格的冗餘要求,則此方法可能不起作用。
現在讓我們來看看 RabbitMQ 叢集如何處理網路分割區。
失去連接
分散式系統的節點透過網路鏈路連接,網路鏈路可以而且將會被斷開。 中斷頻率取決於本地基礎設施或所選雲端的可靠性。 無論如何,分散式系統必須能夠應對它們。 我們再次在可用性和一致性之間做出選擇,好消息是 RabbitMQ 提供了這兩個選項(只是不同時)。
使用 RabbitMQ,我們有兩個主要選項:
- 允許邏輯劃分(裂腦)。 這保證了可用性,但可能會導致資料遺失。
- 禁用邏輯分離。 可能會導致短期可用性喪失,具體取決於客戶端連接到叢集的方式。 也可能導致兩節點叢集完全不可用。
但什麼是邏輯分離呢? 這是由於網路連線遺失而導致集群分裂為兩部分的情況。 每一邊的鏡子都會被提升為大師,這樣最終每回合都會有幾個大師。
米。 17. 主佇列和兩個鏡像,每個鏡像位於一個單獨的節點上。 然後發生網路故障並且一個鏡像斷開。 分離的節點看到另外兩個節點已經脫落,並將其鏡像提升為主節點。 我們現在有兩個主隊列,既可寫又可讀。
如果發布者將資料傳送到兩個主伺服器,我們最終會得到佇列的兩個不同副本。
RabbitMQ 的不同模式提供可用性或一致性。
忽略模式(預設)
此模式確保可訪問性。 失去連接後,就會發生邏輯分離。 連線恢復後,管理員必須決定優先考慮哪個分割區。 失敗的一方將重新開始,並且該方累積的所有資料都將遺失。
米。 18. 三個出版商與三個經紀人有關聯。 在內部,叢集將所有請求路由到 Broker 2 上的主佇列。
現在我們正在失去 Broker 3。他看到其他 Broker 已經掉隊,並將他的鏡像提升為 Master。 這就是邏輯分離發生的方式。
米。 19.邏輯劃分(裂腦)。 記錄進入兩個主隊列,兩個副本分開。
連接已恢復,但邏輯分離仍然存在。 管理員必須手動選擇失敗的一方。 在下面的情況下,管理員重新啟動 Broker 3。他未能成功傳輸的所有訊息都會遺失。
米。 20. 管理員停用 Broker 3。
米。 21. 管理員啟動 Broker 3,它加入集群,失去留在那裡的所有訊息。
在連線遺失期間以及連線恢復後,叢集和該佇列可用於讀取和寫入。
自動修復模式
工作原理與忽略模式類似,不同之處在於集群本身在分裂和恢復連接後自動選擇失敗的一方。 失敗的一方返回到集群為空,隊列將丟失僅發送到該方的所有訊息。
暫停少數派模式
如果我們不想允許邏輯分區,那麼我們唯一的選擇就是放棄叢集分區後較小一側的讀寫操作。 當代理商發現自己處於較小的一側時,它會暫停工作,即關閉所有現有連線並拒絕任何新連線。 它每秒檢查一次連線恢復情況。 一旦連接恢復,它就會恢復操作並加入叢集。
米。 22. 三個出版商與三個經紀人有關聯。 在內部,叢集將所有請求路由到 Broker 2 上的主佇列。
然後,代理 1 和 2 從代理 3 中分離出來。代理 3 不會將其鏡像提升為主鏡像,而是掛起並變得不可用。
米。 23. Broker 3 暫停,斷開所有客戶端連接,並拒絕連接請求。
一旦連線恢復,它就會返回叢集。
讓我們來看另一個範例,其中主隊列位於 Broker 3 上。
米。 24. Broker 3 上的主隊列。
然後會發生同樣的連線遺失。 Broker 3 暫停,因為它位於較小的一側。 另一方面,節點發現 Broker 3 已失效,因此 Brokers 1 和 2 中的舊鏡像被提升為主鏡像。
米。 25. 如果 Broker 2 不可用,則轉換到 Broker 3。
當連線恢復時,Broker 3 將加入叢集。
米。 26. 叢集已恢復正常運作。
這裡要理解的重要一點是我們可以獲得一致性,但我們也可以獲得可用性, 如果 我們將成功地將客戶轉移到大部分區域。 對於大多數情況,我個人會選擇暫停少數模式,但這實際上取決於具體情況。
為了確保可用性,確保客戶端成功連接到主機非常重要。 讓我們看看我們的選擇。
確保客戶連接
對於如何在失去連線後將客戶端定向到叢集的主要部分或工作節點(在一個節點發生故障後),我們有多種選擇。 首先,讓我們記住,特定佇列託管在特定節點上,但路由和策略會在所有節點之間複製。 客戶端可以連接到任何節點,內部路由會將它們引導到需要去的地方。 但是當一個節點掛起時,它會拒絕連接,因此客戶端必須連接到另一個節點。 如果節點脫落,他就無能為力了。
我們的選擇:
- 使用負載平衡器存取集群,該負載平衡器簡單地循環瀏覽節點,客戶端重試連接直到成功。 如果節點已關閉或掛起,則嘗試連接到該節點將失敗,但後續嘗試將轉到其他伺服器(以循環方式)。 這適用於短期失去連線或伺服器宕機但需要快速恢復的情況。
- 透過負載平衡器存取集群,並在檢測到掛起/故障的節點後立即從清單中刪除它們。 如果我們快速做到這一點,並且客戶端能夠重試連接,那麼我們將實現持續的可用性。
- 給每個客戶端一個所有節點的列表,客戶端在連接時隨機選擇其中一個。 如果嘗試連線時收到錯誤,它將移動到清單中的下一個節點,直到連線為止。
- 使用 DNS 刪除故障/掛起節點的流量。 這是使用小 TTL 完成的。
發現
RabbitMQ 群集有其優點和缺點。 最嚴重的缺點是:
- 加入集群時,節點會丟棄其資料;
- 阻塞同步會導致佇列變得不可用。
所有困難的決定都源自於這兩個架構特徵。 如果 RabbitMQ 能夠在叢集重新加入時保存數據,那麼同步會更快。 如果能夠實現非阻塞同步,那就更好支援大隊列了。 解決這兩個問題將大大提高 RabbitMQ 作為容錯和高可用訊息傳遞技術的效能。 在以下情況下,我會猶豫是否推薦使用叢集的 RabbitMQ:
- 網路不可靠。
- 儲存不可靠。
- 隊伍很長。
當涉及高可用性設定時,請考慮以下事項:
ha-promote-on-failure=always
ha-sync-mode=manual
cluster_partition_handling=ignore
(或者autoheal
)- 持久訊息
- 確保當某個節點發生故障時客戶端連接到活動節點
為了一致性(資料安全),請考慮以下設定:
- 發布者確認與消費者端手動確認
ha-promote-on-failure=when-synced
,如果發布者可以稍後再試並且如果您有非常可靠的存儲! 否則放=always
.ha-sync-mode=automatic
(但對於大型非活動佇列可能需要手動模式;也要考慮不可用是否會導致訊息遺失)- 暫停少數派模式
- 持久訊息
我們還沒有涵蓋容錯和高可用性的所有問題; 例如,如何安全地執行管理程式(例如捲動更新)。 我們還需要討論聯邦和 Shovel 外掛程式。
如果我還遺漏了其他內容,請告訴我。
另請參閱我的
系列之前的文章:
1號-
2號-
3號-
來源: www.habr.com