RabbitMQ 高級指南:實現分布式通信
分布式通信
分布式系統是指: 通過網絡把多個組件連接起來,并提供組件之間消息傳遞和協作的系統。 分布式系統要解決的問題很多,異構、伸縮性、開放、安全、容錯等,但是基本問題是—— 提供組件之間消息通信 。沒有這個基礎其他的都無從談起,“異構”、“伸縮”、“開放”、“安全”、“容錯”其實都 是為了更好的通信 而要解決的問題。分布式通信有兩種方式
-
直接通信
通信雙方直接直接調用對方接口相互傳遞數據,Client發送數據到Server,Server響應請求返回數據到Client。我們所接觸到的大部分系統都采用這種通信方式,比如REST API、Netty寫的服務器或者自己寫的Socket服務器。這些系統的區別僅僅是通訊協議不一樣,本質上都可以用一幅圖表示。
直接通信 非常脆弱 ,主要體現在 空間耦合 ,通信一方一定要知道對方的“地址”才可以通信(比如對端IP地址); 時間耦合 ,通信一方發送數據的對端必須“在線”否則此次通信失敗。要解決這個問題其實很簡單,偉大先驅David Wheeler說過:
計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決,除非這個問題是由于太多中間層引起的。(All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections.)
(我給出的是“完整版”的引用,特別注意“except of course for the problem of too many indirections”,多科學)
-
間接通信
通信雙方不直接調用對方而是通過第三方中轉請求,通過中間層來解決“時間耦合”和“空間耦合”。這種方式不常見到,也是本文要介紹的主要內容。
通信的兩個誤區
-
世界上沒有同步通信
《UNIX網絡編程》總結了5種I/O模型,其中一種叫 異步I/O ,特別是書中最后討論了POSIX對同步I/O和異步I/O的定義,很容易讓人“望文知義”以為會有“同步”。不妨仔細看看這段文字,它僅僅是POSIX的對I/O的定義,它對 同步 的理解是 A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes(導致請求進程阻塞, 直到 IO 操作完成)。 如果我們把網絡看成I/O, 同步就是指把數據包交給網卡而不是指把數據包發送到對端。 POSIX做這個定義的原因是由于有些設備支持DMA操作不需要由CPU把數據搬到設備,所以才引入了“同步”、“異步”兩個屬于來表示兩種I/O通訊方式。有DMA的情況下直接由設備取數據,沒有DMA的情況下必須由CPU把數據指派給設備。 同步是指一個事件的產生對每個系統的影響在時間上是一致、統一的。如果是真正的同步數據產生之后一定是同時作用于兩個系統而不涉及到任何“數據傳遞”過程,這種模型在世界上根本不存在(即便是“同時作用于兩個系統”也需要傳遞,兩個系統的網絡也有好有壞,時間也會有有長有短不可能完全統一)。
-
所有的異步操作都要有超時
無論是Http請求一次進程間通訊,所有的異步操作都應該有Timeout否則你的程序很可能永遠等待。我們用HttpClient或者Python的Requests的時候如果連接不上對方服務器會拋出異常,其實這個異常不是由于“連接不上”拋出的而是由于“Timeout”引起的。 不要相信對方服務器會永遠在線,要給所有的異步操作設置上timeout,如果你覺得設置成永遠等待沒問題那么我勸你“保守”一點設置成50年。
阻塞和非阻塞
直接通信和間接通信是從通信模型上的劃分,從通信方式上分為 阻塞 、 非阻塞 。(世界上沒有 異步通信 和 同步通信 的劃分方法,所有的通信都是異步。同步、異步僅僅是針對POSIX中I/O模型的術語,具體原因前面已經解釋過了)
-
阻塞通信,通信一方發送數據后在對端沒有回應之前不會發送接下來的數據。
非阻塞通信,通信一方發送數據后不必等待對端回應直接發送接下來的數據。
阻塞通信因為需要“等待回應”所以它的效率不如非阻塞通信高,非阻塞通信又沒有辦法保證通信的質量。所以有一種介于二者之間的通信方式——基于滑動窗口。發送方一次發送多個數據然后挨個等待回應,沒有收到回應的則重發。 上面聽起來是不是像計算機網絡?其實計算機科學中很多東西都是相互借鑒的,比如上面的滑動窗口的概念你如果有一堆HTTP API需要調用并且要保證所有的API都調用成功,那么用這種批量發送+批量確認的方式是可以提高吞吐率的。
RabbitMQ實現間接通信
借助RabbitMQ我們可以實現間接通信,可以通過它實現阻塞和非阻塞兩種方式(再次重申,沒有異步、同步的說法)。
非阻塞
先說簡單的非阻塞通信,借組MQ是非常直接的,通信的發送方是Publisher,接收方是Consumer。同時借助RabbitMQ強大的Exchange概念我們可以非常靈活的實現。 發送方
接收方
首先借助間接通信發送方和接收方沒有直接依賴關系,發送方在發送數據的時候不需要指定接收方的任何信息,只要指定routingKey(對數據的描述)這實現了 空間解耦 ;發送方在發送數據的時候不要求接收方在線(甚至可以不存在),這實現了 時間解耦 。 從接收方看,接收數據的時候不需要知道發送方的任何信息只需要定義自己感興趣的數據(對數據的描述);也不要求發送方在線,數據世界上是由第三方中轉的。 這個模型非常符合我們的一般認識,我們要給女朋友寫信(好土鱉。。。寫信。。)不會自己親自把信送到女朋友手里而是通過郵局,借助郵局的強大郵政網絡幫自己送信。
阻塞通信
通信都是非阻塞的,除非我們主動“等待”。所以阻塞通信其實是建立在非阻塞的基礎上,過程如下:
-
發送方臨時產生一個Queue,定義routingkey(其實就是queue的名稱);
-
發送方發送數據,在數據頭中包括“reply_queue”;本地設置“計時器”(timeout時間)
-
MQ推送數據到接收方
-
接收方用“reply_queue”作為routingkey發送返回信息給Broker
-
Broker推送數據到發送方
-
如果發送方在收到數據之前timeout,則停止等待,拋出timeout異常
發送方
接收方
上面的過程有兩個不易察覺的問題
-
發送方超時的時候Reply Queue由誰來刪除
AMQP中Declare Queue的時候可以設置 autoDelete ,這個表示,該Queue會在消費者首次連接后如果沒有任何消費者則刪除。簡單理解就是專門針對某個consumer生成的Queue,如果consumer斷開則Queue會被Broker刪除。默認的情況下Java API中queueDeclare這個標識為True
-
再次上線的接收方做“無用功”
因為時間解耦,發送方發送數據的時候接收方可能不在線(甚至過了很久以后接收方才上線)。當接收方再次上線數據可能已經失去了“時效性”(發送者已經產生了timeout異常,reply queue也被從broker刪除)。 為了解決這個問題我們可以給“消息”設置一個TTL 這條數據會在Queue中存在2秒如果沒有被消費則會被刪除
MQ是通信的簡潔之道
分布式系統中主流的通信方案有兩種
-
以HTTP為代表的REST。這個方案幾乎是主流,很多微服務系統也都采用這種架構,但是缺點非常明顯不支持 雙向通信 (沒有長連接)
-
自己設計直接基于TCP/IP協議開發的RPC。比如gRPC、或者自己用Netty寫的RPC Framework。這種架構是非常難以適合“異構”環境,必須有一個龐大的客戶端生態圈(比如像gRPC一樣)
這兩種通信方式都是 原始通信 方式,我們使用TCP/IP協議、私有協議或者HTTP都非常痛苦,必須處理很多細節(時間耦合的問題、擴展問題、速度問題、雙向通信問題…………)。 現代化的通信方式應該提供一層“封裝”,可以忽略底層網絡讓我們只關注應用本身,構建 應用層通信協議 。 RabbitMQ通信模型屬于 間接通信 而ZeroMQ通信模型屬于 直接通信 ,從設計上來說它更加純粹,純粹到直接用Socket這種方式提示別人——“老子是通信協議不是MQ!!!”。但是這種提醒好像是徒勞的,我看到過很多人在吐槽“ZeroMQ丟消息”。這背后隱藏的是——對ZeroMQ力量的一無所知。它解決的是通信問題,而且是直接通信,所以“丟消息”是它的正常反應(想要可靠?你得自己實現!!!)。
微服務???
微服務的本質是分布式系統,它的要解決的問題更加突出——通信。協調各個組件的之間的通信,為各個組件提供運行環境,組件專注于“應用層”的事情。所以我覺得微服務架構和ESB(或者WebService)最大的區別應該是——微服務提供的一個“交互式架構”它的基礎應該是 分布式組件 ,每個組件不但可以調用別的組件也可以被別的組件調用。 ESB則不行,每個服務僅僅是“服務”它暴露的是endpoint。如果整個系統是一個“由積木”堆成的那么這種理想是可以被實現的。而現實情況是系統通常是“錯綜復雜的交互”, 每個“服務”不但是服務的提供者也是服務的消費者 。所以通信必然是雙向的否則就會犯ESB一樣的“理想主義錯誤”。
來自:http://mp.weixin.qq.com/s?__biz=MzIxMjAzMDA1MQ==&mid=2648945655&idx=1&sn=03bbdc91a5fc9098a0d8c00dec3c24f0&chksm=8f5b54fbb82cdded385ea3fc22aea25f9839f71c9eb43936aa5c9ecd33506d6b73d5e0241fe1