from:http://www.07net01.com/program/546261.html
介紹
與其他的基於常規理論基礎的(集中)通信系統不同,幾乎沒有分布式通信系統的什么資料,ØMQ(ZeroMQ)是感興趣的讀者少數能請舉出的一個。
本文的目的是解釋ØMQ架構的基本概念,它們是如何組合起來的,以及它們被如此設計的原因。
拓撲
拓撲是ØMQ最主要的概念,除非你知道“拓撲”代表什么,否則將與其他概念混淆更難理解,甚至理解不當。
按照通俗定義,我們可以說“拓撲”是一組參與同一業務邏輯的應用程序。
比如:假設有一個圖像轉換服務,調整圖像到所需的尺寸和分辨率。所有提供轉換的服務、所有使用該服務的應用程序以及所有的中間節點,比如負載均衡等,共同組成了拓撲。
通常,拓撲具有以下屬性:
- 拓撲是張圖表,這張圖中節點是應用程序、連線時應用程序間的數據通道。
- 所有應用程序就為實現它們的業務邏輯遵從相同的線路協議。
- 圖表力爭緊湊,比如任何兩個節點或直接或通過一個或多個中介機構連接。
第1點很明顯,故意使用單詞“通道”而不是使用“連接”這一點是為了描述模型工作的實際情況,即使IP多播或UDP等無連接的底層傳輸亦是如此。
第2點說明所有拓撲里的應用程序應該就什么達成一致:正被傳送的消息(比如“這是一張需調整尺寸的圖片”或“這是調整后的圖片”)、消息序列(如應用程序中狀態機的實現)、實際數據的編碼(圖像如何被序列化,RGB?CMYK?)等等。
第3點表達了這樣一個事實:即使存在兩個非常相同的(例如兩家公司里的)商務邏輯部署,它們仍然形成了兩個拓撲,除非它們相互通過數據通道連接。
為了直觀地掌握拓撲的概念,明白這個概念是含混不清的非常重要的。
它的含混不清與面向對象編程里的類含混不清相同。正式的定義解釋了類是數據成員和方法的集合,然而,沒有定義解釋商務邏輯的哪一部分應當形成類,哪一部分不能形成類。這完全由編程者決定可封裝哪個商務概念為類,哪個不能封裝為類。編程人員把所有的商務邏輯放在一個單獨的類里也許是錯誤的-因此這實際上在回避面向對象的設計-另外划分這個邏輯為無限多個小類也是錯誤的-它把這個程序轉變為一大堆難以理解的相互依賴
同樣含混不清的是沒有一個單獨的正確方法去分割商務邏輯為多個拓撲。唯一的經驗規則是拓撲是擴展的原子單元。你可以把拓撲作為整體進行擴展,然而你不能只擴展拓撲的某一方面。因此,如果你期望將來有擴展獨立於功能B的功能B的需求,那么你應當為A創建一個獨立的拓撲,為B創建一個獨立的拓撲。
讓我們舉個具體的例子來說明上面所述:
在我們的圖像轉換應用里,有兩個基本功能:調整圖像大小和調整圖像亮度。我們可以選擇要么為這兩個功能創建一個單獨的拓撲,要么分割為“調整大小”的拓撲和“調整亮度”的拓撲。
在前一種情形下,我們將定義線路傳輸協議,這樣就可以傳輸我們感興趣的功能。假如消息的第一個字節是“1”表示調整大小,或者為“2”表示調整亮度。我們還應該意識到這種設計緊緊地耦合了兩個功能。如果我們將來要對這個拓撲增加更多的處理節點,那么它們中的每個節點都應當既能調整大小也能調整亮度。
在后一種情形下,兩個功能沒有關聯。線路傳輸格式里不需要特定的“類型”域,因為所有“調整大小”拓撲的請求都是要求調整圖像大小的,而所有傳遞給“調整亮度”拓撲的請求都是要求調整圖像亮度的。在這樣的設計里我們可以獨立於另一個拓撲而擴展一個拓撲。也就是說,如果我們制造出特定的單目標的FPGA來實現調整圖像大小,那么我們可以簡單地把它們連接到“調整大小”的拓撲,而不會影響“調整亮度”的拓撲。這樣的布署如下圖所示:
注意:客戶端應用既可以(通過拓撲A)請求調整圖像 ,又可以(通過拓撲B)調整亮度。工作者1僅僅做圖像大小調整,工作者3僅僅做亮度調整。而工作者2能夠提供兩種服務 。
最后應該注意的是,應當說幸虧一個拓撲清晰地獨立於其他任何拓撲,才使得拓撲可映射到底層傳輸的一個屬性,比如TCP端口。這就允許底層的網絡按照商務標准規范其行為。例如,它可以測量由特定拓撲消耗的網絡帶寬(因此,特定的商務邏輯,比如調整大小的服務消耗的帶寬與調整亮度服務消耗的帶寬截然相反),這樣可以根據拓撲對流量進行調整,比如通過增加調整亮度等來抑制調整圖像大小。
傳輸
通常,除TCP之外的其他傳輸機制之上要求運行消息層,不管是無限寬帶(性能原因),IP組播(最小化帶寬使用)或者SCTP(多宿主,心跳等)。
最初的方法以TCP傳輸開始的,也許增加了TCP缺乏的比如心跳等特性,后來試圖在其他底層傳輸之上提供完全相同的行為。
這種方法存在兩個問題:
- 一,在特定的傳輸之上建立類TCP的封裝實際上使得傳輸成為多余的了。如果這種方法的行為類似於TCP,那么為什么一開始就不用TCP呢?(性能特性排除在這個規則之外)
- 二,許多傳輸實際上不可能硬塞到TCP模型里。比如,定義的IP廣播發送數據給網絡上所有的機器,而不是像TCP那樣發送給特定的目標。
基於以上問題,ØMQ采用了不同的方法。底層的傳輸仍然保留它們本身的特性,向上不提供通用、所有都包含的接口。然而,提供最小化接口(尤其是:消息分割,消息分段和消息原子化),並且要求更高層能夠統一地處理不同的底層傳輸的特性。
具體的來說,這意味着傳輸層上方的封包是相當簡單的封包,比如消息分割協議(當封裝TCP時),消息分段協議(分割長的消息為適宜於基於包傳輸的幾個包)或者后續連接協議(當連接PGM(實際通用組播協議的)組播流時,丟棄你獲得的消息的結尾部分):
建立拓撲和路由消息
網絡棧里的每一層都是抽象了互聯網絡復雜性的某一部分。IP層抽象了查找目的主機路由這個需求。TCP層抽象了網絡固有的可丟失這個事實而提供了可靠性保證。
ØMQ抽象了指定發送數據到特定網絡位置這樣需求。消息是被發送到拓撲的,而不是發送給特定的終端節點。重新調用哪個與特定商務邏輯緊密相連的拓撲意味着當你發送消息給拓撲的時候,你基本上已經請求所提供的特定的服務,比如調整圖像亮度大小。實際接收消息的終端節點是在ØMQ的透明傳輸方式里選擇的。
為了強化這個原則,ØMQ嚴格地分離拓撲的建立(zmq_bind,zmq_connect)和真實消息的傳遞(zmq_send,zmq_rev)。
前者同底層的傳輸地址協同工作,比如IP地址,而后者僅僅使用處理器(文件描述符)去定位具體的拓撲:
/* Topology establishment */ int s = zmq_socket (...); zmq_connect (s, "tcp://192.168.0.111:5555"); /* message routing */ const char data [] = "ABC"; zmq_send (s, data, sizeof (data), 0);
區分拓撲建立和消息路由嚴格地說不是不可缺少的。畢竟,混合這兩個為一個單獨的函數是很容易的:
zmq_send (s, "tcp://192.168.0.111:5555", data, sizeof (data), 0);
分離的理論基礎即是技術的又是學術的。技術方面的爭論包括:
- 當我們打算以異步的方式接收來自拓撲的消息的時候,我們無論如何都要連接到這個拓撲。同時沒有理由不重復使用這個通道傳送消息。
- 拓撲建立和消息路由的分離很好地映射到BSD套接字API上(bind/connect和send/recv)。
現在,學術方面的爭論甚至更加重要。使用ØMQ該做什么,不該做什么。
底層的協議,比如TCP,允許你發送數據給特定的終端節點。ØMQ構建在底層協議之上,它允許你發送數據給拓撲而不是給特定的終端節點。因此,如果你打算發送數據給特定的終端節點,那么你應當使用TCP或者類似的協議。如果你打算發送數據給拓撲,並且讓拓撲決定目標,那么你應當使用ØMQ。
不幸的是,這個概念似乎相當難以掌握。結果是:要使人們相信ØMQ不能用來定位具體的終端節點以及這一點不是漏洞而是特性幾乎是不可能的。
把拓撲建立和消息路由分離沒有解決問題,不過使得真正的功能更加顯明。未來給這個組合添加名字解析(參閱下面同名的章節)希望使得這個事實完全顯明:
zmq_connect (s, "Brightness-Adjustment-Service"); zmq_send (s, data, sizeof (data), 0);
消息模式
當把拓撲當作路由消息的方式考慮的時候,對不同的拓撲使用不同的路由算法將變得清晰起來。當"納斯達克股票報價“拓撲發布報價給這個拓撲里的所有客戶時,"亮度調整“拓撲傳輸一個客戶的圖像到工作者之一,然后回傳調整好的圖像給起始客戶。
ØMQ通過定義幾個所謂的“消息模式”來展示這樣事實。前者即股票報價拓撲食一個發布/訂閱模式的例子,而后者,亮度調整拓撲是請求/應答模式的例子。
消息模式既定義了節點間通信使用的協議,還定義了單個節點的功能,比如它用來路由消息的的算法。因此不同模式的行為類似於不同的協議。你不能連接發布/訂閱節點到請求/應答節點,就像你不能連接TCP終端節點到SCTP終端節點一樣。因此每個拓撲僅僅實現了一個單獨的消息模式-沒有方法把兩個不同的消息模式連接為一個單獨的拓撲。
這么嚴格的分離要求拓撲作為一個整體的行為提供保證。只要你知道拓撲里的每個節點都遵循發布/訂閱語義,那么你就能夠提供諸如“消息將分發到這個拓撲里的所有節點”這樣的保證。如果這個拓撲的一部分允許以負載均衡方式而不是廣播方式發布,那么你將不能夠做這樣的保證。還有更糟糕的,由於消息模式是開放的,你將不得不希望節點以完全任意的方式行動,因此你根本不能提供任何保證。
下面是網絡棧圖。注意各個消息模式位於棧的同一層而且相互之間沒有關聯:
考慮到一些傳統的消息系統選擇提供通用的路由基礎框架,這個框架實際上允許用戶在其上構建任何路由算法(例如AMQP模型的交換中心,路由綁定和用戶)而不是分發預打包的消息模式(例如 JMS的主題和隊列),因此說明ØMQ選擇后一選項的基本原理是很重要的。
首先,設計功能完整而且無漏洞的消息模式是一項艱巨的任務。即便把創建模式的責任推卸給用戶,我們仍可以確定構建在消息系統之上的大多數應用在某種程度上說是錯誤的。即使這兒模式得到正確地實現,學習和開發的費用還是超過了多次使用預打包模式所花的費用。終究正如DNS設計一篇早期的文章所說:“[用戶]打算使用而不是理解提供給他們的系統。”
其次,正式定義的模式允許增強兩種模式不能共存在同一個拓撲里的要求。消息系統可核查雙方是否實現了同樣的消息模式,如果沒有實現就拒絕連接。如果所實現的模式是用戶任務,那么這樣的檢查就不做了。
第三、常用的路由框架不能自動發布(也稱為自動聯合)。這意味着只能采用簡單的星形結構運行了,一旦你打算跨越這個模型的話,你不得不提供其他信息,也即是回答“什么是消息模式?”這個問題。看看由AMQP上的各種產品構建的聯合機制。這兒“模式”位總是存在,不管是僅僅顯式地或者是隱含地(支持一種模式)。
最后,我們對AMQP的經驗是:即便它提供了豐富的各種可能的消息模式,人們仍然一而再再而三地在其上構建同樣的一對模式,而且完全忽略了其中的其他模式。
互聯網棧最具有獨創性的特征之一就是對逐跳功能(IP)和端對端功能(TCP,UDP,SCP等等)的清晰划分。正是這種划分允許互聯網生態系統得以發展。如果沒有這樣的划分,每個隊端對端協議的微小更改都將如IPv4到IPv6轉換一般痛苦。
理念是網絡上的每個節點都實現了IP,然而,只有使用特定端對端協議比如TCP的終端節點可以意識到它。換句話說,中間節點比如路由器不需要理解位於IP上方的端對端協議層就可以以所希望的方式工作:
划分IP和TCP層的經驗后來一“端對端論點”的形式得到了普及。端對端論點是這樣描述的:如果功能不能由較低層正確地提供(我們例子中的逐跳層),也就是說這個時候它需要更高層的幫助才能按照所期望的運行,所以首先在較低層實現這個功能沒有什么價值。
ØMQ遵循端對端原理,而且划分自身棧為逐跳層(以"X"開頭的套接字類型笨拙地表示)和端對端層(不以“X”開頭的套接字類型)。注意這與上面的TCP/IP圖類似:
類似於TCP/IP,逐跳層負責路由,而端對端層可以提供其他服務,比如可靠性,加密等。
不過,我們不應當繼續太深入的比如為TCP/IP。不像互聯網棧那樣具有一個單獨的逐跳協議(IP)和多個端對端協議(TCP,UDP,SCTP等),在ØMQ里每個端對端協議都擁有自己的逐跳協議。因此這個棧看起來如下:
具有這樣結構的原因是(由逐跳層提供的)路由功能針對特定的消息模式,因此不能由多個模式共享。如果魔門仍然遇到了共享路由同一路由算法的兩個消息模式,而且將來只有通過端對端功能才能區分,那么我們將能夠模仿出一個單獨的逐跳協議上幾個端對端協義層的互聯網棧模型。
最后,讓我們看一看逐跳與端對端划分的一個具體的例子。
請求應答模式意味着客戶端應用傳遞一割請求給一個工作者應用(在這條道路上運行負載均衡的),工作者應用然后處理這個請求,接着生成應答。然后回傳應答給源客戶端:
逐跳層不得不做的事情是發送每個請求給一個上游節點(執行負載均衡),然后發送應答給接收到的與其相關的請求發送的下游節點。
一切運行良好,直到處理請求的工作者失敗或者可能整體拓撲由於網絡失效而離線 。這種情況下,客戶端將永遠卡在這兒,來等待從來不會出現的應答。
為了解決這個問題,客戶端可以等待特定數量的時間,如果到那時應答還沒有到達,那么重新發送這個請求。這還不得不過濾掉延遲的重復的應答。
現在重新回想一下端對端的論點。如果沒有終端節點的幫助,重發功能不可能實現,因此在逐跳層實現重發功能(保存各種可能的優化,比如在磁盤上保存這個請求,然后網失效的節點重新啟動的時候重新發送這個請求)沒有什么意義。
我們得到的最終結果是路由在逐跳層實現,可靠性在逐跳層上的端對端層里實現。
名字解析
現在,ØMQ不能為自己提供名字。為了加入到拓撲里,你不得不使用底層傳輸定義的地址,比如IP地址和TCP端口。
雖然現在是這樣,但將來值得提供把拓撲名字(”ØMQ名字“)轉換為底層傳輸地址的名字解析服務 。例如,字符串"Brightness-Adjustment-Service"可解析為"tcp://192.168.1.111:5555"。
關於這個問題還有許多要思考,不過主要問題似乎是拓撲是由多個節點組成的,而且名字解析服務選擇這些節點之一。決定應當可能是基於管理標准的。例如,當連接到“納斯達克股票報價"拓撲的時候,你需要名字服務連接你到本地的股票報價中心而不是連接到納斯達克自身的報價中心,或者甚至最糟糕的情形,連接你到競爭者的股票報價中心。
技術上來說,名字解析服務應當使用DNS實現,因為DNS是唯一一個廣泛可用的分布式數據庫。而且這兒的名字解析服務的需求似乎完美的匹配DNS所提供的特性。
像DNS這樣以松散地一致性分布式數據庫方式存儲名字的設計結果應當引起注意,比如拓撲應該是長期存活並且很少更改條目,以確保DNS緩沖機制不能損壞名字的解析等等。
附錄:設計原則
附錄總結了用來評估某個特定的消息模式是否是良好設計的原則。
統一性原則
統一性原則描述的是你連接應用到拓撲的哪一個節點應該是無關緊要的。提供的服務應當是相同的。
統一性原則聽起來很明顯,不過破壞它卻非常容易。考慮一下ØMQ里目前已經實現的發布/訂閱模式。 它允許一個拓撲里有多個發布者,這引入了非統一性:
在上圖中,客戶端C將看到不同的數據反饋,這取決於它連接到中間者1還是中間者2 。中間者1既提供發布者A的消息也提供發布者B的消息,而中間者2只提供來自發布者B的消息。
注意統一性原則是怎樣成為互聯網設計里的重要原則之一的:不管你把你筆記本插入到那個插頭里,你使用的是哪種wi-fi或者給你提供互聯網訪問的是哪個ISP,你看到的世界總是相同的。
可擴展性原則
可擴展性原則描述的是當拓撲要么因為節點超負荷要么因為連接擁擠而不能處理負載時,通過給這個拓撲增加新的節點應當可能解決這個問題。而且增加的節點數是隨着負載線性增長的。
讓我們看一看違反可擴展性原則的模式。最簡單的非可擴展性模式是分割一個應用為固定數目的功能塊。想象一個由賬務和人力資源功能組合的大應用。這兒,一個單獨的盒子不能處理這樣的負載,編程人員也許決定分割為賬務功能和人力資源功能為獨立執行部分,這樣它們運行在兩個盒子上:
這樣的設計無法滿足可擴展性測試。當兩個盒子不能處理負載時,在不重寫應用的情況下,沒有辦法增加第三個盒子。
注意:ØMQ里的模式是由一對套接字來表示的。
非可擴展性模式的一個更復雜的例子是分布式日志記錄:
隨着記錄日志的應用數目的增長,日志記錄器的負載就增加,直到它不能處理負載為止。在應用與日志記錄器之間增加中間節點不能真正地解決這個問題。無論是否有中間節點,所有的信息都不得不到達日志記錄器,因此無論如何在某一刻日志記錄器就會爆。
為了使這種“數據收集”的模式可擴展,中間節點不得不聚集信息,即發送固定數量的信息到下游,並且與上游應用的數目無關。這種聚集采用了計算總數、傳遞統計而不是像以上所說傳遞消息等。這種聚集模式在發布/訂閱模式里用來前傳訂閱的。通過控制訂閱請求的絕對數量使得發布者不會超負荷運行,請求會聚集到中間節點,並且只有增量的那部分才進一步發送給上游(這個算法的詳細討論請參考這兒)。
再次提醒,注意互聯網是怎樣遵循可擴展性原則的。可在任何時候增加新節點,不管是最終用戶的盒子還是中間框架,並且不會損害整體互聯網的功能和性能。
插入原則
插入原則描述的是向拓撲里插入中間節點應當不會更改終端節點的行為。(注意插入中間節點可用來擴展這個拓撲。具體的例子可以看看這兒)
讓我們看一個經常請求確定連接到一個特定終端節點的對等節點數的特性的例子。我們看一看下面的轉換,這兒中間節點I是插入到這個拓撲的:
正如所看到的,終端節點A的行為在中間節點插入到這個拓撲的時候更改了。不是匯報三個對等節點,現在它匯報了兩個對等節點。因此展示連接的對等節點數破壞了插入原則。
再次說明,插入原則對互聯網運行的方式來說十分重要。如果在中間更改了拓撲-即當骨干網操作員增加了新的路由器- 將破環終端節點上的應用,那么互聯網將以非常快的速度陷入到崩塌的狀態之中。
結論
這篇文章里所描述的結構展示的是包含兩個意外的ØMQ的當前設計,這兒的兩個意外指的是正在開發的特性。希望這篇文章提供了分布式消息的簡短介紹,並為這個領域里的將做的工作打好基礎。