ZeroMQ及其模式
剛剛這個國慶,對程序員來說,最糟心的事情莫過於 ZeroMQ 的作者 Pieter Hintjens 的安樂死。想必你的朋友圈也傳過了那篇令人感懷的 A protocal for dying。如果你還沒看,翻翻朋友圈,仔細讀一讀,然後收藏起來,一兩年後再看上一看。可敬的 Pieter,臨終前的 last words,也不放過自己搞 messaging 的本行,借用了 Alice 和 Bob(https://en.wikipedia.org/wiki/Alice_and_Bob )調侃了一番。
我對 Pieter 其實並不了解太多,和他之間的唯一橋樑就是 ZeroMQ。平心而論,ZeroMQ 是個很出色的,broker-less(相對於 RabbitMQ,Kafka 這樣的 broker)的 messaging lib(注意不是 message queue,ZeroMQ的名字有些誤導),API 很友好(socket),效率很高(Pull/Push 100 Byte 的message,~2M/s,見https://github.com/zeromq/jeromq/wiki/Performance ),可伸縮性很強(Pub/Sub 下可以有 10k 的 subscriber),然而它也有其顯著的缺點:令人詬病的 zmq_ctx,以及其設計上有缺陷的Concurrency model(http://zeromq.org/whitepapers:architecture )。當然,我寫這篇文章並非為了褒揚或者貶低 ZeroMQ,也不是要普及 ZeroMQ,給大家做個入門(ZeroMQ 入門比較簡單),而是想談談 ZeroMQ 裡面那些寶貴的通訊系統設計的思想,或者說模式,這些模式曾經對我的影響還是不小的。
基礎知識
在講一些思想之前,我們需要先了解一些概念。
一個 messaging system 最重要的事情莫過於消息送達的模式:at least once 或者 at most once。at least once 是指同一個消息會被傳輸 1 到 n 次,而 at most once 是指同一個消息會被傳輸 0 到 1 次。這很好理解,如果 messaging system 內建了重傳機制,並且將消息持久化到磁碟中以保證即便進程崩潰消息依舊能夠送達,那麼這就是 at least once。反之,如果沒有構建任何上述的機制,消息送出後就並不理會,這是 at most once。很可惜,ZeroMQ 並非嚴格意義上的 at least once 或者 at most once,以其 Pub/Sub 模式來說,ZeroMQ 構建了消息確認和重傳機制,卻未對消息進行持久化,那麼內存耗盡或者進程崩潰都會造成消息丟失,而重傳則可能會造成消息被發送 1 到 n 次。這也是為何我認為 ZeroMQ 並非真正意義上的 Message Queue,當然,它可以用來構建一個真正的 MQ。注意,在一個網路環境中,消息的送達只能是上述兩種情況,不可能 exactly once,如果有人這麼說,那麼一定是在誤導。
講到消息重傳,細心的同學可能會疑慮:TCP 內建有重傳機制,為何 ZeroMQ 在消息層面還要多此一舉?這是因為 TCP 的重傳只保證了網路層面報文的重傳,而 ZeroMQ 通過消息層面的重傳,保證了一個消息一旦送達,一定是完整送達。
at least once 的使用場景很容易理解,我們發送一條消息,自然是為了接受者能夠保證接收到。至於保證接收的副作用 —— 重傳的副本,只要消息的處理是冪等(Idempotent)的,就不會有問題。at most once 的使用場景讓人比較困惑,什麼時候我們發了一條消息,丟了也就丟了,並不可惜呢?比如說這些場景:
各種網路拓撲下的 heart beat(當然,大部分場合下 heart beat 可以直接用 IP/UDP,不必使用 messaging),偶爾丟幾個消息無關痛癢
密集的 status report message 或者 tracking event。丟失的消息對全局並不構成威脅。
最後一個概念是 back pressure。但凡一個 messaging system 里,消息的兩端,生產者和消費者間,都會產生處理速度不一致的問題。如果生產者發送消息的速度過快,消費者趕不及處理,就會造成消息的擁塞,進而不斷把壓力回溯給上游,最終一層層回溯到消息的生產者,使其停止產生更多的內容。
消息通訊的模式
搞定了一些基礎知識後,我們看 ZeroMQ 涉及到的一些消息通訊的模式。
REQ/REP

REQ/REP 是最基本的模式。客戶端發送數據請求伺服器的響應。
PUB/SUB

Pub/Sub 是消息傳輸非常常見也是非常有用的一種模式,它和 observer pattern 師出同門,將數據的發布者和訂閱者解耦 —— 發布者者只管產生數據,而不必關心誰是訂閱者,有多少訂閱者。比如說你要建一個聊天室,每個人都是發布者,也都是訂閱者。發布者不必關心訂閱者的加入和離開,消息會以 1:N 的方式擴散到每個訂閱者。
PUB/SUB (forward proxy)

Pub/Sub 自身組合使用可以解決很多實際問題。比如你有很多數據要發布給內部應用和外部應用使用,而外部應用可以訪問的數據是內部應用的一個子集。通過組合 Pub/Sub,讓其中一個(或者多個)訂閱者在收到數據後,過濾出想要對外發布的 topic(或者 channel),然後再重新發布出去,供外網的應用訂閱。
Push/Pull (map reduce)

Push/Pull 是消息傳輸的另一個重要的 pattern。Push/Pull 的特點是無論是 Push 端還是 Pull 端都可以做 server,bind 到某個地址等待對方訪問。如果我們在 Push 端綁定地址,那麼這是一個 Push server,對應的 Pull clients 可以 connect 到這個 Push server 往外拉數據;反之,如果我們建立一個 Pull server,對應的 Push clients 可以 connect 到這個 Pull server 往裡壓數據。由此,我們可以輕鬆實現一個 task 的 map reduce 的 framework。如上圖所示,中間的 worker 可以隨需增減。

如果我們更進一步,當所有的 task aggregate(reduce)完畢後,我們想終止所有的 worker,可以在這個系統里加上 Pub/Sub 的機制:sink 進行控制信息的發布,所有的 worker 訂閱這個 channel,在收到 SIGTERM 後,結束自己的進程。
Push/Pull (fair queue)

Push/Pull 模式的另外一個應用場景是 fair queue — Push clients 輪番往 Pull server 寫入數據。
Router/Dealer

Router/Dealer 模式是典型的 broker 模式。在多對多的網路中, 掮客起到在網路的兩端雙方互不認識的情況下,促成雙方的交易。超市就是一個典型的掮客。顧客不必和所有的供應商一一打交道,每個供應商也不需要認識所有的顧客來促成交易 —— 整個交易在超市的促成下完成,雙方几乎都不知道對方的存在。
多對多的網路中,Router/Dealer 模式很有用。假設我們有 N 個 Reply server,M 個 Request client,若要保證高可用性,正常而言,雙方需要一個 M x N 的 full mesh 的網路才能保證任何一個 client 能夠和任何一個 server 建立連接。通過在中間加一層 Router/Dealer,M x N 的連接被簡化成 M + N。網路的複雜度大大降低。
綜合
結合 Pub/Sub,Router/Dealer,Pull/Push等模式,我們可以很容易支撐非常複雜的網路應用,如上圖所示。
先講這麼多,對此感興趣的同學可以看 ZeroMQ 的官方 guide,Pieter 親自操刀撰寫的,非常贊,即使你不打算在你的應用中使用 ZeroMQ,但讀讀這個文檔也是非常有益的。下次我把今天就這個話題在 TubiTV 做的 BBL的 slides 和 video 放上來,有興趣的同學可以看看。
推薦閱讀:
