本次分享內容由三個部分組成:
-
微服務架構與MQ
-
RabbitMQ場景分析與優化
-
RabbitMQ在網易蜂巢中的應用和案例分享
1微服務架構與MQ
微服務架構是一種架構模式,它將單體應用划分成一組微小的服務,各服務之間使用輕量級的通信機制交互。

上圖左邊是單體架構應用,把所有業務功能放在單個進程中,需要擴展時以進程為單位水平復制到多台機器。
上圖右邊是微服務架構應用,將每個業務功能以獨立進程(服務)的方式部署,可以按需將微服務分別部署在多台機器,實現水平擴展。
微服務各服務之間使用“輕量級”的通信機制。所謂輕量級,是指通信協議與語言無關、與平台無關。
微服務通信方式:
-
同步:RPC,REST等
-
異步:消息隊列
同步通信方式
優點:
-
實現方便。
-
協議通用,比如HTTP大家都非常熟知。
-
系統架構簡單,無需中間件代理。
缺點:
-
客戶端耦合服務方。
-
通信雙方必須同時在線,否則會造成阻塞。
-
客戶端需要知道服務方的Endpoint,或者需要支持服務發現機制。
異步通信方式
優點:
-
系統解耦和。
-
通信雙方可以互相對對方無感知。
缺點:
-
額外的編程復雜性。比如需要考慮消息可靠傳輸、高性能,以及編程模型的變化等。
-
額外的運維復雜性。比如消息中間件的穩定性、高可用性、擴展性等非功能特性。
今天的主題是消息隊列在微服務架構中的應用與實踐。
消息隊列中間件如何選型?主要會考慮以下幾點:
-
協議:AMQP、STOMP、MQTT、私有協議等
-
消息是否需要持久化
-
吞吐量
-
高可用支持,是否單點
-
分布式擴展能力
-
消息堆積能力和重放能力
-
開發便捷,易於維護
-
社區成熟度
選擇RabbitMQ的原因:
-
開源,跨平台
-
靈活的消息路由策略
-
持久化,消息可靠傳輸
-
透明集群,HA支持
-
支持高性能高並發訪問
-
支持多種消息協議
-
豐富的客戶端、插件和平台支持
-
支持RPC解決方案
2RabbitMQ場景分析與優化
RabbitMQ是一個實現了AMQP(高級消息隊列協議)協議的消息隊列中間件。
AMQP基本模型:
1. Queue
2. Exchange: Direct, Fanout, Topic, Header
3. Binding: BindingKey, RouteKey

總結:生產者將消息發送到Exchange,Exchange通過匹配BindingKey和消息中的RouteKey來將消息路由到隊列,最后隊列將消息投遞給消費者。
消息可靠傳輸是業務系統接入MQ時首先要考慮的問題。一般消息可靠性有三個等級:
-
At most once: 最多一次
-
At least once: 最少一次
-
Exactly once: 恰好一次
RabbitMQ支持其中的“最多一次”和“最少一次”兩種。其中“最少一次”投遞實現機制:
-
生產者confirm。如何開啟:使用confirm.select
-
消息持久化。
-
消費者ack。如何開啟:使用basic.consume(…, no-ack=false)

這里說下RabbitMQ消息持久化(寫磁盤)的兩個場景:
-
顯式指定消息屬性:delivery-mode=2
-
內存吃緊時,消息(包括非持久化消息)轉存到磁盤,由memory_high_watermark_paging_ratio參數指定閾值。
RabbitMQ的消息持久化是通過以下機制實現的:
-
消息體寫文件
-
異步刷盤,合並請求,減少fsync系統調用次數
-
當進程mailbox沒有新消息時,實時刷盤
-
confirm機制下,等fsync系統調用完成后返回basic.ack確認
RabbitMQ開啟消息可靠性參數需要注意:
-
unack消息在服務器端沒有超時,只能等待客戶端連接斷開,重新入隊等待投遞。
-
消息存在重復投遞的情況,需客戶端去重:
a)基於業務層的MsgId。
b)基於RabbitMQ的Redelivered flag標記: 不完全靠譜,仍舊可能收到重復消息。
-
保障性能:
a)批量publish, ack。
b)更快的磁盤(SSD,RAID等)。
c)少堆積。
-
消息亂序。
生產者confirm機制是三個可靠性參數中對性能影響最大的。一般來說有三種編程方式:
-
普通confirm模式。每發送一條消息后,調用waitForConfirms()方法,等待服務器端confirm。實際上是一種串行confirm。
-
批量confirm模式。每次發送一批消息后,調用waitForConfirms()方法,等待服務器端confirm。
-
異步confirm模式。注冊一個回調方法,服務器端confirm了一條(或多條)消息后SDK會回調這個方法。
下面是一些生產者confirm機制的專項性能測試數據:

總結:
-
遵循線程數越大,吞吐量越大的規律。當線程數量達到一個閾值之后,吞吐量開始下降。
-
不論哪種confirm模式,通過調整客戶端線程數量,都可以達到一個最大吞吐量值。
-
異步和批量confirm模式兩者沒有明顯的性能差距。所以,只需從可編程性的角度選擇異步或批量或者兩者結合的模式即可。相比而言,普通confirm模式只剩編程簡單這個理由了。
下面講下RabbitMQ的高可用機制。
官方提供的高可用方案:cluster + ha policy
cluster機制:多個全聯通節點之間元信息(exchange、queue、binding等)保持強一致,但是隊列消息只會存儲在其中一個節點。

優點:提高吞吐量,部分解決擴展性問題。
缺點:不能提升數據可靠性和系統可用性。
ha policy機制:在cluster機制基礎上可以指定集群內任意數量隊列組成鏡像隊列,隊列消息會在多節點間復制。實現數據高可靠和系統高可用。

設置參數:ha-mode和ha-params可以細粒度(哪些節點,哪些隊列)設置鏡像隊列。
設置參數:ha-sync-mode=manual(默認)/automatic可以指定集群中新節點的數據同步策略。
有一點需要注意:鏡像隊列對網絡抖動非常敏感,默認參數配置下,出現腦裂后RabbitMQ集群不會自我恢復,需要人工介入恢復,務必加好監控和報警。
RabbitMQ流控機制 流控類型:
-
內存流控:由vm_memory_high_watermark參數控制,默認0.4
-
磁盤流控:由disk_free_limit參數控制,默認50M
-
單條連接流控:觸發條件是下游進程的處理速度跟不上上游進程。

RabbitMQ內部進程關系調用圖
注意:當觸發流控(全局內存/磁盤流控,單條連接流控)時,生產者端的publish方法會被阻塞,生產者需要做的是:
-
注冊block事件,被流控時,會收到一個回調通知。
-
異步化處理生產者發送消息,不要阻塞主流程。
3RabbitMQ在網易蜂巢中的應用和案例分享
網易蜂巢平台的服務化架構如下,服務間通過RabbitMQ實現通信:


網易蜂巢消息服務器設計目標:實現一個路由靈活、數據可靠傳輸、高可用、可擴展的消息服務器。

設計要點:
-
Exchange類型為topic。
-
BindingKey就是隊列名。
-
每個服務(圖例中的REPO/CTRL/WEB等)啟動后會初始化一條AMQP連接,由3個channel復用:一個channel負責生產消息,一個channel從TYPE(REPO/CTRL/WEB等)類型的隊列消費消息,一個channel從TYPE.${hostname}類型的隊列消費消息。
應用場景舉例:
-
點對點(P2P)或請求有狀態服務:消息的RouteKey設置為TYPE.${HOSTNAME}。如host1上的WEB服務需要向host2上的REPO服務發送消息,只需將消息的RouteKey設置為REPO.host2投遞到Exchange即可。
-
請求無狀態服務:如果服務提供方是無狀態服務,服務調用方不關心由哪個服務進行響應,那么只需將消息的RouteKey設置為TYPE。比如CTRL是無狀態服務,host1上的WEB服務只需將消息的RouteKey設置為CTRL即可。CTRL隊列會以Round-Robin的均衡算法將消息投遞給其中的一個消費者。
-
組播:如果服務調用方需要與某類服務的所有節點通信,可以將消息的RouteKey設置為TYPE.*,Exchange會將消息投遞到所有TYPE.${HOSTNAME}隊列。比如WEB服務需通知所有CTRL服務更新配置,只需將消息的RouteKey設置為CTRL.*。
-
廣播:如果服務調用方需要與所有服務的所有節點通信,也就是說對當前系統內所有節點廣播消息,可以將消息的RouteKey設置為*.*。
總結:通過對RouteKey和BindingKey的精心設計,可以滿足點對點(私信)、組播、廣播等業務場景的通信需求。
優缺點分析:
優點:
-
路由策略靈活。
-
支持負載均衡。
-
支持高可用部署。
-
支持消息可靠傳輸(生產者confirm,消費者ack,消息持久化)。
-
支持prefetch count,支持流控。
缺點:
-
存在消息重復投遞的可能性。
-
超時管理,錯誤處理等需業務層處理。
-
對於多服務協作的場景支持度有限。比如以下場景:WEB=> CTRL=>REPO=>CTRL=> WEB。這個時候就需要CTRL緩存WEB請求,直至REPO響應。
實踐案例分享
案例一:GC引起的MQ crash
1.環境參數:

2.現象:
RabbitMQ崩潰,產生的erl_crash.dump文件內容如下:

3.直接原因:
從數據來看,虛擬機內存共4G,Erlang虛擬機已占用約1.98G內存(其中分配給Erlang進程的占1.56G),此時仍向操作系統申請1.82G,因為操作系統本身以及其他服務也占用一些內存,當前系統已經分不出足夠的內存了,所以Erlang虛擬機崩潰。
4.分析:
本例有兩個不符合預期的數據:
1. 內存流控閾值控制在1.67G左右(4G*0.4),為什么崩潰時顯示占用了1.98G?
2. 為什么Erlang虛擬機會額外再申請1.82G內存?
因為:
-
RabbitMQ每個隊列設計為一個Erlang進程,由於進程GC也是采用分代策略,當新老生代一起參與Major GC時,Erlang虛擬機會新開內存,根據root set將存活的對象拷貝至新空間,這個過程會造成新老內存空間同時存在,極端情況下,一個隊列可能短期內需要兩倍的內存占用量。這就是RabbitMQ將內存流控的安全閾值默認設置為0.4的原因。
-
內存流控參數vm_memory_high_watermark 為0.4的意思是,當RabbitMQ的內存使用量大於40%時,開始進行生產者流控,但是該參數並不承諾RabbitMQ的內存使用率不大於40%。
5.如何解決(規避)?
-
RabbitMQ獨立部署,不與其他Server共享內存資源。
-
進一步降低vm_memory_high_watermark值,比如設置成0.3,但是這種方式會造成內存資源利用率太低。
-
升級RabbitMQ至新版(3.4+),新版RabbitMQ對內存管理有過改進。
案例二:鏡像隊列的單節點磁盤故障造成消息丟失
1.環境參數:RabbitMQ: 3.1.5 (ha policy, ha-mode:all)
2.現象:
a)節點A的數據盤故障(磁盤控制器故障、無法讀寫),所有原本A上的生產者消費者failover到B節點。
b)從節點B的WebUI上看,所有隊列信息(包括隊列元信息和數據)丟失,但是exchange、binding、vhost等依舊存在。
c)節點B的日志中出現大量關於消費請求的錯誤日志:

d)從生產者端看來一切正常,依舊會收到來自節點B的confirm消息(basic.ack amqp方法)。
3.分析:
上述現象實際上有兩個坑在里面:
-
在數據可靠傳輸方面,鏡像隊列也不完全可靠。這是一個Bug。RabbitMQ 3.5.1版本以下都存在這個問題。
-
要保證消息可靠性,生產者端僅僅采用confirm機制還不夠。對於那些路由不可達的消息(根據RouteKey匹配不到相應隊列),RabbitMQ會直接丟棄消息,同時confirm客戶端。
4.如何解決(規避)?
-
升級RabbitMQ到新版,至少3.5.1及以上。
-
生產者basic.publish方法設置mandatory參數,它的作用是:對於那些路由不可達的消息,RabbitMQ會先通過basic.return消息向生產者返回消息內容,然后再發送basic.ack消息進行確認
Q & A
Q1:為保障消息隊列穩定性,消息隊列監控點主要有哪些?
A1:首先是服務器的基礎監控是要的。CPU,內存,磁盤IO都是敏感項。特別是內存,報警閾值可能要設置在50%以下,原因前面也說過,極端情況下一個隊列的內存占用量可能是兩倍當前值。
然后MQ業務的監控也需要加,比如消息堆積數,unack的消息數,連接數,channel數等等。RabbitMQ有提供rest api可以拿到這些監控。
還有因為RabbitMQ對網絡分區非常敏感,所以日志監控也要加。RabbitMQ出現網絡分區或者觸發流控都會在它的運行日志中有輸出。
大致就是這么幾點。
Q2:rabbitmq有嘗試結合netty提高網絡處理能力?
A2:erlang是actor模型代表,我覺得它的網絡處理能力也不比netty差的。
