本文主要是探究學習比較流行的一款消息層是如何設計與實現的程序員
ØMQ是一種消息傳遞系統,或者樂意的話能夠稱它爲「面向消息的中間件」。它在金融服務,遊戲開發,嵌入式系統,學術研究和航空航天等多種環境中被使用。算法
消息傳遞系統基本上像應用程序的即時消息同樣工做。應用程序決定將事件傳送到另外一個應用程序(或多個應用程序),它組裝要發送的數據,點擊「發送」按鈕,消息傳遞系統負責其他的事情。然而,與即時消息傳遞不一樣,消息傳遞系統沒有GUI,而且在出現問題時,在端點處沒有人可以進行智能干預。 所以,消息系統必須是容錯的而且比常見的即時消息傳送快得多。緩存
- ØMQ最初被構想用因而一個針對股票交易的極速的消息傳遞系統,因此重點是極端優化。該項目的第一年用於設計基準方法,並嘗試定義一個儘量高效的架構。
- 後來,大約在第二年的發展時,重點轉向了提供一個通用系統,該系統用於構建分佈式應用程序和支持任意消息模式,多種傳輸機制,任意語言綁定等。
- 在第三年,重點主要是提升可用性和扁平化學習曲線。 咱們採用了BSD套接字API,試圖清除單個消息模式的語義,等等。
本文將深刻了解上述三個目標如何轉化爲ØMQ的內部架構,併爲那些正在努力解決相同問題的人提供一些提示或技巧。
從第三年開始,ØMQ它的代碼庫已經增加地過大; 因此有一個倡議來標準化其使用的有線協議,以及在Linux內核中實驗性地實現一個相似ØMQ的消息系統等。這些主題在這裏就不涉及了。 可是,你能夠獲取在線資源( online resources)以獲取更多詳細信息。
Application vs. Library
ØMQ是一個消息庫,而不是一個消息服務器。咱們花了幾年時間研究AMQP協議(一個金融行業嘗試標準化企業消息傳遞的有線協議),爲其編寫參考實現並參與了好幾個大規模的基於消息傳遞技術的大型項目,並最終意識到意識到使用經典客戶端/服務器模型的智能消息傳遞服務器(代理)和啞消息傳遞客戶端的方法有問題。服務器
咱們首要關注的是性能:若是中間有一個服務器,每一個消息必須經過網絡兩次(從發送方到代理,從代理到接收方),這在延遲和吞吐量方面都會有必定代價。 此外,若是全部消息都經過代理傳遞,在某一時刻,服務器必然成爲瓶頸。 網絡
次要關注的是大規模部署:當部署跨組織(如:公司等)時,管理整個消息流的中央受權的概念再也不適用。因爲商業祕密和法律責任,沒有公司願意將控制權交給不一樣公司的服務器。在實踐中的結果是,每一個公司有一個消息服務器,用橋接器鏈接到其餘公司的消息傳遞系統。整個系統所以嚴重分散,而且爲每一個涉及的公司維護大量的橋接器不會使狀況更好。爲了解決這個問題,咱們須要一個徹底分佈式的架構,該架構中每一個組件均可能由不一樣的業務實體控制。考慮到基於服務器的架構中的管理單元是服務器,咱們能夠經過爲每一個組件安裝單獨的服務器來解決上述問題。在這種狀況下,咱們能夠經過使服務器和組件共享相同的進程來進一步優化設計。這樣咱們最終獲得一個消息庫。 多線程
ØMQ開始時,咱們有一個想法,即如何使消息工做沒有中央服務器。 它須要將消息的整個概念顛倒過來,而且基於端到端原則,使用「智能端點,啞網絡」架構來替換自主集中存儲網絡中心的消息的模型。 這個決定的技術將決定ØMQ從一開始就是是一個消息庫,而不是一個應用程序。
咱們已經可以證實這種架構比標準方法更高效(更低的延遲,更高的吞吐量)和更靈活(很容易構建任意複雜的拓撲,而不是限定爲經典的hub-and-spoke模型)。架構
其中一個出乎意料的結果是,選擇庫模型改善了產品的可用性。 一次又一次,用戶因沒必要安裝和管理獨立的消息服務器而感到開心。 事實證實,沒有服務器是一個首選項,由於它下降了運營成本(不須要有一個消息服務器管理員),並加快上線時間(無需與客戶協商是否運行服務器,以及管理或運營團隊的問題) 。併發
學到的教訓是,當開始一個新的項目時,若是可能的話應該選擇庫設計。從一個簡單的程序調用庫能夠很容易建立一個應用程序; 然而,幾乎不可能從現有的可執行文件建立庫。 庫模型爲用戶提供了更多的靈活性,同時節省了他們沒必要要的管理工做。
Global State
全局變量不能很好地與庫交互。 即便只有一組全局變量,庫可能在進程中也會加載屢次。 圖1顯示了一個從兩個不一樣的獨立庫中使用的ØMQ庫的狀況。 而後應用程序使用這兩個庫的示例框架
圖1: ØMQ 庫在兩個不一樣的獨立庫中被使用異步
當這種狀況發生時,ØMQ的兩個實例訪問相同的變量,致使競態條件,奇怪的錯誤和未定義的行爲。爲了防止這個問題的出現,ØMQ庫中沒有全局變量。相反,庫的用戶負責顯式地建立全局狀態變量。包含全局狀態的對象稱爲context。 雖然從用戶的角度來看,context看起來或多或少像一個工做線程池,但從ØMQ的角度來看,它只是一個存儲任何咱們碰巧須要的全局狀態的對象。在上圖中,libA有本身的context,libB也有本身的context。沒有辦法讓他們中的一個破壞或顛覆另外一個。
這裏的教訓很明顯:不要在庫中使用全局狀態。若是你這樣作,當它剛好在同一個進程中被實例化兩次時,庫極可能會被中斷。
Performance
當ØMQ項目啓動時,其主要目標是優化性能。 消息傳遞系統的性能使用兩個度量來表示:吞吐量 - 在給定時間內能夠傳遞多少消息; 延遲 - 消息從一個端點到另外一個端點須要多長時間。
咱們應該關注哪一個指標? 二者之間的關係是什麼? 不是很明顯嗎? 運行測試,將測試的總時間除以傳遞的消息數,獲得的是延遲。 單位時間內的消息數是吞吐量。 換句話說,延遲是吞吐量的逆值。 簡單,對吧?
咱們花了幾個星期詳細評估性能指標而不是當即開始編碼,從而發現吞吐量和延遲之間的關係遠沒有那麼簡單,並且是與直覺相反的。
想象A發送消息到B(參見圖2)。 測試的總時間爲6秒。 有5個消息已經過。 所以,吞吐量爲0.83個消息/秒(5/6),延遲爲1.2秒(6/5),對嗎?
圖二:從A發送消息到B
再看看圖二。 每一個消息從A到B須要不一樣的時間:2秒,2.5秒,3秒,3.5秒,4秒。 平均值是3秒,這與咱們原來計算的1.2秒相差很大。 這個例子顯示了人們對性能指標直觀傾向的誤解。
如今來看看吞吐量。 測試的總時間爲6秒。 然而,對於A而言,它只須要2秒就能夠發送完全部的消息。 從A的角度來看,吞吐量爲2.5 msgs / sec(5/2)。 對於B而言,接收全部消息須要4秒。 因此從B的角度來看,吞吐量爲1.25 msgs / sec(5/4)。 這些數字都不符合咱們原來計算的1.2 msgs / sec的結果。
長話短說:延遲和吞吐量是兩個不一樣的指標; 這很明顯。重要的是要了解二者之間的差別及其關係。延遲只能在系統中的兩個不一樣點之間度量; 單獨在點A處沒有延遲的概念。每一個消息具備其本身的延遲。你能夠獲得多個消息的平均延遲; 而消息流是沒有延遲的。
另外一方面,只能在系統的單個點處測量吞吐量。發送端有一個吞吐量,接收端有一個吞吐量,二者之間的任何中間點都有一個吞吐量,可是沒有整個系統的總體吞吐量。而吞吐量只對一組消息有意義; 沒有單個消息的吞吐量的概念。
至於吞吐量和延遲之間的關係,事實證實真的有一種關係; 然而,公式涉及積分,咱們不會在這裏討論它。 有關更多信息,請閱讀有關排隊理論的文獻。 在基準化消息系統中有不少的陷阱,咱們不會進一步深刻。 咱們應該把精力放在學到的教訓上:確保你理解你正在解決的問題。 即便一個簡單的問題,「讓程序更快」也須要大量的工做才能正確理解。 更重要的是,若是你不理解這個問題,你可能會在你的代碼中構建隱式假設和流行的神話,使得解決方案有缺陷,或者至少要複雜得多或者比可能的少。
Critical Path
咱們在優化過程當中發現三個因素對性能有相當重要的影響:
- 內存分配數
- 系統調用數
- 併發模型
然而,不是每一個內存分配或每一個系統調用對性能有相同的影響。咱們對消息傳遞系統感興趣的性能是在給定時間內咱們能夠在兩個端點之間傳輸的消息數。或者,咱們可能感興趣的是消息從一個端點到另外一個端點須要多長時間。
然而,鑑於ØMQ是爲具備長鏈接的場景設計的,創建鏈接所需的時間或處理鏈接錯誤所需的時間基本上是不相關的。這些事件不多發生,所以它們對總體性能的影響能夠忽略不計。
一個代碼庫的反覆頻繁使用的部分被稱爲關鍵路徑; 優化應該關注關鍵路徑。
讓咱們看看一個例子:ØMQ並無在內存分配方面進行極大優化。例如,當操做字符串時,它一般爲轉換的每一箇中間階段分配一個新字符串, 可是,若是咱們嚴格查看關鍵路徑(實際的消息傳遞),咱們會發現它幾乎不使用內存分配。若是消息很小,則每256個消息只有一個內存分配(這些消息保存在一個大的分配的內存塊中)。此外,若是消息流穩定,沒有巨大的流量峯值,則關鍵路徑上的內存分配數量將降至零(已分配的內存塊不會返回到系統,而是重複使用)。
經驗教訓:優化產生顯著差別的地方。優化不在關鍵路徑上的代碼段是是無效的。
Allocating Memory
假設全部基礎設施都已初始化,而且兩個端點之間的鏈接已創建,則在發送消息時只須要爲一個東西分配內存:消息自己。所以,爲了優化關鍵路徑,咱們必須研究如何爲消息分配內存並在堆棧中上下傳遞。
在高性能網絡領域中的常識是,經過仔細平衡消息分配內存的成本和消息複製的成本(例如,對小,中和大消息的不一樣處理)來實現最佳性能。對於小消息,複製比分配內存要代價小。根本不分配新的存儲器塊,而是在須要時將消息複製到預分配的存儲器是有意義的。另外一方面,對於大消息,複製比內存分配代價大。將消息分配一次,並將指針傳遞到分配的塊,而不是複製數據是有意義的。這種方法稱爲「零拷貝」。
ØMQ以透明的方式處理這兩種狀況。 ØMQ消息由不透明句柄表示。 很是小的消息的內容直接編碼在句柄中。 所以,複製句柄實際上覆制了消息數據。當消息較大時,它被分配在單獨的緩衝區中,而且句柄僅包含指向緩衝區的指針。建立句柄的副本不會致使複製消息數據,這在消息是兆字節長時是有意義的(圖3)。 應當注意,在後一種狀況下,緩衝器被引用計數,使得其能夠被多個句柄引用,而不須要複製數據。
圖三:消息拷貝(或沒有消息拷貝)
經驗教訓:在考慮性能時,不要假設有一個單一的最佳解決方案。可能發生的是,存在問題的多個子類(例如,小消息 vs. 大消息),每一個都具備其本身的最佳算法。
Batching
已經提到,消息系統中的必定系統調用的數量可能致使性能瓶頸。其實,這個問題比那個更廣泛。 遍歷調用堆棧相關時會有不小的性能損失,所以,當建立高性能應用程序時,避免儘量多的堆棧遍歷是明智的。
考慮圖4.要發送四個消息,你必須遍歷整個網絡棧四次(ØMQ,glibc,用戶/內核空間邊界,TCP實現,IP實現,以太網層,NIC自己和從新備份棧)。
圖四:發送四個消息
可是,若是您決定將這些消息合併到單個批消息中,則只有一次遍歷堆棧(圖5)。對消息吞吐量的影響多是很是顯著的:高達兩個數量級,特別是若是消息很小,而且其中幾百個能夠打包成一個批消息時。
圖五:Batching messages
另外一方面,批量化會對延遲產生負面影響。讓咱們舉個例子,知名的Nagle算法,在TCP中實現。它將出站消息延遲必定量的時間,並將全部累積的數據合併到單個數據包中。顯然,分組中的第一消息的端到端等待時間比最後一個的等待時間多得多。所以,對於須要得到一致的低延遲來關閉Nagle算法的應用程序來講,這是很常見的。甚至經常在堆棧的全部層次上關閉批量化(例如,NIC的中斷合併功能)。可是沒有批量化意味着大量遍歷堆棧並致使低消息吞吐量。咱們彷佛陷入了權衡吞吐量和延遲的困境。
ØMQ嘗試使用如下策略提供一致的低延遲和高吞吐量:當消息流稀疏而且不超過網絡堆棧的帶寬時,ØMQ關閉全部批量化以提升延遲。這裏的權衡在某種程度上是會使CPU使用率變高(咱們仍然須要常常遍歷堆棧)。 然而,這在大多數狀況下不被認爲是問題。
當消息速率超過網絡棧的帶寬時,消息必須排隊(存儲在存儲器中),直到棧準備好接受它們。排隊意味着延遲將增加。若是消息在隊列中花費了一秒鐘,則端到端延遲將至少爲1秒。 更糟糕的是,隨着隊列的大小增長,延遲將逐漸增長。若是隊列的大小沒有限制,則延遲可能會超過任何限制。
已經觀察到,即便網絡堆棧被調到儘量低的延遲(Nagle的算法被關閉,NIC中斷合併被關閉,等等),因爲排隊效應,延遲仍然多是使人沮喪的,如上所述。
在這種狀況下,大量開始批量化處理是有意義的。沒有什麼會丟失,由於延遲已經很高。另外一方面,大量的批處理提升了吞吐量,而且能夠清空未完成消息的隊列 - 這反過來意味着等待時間將隨着排隊延遲的減小而逐漸下降。一旦隊列中沒有未完成的消息,則能夠關閉批量化處理,以進一步改善延遲。
另外一個觀察是,批量化只應在最高層次進行。 若是消息在那裏被批量化,則較低層不管如何都不須要批處理,所以下面的全部分批算法不作任何事情,除了引入附加的等待時間。
經驗教訓:爲了在異步系統中得到最佳吞吐量和最佳響應時間,請關閉堆棧的最底層上的批量化算法而且在在最高層次進行批量化。只有當新數據的到達速度比可處理的數據快時才進行批量化處理。
Architecture Overview
到目前爲止,咱們專一於使ØMQ快速的通用原則。如今,讓咱們看看系統的實際架構(圖6)。
圖六:ØMQ architecture
用戶使用所謂的「sockets」與ØMQ交互。 它們很是相似於TCP套接字,主要的區別是每一個套接字能夠處理與多個對等體的通訊,有點像未綁定的UDP套接字。
套接字對象存在於用戶線程中(參見下一節中的線程模型的討論)。除此以外,ØMQ運行多個工做線程來處理通訊的異步部分:從網絡讀取數據,排隊消息,接受接入鏈接等。
在工做線程中存在各類對象。每一個對象都由一個父對象擁有(全部權由圖中的簡單實線表示)。父對象能夠在與子對象不一樣的線程中。大多數對象直接由套接字擁有; 然而,有幾種狀況下,對象由套接字擁有的對象所擁有。 咱們獲得的是一個對象樹,每一個套接字有一個這樣的樹。 這種樹在關閉期間使用; 沒有對象能夠本身關閉,直到它關閉全部的子對象。 這樣咱們能夠確保關機過程按預期工做; 例如,等待的出站消息被推送到網絡優先於結束髮送過程。
大體來講,有兩種異步對象:在消息傳遞中不涉及的對象和另一些對象。前者主要作鏈接管理。例如,TCP偵聽器對象偵聽傳入的TCP鏈接,併爲每一個新鏈接建立引擎/會話對象。相似地,TCP鏈接器對象嘗試鏈接到TCP對等體,而且當它成功時,它建立一個引擎/會話對象來管理鏈接。 當此類鏈接失敗時,鏈接器對象嘗試從新創建鏈接。
後者是正在處理數據傳輸自己的對象。 這些對象由兩部分組成:會話對象負責與ØMQ套接字交互,引擎對象負責與網絡通訊。 只有一種會話對象,可是對於ØMQ支持的每一個底層協議有不一樣的引擎類型。 所以,咱們有TCP引擎,IPC(進程間通訊)引擎,PGM引擎(可靠的多播協議,參見RFC 3208)等。引擎集是可擴展的 (在未來咱們能夠選擇實現 WebSocket引擎或SCTP引擎)。
會話與套接字交換消息。 有兩個方向傳遞消息,每一個方向由管道對象處理。每一個管道基本上是一個優化的無鎖隊列,用於在線程之間快速傳遞消息。
最後,有一個context對象(在前面的部分中討論,但沒有在圖中顯示),它保存全局狀態,而且能夠被全部的套接字和全部的異步對象訪問。
Concurrency Model
ØMQ的要求之一是利用計算機的多核; 換句話說,能夠根據可用CPU內核的數量線性擴展吞吐量。
咱們之前的消息系統經驗代表,以經典方式使用多個線程(臨界區,信號量等)不會帶來不少性能改進。 事實上,即便在多核上測量,消息系統的多線程版本可能比單線程版本慢。 單獨的線程花費太多時間等待對方,同時引起了大量的上下文切換,從而使系統減速。
考慮到這些問題,咱們決定採用不一樣的模式。 目標是避免徹底鎖定,讓每一個線程全速運行。 線程之間的通訊是經過在線程之間傳遞的異步消息(事件)提供的。 這正是經典的Actor模型。
這個想法的思想是爲每一個CPU核心啓動一個工做線程(有兩個線程共享同一個核心只會意味着不少上下文切換沒有特別的優點)。每一個內部ØMQ對象,好比說,一個TCP引擎,將綁定到一個特定的工做線程。 這反過來意味着不須要臨界區,互斥體,信號量等。 此外,這些ØMQ對象不會在CPU核心之間遷移,從而避免高速緩存污染對性能的負面影響(圖7)
圖七:Multiple worker threads
這個設計使不少傳統的多線程問題消失了。 然而,須要在許多對象之間共享工做線程,這反過來意味着須要某種協做多任務。 這意味着咱們須要一個調度器; 對象須要是事件驅動的,而不是控制整個事件循環。 也就是說,咱們必須處理任意事件序列,即便是很是罕見的事件,咱們必須確保沒有任何對象持有CPU太長時間; 等等
簡而言之,整個系統必須徹底異步。 沒有對象能夠作阻塞操做,由於它不只會阻塞自身,並且會阻塞共享同一個工做線程的全部其餘對象。 全部對象必須成爲狀態機,不管是顯式仍是隱式。 有數百或數千個狀態機並行運行,你就必須處理它們之間的全部可能的交互,而且最重要的是關閉過程。
事實證實,以乾淨的方式關閉徹底異步系統是一個很是複雜的任務。 試圖關閉一千個移動部件,其中一些工做,一些空閒,一些在啓動過程當中,其中一些已經自行關閉,容易出現各類競態條件,資源泄漏和相似狀況。 關閉子系統絕對是ØMQ中最複雜的部分。 對Bug跟蹤器的快速檢查代表,大約30%-50%的報告的錯誤與以某種方式關閉相關。
得到的經驗:在努力實現最佳性能和可擴展性時,請考慮actor模型; 它幾乎是這種狀況下惟一的方法。 可是,若是你不使用像Erlang或ØMQ這樣的專用系統,你必須手工編寫和調試大量的基礎設施。 此外,從一開始,想一想關閉系統的過程。 它將是代碼庫中最複雜的部分,若是你不清楚如何實現它,你應該能夠從新考慮使用actor模型。
Lock-Free Algorithms
無鎖算法最近一直流行起來。 它們是線程間通訊的簡單機制,它不依賴於內核提供的同步原語,例如互斥體或信號量; 相反,它們使用原子CPU操做(諸如原子compare-and-swap(CAS))來進行同步。 應當理解,它們不是字面上無鎖的,而是在硬件級別的幕後進行鎖定。
ØMQ在管道對象中使用無鎖隊列在用戶的線程和ØMQ的工做線程之間傳遞消息。 ØMQ如何使用無鎖隊列有兩個有趣的方面。
首先,每一個隊列只有一個寫線程和一個讀線程。 若是須要1對N通訊,則建立多個隊列(圖8)。 考慮到這種方式,隊列沒必要關心同步寫入器(只有一個寫入器)或讀取器(只有一個讀取器),它能夠以額外的高效方式實現。
圖八:Queues
第二,咱們意識到雖然無鎖算法比傳統的基於互斥的算法更高效,但原子CPU操做仍然代價較高(尤爲是在CPU核心之間存在爭用時),而且對每一個寫入的消息和/或每一個消息執行原子操做讀的速度比咱們能接受的要慢。
加快速度的方法是再次批量處理。 想象一下,你有10條消息要寫入隊列。 例如,當收到包含10條小消息的網絡包時,可能會發生這種狀況。 接收分組是原子事件; 因此你不會只獲得一半。 這個原子事件致使須要向無鎖隊列寫入10條消息。 對每條消息執行原子操做沒有太多意義。 相反,能夠在隊列的「預寫」部分中累積消息,該部分僅由寫入程序線程訪問,而後使用單個原子操做刷新它。
這一樣適用於從隊列讀取。 想象上面的10個消息已經刷新到隊列。 閱讀器線程可使用原子操做從隊列中提取每一個消息。 然而,它是超殺; 相反,它可使用單個原子操做將全部未決消息移動到隊列的「預讀」部分。 以後,它能夠逐個從「預讀」緩衝區檢索消息。 「預讀」僅由讀取器線程擁有和訪問,所以在該階段不須要任何同步。
圖9左側的箭頭顯示瞭如何經過修改單個指針能夠將預寫緩衝區刷新到隊列。 右邊的箭頭顯示了隊列的整個內容如何能夠經過不作任何事情而修改另外一個指針來轉移到預讀。
圖九:Lock-free queue
得到的教訓:無鎖算法很難發明,麻煩執行,幾乎不可能調試。 若是可能,請使用現有的成熟算法,而不是發明本身的。 當須要最佳性能時,不要僅依賴無鎖算法。 雖然它們速度快,但經過在它們之上進行智能批處理能夠顯着提升性能。
API
用戶接口是任何產品的最重要的部分。 這是你的程序中惟一能夠看到的外部世界。 在最終用戶產品中,它是GUI或命令行界面。 在庫中它是API。
在早期版本的ØMQ中,API基於AMQP的交換和隊列模型。 (參見AMQP specification。)從歷史的角度看,有趣的是看看2007年的白皮書(white paper from 2007),它試圖權衡AMQP與無代理的消息模型。 我花了2009年年末重寫它幾乎從零開始使用BSD套接字API。 這是轉折點; ØMQ從那時起就被快速採用。 雖然以前它是一個被一羣消息專家使用的niche產品,後來它成爲任何人的一個方便的常見工具。 在一年多的時間裏,社區的規模增長了十倍,實現了約20種不一樣語言的綁定等。
用戶接口定義產品的感知。 基本上沒有改變功能 - 只是經過更改API - ØMQ從「企業消息傳遞系統」產品更改成「網絡消息傳遞系統」產品。 換句話說,感受從「大型銀行的一個複雜的基礎設施」改變爲「嗨,這有助於我將個人10字節長的消息從應用程序A發送到應用程序B」。
得到的經驗:瞭解您想要的項目是什麼,並相應地設計用戶接口。 不符合項目願景的用戶接口是100%要失敗的。
遷移到BSD Sockets API的一個重要方面是,它不是一個革命性的新發明的API,而是一個現有的和知名的。 實際上,BSD套接字API是今天仍在使用的最古老的API之一; 它可追溯到1983年和4.2BSD Unix。 它被普遍穩定了使用幾十年。
上述事實帶來了不少優勢。 首先,它是一個你們都知道的API,因此學習曲線很是短。 即便你歷來沒有據說過ØMQ,你能夠在幾分鐘內構建你的第一個應用程序,由於你可以重用你的BSD套接字知識。
此外,使用普遍實現的API能夠實現ØMQ與現有技術的集成。 例如,將ØMQ對象暴露爲「套接字」或「文件描述符」容許在同一事件循環中處理TCP,UDP,管道,文件和ØMQ事件。 另外一個例子:實驗項目給Linux內核帶來相似ØMQ的功能,實現起來很簡單。 經過共享相同的概念框架,它能夠重用許多已經到位的基礎設施。
最重要的是,BSD套接字API已經存活了近三十年,儘管屢次嘗試更換它意味着在設計中有一些固有的合理的地方。 BSD套接字API設計者已經(不管是故意仍是偶然) 作出了正確的設計決策。 經過採用這套API,咱們能夠自動共享這些設計決策,甚至能夠不知道他們是什麼,他們要解決什麼問題。
經驗教訓:雖然代碼重用已經從好久前獲得重視而且模式重用在後來被加以考慮,但重要的是以更通用的方式考慮重用。 在設計產品時,請看看相似的產品。 檢查哪些失敗,哪些已成功; 從成功的項目中學習。 Don't succumb to Not Invented Here syndrome。 重用思想,API,概念框架,以及不管你以爲合適的東西。 經過這樣作,能夠作到容許用戶重用他們現有的知識。 同時,可能會避免目前還不知道的技術陷阱。
Messaging Patterns
在任何消息系統中,最重要的設計問題是如何爲用戶提供一種方式來指定哪些消息被路由到哪些目的地。 有兩種主要方法,我認爲這種二分法是很是通用的,而且適用於基本上在軟件領域遇到的任何問題。
一種方法是採用UNIX的「作一件事,並作好」的哲學。 這意味着,問題領域應該被人爲地限制在一個小的而且易於理解的區域。 而後程序應該以正確並詳盡的方式解決這個限制的問題。 消息傳遞領域中的這種方法的示例是MQTT。 它是一種用於向一組消費者分發消息的協議。 它不能用於任何其餘用途(好比說RPC),但它很容易使用,而且用作消息分發很好。
另外一種方法是關注通用性並提供強大且高度可配置的系統。 AMQP是這樣的系統的示例。 它的隊列和交換的模型爲用戶提供了幾乎任何路由算法定義的方法。 固然,權衡,須要關心不少選項。
ØMQ選擇前一個模型,由於它容許基本上任何人使用最終產品,而通用模型須要消息傳遞專家使用它。 爲了演示這一點,讓咱們看看模型如何影響API的複雜性。 如下是在通用系統(AMQP)之上的RPC客戶端的實現:
1 connect ("192.168.0.111")
2 exchange.declare (exchange="requests", type="direct", passive=false,
3 durable=true, no-wait=true, arguments={})
4 exchange.declare (exchange="replies", type="direct", passive=false,
5 durable=true, no-wait=true, arguments={})
6 reply-queue = queue.declare (queue="", passive=false, durable=false,
7 exclusive=true, auto-delete=true, no-wait=false, arguments={})
8 queue.bind (queue=reply-queue, exchange="replies",
9 routing-key=reply-queue)
10 queue.consume (queue=reply-queue, consumer-tag="", no-local=false,
11 no-ack=false, exclusive=true, no-wait=true, arguments={})
12 request = new-message ("Hello World!")
13 request.reply-to = reply-queue
14 request.correlation-id = generate-unique-id ()
15 basic.publish (exchange="requests", routing-key="my-service",
16 mandatory=true, immediate=false)
17 reply = get-message ()
另外一方面,ØMQ將消息傳遞分爲所謂的「消息模式」。 模式的示例是「發佈/訂閱」,「請求/回覆」或「並行化流水線」。 每一個消息模式與其餘模式徹底正交,而且能夠被認爲是一個單獨的工具。
如下是使用ØMQ的請求/回覆模式從新實現上述應用程序。 注意如何將全部選項調整減小到選擇正確的消息模式(「REQ」)的單一步驟:
1 s = socket (REQ)
2 s.connect ("tcp://192.168.0.111:5555")
3 s.send ("Hello World!")
4 reply = s.recv ()
到目前爲止,咱們認爲具體的解決方案比通用解決方案更好。咱們但願咱們的解決方案儘量具體。然而,同時,咱們但願爲咱們的客戶提供儘量普遍的功能。咱們如何才能解決這個明顯的矛盾?
答案包括兩個步驟:
- 定義堆棧的層以處理特定問題區域(傳輸,路由,呈現等)。
- 提供該層的多個實現。對於每一個用例應該有一個單獨的不相交的實現。
讓咱們來看看Internet棧中傳輸層的例子。它意味着在網絡層(IP)的頂部上提供諸如傳送數據流,應用流控制,提供可靠性等的服務。它經過定義多個不相交解決方案:TCP面向鏈接的可靠流傳輸,UDP無鏈接不可靠數據包傳輸,SCTP傳輸多個流,DCCP不可靠鏈接等。
注意每一個實現是徹底正交的:UDP端點不能說TCP端點。 SCTP端點也不能與DCCP端點通訊。這意味着新的實現能夠在任什麼時候候添加到堆棧,而不會影響堆棧的現有部分。相反,失敗的實現能夠被忘記和丟棄而不損害做爲總體的傳輸層的可行性。
相同的原則適用於由ØMQ定義的消息模式。消息模式在傳輸層(TCP和朋友)之上造成層(所謂的「可伸縮性層」)。單獨的消息模式是該層的實現。它們是嚴格正交的 - 發佈/訂閱端點不能說請求/回覆端點等。模式之間的嚴格分離意味着能夠根據須要添加新模式,而且失敗的新模式的實驗贏得「不利於現有模式。
得到的經驗:在解決複雜和多方面的問題時,可能會發現單一通用解決方案可能不是最好的解決方法。相反,咱們能夠將問題區域看做一個抽象層,並提供該層的多個實現,每一個集中在一個特定的定義良好的用例。在這樣作時,請仔細描述用例。確保範圍,什麼不在範圍內。太明顯地限制用例,應用程序可能會受到限制。然而,若是定義的問題太寬泛,產品可能變得太複雜,模糊,並使用戶產生混淆。
Conclusion
隨着咱們的世界變得充滿了許多經過互聯網鏈接的小型計算機 - 移動電話,RFID閱讀器,平板電腦和筆記本電腦,GPS設備等 - 分佈式計算的問題再也不是學術科學的領域,而且成爲常見的平常問題 爲每一個開發者解決。 不幸的是,解決方案主要是具體領域的hacks。 本文總結了咱們系統地構建大規模分佈式系統的經驗。 關注從軟件架構的角度來看有趣的問題,但願開源社區的設計師和程序員會發現它有用。
MartinSústrik是消息傳遞中間件領域的專家。 他參與了AMQP標準的建立和參考實施,並參與了金融行業的各類消息傳遞項目。 他是ØMQ項目的創始人,目前正在致力於將消息傳遞技術與操做系統和Internet棧進行集成。 本文摘自並修改自《The Architecture of Open Source Applications: Volume II》。
原文連接:ZeroMQ: The Design of Messaging Middleware