RabbitMQ實戰:可用性分析和實現
本系列是「RabbitMQ實戰:高效部署分布式消息隊列」書籍的總結筆記。
上一篇介紹了各種場景下的最佳實踐,大部分場景可以使用「發後即忘」的模式,不需要響應,如果需要響應,可以使用RabbitMQ的RPC模型。
RabbitMQ以異步的方式解耦系統間的關系,調用者將業務請求發送到Rabbit服務器,就可以返回了,Rabbit會確保請求被正確處理,即使遇到網絡異常、Rabbit服務器崩潰、整個機房斷電等特殊場景,針對這些場景,Rabbit提供了各種機制確保其可用性。
本篇通過總結可能出現的特殊場景,對Rabbit提供的可用性保證進行分析,學習它的實現方式,你會了解到:
- 總結異常場景
- 集群並處理失敗
- 連接丟失和故障轉移
- 主/備方式
- 跨機房復制
異常場景
在實際工作中,有很大一部分時間用在解決各種異常情況,比如針對用戶輸入的驗證,JDK中提供的各種異常類,網絡異常等,這些相對來說比較好解決。
Rabbit服務作為調用者和處理者的橋梁,至關重要,如果因為網絡異常、單臺服務器崩潰、機房癱瘓等原因導致Rabbit服務不可用,會影響所有依賴的業務系統。
網絡異常
處理者和服務端是通過長連接交互的,這樣可以將消息實時推送,網絡異常可能會導致長連接斷開,如果客戶端無法感知,處理者將接收不到任何消息,這種情況稱為「連接丟失」。
通過捕獲連接異常,進行重連,可以解決這種問題,另外,Rabbit客戶端進行了封裝,很容易處理這種問題。
服務器崩潰
如果只有一臺服務器服務,服務器崩潰將導致服務不可用,一般會使用集群將多個服務器看成一個整體對外提供服務,這樣,單臺服務器崩潰不會影響整體的服務。
使用集群後,就要考慮一些問題:
- 客戶端連接到哪臺服務器是隨機的,而一個隊列只會在某個服務器中,所以,每臺服務器都要保存隊列元數據(類似索引),並且可從其他服務器獲取實際的隊列數據;
- 服務器崩潰,會導致非持久化的隊列、交換器丟失,客戶端端重連後,要再次進行創建,但未消費的消息將無法恢復;
- 如果隊列、交換器、消息等是持久化的,如何進行恢復呢,Rabbit提供了幾種方式進行處理,後面會詳細介紹;
- 訂閱者也需要重新建立連接,進行監聽;
機房癱瘓
如果考慮機房癱瘓,就要建多個數據中心,RabbitMQ提供了一種機制,可以方便地在不同數據中心的Rabbit間復制消息。
集群並處理失敗
RabbitMQ最優秀的功能之一就是其內建集群,主要用於完成2個目標:
- 允許消費者和生產者在Rabbit節點崩潰的情況下繼續運行;
- 通過添加更多的節點線性擴展消息通信的吞吐量;
集群架構
RabbitMQ會始終記錄四種類型的內部元數據(類似索引):
- 隊列元數據:隊列名稱和它的屬性;
- 交換器元數據:交換器名稱、類型和屬性;
- 綁定元數據:一張簡單的表格展示了如何將消息路由到隊列;
- vhost元數據:為vhost內的隊列、交換器和綁定提供命名空間和安全屬性;
當引入集群時,就需要追蹤新的元數據類型:集群節點位置,以及節點與已記錄的其他類型元數據的關系。
不是每個節點都有所有隊列的完全拷貝,如果在集群中創建隊列,只會在單個節點上創建完整的隊列信息(元數據、狀態、內容),所有其他節點只知道隊列的元數據和指向該隊列的節點指針。
如果節點崩潰了,附加在隊列上的消費者也就無法接收新的消息了。可以讓消費者重連到集群並重新創建隊列,這種做法僅當隊列沒設置持久化時才可行,這是為了確保當失敗的節點恢復後加入集群,節點上的隊列消息不會丟失。
為什麽不將隊列內容和狀態復制到所有節點:第一,存儲空間,如果每個集群節點都擁有所有隊列的完全拷貝,添加新節點不會帶來更多存儲空間;第二,性能,消息的發布者需要將消息復制到每一個集群節點,對於持久化消息,網絡和磁盤復制都會增加。
而交換器只是一張查詢表,而非實際的消息路由器,因此將交換器在整個集群中進行復制會更加簡單
可以把每個隊列想象成節點上運行的進程,每個進程擁有自己的進程ID,交換器只是路由模式列表和匹配消息應發往的隊列進程ID列表。
每個Rabbit節點,要麽是內存節點,要麽是磁盤節點,單節點系統只運行磁盤類型的節點,在集群中,可以選擇配置部分節點為內存節點。
在集群中聲明隊列、交換器或綁定的時候,這些操作直到所有集群節點都成功提交元數據變更後才返回。
RabbitMQ只要求集群中至少有一個磁盤節點,如果只有一個磁盤節點,剛好又崩潰了,集群可以繼續路由消息,但不能創建隊列、交換器、綁定、添加用戶、更改權限等操作。所以,建議設置兩個磁盤節點,當內存節點重啟後,會連接到預先配置的磁盤節點,下載當前集群元數據拷貝,所以要將所有磁盤節點告訴內存節點。
鏡像隊列
前面提到,隊列只會在集群中的一個節點,節點崩潰後,隊列消息就會丟失,RabbitMQ2.6版本之後,提供了鏡像隊列,一旦主隊列不可用,從隊列將被選舉為新的主隊列。
對於鏡像隊列,除了將消息按照路由綁定規則投遞到合適的隊列,也會將消息投遞到鏡像隊列的從拷貝。
對於發送方確認消息,Rabbit會在所有隊列和隊列的從拷貝安全地接收到消息時,才會通知發送方。
另外,使用鏡像隊列時,有一個問題:如果主拷貝節點發送故障,從隊列會選舉Wie主隊列,所有該隊列的消費者需要重新附加並監聽新的隊列主拷貝。對於通過故障節點進行連接的消費者,可以通過丟失到節點的TCP連接檢測到,但對於那些通過節點附加到鏡像隊列且正常運行的消費者將無法檢測到。
Rabbit通過給消費者發送一個消費者取消通知,告知不再附加在隊列主拷貝了,需要重新連接。
連接丟失和故障轉移
這一小節主要討論消費者如何檢測連接丟失,並進行重連操作。
處理到集群的重連有多重策略,比較好的一種方式是使用負載均衡,不僅可以減少應用程序處理節點故障代碼的復雜性,又能確保在集群中連接的平均分配。
關於負載均衡,網上介紹的比較多了,這裏就不再過多介紹了,主要看看如何感知故障,並進行重連操作。
感知故障比較簡單,當長連接斷開時,會拋出異常,捕獲對應的異常即可。
當集群節點出現故障時,應用程序需要考慮:下一個該連向哪裏?這個工作已經交由負載均衡器決定。
關於重連處理,要考慮:
- 如果重連到新的服務器,信道以及其上的所有消費循環都會失效,需要對他們進行重建;
- 當進行重連時,所有的隊列、綁定有可能都不存在了,需要重新構造隊列和綁定。
主/備方式
當對可用性要求特別高時,不允許消息丟失,需要將隊列、交換器、消息設置成持久化,如果一個節點崩潰了,在恢復之前,將無法轉發消息,因為默認的群集架構不允許在集群其他節點創建隊列,防止故障節點恢復後,歷史消息丟失。
可以通過構建主/備機的獨立RabbitMQ,也就是warren模式,解決這個問題。一個warren是指一對主/備獨立服務器,並前置一套負載均衡器來處理故障轉移。
主服務器和備用服務器之間沒有協作,只有當主服務器崩潰時,備用服務器才會處理消息。可以保證,主節點故障後,通過備用節點重新創建隊列、交換器繼續服務,故障節點恢復後,可以繼續消費主節點未消費的消息。
跨機房復制
在只有一個數據中心的時候,RabbitMQ集群對於提升消息通信性能來說是很棒的方案,但需要把消息從一個程序路由到另一個城市的時候,就比較麻煩了,可以通過Shovel解決。
Shovel是RabbitMQ的一個插件,可以使你能夠定義RabbitMQ上的隊列和另一個RabbitMQ上的交換器之間的復制關系。說白了就是生產者和消費者離得比較遠。
通過在機房1創建一個新的隊列,用於接收網站發布的消息,然後讓shovel消費這些消息並重新將消息通過WAN連接發布到機房2上的交換器。
這樣對於用戶來說,只要發布到機房1的隊列即可返回,減少了響應時間。機房1可以持續將消息發布到機房2上。
通過上面的介紹可以看到,保證高可用需要做很多工作,可以根據業務對可用性的要求,選擇不同的架構方式。
下一篇重點介紹RabbitMQ管理界面和監控。
歡迎掃描下方二維碼,關註我的個人微信公眾號 ~
RabbitMQ實戰:可用性分析和實現