分佈式專題-分佈式消息通信之RabbitMQ02-RabbitMQ高可用

前言

前面的章節我們分析了《Kafka使用以及原理分析》《ActiveMQ的使用以及原理分析》,以及初步認識了RabbitMQ,這一節,我們接着說RabbitMQ。

關於RabbitMQ,共計分爲兩小節進行闡述

本節,我會圍繞以下幾點進行展開

  • 可靠性投遞
  • 高可用架構
  • 網絡分區
  • 廣域網的同步方案
  • 實踐經驗總結

Tips:本文末有福利哦,不要錯過!另本節演示的代碼已上傳GitHub,地址我也在文末一併送出~

可靠性投遞

首先需要明確,效率與可靠性是無法兼得的,如果要保證每一個環節都成功,勢必會對消息的收發效率造成影響。

如果是一些業務實時一致性要求不是特別高的場合,可以犧牲一些可靠性來換取效率。
在這裏插入圖片描述
① 代表消息從生產者發送到Exchange;

② 代表消息從Exchange路由到Queue;

③ 代表消息在Queue中存儲;

④ 代表消費者訂閱Queue並消費消息。

1、確保消息發送到RabbitMQ服務器

可能因爲網絡或者Broker的問題導致①失敗,而生產者是無法知道消息是否正確發送到Broker的。

有兩種解決方案,第一種是Transaction(事務)模式,第二種Confirm(確認)模式。

// 將channel設置成事務模式
channel.txSelect();

// 提交事務
channel.txCommit();

// 事務回滾
channel.txRollback();

在通過channel.txSelect方法開啓事務之後,我們便可以發佈消息給RabbitMQ了,如果事務提交成功,則消息一定到達了RabbitMQ中,如果在事務提交執行之前由於RabbitMQ異常崩潰或者其他原因拋出異常,這個時候我們便可以將其捕獲,進而通過執行channel.txRollback方法來實現事務回滾。使用事務機制的話會“吸乾”RabbitMQ的性能,一般不建議使用。

// 將channel設置爲Confirm模式 
channel.confirmSelect();

if (channel.waitForConfirms()) {
     // 消息發送成功
}

生產者通過調用channel.confirmSelect方法(即Confirm.Select命令)將信道設置爲confirm模式。一旦消息被投遞到所有匹配的隊列之後,RabbitMQ就會發送一個確認(Basic.Ack)給生產者(包含消息的唯一ID),這就使得生產者知曉消息已經正確到達了目的地了。

代碼演示地址:
rabbitmq-demo/rabbitmq-javaapi/com.test.transaction
rabbitmq-demo/rabbitmq-javaapi/com.test.confirm

2、確保消息路由到正確的隊列

可能因爲路由關鍵字錯誤,或者隊列不存在,或者隊列名稱錯誤導致②失敗。
使用mandatory參數和ReturnListener,可以實現消息無法路由的時候返回給生產者。
另一種方式就是使用備份交換機(alternate-exchange),無法路由的消息會發送到這個交換機上。

      // 在聲明交換機的時候指定備份交換機
        Map<String,Object> arguments = new HashMap<String,Object>();
        arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
        channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);

代碼演示地址:rabbitmq-demo/rabbitmq-javaapi/com.test.returnlistener

3、確保消息在隊列正確地存儲

可能因爲系統宕機、重啓、關閉等等情況導致存儲在隊列的消息丟失,即③出現問題。

解決方案:

  1. 隊列持久化
//String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments

channel.queueDeclare(QUEUE_NAME, true, false, false, null);
  1. 交換機持久化
// String exchange, boolean durable

channel.exchangeDeclare("MY_EXCHANGE","true");
  1. 消息持久化
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
// 2代表持久化,其他代表瞬態
.deliveryMode(2)	
.build();
channel.basicPublish("", QUEUE_NAME, properties, msg.getBytes());
  1. 集羣,鏡像隊列,參考下一節

4、確保消息從隊列正確地投遞到消費者

如果消費者收到消息後未來得及處理即發生異常,或者處理過程中發生異常,會導致④失敗。

爲了保證消息從隊列可靠地達到消費者,RabbitMQ提供了消息確認機制(message acknowledgement)。消費者在訂閱隊列時,可以指定autoAck參數,當autoAck等於false時,RabbitMQ會等待消費者顯式地回覆確認信號後才從隊列中移去消息。

如果消息消費失敗,也可以調用Basic.Reject或者Basic.Nack來拒絕當前消息而不是確認。如果requeue參數設置爲true,可以把這條消息重新存入隊列,以便發給下一個消費者(當然,只有一個消費者的時候,這種方式可能會出現無限循環重複消費的情況,可以投遞到新的隊列中,或者只打印異常日誌)。

5、消費者回調

消費者處理消息以後,可以再發送一條消息給生產者,或者調用生產者的API,告知消息處理完畢。

參考:二代支付中異步通信的回執,多次交互。某提單APP,發送碎屏保消息後,消費者必須回調API。

6、補償機制

對於一定時間沒有得到響應的消息,可以設置一個定時重發的機制,但要控制次數,比如最多重發3次,否則會造成消息堆積。

參考:ATM存款未得到應答時發送5次確認;ATM取款未得到應答時,發送5次衝正。根據業務表狀態做一個重發。

7、消息冪等性

服務端是沒有這種控制的,只能在消費端控制。

如何避免消息的重複消費?

消息重複可能會有兩個原因:

  1. 生產者的問題,環節①重複發送消息,比如在開啓了Confirm模式但未收到確認。

  2. 環節④出了問題,由於消費者未發送ACK或者其他原因,消息重複投遞。

對於重複發送的消息,可以對每一條消息生成一個唯一的業務ID,通過日誌或者建表來做重複控制。

參考:銀行的重賬控制環節。

8、消息的順序性

消息的順序性指的是消費者消費的順序跟生產者產生消息的順序是一致的。

在RabbitMQ中,一個隊列有多個消費者時,由於不同的消費者消費消息的速度是不一樣的,順序無法保證。

參考:消息在1、新增門店 2、綁定產品 3、激活門店,這種情況下消息消費順序不能顛倒。

高可用架構

  • 基於Haproxy+Keepalived實現的RabbitMQ高可用架構圖
    在這裏插入圖片描述
  • 基於LVS負載實現的RabbitMQ高可用架構圖
    在這裏插入圖片描述

RabbitMQ集羣

集羣主要用於實現高可用與負載均衡。

RabbitMQ通過/var/lib/rabbitmq/.erlang.cookie來驗證身份,需要在所有節點上保持一致。

集羣有兩種節點類型,一種是磁盤節點,一種是內存節點。集羣中至少需要一個磁盤節點以實現元數據的持久化,未指定類型的情況下,默認爲磁盤節點。

集羣通過25672端口兩兩通信,需要開放防火牆的端口。

需要注意的是,RabbitMQ集羣無法搭建在廣域網上,除非使用federation或者shovel等插件。

集羣的配置步驟:

  1. 配置hosts
    三臺機器的hosts都配置
    vi /etc/hosts

192.168.200.111 rabbit1 (磁盤節點)
192.168.200.112 rabbit2(內存節點)
192.168.200.113 rabbit3(內存節點)

  1. 同步erlang.cookie
    保持三臺機器的.erlang.cookie同步

/var/lib/rabbitmq/.erlang.cookie

在第二臺機器200.112執行:

scp .erlang.cookie [email protected]:/var/lib/rabbitmq/
chown rabbitmq:rabbitmq .erlang.cookie

在第三臺機器200.113執行:

scp .erlang.cookie [email protected]:/var/lib/rabbitmq/
chown rabbitmq:rabbitmq .erlang.cookie

重啓服務

systemctl stop rabbitmq-server.service
systemctl start rabbitmq-server.service

或:

systemctl restart rabbitmq-server.service
查看服務狀態:systemctl status rabbitmq-server.service

如果啓動報錯:
Job for rabbitmq-server.service failed because the control process exited with error code. See “systemctl status rabbitmq-server.service” and “journalctl -xe” for details.

如果是因爲服務停不掉,就要kill 端口。

  1. 加入集羣

首先開放集羣通信端口:

# firewall-cmd --permanent --add-port={5672/tcp,4369/tcp,25672/tcp}
# firewall-cmd --reload
setsebool -P nis_enabled 1

在第二臺200.112,第三臺機200.113上執行:

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@rabbit1 --ram

創建用戶:三臺服務器都執行

firewall-cmd --permanent --add-port=15672/tcp
firewall-cmd –-reload
rabbitmqctl add_user admin admin
rabbitmqctl set_user_tags admin administrator
rabbitmqctl set_permissions -p / admin “." ".” “.*”

  1. 高可用集羣

在任何一個節點發送消息,其他節點都可以接收到消息。
在這裏插入圖片描述

RabbitMQ鏡像隊列

集羣方式下,隊列和消息是無法在節點之間同步的,因此需要使用RabbitMQ的鏡像隊列機制進行同步。

操作方式 命令或步驟
rabbitmqctl(Windows) rabbitmqctl set_policy ha-all “^ha.” “{”“ha-mode”":"“all”"}"
HTTP API PUT /api/policies/%2f/ha-all {“pattern”:"^ha.", “definition”:{“ha-mode”:“all”}}
Web UI Navigate to Admin > Policies > Add / update a policyName輸入:mirror_image Pattern輸入:^(代表匹配所有) Definition點擊 HAmode,右邊輸入:all

如圖:
在這裏插入圖片描述
參考資料:
RabbitMQ之鏡像隊列

HAproxy負載+Keepalived高可用

VIP 爲 192.168.200.1

1)安裝Keepalived

yum -y install keepalived

2)修改配置文件

vim /etc/keepalived/keepalived.conf

內容改成(物理網卡和當前主機IP要修改):

global_defs {
   notification_email {
     [email protected]
     [email protected]
     [email protected]
   }
   notification_email_from [email protected]
   smtp_server 192.168.200.1
   smtp_connect_timeout 30
   router_id LVS_DEVEL
   vrrp_skip_check_adv_addr
   # vrrp_strict    # 註釋掉,不然訪問不到VIP
   vrrp_garp_interval 0
   vrrp_gna_interval 0
}
global_defs {
   notification_email {
     [email protected]
     [email protected]
     [email protected]
   }
   notification_email_from [email protected]
   smtp_server 192.168.200.1
   smtp_connect_timeout 30
   router_id LVS_DEVEL
   vrrp_skip_check_adv_addr
   # vrrp_strict    # 註釋掉,不然訪問不到VIP
   vrrp_garp_interval 0
   vrrp_gna_interval 0
}

# 檢測任務
vrrp_script check_haproxy {
    # 檢測HAProxy監本
    script "/etc/keepalived/script/check_haproxy.sh"
    # 每隔兩秒檢測
    interval 2
    # 權重
    weight 2
}

# 虛擬組
vrrp_instance haproxy {
    state MASTER # 此處爲`主`,備機是 `BACKUP`【此處要修改】
    interface ens33 # 物理網卡,根據情況而定 【此處要修改】
    mcast_src_ip 192.168.200.111 # 當前主機ip 【此處要修改】
    virtual_router_id 51 # 虛擬路由id,同一個組內需要相同
    priority 100 # 主機的優先權要比備機高
    advert_int 1 # 心跳檢查頻率,單位:秒
    authentication { # 認證,組內的要相同
        auth_type PASS
        auth_pass 1111
    }
    # 調用腳本
    track_script {
        check_haproxy
    }
    # 虛擬ip,多個換行
    virtual_ipaddress {
        192.168.200.2
    }
}

3)啓動keepalived

keepalived -D

網絡分區

爲什麼會出現分區?因爲RabbitMQ對網絡延遲非常敏感,爲了保證數據一致性和性能,在出現網絡故障時,集羣節點會出現分區。

在這裏插入圖片描述

RabbitMQ Network Partitions

RabbitMQ Network Partitions 處理策略

模擬RabbitMQ網絡分區

廣域網的同步方案

federation插件

shovel插件

實踐經驗總結

1、配置文件與命名規範

集中放在properties文件中

體現元數據類型(_VHOST _EXCHANGE _QUEUE);

體現數據來源和去向(XXX_TO_XXX);

2、調用封裝

可以對Template做進一步封裝,簡化消息的發送。

3、信息落庫+定時任務

將需要發送的消息保存在數據庫中,可以實現消息的可追溯和重複控制,需要配合定時任務來實現。

4、運維監控

參考:
zabbix系列zabbix3.4監控rabbitmq

5、插件

tracing
https://www.rabbitmq.com/plugins.html
在這裏插入圖片描述

6、如何減少連接數

合併消息的發送,建議單條消息不要超過4M(4096KB)

思考

消費者的集羣或者微服務的多個實例,會不會重複接收消息?生產者先發送消息還是先登記業務表?(打款錯誤的例子)誰來創建對象(交換機、隊列、綁定關係)?

重複創建會有什麼問題?

持久化的隊列和非持久化的交換機可以綁定嗎?可以

如何設計一個MQ服務? http://www.xuxueli.com/xxl-mq/#/

後記

更多架構知識,歡迎關注本套Java系列文章,地址導航Java架構師成長之路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章