新世紀通訊函式庫 – ZeroMQ

2012年就快到了,離2015年第三次衝擊也快到了,聽不懂我在說什麼就算了,跟主題沒什麼關聯,在介紹主角之前,我總喜歡描述一下主角的背景

欲上雲端,必先分散

這幾年,雲端炒得正熱門,愛跟風的台灣人當然也不能錯過,不管是賣主機的聲稱他們是做雲端的,連賣房子的也稱他們的是雲端,哪天就連在路邊看見雲端雞排都請不要大驚小怪,因為那簡直和奈米光觸媒雞排一樣有異曲同工之妙,然而口口聲聲說雲端,但前陣子iPhone4S預購就給了各家電信商一個大巴掌,他們的網頁伺服器都在一瞬間被大量擁入的使用者塞暴了,更別說其中還有不乏自家推出雲端服務的電信商,說真的,連分散運算的基本功都做不到,跟人家雲什麼端? 你說是吧?

但今天的主角不在於雲端是什麼,或是什麼才算真正的雲端之類的無聊話題,今天的主軸是分散式運算,因為要達到規模的運算和高可得性就一定要扯到分散式的運算,而分散式運算一直以來都不是什麼簡單的議題,最麻煩的就要算是溝通上的問題了,如果你有 三個節點之間想要進行溝通,該怎麼做? 你說簡單,直接串在一起不就好了?

好吧,或許你只有固定的三個節點,這好辦,但是要是你有更多的節點呢? 考慮一下十個節點、一百個節點,光想像就能知道,隨著節點的增加,他們之間的連線數量呈指數在成長,不光是連線本身的問題,連這些節點要如何知道對方的存在以及管理都是相當麻煩的問題

中央集權

光想就令人頭皮發麻,為了解決這樣的問題,一個經典的模式被設計且大量使用,那就是所謂的 Broker (掮客),也有稱Message Queue、Message bus等,簡單的概念,就是所有節點都連向中間的訊息交換伺服器,而溝通都透過這個中央的交換中心來進行,節點與節點之間無需去在意到底誰在和我溝通,只需要在意訊息的種類和內容即可

 

雖然這樣的模式解決了一些問題,但同時也引入了新的問題,分散式系統的頭號公敵 – 單點失效,正因為所有節點都依賴著這個中間的掮客幫忙轉送訊息,這也意味著當掮客網路斷線、當機等等意外的發生,都會讓整個系統陷入停擺的狀態,除此之外還有另一個嚴重的問題,就是當節點數量增加,掮客的工作量也會一直往上升,在無法擴增的情況下會造成整個系統的擴展性受到限制,效能也會因為掮客受限制

各自為政

正因為中央集權造成了問題,所以有人提出了各自為政來解決問題,可以想見的,在分散式的系統裡,並不是所有的節點都需要和所有人進行溝通,他們通常只需要和特定的節點溝通,舉個例子,假設你設計的是一個多媒體檔案的處理系統,在第一個節點可能做的是Hash,用來產生該檔案的唯一識別編號,節點二做的是轉檔,節點三做的是儲存,節點四做的是歸檔,那麼你需要的就只是這樣的結構,如果以物件導向的設計模式來看,我們稱這樣的結構為責任鏈

 

再看另一個例子,如果我們是氣象局,想發佈各種天氣的消息,那麼你需要的是一個伺服器,讓大家去訂閱他們有興趣的主題,這樣的結構以物件導向的觀點來看就叫做觀察者模式,與我們先前見到的Broker做的是一樣的事情,然而在這裡的重點在於該伺服器只負責發佈天氣消息,並不參與訊息的交換

整體的概念就是各自有不同的子系統,我們透過通訊的方式將它們串在一起,這樣做有個好處就是效能好,再來就是設計得當的話,某子系統雖然停擺,但不會影響到所有的系統

各自為政的代價

雖然各自為政的做法有其好處,但相對的也有它的代價,光是通訊的協定就是很腦人的問題,考慮一下各種不同的子系統間該用什麼協定來溝通,連線中斷了怎麼處理,負載平衡? 這些問題都要花相當大的心力來解決

而在不同的環境下,使用不同的通訊方式都各有好處,使用TCP/IP的話,不管你的節點在同一台機器或是遠端,都可以連線,缺點是在同一台機器會有一定的效能耗損,而且遇到廣播的訊息,同樣的訊息被傳送N次,就沒有UDP廣播來得划算,如果節點是在同一台機器上溝通,使用IPC的方式效率好,如果是在同一個thread裡,那麼分享記憶體的方式最快,IPC反而會拖慢速度,正因為有各種不同的考量,使得想要達成高效率的分散式系統是一件困難的事情

是主角登場的時候了 – ZeroMQ

講了半天,ZeroMQ到底是做什麼用的? 簡單的來說,它是一套網路通訊函式庫,用來解決上面所提到的問題,考慮一下上面所提到的物件導向設計模式,你想,即然那樣的模式一再出現,為什麼我們不能將這些常見的通訊方式變成可以輕易重覆使用的形式? ZeroMQ所做到的即是如此,它將常見的通訊方式定義成不同行為的socket,讓你可以輕易地重覆使用這些樣式去組出強健的分散式系統

老樣子 Hello world.. 不! 是Hello baby

雖然大部份程式範例都喜歡用Hello world,但我不喜歡和他們一樣,我喜歡Hello baby,我們來看一下簡單的Hello baby範例

Client端

import zmq

context = zmq.Context()

socket = context.socket(zmq.REQ)
socket.connect ("tcp://127.0.0.1:7788")

socket.send('hello')
print socket.recv()

Server端

import zmq

context = zmq.Context()

socket = context.socket(zmq.REP)
socket.bind ("tcp://*:7788")

print socket.recv()
socket.send('baby')

有了這兩隻Python程式,你可以在本機執行,不管Client或Server端先執行都可,你可能覺得我在說笑,哪有Client先執行的道理,接著會解釋,他們輸出的結果會像這樣

Client端:

baby

Server端:

hello

所以這之間到底發生了什麼事? 很簡單的在Client方,以TCP的方式連接了本機端的7788 port,接著送了一個request,然後接收response然後印出來,而Server方以TCP的方式綁定了本地端的7788 port,然後讀了一個request後送了一個response回去,這有什麼特別? 目前為止 … 沒有,但是,讓我們看下去

ZeroMQ的特異功能

上面的兩隻程式整體看起來和一般的socket沒兩樣,而行為也沒太大差別,所以它的特異功能到底在哪裡? 難不成它可以用手指讀封包嗎? 事實上它的特別之處可多了,這兩個程式並沒有表現出太特別的地方,我們改寫上面的程式讓它變成更能突顯ZeroMQ特色的程式

req.py

import zmq
import random
import time

context = zmq.Context()

socket = context.socket(zmq.REQ)
socket.bind("tcp://*:7788")

# wait all worker connected
time.sleep(1)

for i in range(9):
    a = random.randint(0, 100)
    b = random.randint(0, 100)
    print 'Compute %s + %s ...' % (a, b)

    # send request to peer
    socket.send_multipart([str(a), str(b)])

    # receive response from peer
    rep = socket.recv()
    print ' =', rep

rep.py

import os
import zmq

context = zmq.Context()

socket = context.socket(zmq.REP)
socket.connect("tcp://localhost:7788")

print 'Worker %s is running ...' % os.getpid()

while True:
    # receive request
    a, b = socket.recv_multipart()
    a = int(a)
    b = int(b)

    print 'Compute %s + %s and send response' % (a, b)
    socket.send(str(a + b))

接著我們先執行三個rep.py,然後再執行一個req.py,看發生了什麼事

req.py

Compute 43 + 91 ...
 = 134
Compute 21 + 63 ...
 = 84
Compute 17 + 93 ...
 = 110
Compute 29 + 98 ...
 = 127
Compute 90 + 55 ...
 = 145
Compute 14 + 74 ...
 = 88
Compute 3 + 85 ...
 = 88
Compute 12 + 73 ...
 = 85
Compute 73 + 21 ...
 = 94

rep.py 1

Worker 7296 is running ...
Compute 43 + 91 and send response
Compute 29 + 98 and send response
Compute 3 + 85 and send response

rep.py 2

Worker 6532 is running ...
Compute 21 + 63 and send response
Compute 90 + 55 and send response
Compute 12 + 73 and send response

rep.py 3

Worker 5928 is running ...
Compute 17 + 93 and send response
Compute 14 + 74 and send response
Compute 73 + 21 and send response

這兩個程式做的事情很簡單,req.py負責產生兩個亂數,將其當作request送給socket的另一端,而rep.py則是接收兩個亂數request,算出結果來送回給發出者,但是這好像哪裡不太對勁是嗎? 三個socket連到一個socket? 為什麼送request的一方居然變成bind而不是connect了? 為什麼connect的一方先執行居然也能連線? 我已經聽見你在電腦前的吼叫,讓我們來說明一下這到底是怎麼回事

連線先後順序無關、自動重連機制

ZeroMQ的socket有個特色就是對於誰先bind誰後connect之類的完全都不在乎,如果是connect先行,發線連線無法建立,ZeroMQ會自動重試,當bind也確立了,連線就會自動接上了,有了這樣的特性,只要管怎樣連接和對方的位址是什麼即可

通常bind那端都是位址為大家所熟知的那端,connect都是位址不為人知的,以我們這樣的例子,因為worker可能有很多個,所以我們將REQ端換成bind,而REP端換成connect,方便多個rep.py連接req.py

除此之外,REQ和REP兩種角色不管哪方bind哪方connect運算的方式都是一樣的

多個Socket連線、多位址綁定

ZeroMQ的socket之間可以多個互相連線,所以一個socket的另一端可能有N個節點連接,除此之外,同一個socket也可以綁在不同的位址上

自動負載平衡

如果你仔細觀察上面程式的輸出,就會發現request是依序分配給三個rep.py,這也是ZeroMQ的特色之一,REQ端會將send的message用Round-robin的方式分給所有的遠端連線,而你有多少個連線,他都會照一樣的規則分配

但因為先前有提到自動連線,因此會有個問題,當第一個連線接上時,其它連線還來不及連上,此時request可能已經大量分配給第一個連上的,為了解決這問題,我們安排了time.sleep(1)來等所有worker連上線,以避免早起的鳥兒吃光了蟲子

訊息傳輸

你可以發現我們送資料和處理的都是訊息,ZeroMQ可以提供你傳送多段資料在一筆訊息裡,因此我們在這看到的send_multipart和recv_multipart作用即為如此,有了這樣的特性,你可以不用擔心通訊協定的問題,只管專心處理訊息即可

支援不同的通訊方式

看見了我們在程式中所寫的 “tcp://*:7788″ 和 “tcp://localhost:7788″ 嗎? 它們暗示了TCP只不過只是支援的其中一種通訊方式而已,ZeroMQ還支援IPC,但目前只支援Linux下的domain socket,例如 “ipc:///tmp/req.socket”,甚至它還支援thread之間的通訊方式,因此如果你的兩個節點放在同一個process裡,為了效能考量,你可以用這種協定讓通訊效率最佳化

為了節省傳輸的封包,ZeroMQ甚至提供了基於UDP的廣播通訊方式,因此當你將節點放在同一個網路下,廣播的功能就能節省大量的重覆封包傳送

支援N種語言

雖然我們的範例都是用Python寫的,但ZeroMQ目前已經支援了幾乎所有你能在臺面上看到的主流語言,你喜歡用Lua寫子系統A? OK! 你喜歡用Perl寫系統B? 可以! C語言? 當然沒問題,你喜歡PHP? 厄… 也可以啦,總之,ZeroMQ讓你從不同語言的通訊中解藕開來,只要專注在於訊息的處理上即可

更多的樣式

我們做為範例的REQ/REP只不過是ZeroMQ所提供的樣式中的一種,它的特性就是一個request一個response,而REQ端會對所有的連線做fairly queue,也就是會公平地把request塞給REP端,借用官網的圖

另一種常見的樣式是PUB/SUB,也就是我們先前提到的觀察者模式,它的特色是所有PUB發送的消息會廣播給所有SUB的連線,而且SUB可以設定只要某段字串開頭的訊息

還有很常見的需求就是我們想將資料往某個方向負載平衡地推送,這時PUSH/PULL樣式就派上用場了,PUSH會將負載分散給PULL端,而且只能由PUSH推往PULL

還有另一種樣式是跟一般socket沒兩樣的一對一連線,叫做PAIR

參在一起做撒尿牛丸

事實上ZeroMQ還有更高級的樣式,但在本文就不介紹了,接著要大略介紹的是官網的一個例子,將這些樣式組合在一起,官網的例子是說當你需要分散式的Key/Value形式的伺服器,利用ZeroMQ來達成其組合方式就像這樣

伺服器端負責儲存一份快取,且會透過PUB/SUB讓client保持更新,而Client端被更動時,透過PUSH丟給伺服器,如果新的Client上線了,就透過REQ向server要最新的一份快取

這樣就是用ZeroMQ實作分散式系統的一個簡易的實例,事實上它能做遠比這個還要更複雜的東西

自從用了ZeroMQ,我人也高了,頭也壯了,考試都得一百分呢

有了ZeroMQ,分散式系統從又難又腦人變成很難而已,因為通訊的部份由ZeroMQ來完成了,剩下的你只要專心來考慮節點之間的拓撲與連接方式、通訊方式,以及訊息的處理,不同語言的支持也讓你不再擔心要用什麼語言來實作,或是你的同事懂不懂某種語言,真正的心力被花費在設計上,分散式系統因此也能更輕易地設計與實作,而ZeroMQ的本意是用於即時地處理大量的金融資料,效率更是ZeroMQ的金字招牌之一,如果你需要複雜且高效的分散式系統,ZeroMQ絕對是你的好朋友

最後,值得一提的是有人基於ZeroMQ的特性設計了與程式語言無關的網頁框架,或著更精確的來說是與伺服器溝通的協定,類似CGI或WSGI那樣,但是透過ZeroMQ因此和語言無關,叫Mongrel,目前出到第二版,有興趣可以研究看看,或許哪天也會寫篇文章來介紹

This entry was posted in 中文文章, 分享 and tagged , , , , , , , , , , , , , . Bookmark the permalink.

8 Responses to 新世紀通訊函式庫 – ZeroMQ

  1. odie says:

    看到第二次衝擊時我笑了
    真是一篇好文阿,讓我眼界大開
    之前對分散式的運算架構很有興趣,希望能找到點靈感

  2. EriCSN says:

    其實 2015 那次已經是第三次衝擊了… (畫錯重點
    雖然現在還不太明瞭 ZeroMQ 的好,但還是先收下了,真是一篇好文^^

    未來開始使用 web socket 之後應該就能瞭解這是奪大的福音了吧@@

  3. thomasy says:

    import zmq
    import random

    context = zmq.Context()

    socket = context.socket(zmq.REQ)
    socket.bind(“tcp://*:7788″)

    # wait all worker connected
    time.sleep(1)
    ========================
    上面的程式碼應該要import time

  4. Ten says:

    REP/REQ 的範例是在同一個主機下測試,如果在不同IP 同PORT 的情況,以下代碼一樣可以自動處理多個IP?
    socket = context.socket(zmq.REQ)
    socket.bind(“tcp://*:7788″)

  5. Hermann says:

    這篇介紹文章寫得太好了
    如果只看zmq 的官網恐怕還是一頭霧水

  6. Pingback: zeromq(1)-初探

Leave a Reply