<摘錄>開源軟件架構-ZeroMQ

原文連接:http://www.aosabook.org/en/zeromq.htmlhtml

ØMQ是一個消息通訊系統,若是你願意的話也能夠稱其爲「面向消息的中間件」。ØMQ的應用環境很普遍,包括金融服務、遊戲開發、嵌入式系統、學術研究以及航空航天等領域。程序員

消息通訊系統完成的工做基本上可看做爲負責應用程序之間的即時消息通訊。一個應用程序決定發送一個事件給另外一個應用程序(或者多個應用程序),它將須要發送的數據組合起來,點擊「發送」按鈕就好了——消息通訊系統會搞定剩下的工做。算法

不一樣於即時消息通訊的是,消息通訊系統沒有圖形用戶界面,並假設當出現錯誤時,對端並不會有人爲干預的智能化處理。所以,消息通訊系統必須既要有高度的容錯性,也要比通常的即時消息通訊更快速。編程

ØMQ最初的設想是做爲股票交易中的一個極快速的消息通訊系統,所以重點放在了高度優化上。項目開始的頭一年都花在制定性能基準測試的方法上了,並嘗試設計出一個儘量高效的架構。緩存

以後,大約是在項目進行的第二年裏,開發的重點轉變成爲構建分佈式應用程序而提供的一個通用系統,支持任意模式的消息通訊、多種傳輸機制、對多種編程語言的綁定等等。性能優化

在開發的第三年裏,重點主要集中於提升系統的可用性,將學習曲線平坦化。咱們已經採用了BSD套接字API,嘗試整理單個消息通訊模式的語義等等。服務器

本章試圖向讀者介紹,ØMQ爲達到上述三個目標是如何設計其內部架構的,也但願給一樣面對這些問題的人提供一些啓示。網絡

啓動ØMQ項目的第三年裏,其代碼庫已經膨脹的過於龐大。有一項提議要標準化ØMQ中所使用的協議,以及實驗性地實現一個類ØMQ的消息通訊系統以加入到Linux內核中等等。不過,本書並未涵蓋這些主題,更多細節能夠參考:http://www.250bpm.com/conceptshttp://groups.google.com/group/sp-discuss-group,和http://www.250bpm.com/hitssession

24.1 應用程序 vs 程序庫

ØMQ是一個程序庫,不是消息通訊服務器。咱們花了好幾年時間在AMQP上,這是一種在金融行業中嘗試標準化用於商業消息通訊的協議。咱們爲其編寫了一個參考性的實現,而後部署到幾個主要基於消息通訊技術的大型項目中使用——由此咱們意識到,智能消息服務器(代理/broker)和啞客戶端之間的這種經典的客戶機/服務器模型是有問題的。多線程

當時咱們主要關心的是性能:若是中間有個服務器的話,每條消息都不得不穿越網絡兩次(從發送者到服務器,而後從服務器再到接收者),還附帶有延遲和吞吐量方面的損耗。此外,若是全部的消息都要經過服務器傳遞的話,某一時刻它就必然會成爲性能的瓶頸。

第二點須要關心的是關於大規模部署的問題:當消息通訊須要跨越公司的界限時,這種中央集權式管理全部消息流的概念就再也不有效了。沒有一家公司願意把對服務器的控制權放在別的公司裏,這包含有商業機密以及法律責任相關的問題。實際結果就是每家公司都有一個消息通訊服務器,可經過手動橋接的方式鏈接到其餘公司的消息通訊系統中。所以整個經濟系統被極大的劃分開來,可是爲每一個公司維護這樣大量的橋接並無使狀況變得更好。要解決這個問題,咱們須要一個分佈式的架構。在這種架構中每個組件均可以由一個不一樣的商業實體來管轄。鑑於基於服務器架構的管理單元就是服務器,咱們能夠經過爲每一個組件設置一個單獨的服務器來解決這個問題。在這種狀況下,咱們能夠經過使服務器和組件共享同一個進程來進一步地優化設計。咱們最終獲得的就是一個消息通訊的程序庫。

當咱們開始設想一種不須要中間服務器的消息通訊機制時,也就是ØMQ項目開始之時。這須要自下而上的將整個消息通訊的概念顛倒過來,將位於網絡中央的集中信息存儲模型替換爲基於端到端機制的「智能型終端,沉默化網絡」的架構。正是因爲這樣的技術決策,ØMQ從一開始就做爲一個庫而存在,它不是應用程序。 同時,咱們也已經證實了這種架構更加高效(低延遲,高吞吐量)也更加靈活(很容易在此之上構建任意複雜的拓撲結構,而沒必要拘泥於經典的中心輻射模型)。

然而選擇以庫的形式發佈,這其中還有一個意想不到的結果,那就是這麼作提升了產品的可用性。用戶反覆地表示因爲他們再也不須要安裝和管理一個獨立的消息通訊服務器了,爲此他們感到很慶幸。事實證實,去掉中間服務器是首選方案,由於這麼作下降了運營的成本(不須要爲消息通訊服務器安排管理員),也加快了市場響應的時間(沒有必要對客戶、管理層或運營團隊談判溝通是否要運行服務器)。

咱們從中學到的是,當開始一個新項目時,你應該儘量的選擇以庫的形式來設計。咱們能夠很容易的經過從小型程序中調用庫的實現而建立出一個應用,可是卻幾乎不可能從已有的可執行程序中建立一個庫。庫對用戶來講能夠提供更高的靈活性,同時也不須要花費他們不少精力來管理。

24.2 全局狀態

全局變量不適於在庫中使用。由於一個進程可能會加載同一個庫幾回,而它們會共用一組全局變量。在圖24.1中,ØMQ庫被兩個不一樣的、彼此獨立的庫所調用,而應用自己調用了這兩個庫。

圖24.1 不一樣的庫在使用ØMQ

當出現這種狀況時,兩個ØMQ的實例會訪問到相同的變量,這會產生競爭條件,出現奇怪的錯誤和未定義的行爲。

要防止出現這種問題,ØMQ中沒有使用任何全局變量。相反地,是由庫的使用者來負責顯式地建立全局狀態。包含全局狀態的對象稱爲context。從用戶的角度來看,context或多或少相似一個工做者線程池,而從ØMQ的角度來看,它僅僅是一個存儲咱們所須要的任意全局狀態的對象。在上圖中,libA會有它本身的context,而libB也會有它本身的context。它們之間沒法互相干擾。

看到這裏應該已經很是明顯了:毫不要在庫中使用全局狀態。若是你這麼作了,當庫剛好須要在同一個進程中實例化兩次時,它極可能會崩潰。

24.3 性能

當ØMQ項目開始以後,主要的目標是優化性能。消息通訊系統的性能能夠用兩個指標來界定:吞吐量——在一段給定的時間內能夠傳遞多少條消息;以及時延——一條消息從一端傳到另外一端須要花費多長時間。

咱們應該重點關注哪一個指標?這二者之間的關係是什麼?這還不明擺着嗎?跑測試,用測試的總時間除以消息的數量,你獲得的就是時延。用消息的數量除以總時間,你獲得的就是吞吐量。換句話說,時延是吞吐量的倒數。很簡單,不是嗎?

咱們並無直接開始編碼,而是花了幾周的時間詳細調查性能指標,咱們發現吞吐量和時延之間的關係絕非如此簡單,一般這個指標數是至關違反直覺的。

假設A發送消息給B(見圖24.2),測試的總時間是6秒,總共有5條消息傳遞。所以吞吐量是0.83條消息/每秒(5/6),而時延是1.2秒(6/5),對吧?

圖24.2 從A到B發送消息

請再看看這副圖。每條消息從A到B所花費的時間是不一樣的:2秒、2.5秒、3秒、3.5秒、4秒。平均計算是3秒鐘,這和咱們以前計算出的1.2秒相比差太遠了。這個例子很直觀的代表,人們很容易對性能指標產生誤解。

如今來看看吞吐量。測試的總時間是6秒。可是,在A點總共花費了2秒才把全部的消息都發送完畢。從A的角度來看,吞吐量是2.5條消息/秒(5/2)。在B點共花費了4秒纔將全部的消息都接收完畢。所以,從B的角度來看,吞吐量是1.25條消息/秒(5/4)。這兩個數據都同以前計算得出的1.2條消息/秒不吻合。

長話短說吧,時延和吞吐量是兩個不一樣的指標,這是很是明顯的。重要的是理解這二者之間的區別以及它們的相互關係。時延只能在系統的兩個不一樣端點之間才能測量,A點自己並無什麼時延。每條消息都有它們本身的時延,你能夠經過多條消息來計算平均時延,可是,對於一個消息流來講並無什麼時延。

換句話說,吞吐量只能在系統的某個端點處才能測量。發送端有吞吐量,接收端有吞吐量,這二者之間的任意中間結點也有吞吐量,但對整個系統來講就沒有什麼總吞吐量的概念了。另外,吞吐量只對一組消息有意義,單條消息是沒有什麼吞吐量可言的。

至於吞吐量和時延之間的關係,咱們已經證實了原來它們之間確實有關係。可是,公式表達中涉及到積分,咱們就不在這裏討論了。要獲得更多的信息,能夠去讀一讀有關隊列的論文。

關於對消息通訊系統進行的基準測試還有許多缺陷存在,但咱們不會進一步探討了。這裏應該再次強調咱們爲此獲得的教訓:確保理解你正在解決的問題。即便是一個「讓它更快」這樣簡單的問題也會耗費你大量的工做才能正確理解之。更況且若是你不理解問題,你極可能會隱式地將假設和某種流行的觀點置入代碼中,這使得解決方案要麼是有缺陷的或者至少會變得很是複雜,又或者會使得該方案沒有達到它應有的適用範圍。

24.4 關鍵路徑

咱們在性能優化的過程當中發現有3個因素會對性能產生嚴重的影響:

  • 內存分配的次數
  • 系統調用的次數
  • 併發模型

可是,並非每一個內存分配或者每一個系統調用都會對性能產生一樣的影響。對於消息通訊系統的性能,咱們所感興趣的是在給定的時間內能在兩點間傳送的消息數量。另外,咱們可能會感興趣的是消息從一點傳送到另外一點須要多久。

考慮到ØMQ被設計爲針對長期鏈接的場景,所以創建一個鏈接或者處理一個鏈接錯誤所花費的時間基本上可忽略。這些事件極少發生,所以它們對整體性能的影響能夠忽略不計。

代碼庫中某個一遍又一遍被頻繁使用的部分,咱們稱之爲關鍵路徑。優化應該集中到這些關鍵路徑上來。 讓咱們看一個例子:ØMQ在內存分配方面並無作高度優化。好比,當操做字符串時,經常是在每一個轉化的中間階段分配一個新的字符串。可是,若是咱們嚴格審查關鍵路徑——實際完成消息通訊的部分——咱們會發現這部分幾乎沒有使用任何內存分配。若是是短消息,那麼每256個消息纔會有一次內存分配(這些消息都被保存到一個單獨的大內存塊中)。此外,若是消息流是穩定的,在不出現流峯值的狀況下,關鍵路徑部分的內存分配次數會降爲零(已分配的內存塊不會返回給系統,而是不斷的進行重用)。

咱們從中學到的是:只在對結果能產生影響的地方作優化。優化非關鍵路徑上的代碼只是在作無用功。

24.5 內存分配

假設全部的基礎組件都已經初始化完成,兩點之間的一條鏈接也已經創建完成,此時要發送一條消息時只有同樣東西須要分配內存:消息體自己。所以,要優化關鍵路徑,咱們就必須考慮消息體是如何分配的以及是如何在棧上來回傳遞的。

在高性能網絡編程領域中,最佳性能是經過仔細地平衡消息的分配以及消息拷貝所帶來的開銷而實現的,這是常識(好比,http://hal.inria.fr/docs/00/29/28/31/PDF/Open-MX-IOAT.pdf 參見針對「小型」、「中型」、「大型」消息的不一樣處理)。對於小型的消息,拷貝操做比內存分配要經濟的多。只要有須要,徹底不分配新的內存塊而直接把消息拷貝到預分配好的內存塊上,這麼作是有道理的。另外一方面,對於大型的消息,拷貝操做比內存分配的開銷又要昂貴的多。爲消息體分配一次內存,而後傳遞指向分配塊的指針,而不是拷貝整個數據。這種方式被稱爲「零拷貝」。

ØMQ以透明的方式同時處理這兩種狀況。一條ØMQ消息由一個不透明的句柄來表示。對於很是短小的消息,其內容被直接編碼到句柄中。所以,對句柄的拷貝實際上就是對消息數據的拷貝。當遇到較大的消息時,它被分配到一個單獨的緩衝區內,而句柄只包含一個指向緩衝區的指針。對句柄的拷貝並不會形成對消息數據的拷貝,當消息有數兆字節長時,這麼處理是頗有道理的(圖24.3)。須要提醒的是,後一種狀況裏緩衝區是按引用計數的,所以能夠作到被多個句柄引用而沒必要拷貝數據。

圖24.3 消息拷貝(或者不拷貝)

咱們從中學到的是:當考慮性能問題時,不要假設存在有一個最佳解決方案。極可能這個問題有多個子問題(例如,小型消息和大型消息),而每個子問題都有各自的優化算法。

24.6 批量處理

前面已經提到過,在消息通訊系統中,系統調用的數量太多的話會致使出現性能瓶頸。實際上,這個問題絕非通常。當須要遍歷調用棧時會有不小的性能損失,所以,明智的作法是,當建立高性能的應用時應該儘量多的去避免遍歷調用棧。

參見圖24.4,爲了發送4條消息,你不得不遍歷整個網絡協議棧4次(也就是,ØMQ、glibc、用戶/內核空間邊界、TCP實現、IP實現、以太網鏈路層、網卡自己,而後反過來再來一次)。

圖24.4 發送4條消息

可是,若是你決定將這些消息集合到一塊兒成爲一個單獨的批次,那麼就只須要遍歷一次調用棧了(圖24.5)。這種處理方式對消息吞吐量的影響是巨大的:可大至2個數量級,尤爲是若是消息都比較短小,數百個這樣的短消息才能包裝成一個批次。

圖24.5 批量處理消息

另外一方面,批量處理會對時延帶來負面影響。咱們來分析一下,好比,TCP實現中著名的Nagle算法。它爲待發出的消息延遲必定的時間,而後將全部的數據合併成一個單獨的數據包。顯然,數據包中的第一條消息,其端到端的時延要比最後一條消息嚴重的多。所以,若是應用程序須要持續的低時延的話,常見作法是將Nagle算法關閉。更常見的是取消整個調用棧層次上的批量處理(好比,網卡的中斷匯聚功能)。

但一樣,不作批量處理就意味着須要大量穿越整個調用棧,這會致使消息吞吐量下降。彷佛咱們被困在吞吐量和時延的兩難境地中了。

ØMQ嘗試採用如下策略來提供一致性的低時延和高吞吐量。當消息流比較稀疏,不超過網絡協議棧的帶寬時,ØMQ關閉全部的批量處理以改善時延。這裏的權衡是CPU的使用率會變得略高——咱們仍然須要常常穿越整個調用棧。可是在大多數狀況下,這並非個問題。

當消息的速率超過網絡協議棧的帶寬時,消息就必須進行排隊處理了——保存在內存中直到協議棧準備好接收它們。排隊處理就意味着時延的上升。若是消息在隊列中要花費1秒時間,端到端的時延就至少會達到1秒。更糟糕的是,隨着隊列長度的增加,時延會顯著提高。若是隊列的長度沒有限制的話,時延就會超過任何限定值。

據觀察,即便調整網絡協議棧以追求最低的時延(關閉Nagle算法,關閉網卡中斷匯聚功能,等等),因爲受前文所述的隊列的影響,時延仍然會比較高。

在這種狀況下,積極的採起批量化處理是有意義的。反正時延已經比較高了,也沒什麼好顧慮的了。另外一方面,積極的採用批量處理可以提升吞吐量,並且能夠清空隊列中等待的消息——這反過來又意味着時延將逐步下降,由於正是排隊才形成了時延的上升。一旦隊列中沒有未發送的消息了,就能夠關閉批量處理,進一步的改善時延。

咱們觀察到批量處理只應該在最高層進行,這是須要額外注意的一點。若是消息在最高層匯聚爲批次,在低層次上就沒什麼可作批量處理的了,並且全部低層次的批量處理算法除了會增長整體時延外什麼都沒作。 咱們從中學到了:在一個異步系統中,要得到最佳的吞吐量和響應時間,須要在調用棧的底層關閉批量處理算法,而在高層開啓。僅在新數據到達的速率快於它們被處理的速率時才作批量處理。

24.7 架構概覽

到目前爲止,咱們都專一於那些使ØMQ變得快速的通用性原則。從如今起,咱們能夠看一看實際的系統架構了(圖24.6)。

圖24.6 ØMQ的架構框圖

用戶使用被稱爲「套接字」的對象同ØMQ進行交互。它們同TCP套接字很類似,主要的區別在於這裏的套接字可以處理同多個對端的通訊,有點像非綁定的UDP套接字。

套接字對象存在於用戶線程中(見下一節的線程模型討論)。除此以外,ØMQ運行多個工做者線程用以處理通訊中的異步環節:從網絡中讀取數據、將消息排隊、接受新的鏈接等等。

工做者線程中存在着多個對象。每個對象只能由惟一的父對象所持有(全部權由圖中一個簡單的實線來標記)。與子對象相比,父對象能夠存在於其餘線程中。大多數對象直接由套接字sockets所持有。可是,這裏有幾種狀況下會出現一個對象由另外一個對象所持有,而這個對象又由socket所持有。咱們獲得的是一個對象樹,每一個socket都有一個這樣的對象樹。咱們在關閉鏈接時會用到對象樹,在一個對象關閉它全部的子對象前,任何對象都不能自行關閉。這樣咱們能夠確保關閉操做能夠按預期的行爲那樣正常工做。好比,在隊列中等待發送的消息要先發送到網絡中,以後才能終止發送過程。

大體來講,這裏有兩種類型的異步對象。有的對象不會涉及到消息傳遞,而有些須要。前者主要負責鏈接管理。好比,一個TCP監聽對象在監聽接入的TCP鏈接,併爲每個新的鏈接建立一個engine/session對象。相似的,一個TCP鏈接對象嘗試鏈接到TCP對端,若是成功,它就建立一個engine/session對象來管理這個鏈接。若是失敗了,鏈接對象會嘗試從新創建鏈接。

然後者用來負責數據的傳輸。這些對象由兩部分組成:session對象負責同ØMQ的socket交互,而engine對象負責同網絡進行通訊。session對象只有一種類型,而對於每一種ØMQ所支持的協議都會有不一樣類型的engine對象與之對應。所以,咱們有TCP engine,IPC(進程間通訊)engine,PGM engine(一種可靠的多播協議,參見RFC 3208),等等。engine的集合很是普遍——將來咱們可能會選擇實現好比WebSocket engine或者SCTP engine。

session對象同socket之間交換消息。能夠由兩個方向來傳遞消息,在每一個方向上由一個pipe對象來處理。基本上來講,pipe就是一個優化過的用來在線程之間快速傳遞消息的無鎖隊列。

最後咱們來看看context對象(在前一節中提到過,但沒有在圖中表示出來),該對象保存全局狀態,全部的socket和異步對象均可以訪問它。

24.8 併發模型

ØMQ須要充分利用多核的優點,換句話說就是隨着CPU核心數的增加可以線性的擴展吞吐量。

以咱們以前對消息通訊系統的經驗代表,採用經典的多線程方式(臨界區、信號量等等)並不會使性能獲得較大提高。事實上,就算是在多核環境下,一個多線程版的消息通訊系統可能會比一個單線程的版本還要慢。有太多時間都花在等待其餘線程上了,同時,引入了大量的上下文切換拖慢了整個系統。

針對這些問題,咱們決定採用一種不一樣的模型。目標是徹底避免鎖機制,並讓每一個線程可以全速運行。線程間的通訊是經過在線程間傳遞異步消息(事件)來實現的。內行人都應該知道,這就是經典的actor模式。

咱們的想法是在每個CPU核心上運行一個工做者線程——讓兩個線程共享同一個核心只會意味着大量的上下文切換而沒有獲得任何別的優點。每個ØMQ的內部對象,好比說TCP engine,將會緊密地關聯到一個特定的工做者線程上。反過來,這意味着咱們再也不須要臨界區、互斥鎖、信號量等等這些東西了。此外,這些ØMQ對象不會在CPU核之間遷移,從而能夠避免因爲緩存被污染而引發性能上的降低(圖24.7)。

圖24.7 多個工做者線程

這個設計讓不少傳統多線程編程中出現的頑疾都消失了。然而,咱們還須要在許多對象間共享工做者線程,這反過來又意味着必需要有某種多任務間的合做機制。這表示咱們須要一個調度器,對象必須是事件驅動的,而不是在整個事件循環中來控制。咱們必須考慮任意序列的事件,甚至很是罕見的狀況也要考慮到。咱們必須確保不會有哪一個對象持有CPU的時間過長等等。

簡單來講,整個系統必須是全異步的。任何對象都沒法承受阻塞式的操做,由於這不只會阻塞其自身,並且全部共享同一個工做者線程的其餘對象也都會被阻塞。全部的對象都必須或顯式或隱式的成爲一種狀態機。隨着有數百或數千的狀態機在並行運轉着,你必須處理這些狀態機之間的全部可能發生的交互,而其中最重要的就是——關閉過程。

事實證實,要以一種清晰的方式關閉一個全異步的系統是一個至關複雜的任務。試圖關閉一個有着上千個運轉着的部分的系統,其中有的正在工做中,有的處於空閒狀態,有的正在初始化過程當中,有的已經自行關閉了,此時極易出現各類競態條件、資源泄露等諸如此類的狀況。ØMQ中最爲複雜的部分確定就是這個關閉子系統了。快速檢查一下bug跟蹤系統的記錄顯示,約30%到50%的bug都同關閉有某種聯繫。

咱們從中學到的是:當要追求極端的性能和可擴展性時,考慮採用actor模型。在這種狀況下這幾乎是你惟一的選擇。不過,若是不使用像Erlang或者ØMQ這種專門的系統,你將不得不手工編寫並調試大量的基礎組件。此外,從一開始就要好好思考關於系統關閉的步驟。這將是代碼中最爲複雜的部分,而若是你沒有清晰的思路該如何實現它,你可能應該從新考慮在一開始就使用actor模型。

24.9 無鎖算法

最近比較流行使用無鎖算法。它們是用於線程間通訊的一種簡單機制,同時並不會依賴於操做系統內核提供的同步原語,如互斥鎖和信號量。相反,它們經過使用CPU原子操做來實現同步,好比原子化的CAS指令(比較並交換)。咱們應該理解清楚的是它們並非字面意義上的無鎖——相反,鎖機制是在硬件層面實現的。

ØMQ在pipe對象中採用無鎖隊列來在用戶線程和ØMQ的工做者線程之間傳遞消息。關於ØMQ是如何使用無鎖隊列的,這裏有兩個有趣的地方。

首先,每一個隊列只有一個寫線程,也只有一個讀線程。若是有1對多的通訊需求,那麼就建立多個隊列(圖24.8)。鑑於採用這種方式時隊列不須要考慮對寫線程和讀線程的同步(只有一個寫線程,也只有一個讀線程),所以能夠以很是高效的方式來實現。

圖24.8 隊列

其次,儘管咱們意識到無鎖算法要比傳統的基於互斥鎖的算法更加高效,CPU的原子操做開銷仍然很是高昂(尤爲是當CPU核心之間有競爭時),對每條消息的讀或者寫都採用原子操做的話,效率將低於咱們所能接受的水平。

提升速度的方法——再次採用批量處理。假設你有10條消息要寫入到隊列。好比,可能會出現當你收到一個網絡數據包時裏面包含有10條小型的消息的狀況。因爲接收數據包是一個原子事件,你不能只接收一半,所以這個原子事件致使須要寫10條消息到無鎖隊列中。那麼對每條消息都採用一次原子操做就顯得沒什麼道理了。相反,你可讓寫線程擁有一塊本身獨佔的「預寫」區域,讓它先把消息都寫到這裏,而後再用一次單獨的原子操做,總體刷入隊列。

一樣的方法也適用於從隊列中讀取消息。假設上面提到的10條消息已經刷新到隊列中了。讀線程能夠對每條消息採用一個原子操做來讀取,可是,這種作法過於重量級了。相反,讀線程能夠將全部待讀取的消息用一個單獨的原子操做移動到隊列的「預讀取」部分。以後就能夠從「預讀」緩存中一條一條的讀取消息了。「預讀取」部分只能由讀線程單獨訪問,所以這裏沒有什麼所謂的同步需求。

圖24.9中左邊的箭頭展現瞭如何經過簡單地修改一個指針來將預寫入緩存刷新到隊列中的。右邊的箭頭展現了隊列的整個內容是如何經過修改另外一個指針來移動到預讀緩存中的。

圖24.9 無鎖隊列

咱們從中學到的是:發明新的無鎖算法是很困難的,並且實現起來很麻煩,幾乎不可能對其調試。若是可能的話,可使用現有的成熟算法而不是本身來發明輪子。當須要追求極度的性能時,不要只依靠無鎖算法。雖然它們的速度很快,但能夠在其之上經過智能化的批量處理來顯著提升性能。

24.10 API

用戶接口是任何軟件產品中最爲重要的部分。這是你的程序惟一暴露給外部世界的部分,若是搞砸了全世界都會恨你的。對於面向最終用戶的產品來講,用戶接口就是圖形用戶界面或者命令行界面,而對於庫來講,那就是API了。

在ØMQ的早期版本中,其API是基於AMQP的交易和隊列模型的(參見AMQP規範)。從歷史的角度來看,2007年的白皮書嘗試要將AMQP同一個代理模式的消息通訊系統相整合,這頗有趣。我於2009年末從新使用BSD套接字API從零開始重寫了整個項目。那就是轉折點,從那一刻起ØMQ的用戶數量開始猛增。以前的ØMQ是由消息通訊領域的專家們所使用的產品,而如今成爲任何人都能方便使用的普通工具。在1年左右的時間裏,ØMQ的用戶社羣擴大了10倍之多,咱們還實現了對20多種不一樣編程語言的綁定等等。

用戶接口定義了人們對產品的感觀。基本沒有改變功能——僅僅經過修改了API——ØMQ就從一個「企業級消息通訊」產品轉變爲一個「網絡化」的產品。換句話說,人們對ØMQ的感觀從一個「大金融機構所使用的複雜基礎組件」轉變爲「嘿,這工具能夠幫助我從程序A發送10字節長的消息到程序B」。

咱們從中學到的是:正確理解你的項目,根據你對項目的願景來合理地設計用戶接口。用戶接口同項目的願景不相符合的話,能夠100%保證該項目註定會失敗。

將ØMQ的用戶接口替換爲BSD套接字API,這其中有個很重要的因素,那就是BSD套接字API並非一個新的發明,而是早就爲人們所熟悉了。事實上,BSD套接字API是當今仍在使用中的最爲古老的API之一了。那得回溯到1983年以及4.2版BSD Unix的時代。它已經被普遍且穩定的使用了幾十年了。

上面的事實帶來了不少優點。首先,人人都知道BSD套接字API,所以學習的難度曲線很是平坦。就算你從未據說過ØMQ,你也能夠在幾分鐘內建立出一個應用程序,這都得感謝你能夠重用過去在BSD套接字上積累的經驗。

其次,使用這樣一種被普遍支持的API使得ØMQ能夠同已有的技術進行融合。好比,將ØMQ對象暴露爲「套接字」或者「文件描述符」,這可讓咱們在一樣的事件循環中處理TCP、UDP、管道、文件以及ØMQ事件。另外一個例子是:要將相似ØMQ的功能加入到Linux內核中,這個實驗性的項目就變得很是容易實現了。經過共享相同的概念框架,ØMQ能夠複用不少已有的基礎組件。

第三,也許也是最重要的一點,那就是BSD套接字API已經存活了將近30年的時間了,儘管中間人們曾屢次嘗試替換它。這意味着設計中有某種固有的正確性。BSD套接字API的設計者——不管是故意的仍是偶然的——都作出了正確的設計決策。經過借用這套API,咱們能夠自動分享到這些設計決策,而沒必要知道這些決策到底是什麼,或者它們到底解決了什麼問題。

咱們從中學到的是:雖然代碼複用的思想從遠古時代就有了,隨後模式複用的概念也加入了進來,重要的是要以一種更通常化的方式來思考複用。當作產品設計時,參考一下其餘類似的產品。調查一下哪些方面是失敗的,哪些方面是成功的,從成功的項目中學習。不要以爲沒有創新就接受不了。複用好的點子、API、概念框架,任何你以爲合適的東西均可以複用。這麼作的好處是你可讓用戶重用他們以前的知識,同時你也能夠避免當前你並不瞭解的技術方面的陷阱。

24.11 消息模式

在任何消息通訊系統中,所面臨的最重要的設計問題是如何提供一種方式可讓用戶指定哪條消息能夠路由到哪一個目的地。這裏主要有兩種方法,並且我相信這兩種方法是至關通用的,基本可適用於軟件領域中遇到的任何問題。

第一種方式是吸取Unix哲學中的「只作一件事,並把它作好」的原則。這意味着問題域應該人爲地限制在一個較小且易理解的範圍內。而後,程序應該以正確和詳盡的方式來解決這個受限制的問題。在消息通訊領域中,一個採用這種方式的例子是MQTT。這是一種將消息分發給一組消費者的協議。它很容易使用,並且在消息分發方面作得很出色,但除此以外它不能用於任何其餘用途(好比說RPC)。

另外一種方式是致力於通常性,並提供一種功能強大且高度可配置的系統。AMQP就是這樣一個例子。它的隊列和互換的模式提供給用戶可編程的能力,幾乎能夠定義出他們可想到的任意一種路由算法。固然了,有得必有失,取捨的結果就是增長了許多選項須要咱們去處理。

ØMQ選擇了前一種方式,由於這種方式下的產品幾乎全部的人均可以使用,而通用的方式下的產品須要消息通訊方面的專家才能用上。爲了闡明這個觀點,讓咱們看看模式是如何對API的複雜度產生影響的。以下代碼是在通用系統(AMQP)之上的RPC客戶端實現:

connect ("192.168.0.111") exchange.declare (exchange="requests", type="direct", passive=false, durable=true, no-wait=true, arguments={}) exchange.declare (exchange="replies", type="direct",passive=false, durable=true, no-wait=true, arguments={}) reply-queue=queue.declare(queue="", passive=false, durable=false, exclusive=true, auto-delete=true, no-wait=false, arguments={}) queue.bind (queue=reply-queue, exchange="replies", routing-key=reply-queue) queue.consume (queue=reply-queue, consumer-tag="", no-local=false, no-ack=false, exclusive=true, no-wait=true, arguments={}) request = new-message ("Hello World!") request.reply-to = reply-queue request.correlation-id = generate-unique-id () basic.publish (exchange="requests", routing-key="my-service", mandatory=true, immediate=false) reply = get-message ()

而另外一方面,ØMQ將消息劃分爲所謂的「消息模式」。幾個模式方面的例子有「發佈者/訂閱者」,「請求/回覆」或者「並行管線」。每一種消息通訊的模式之間都是徹底正交的,可被看作是一個單獨的工具。

接下來採用ØMQ的請求/回覆模式對上面的應用進行重構,注意ØMQ將繁雜的選擇縮減爲一個單一的步驟,這隻要經過選擇正確的消息模式「REQ」就能夠了。

s = socket (REQ) s.connect ("tcp://192.168.0.111:5555") s.send ("Hello World!") reply = s.recv ()

到這裏爲止,咱們已經能夠認爲具體化的解決方案比通用型解決方案要更好。咱們但願本身的解決方案能儘量的具體化。可是,同時咱們又但願提供給用戶的功能面儘量的廣。咱們該如何解決這個明顯的矛盾?

答案分兩步:

  1. 定義一個堆棧層,用以處理某個特定的問題領域。(好比,傳輸、路由、演示等)
  2. 爲該層提供多種實現方式。對於每種實現的使用,都應該是非互相干擾的。

讓咱們看看網絡協議棧中有關傳輸層的例子。傳輸層意味着須要在網絡層(IP)之上提供例如數據流傳輸、流控、可靠性等服務。它是經過定義多種互不干擾的解決方案來實現的:TCP做爲面向鏈接的可靠數據流傳輸機制、UDP做爲面向非鏈接的非可靠式數據包傳輸機制、SCTP做爲多個流的傳輸、DCCP做爲非可靠性鏈接等等。

注意,這裏每種實現都是徹底正交的:UDP端不能同TCP端通訊,SCTP端也不能同DCCP端通訊。這意味着新的實現能夠在任意時刻加到這個棧上,而不會對棧中已有的部分產生影響。相反若是實現是失敗的,則能夠被徹底丟棄而不會影響傳輸層的總體能力。

一樣的道理也適用於ØMQ中定義的消息模式。消息模式在傳輸層(TCP及其它成員)之上組成了新的一層(所謂的「可擴展性層」)。每一個消息模式都是這一層的具體實現。它們都是嚴格正交的——「發佈者/訂閱者」端沒法同「請求/回覆」端通訊,等等之類。消息模式之間的嚴格分離反過來又意味着新的模式能夠按照需求增長進來,開發新模式的實驗若是失敗了,也不會對已有的模式產生影響。

咱們從中學到的是:當解決一個複雜且多面化的問題時,單個通用型的解決方案可能並非最好的方式。相反,咱們能夠把問題的領域想象成一個抽象層,並基於這個層次提供多個實現,每種實現只致力於解決一種定義良好的狀況。當咱們這麼作時,要仔細劃定用例狀況。要確認什麼在範圍內,什麼不在範圍內。若是對使用範圍限制的太過於嚴格,軟件的應用性就會受到限制。若是對問題定義的太廣,那麼產品就會變得很是複雜,給用戶帶來模糊和混亂的感受。

24.12 結論

因爲咱們的世界裏已經充斥着大量經過互聯網相連的小型計算機——移動電話、RFID閱讀器、平板電腦以及便攜式計算機、GPS設備等等。分佈式計算已經再也不侷限於學術領域了,成爲了每位開發者須要去解決的平常問題。不幸的是,對此的解決方案大多數都是領域相關的獨門祕技。本文以系統化的方式總結了咱們在構建大規模分佈式系統中的經驗。本文主要側重於從軟件架構的觀點來闡明咱們須要面對的挑戰,但願開源社區中的架構師和程序員會以爲本文頗有幫助。

相關文章
相關標籤/搜索