以ZeroMQ談訊息中介軟體的設計

來源:互聯網
上載者:User

本文主要是探究學習比較流行的一款訊息層是如何設計與實現的

      ØMQ是一種訊息傳遞系統,或者樂意的話可以稱它為“面向訊息的中介軟體”。它在金融服務,遊戲開發,嵌入式系統,學術研究和航空航天等多種環境中被使用。

      訊息傳遞系統基本上像應用程式的立即訊息一樣工作。應用程式決定將事件傳送到另一個應用程式(或多個應用程式),它組裝要發送的資料,點擊“發送”按鈕,訊息傳遞系統負責其餘的事情。然而,與立即訊息不同,訊息傳遞系統沒有GUI,並且在出現問題時,在端點處沒有人能夠進行智能干預。 因此,訊息系統必須是容錯的並且比常見的立即訊息傳送快得多。 ØMQ最初被構想用於是一個針對股票交易的極速的訊息傳遞系統,所以重點是極端最佳化。該項目的第一年用於設計基準方法,並嘗試定義一個儘可能高效的架構。 後來,大約在第二年的發展時,重點轉向了提供一個通用系統,該系統用於構建分布式應用程式和支援任意訊息模式,多種傳輸機制,任意語言綁定等。 在第三年,重點主要是提高可用性和扁平化學習曲線。 我們採用了BSD通訊端API,試圖清除單個訊息模式的語義,等等。        本文將深入瞭解上述三個目標如何轉化為ØMQ的內部架構,並為那些正在努力解決相同問題的人提供一些提示或技巧。       從第三年開始,ØMQ它的程式碼程式庫已經增長地過大; 所以有一個倡議來標準化其使用的有線協議,以及在Linux核心中實驗性地實現一個類似ØMQ的訊息系統等。這些主題在這裡就不涉及了。 但是,你可以擷取線上資源( online resources)以擷取更多詳細資料。 Application vs. Library

      ØMQ是一個訊息庫,而不是一個Message Service器。我們花了幾年時間研究AMQP協議(一個金融行業嘗試標準化企業訊息傳遞的有線協議),為其編寫參考實現並參與了好幾個大規模的基於訊息傳遞技術的大型項目,並最終意識到意識到使用經典用戶端/伺服器模型的智能訊息傳遞伺服器(代理)和啞訊息傳遞用戶端的方法有問題。

      我們首要關注的是效能:如果中間有一個伺服器,每個訊息必須通過網路兩次(從發送方到代理,從代理到接收方),這在延遲和輸送量方面都會有一定代價。 此外,如果所有訊息都通過代理傳遞,在某一時刻,伺服器必然成為瓶頸。 

      次要關注的是大規模部署:當部署跨組織(如:公司等)時,管理整個訊息流程的中央授權的概念不再適用。由於商業秘密和法律責任,沒有公司願意將控制權交給不同公司的伺服器。在實踐中的結果是,每個公司有一個Message Service器,用橋接器串連到其他公司的訊息傳遞系統。整個系統因此嚴重分散,並且為每個涉及的公司維護大量的橋接器不會使情況更好。為瞭解決這個問題,我們需要一個完全分布式的架構,該架構中每個組件都可能由不同的業務實體控制。考慮到基於伺服器的架構中的嵌入式管理單元是伺服器,我們可以通過為每個組件安裝單獨的伺服器來解決上述問題。在這種情況下,我們可以通過使伺服器和組件共用相同的進程來進一步最佳化設計。這樣我們最終得到一個訊息庫。        ØMQ開始時,我們有一個想法,即如何使訊息工作沒有中央伺服器。 它需要將訊息的整個概念顛倒過來,並且基於端到端原則,使用“智能端點,啞網路”架構來替換自主集中儲存網路中心的訊息的模型。 這個決定的技術將決定ØMQ從一開始就是是一個訊息庫,而不是一個應用程式。

      我們已經能夠證明這種架構比標準方法更高效(更低的延遲,更高的輸送量)和更靈活(很容易構建任意複雜的拓撲,而不是限定為經典的hub-and-spoke模型)。

      其中一個出乎意料的結果是,選擇庫模型改善了產品的可用性。 一次又一次,使用者因不必安裝和管理獨立的Message Service器而感到開心。 事實證明,沒有伺服器是一個喜好設定,因為它降低了運營成本(不需要有一個Message Service器管理員),並加快上線時間(無需與客戶協商是否運行伺服器,以及管理或運營團隊的問題) 。 學到的教訓是,當開始一個新的項目時,如果可能的話應該選擇庫設計。從一個簡單的程式調用庫可以很容易建立一個應用程式; 然而,幾乎不可能從現有的可執行檔建立庫。 庫模型為使用者提供了更多的靈活性,同時節省了他們不必要的管理工作。  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訊息由不透明控制代碼表示。 非常小的訊息的內容直接編碼在控制代碼中。 因此,複製控制代碼實際上複製了訊息資料。當訊息較大時,它被分配在單獨的緩衝區中,並且控制代碼僅包含指向緩衝區的指標。建立控制代碼的副本不會導致複製訊息資料,這在訊息是MB長時是有意義的(圖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

獲得的教訓:無鎖演算法很難發明,麻煩執行,幾乎不可能調試。 如果可能,請使用現有的成熟演算法,而不是發明自己的。 當需要最佳效能時,不要僅依賴無鎖演算法。 雖然它們速度快,但通過在它們之上

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.