本文比較抽象,不過是Reactive/反應式背後的理念,這些理念在各種各樣的Reactive Programming架構上都有落實,細細咀嚼本文,友善大家了解、複用、遷移自己現有的一些知識體系,将其應用到Reactive的系統架構下來。一些詞的标準譯法還在揣摩,是以會提供A/B這也的形式,歡迎交流。
版本 2.0,2014 年 9 月 16 日釋出
在不同的領域深耕的各個組織都獨立地發現了一種如出一轍的軟體構模組化式。 這些系統更加的健壯、更加具有回彈性/韌性、更加靈活,也能更好地滿足現代化的需求。
這些變化方興未艾, 因為近幾年應用程式需求已經發生了顯著的變化。 僅在幾年前, 一個大型應用程式通常擁有數十台伺服器、 數秒的響應時間、 數小時的維護時間以及GB級别的資料。 而今,應用程式被部署到了所有形态上, 從移動裝置到運作着數以千計的多核心處理器的雲端叢集。 使用者期望毫秒級的響應時間,以及100%的正常運作。 資料則以PB記。 以前的軟體架構已經根本無法滿足而今的需求了。
我們相信需要一種一緻而連貫的的系統架構方法, 而其中所有必要的方面都已經得到了單獨的認可: 我們需要系統具備以下特質:即時響應性(Responsive)、回彈性/韌性(Resilient)、 适應性/彈性(Elastic)以及消息驅動(Message Driven)。 對于這樣的系統,我們稱之為反應式系統(Reactive System)。
使用反應式方式建構的反應式系統更加靈活、 松散耦合而且是可伸縮的(參見 C.2.15)。 這使得它們更加容易被開發和調整。 它們對系統的失敗(failure)(參見 C.2.7)也更加的包容, 而當失敗着實發生時, 它們将用優雅而不是災難性的方式來應對。 反應式系統具有高度的即時響應性, 為使用者(參見 C.2.17)提供了高效的互動回報。
反應式系統的特質:
即時響應性(Responsive): 隻要有可能, 系統(參見 C.2.16)就會及時地做出響應。 即時響應是可用性和實用性的基石, 但是更加重要的是,即時響應意味着可以快速地檢測到問題并且行之有效地解決它。 即時響應的系統專注于提供快速而一緻的響應時間, 确立可靠的上界, 進而提供一緻的服務品質。 反過來,這種一緻的行為簡化了錯誤處理、 建立了最終使用者的信任、 并鼓勵他們進行進一步的互動。
回彈性/韌性(Resilient): 系統在出現失敗(參見 C.2.7)時依然保持即時響應性。 這不僅适用于高可用的、 任務關鍵型系統——任何不具備回彈性/韌性的系統都将會在發生失敗之後丢失即時響應性。 回彈性/韌性是通過複制(參見 C.2.13)、 遏制、 隔離(參見 C.2.8)以及委派(參見 C.2.5)來實作的。 失敗被包含在了每個元件(參見 C.2.4)内部, 與其它元件互相隔離, 進而確定了系統的各個部分能夠在不危及整個系統的情況下失敗和恢複。 每個元件的恢複都被委派給了另一個(外部的)元件, 此外,在必要時可以通過資料副本來保障高可用性。 (是以)元件的用戶端(也就)沒有了處理元件失敗的負擔。
适應性/彈性(Elastic): 系統在不斷變化的工作負載之下依然保持即時響應性。 反應式系統可以對輸入(負載)的速率變化做出反應,比如通過增加或者減少被配置設定用于服務這些輸入(負載)的資源(參見 C.2.14)。 這意味着設計上并沒有争用點和中心化的瓶頸, 進而可以分片或者複制元件, 并能夠在它們之間分發輸入(負載)。 通過提供相關的實時性能名額, 反應式系統支援預測式以及反應式伸縮算法。 它們在正常的硬體以及軟體平台上實作了成本高效的适應性/彈性(參見 C.2.6)。
消息驅動: 反應式系統依賴異步的(參見 C.2.1)消息傳遞(參見 C.2.10),進而在確定了松散耦合、 隔離和位置透明性(參見 C.2.9)的元件之間确立邊界。 這一邊界還提供了将失敗(參見 C.2.7)作為消息委派出去的手段。 使用顯式的消息傳遞,可以通過在系統中形成并監視消息流隊列, 并在必要時應用回壓(參見 C.2.2), 進而實作負載管理、 适應性/彈性以及流控制。 使用位置透明的消息傳遞作為通信的手段, 使得跨叢集或者在單個主機中使用相同的構造和語義來管理失敗成為了可能。 非阻塞的(參見 C.2.11)通信使得接收者可以隻在活動時才消耗資源(參見 C.2.14), 進而減少系統開銷。
大型系統由較小的系統所構成, 是以取決于它們的構成部分的反應式特性。 這意味着, 反應式系統應用了一些設計原則, 是以這些屬性也适用于所有級别的規模, 使得這些原則可以組合。 世界上最大型的系統都依賴于基于這些屬性的架構, 并每日服務于數十億人的需求。 現在,是時候從一開始就有意識地應用這些設計原則, 而不是每次都重新“發現”它們了。
牛津詞典把“asynchronous(異步的)”定義為“不同時存在或發生的”。 在本宣言的上下文中, 我們的意思是: 在來自用戶端的請求被發送到了服務之後, 對于該請求的處理可以發生這之後的任意時間點。 對于發生在服務内部的執行過程, 用戶端不能直接對其進行觀察, 或者與之進行同步。 這是同步處理(synchronous processing)的反義詞, 同步處理意味着用戶端隻能在服務已經處理完成了該請求之後, 才能恢複它自己的執行。
當某個元件(參見 C.2.4)(struggling to keep-up)正竭力地跟上(負載或者輸入的速率)時, 整個系統(參見 C.2.16)就需要以合理地方式作出反應。 對于正遭受壓力的元件來說, 無論是進行災難性地失敗, 還是不受控地丢棄消息, 都是不能接受的。 因為它既不能(成功地)應對(壓力), 又不能(直接地)失敗, 是以它應該向其上遊元件傳達其正在遭受壓力的事實, 并讓它們(該元件的上遊元件)降低負載。 這種回壓(back-pressure)是一種重要的回報機制, 使得系統得以優雅地響應負載, 而不是在負載下崩潰。 回壓可以一路級聯到(系統的)使用者, 在這時即時響應性可能有所降低, 但是這種機制将確定系統在負載之下具有回彈性/韌性, 并将提供資訊,進而允許系統本身通過利用其它資源來幫助分發負載,參見适應性/彈性(參見 C.2.6)。
目前計算機為反複執行同一項任務而進行了優化: 在(CPU的)時鐘頻率保持不變的情況下, 指令緩存和分支預測增加了每秒可以被處理的指令數。 這就意味着,快速連續地将不同的任務遞交給相同的CPU核心,将不能從本來可以實作的完全(最高使用率的)性能中獲益: 如果可能, 我們應該構造這樣的應用程式, 它的執行邏輯在不同的任務之間交替的頻率更低。 這就意味着可以成批地處理一組資料元素, 這也可能意味可以在專門的硬體線程(指CPU的邏輯核心)上執行不同處理步驟。
同樣的道理也适用于對于需要同步和協調的外部資源(參見 C.2.14)的使用。 當從單一線程(即CPU核心)發送指令, 而不是從所有的CPU核心争奪帶寬時, 由持久化儲存設備所提供的I/O帶寬将可以得到顯著提高。 使用單一入口的額外的效益,即多個操作可以被重新排序, 進而更好地适應裝置的最佳通路模式(當今的儲存設備的線性存取性能要優于随機存取的性能)。
此外, 批量處理還提供了分攤昂貴操作(如I/O)或者昂貴計算的成本的機會。 例如, 将多個資料項打包到同一個網絡資料包或者磁盤存儲塊中, 進而提高效能并降低使用率。
我們所描述的是一個子產品化的軟體架構, 其(實際上)是一個非常古老的概念, 參見Parnas(1972)。 我們正使用“元件(component)”(參見 C.2.8)這個術語, 因為它和“區劃(compartment)”聯系緊密, 其意味着每個元件都是自包含的、封閉的并和其它的元件相隔離。 這個概念首先适用于系統的運作時特征, 但是它通常也會反映在源代碼的子產品化結構中。 雖然不同的元件可能會使用相同的軟體子產品來執行通用的任務, 但是定義了每個元件的頂層行為的程式代碼則是元件本身的一個子產品。 元件邊界通常與問題域中的有界上下文(BoundedContext)緊密對齊。 這意味着,系統設計傾向于反應問題域, 并是以在保持隔離的同時也更加容易演化。 消息協定(參見 C.2.12)為多個有界上下文(BoundedContext)(元件)之間提供了自然的映射和通信層。
将任務異步地(參見 C.2.1)委派給另一個元件(參見 C.2.4)意味着該任務将會在另一個元件的上下文中被執行, 舉幾個可能的情況: 這個被委派的上下文可能需要在一個不同的錯誤處理上下文中、 在一個不同的線程上、 不同的程序中或者在一個不同的網絡節點上運作。 委派的目的是将處理某個任務的職責移交給另外一個元件, 以便發起委派的元件可以執行其它的處理、 或者有選擇性地觀察被委派的任務的進度, 以防需要執行額外的操作(如處理失敗或者報告進度)。
适應性/彈性意味着當資源根據需求按比例地減少或者增加時, 系統的吞吐量将自動地向下或者向上縮放, 進而滿足不同的需求。系統需要具有可伸縮性(參見可伸縮性 C.2.15), 以使得其可以從在運作時動态地添加或者删除資源中獲益。 是以,适應性/彈性是建立在可伸縮性的基礎之上的, 并通過添加自動的資源(參見 C.2.14)管理概念對其進行了擴充。
失敗是服務内部的意外事件, 其阻止了服務繼續正常地運作。 失敗通常會阻止對于目前的、 并可能所有接下來的用戶端請求的響應。 和錯誤相對照, 錯誤是意料之中的, 并且針各種情況進行了處理( 例如, 在輸入驗證的過程中所發現的錯誤), 将會作為該消息的正常處理過程的一部分傳回給用戶端。 而失敗是意料之外的, 并且在系統(參見 C.2.16)能夠恢複至(和之前)相同的服務水準之前,需要進行幹預。 這并不意味着失敗總是緻命的(fatal), 雖然在失敗發生之後, 系統的某些服務能力可能會被降低。 錯誤是正常操作流程預期的一部分, 在錯誤發生之後, 系統将會立即地對其進行處理, 并将繼續以相同的服務能力繼續運作。
失敗的例子有: 硬體故障、 由于緻命的資源耗盡而引起的程序意外終止,以及導緻系統内部狀态損壞的程式缺陷。
隔離可以定義為在時間和空間上的解耦。 在時間上解耦意味着發送者和接收者可以擁有獨立的生命周期—— 它們不需要在同時存在,進而使得互相通信成為可能。 通過在元件(參見 C.2.4)之間添加異步(參見 C.2.1)邊界, 以及通過消息傳遞(參見 C.2.10)來實作了這一點。 在空間上解耦(定義為位置透明性(參見 C.2.9))意味着發送者和接收者不必運作在同一個程序中。 不管運維部門或者運作時本身決策的部署結構是多麼的高效——但是在應用程式的生命周期之内,這一切都可能會發生改變。
真正的隔離超出了大多數面向對象的程式設計語言中的常見的封裝概念, 并使得我們可以劃分和遏制:
狀态和行為:它支援無共享的設計,并最大限度地減少了競争和一緻性成本(如通用伸縮性原則(Universal Scalability Law)中所定義的);
失敗:它支援在細粒度上捕獲、發出失敗信号以及管理失敗(參見 C.2.1), 而不是将其級聯擴散(cascade)到其它元件。
元件之間的強隔離性是建立在明确定義的協定(參見 C.2.12)的通信之上的, 并支援解耦, 進而使得系統更加容易被了解、擴充、測試和演化。
适應性/彈性(參見 C.2.6)系統需要能夠自适應, 并不間斷地對需求的變化做出反應。 它們需要優雅而高效地擴大或者縮減(部署)規模。 極大地簡化這個問題的一個關鍵洞察是:認識到我們一直都在處理分布式計算。 無論我們是在一台單獨的(具有多個獨立CPU,并通過快速通道互聯(QPI)通信的)節點之上, 還是在一個(具有多台通過網絡進行通信的獨立節點的)機器叢集之上運作我們的系統, 都是如此。 擁抱這一事實意味着, 在多核心之上進行垂直縮放和在叢集之上進行水準伸縮并沒有什麼概念上的差異。
如果我們所有的元件(參見 C.2.4)都支援移動性, 而本地通信隻是一項優化。 那麼我們根本不需要預先定義一個靜态的系統拓撲和部署結構。 可以将這個決策留給運維人員或者運作時, 讓他(它)們其可以根據系統的使用情況來對其進行調整和優化。
這種通過異步的(參見 C.2.1)消息傳遞(參見 C.2.10)實作的在空間上的(請參見隔離的定義, C.2.4 )解耦, 以及将運作時執行個體和它們的引用解耦,就是我們所謂的位置透明性。 位置透明性通常被誤認為是“透明的分布式計算”, 然而實際上恰恰相反: 我們擁抱網絡, 以及它所有的限制——如部分失敗、 網絡分裂、 消息丢失, 以及它的異步性和與生俱來的基于消息的性質,并将它們作為程式設計模型中的一等公民, 而不是嘗試在網絡上模拟程序内的方法調用(如RPC、XA等)。 我們對于位置透明性的觀點與Waldo等人著的A Note On Distributed Computing中的觀點完全一緻。
消息是發送到特定目的地的資料項, 事件是元件(參見 C.2.4)在達到了某個給定狀态時所發出的信号。 在消息驅動的系統中, 可尋址的接收者等待消息的到來, 并對消息做出反應, 否則隻是休眠(即異步非阻塞地等待消息的到來)。 而在事件驅動的系統中, 通知監聽器被附加到了事件源, 以便在事件被發出時調用它們(指回調)。 這也就意味着, 事件驅動的系統關注于可尋址的事件源, 而消息驅動的系統則着重于可尋址的接收者。 消息可以包含編碼為它的有效載荷的事件。
由于事件消耗鍊的短暫性, 是以在事件驅動的系統中很難實作回彈性/韌性: 當處理過程已經就緒,監聽器已經設定好, 以便于響應結果并對結果進行變換時, 這些監聽器通常都将直接地處理成功或者失敗(參見 C.2.7), 并向原始的用戶端報告執行結果。(這些監聽器)響應元件的失敗, 以便于恢複它(指失敗的元件)的正常功能,而在另外一方面, 需要處理的是那些并沒有與短暫的用戶端請求捆綁在一起的, 但是影響了整個元件的健康狀況的失敗。
在并發程式設計中, 如果争奪資源的線程并沒有被保護該資源的互斥所無限期地推遲執行, 那麼該算法則被認為是非阻塞的。 在實踐中, 這通常縮影為一個 API, 當資源可用時, 該API将允許通路該資源(參見 C.2.14), 否則它将會立即地傳回, 并通知調用者該資源目前不可用, 或者該操作已經啟動了,但是尚未完成。 某個資源的非阻塞 API 使得其調用者可以進行其它操作, 而不是被阻塞以等待該資源變為可用。 此外,還可以通過允許資源的用戶端注冊, 以便讓其在資源可用時,或者操作已經完成時獲得通知。
協定定義了在元件(參見 C.2.4)之間交換或者傳輸消息的方法與規範。 協定由會話參與者之間的關系、 協定的累計狀态以及允許發送的消息集所構成。 這意味着, 協定描述了會話參與者在何時可以發送什麼樣的消息給另外一個會話參與者。 協定可以按照其消息交換的形式進行分類, 一些常見的類型是:請求——響應模式、 重複的請求——響應模式(如 HTTP 中)、 釋出——訂閱模式、 以及(反應式)流模式(同時包含(動态地)推送和拉取)。
和本地程式設計接口相比, 協定則更加通用, 因為它可以包含兩個以上的參與者, 并且可以預見到消息交換的進展, 而接口僅僅指定了調用者和接收者之間每次一次的互動過程。
需要注意的是, 這裡所定義的協定隻指定了可能會發送什麼樣的消息, 而不是它們應該如何被編碼、解碼(即編解碼), 而且傳輸機制對于使用該協定的元件來說是透明的。
在不同的地方同時地執行一個元件(參見 C.2.4)被稱為複制。 這可能意味着在不同的線程或者線程池、 程序、 網絡節點或者計算中心中執行。 複制提供了可伸縮性(參見 C.2.15), 其中傳入的工作負載将會被分發到跨元件的多個執行個體中, 以及回彈性/韌性, 其中傳入的工作負載将會被複制到多個并行地處理相同請求的多個執行個體中。 這些方式可以結合使用, 例如, 在確定該元件的某個确定使用者的所有相關事務都将由兩個執行個體執行的同時, 執行個體的總數則又根據傳入的負載而變化,(參見适應性/彈性 ,C.2.6節)。
在複制有狀态的元件時,必須要小心同步副本之間的狀态資料,否則該元件的客戶則需要知道同步的模式,并且還違反了封裝的目的。通常,同步方案的選擇需要在一緻性和可用性之間進行權衡,如果允許被複制的副本可以在有限的時間段内不一緻(最終一緻性),那麼将會得到最佳的可用性,同時,完美的一緻性則要求所有的複制副本以一種步調一緻(lock-step)的方式推進它們的狀态。在這兩種“極端”之間存在着一系列的可能解決方案,是以每個元件都應該選擇最适合于其需要的方式。
元件(參見 C.2.4)執行其功能所依賴的一切都是資源, 資源必須要根據元件的需要而進行調配。 這包括 CPU 的配置設定、 記憶體以及持久化存儲以及網絡帶寬、 記憶體帶寬、 CPU 緩存、 内部插座的 CPU 連結、 可靠的計時器以及任務排程服務、 其它的輸入和輸出裝置、 外部服務(如資料庫或者網絡檔案系統等)等等。 所有的這些資源都必須要考慮到适應性/彈性(參見 C.2.6)和回彈性/韌性, 因為缺少必需的資源将妨礙元件在被需要時發揮正常作用。
一個系統(參見 C.2.16)通過利用更多的計算資源(參見 C.2.14)來提升其性能的能力, 是通過系統吞吐量的提升比上資源所增加的比值來衡量的。 一個完美的可伸縮性系統的特點是這兩個數字是成正比的。 所配置設定的資源加倍也将使得吞吐量翻倍。 可伸縮性通常受限于系統中所引入的瓶頸或者同步點, 參見Amdahl 定律以及 Gunther 的通用可伸縮模型( Amdahl’s Law and Gunther’s Universal Scalability Model)。
系統為它的使用者(參見 C.2.17)或者用戶端提供服務。 系統可大可小, 它們可以包含許多元件或者隻有少數幾個元件(參見 C.2.4)。 系統中的所有元件互相協作,進而提供這些服務。 在很多情況下, 位于相同系統中的多個元件具有某種用戶端——伺服器關系(例如,考慮一下,前端元件依賴于後端元件)。 一個系統将共享了一個通用的回彈性/韌性模型, 我們的意思是, 某個元件的失敗(參見 C.2.7)将會在該系統的内部得到處理, 并由一個元件委派(參見 C.2.5)給另外一個元件。 如果系統中的一組元件的功能、資源(參見 C.2.14)或者失敗模型都和系統中的其餘部分互相隔離, 那麼将這一組元件看作是系統的子系統将有所脾益。
我們使用這個術語來非正式地指代服務的任何消費者,可以是人或者其它服務。
本文旨在快速推廣Reactive/反應式 的概念,及其背後的思考,希望能夠有更多的人看到、進而思考并且一起推動Reactive/反應式架構、設計、程式設計的落地。