本文主要是探究學習比較流行的一款消息層是如何設計與實現的 ØMQ是一種消息傳遞系統,或者樂意的話可以稱它為“面向消息的中間件”。它在金融服務,游戲開發,嵌入式系統,學術研究和航空航天等多種環境中被使用。 消息傳遞系統基本上像應用程式的即時消息一樣工作。應用程式決定將事件傳送到另一個應用程式(或多個應 ...
本文主要是探究學習比較流行的一款消息層是如何設計與實現的
ØMQ是一種消息傳遞系統,或者樂意的話可以稱它為“面向消息的中間件”。它在金融服務,游戲開發,嵌入式系統,學術研究和航空航天等多種環境中被使用。
消息傳遞系統基本上像應用程式的即時消息一樣工作。應用程式決定將事件傳送到另一個應用程式(或多個應用程式),它組裝要發送的數據,點擊“發送”按鈕,消息傳遞系統負責其餘的事情。然而,與即時消息傳遞不同,消息傳遞系統沒有GUI,並且在出現問題時,在端點處沒有人能夠進行智能幹預。 因此,消息系統必須是容錯的並且比常見的即時消息傳送快得多。
- ØMQ最初被構想用於是一個針對股票交易的極速的消息傳遞系統,所以重點是極端優化。該項目的第一年用於設計基準方法,並嘗試定義一個儘可能高效的架構。
- 後來,大約在第二年的發展時,重點轉向了提供一個通用系統,該系統用於構建分散式應用程式和支持任意消息模式,多種傳輸機制,任意語言綁定等。
- 在第三年,重點主要是提高可用性和扁平化學習曲線。 我們採用了BSD套接字API,試圖清除單個消息模式的語義,等等。
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