《反應式宣言》——TheReactiveManifesto

千萬別惹貓發表於2018-02-27

引子

本文比較抽象,不過是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), 從而減少系統開銷。

大型系統由較小的系統所構成, 因此取決於它們的構成部分的反應式特性。 這意味著, 反應式系統應用了一些設計原則, 因此這些屬性也適用於所有級別的規模, 使得這些原則可以組合。 世界上最大型的系統都依賴於基於這些屬性的架構, 並每日服務於數十億人的需求。 現在,是時候從一開始就有意識地應用這些設計原則, 而不是每次都重新“發現”它們了。

《反應式宣言》詞彙

C.2 詞彙表

C.2.1 非同步

牛津詞典把“asynchronous(非同步的)”定義為“不同時存在或發生的”。 在本宣言的上下文中, 我們的意思是: 在來自客戶端的請求被髮送到了服務之後, 對於該請求的處理可以發生這之後的任意時間點。 對於發生在服務內部的執行過程, 客戶端不能直接對其進行觀察, 或者與之進行同步。 這是同步處理(synchronous processing)的反義詞, 同步處理意味著客戶端只能在服務已經處理完成了該請求之後, 才能恢復它自己的執行。

C.2.2 回壓

當某個元件(參見 C.2.4)(struggling to keep-up)正竭力地跟上(負載或者輸入的速率)時, 整個系統(參見 C.2.16)就需要以合理地方式作出反應。 對於正遭受壓力的元件來說, 無論是進行災難性地失敗, 還是不受控地丟棄訊息, 都是不能接受的。 因為它既不能(成功地)應對(壓力), 又不能(直接地)失敗, 所以它應該向其上游元件傳達其正在遭受壓力的事實, 並讓它們(該元件的上游元件)降低負載。 這種回壓(back-pressure)是一種重要的反饋機制, 使得系統得以優雅地響應負載, 而不是在負載下崩潰。 回壓可以一路級聯到(系統的)使用者, 在這時即時響應性可能有所降低, 但是這種機制將確保系統在負載之下具有回彈性/韌性, 並將提供資訊,從而允許系統本身通過利用其它資源來幫助分發負載,參見適應性/彈性(參見 C.2.6)。

C.2.3 批量處理

當前計算機為反覆執行同一項任務而進行了優化: 在(CPU的)時脈頻率保持不變的情況下, 指令快取和分支預測增加了每秒可以被處理的指令數。 這就意味著,快速連續地將不同的任務遞交給相同的CPU核心,將不能從本來可以實現的完全(最高利用率的)效能中獲益: 如果可能, 我們應該構造這樣的應用程式, 它的執行邏輯在不同的任務之間交替的頻率更低。 這就意味著可以成批地處理一組資料元素, 這也可能意味可以在專門的硬體執行緒(指CPU的邏輯核心)上執行不同處理步驟。

同樣的道理也適用於對於需要同步和協調的外部資源(參見 C.2.14)的使用。 當從單一執行緒(即CPU核心)傳送指令, 而不是從所有的CPU核心爭奪頻寬時, 由持久化儲存裝置所提供的I/O頻寬將可以得到顯著提高。 使用單一入口的額外的效益,即多個操作可以被重新排序, 從而更好地適應裝置的最佳訪問模式(當今的儲存裝置的線性存取效能要優於隨機存取的效能)。

此外, 批量處理還提供了分攤昂貴操作(如I/O)或者昂貴計算的成本的機會。 例如, 將多個資料項打包到同一個網路資料包或者磁碟儲存塊中, 從而提高效能並降低使用率。

C.2.4 元件

我們所描述的是一個模組化的軟體架構, 其(實際上)是一個非常古老的概念, 參見Parnas(1972)。 我們正使用“元件(component)”(參見 C.2.8)這個術語, 因為它和“區劃(compartment)”聯絡緊密, 其意味著每個元件都是自包含的、封閉的並和其它的元件相隔離。 這個概念首先適用於系統的執行時特徵, 但是它通常也會反映在原始碼的模組化結構中。 雖然不同的元件可能會使用相同的軟體模組來執行通用的任務, 但是定義了每個元件的頂層行為的程式程式碼則是元件本身的一個模組。 元件邊界通常與問題域中的有界上下文(BoundedContext)緊密對齊。 這意味著,系統設計傾向於反應問題域, 並因此在保持隔離的同時也更加容易演化。 訊息協議(參見 C.2.12)為多個有界上下文(BoundedContext)(元件)之間提供了自然的對映和通訊層。

C.2.5 委派

將任務非同步地(參見 C.2.1)委派給另一個元件(參見 C.2.4)意味著該任務將會在另一個元件的上下文中被執行, 舉幾個可能的情況: 這個被委派的上下文可能需要在一個不同的錯誤處理上下文中、 在一個不同的執行緒上、 不同的程式中或者在一個不同的網路節點上執行。 委派的目的是將處理某個任務的職責移交給另外一個元件, 以便發起委派的元件可以執行其它的處理、 或者有選擇性地觀察被委派的任務的進度, 以防需要執行額外的操作(如處理失敗或者報告進度)。

C.2.6 適應性/彈性(與”可伸縮性”對照)

適應性/彈性意味著當資源根據需求按比例地減少或者增加時, 系統的吞吐量將自動地向下或者向上縮放, 從而滿足不同的需求。系統需要具有可伸縮性(參見可伸縮性 C.2.15), 以使得其可以從在執行時動態地新增或者刪除資源中獲益。 因此,適應性/彈性是建立在可伸縮性的基礎之上的, 並通過新增自動的資源(參見 C.2.14)管理概念對其進行了擴充。

C.2.7 失敗(和錯誤相對照)

失敗是服務內部的意外事件, 其阻止了服務繼續正常地執行。 失敗通常會阻止對於當前的、 並可能所有接下來的客戶端請求的響應。 和錯誤相對照, 錯誤是意料之中的, 並且針各種情況進行了處理( 例如, 在輸入驗證的過程中所發現的錯誤), 將會作為該訊息的正常處理過程的一部分返回給客戶端。 而失敗是意料之外的, 並且在系統(參見 C.2.16)能夠恢復至(和之前)相同的服務水平之前,需要進行干預。 這並不意味著失敗總是致命的(fatal), 雖然在失敗發生之後, 系統的某些服務能力可能會被降低。 錯誤是正常操作流程預期的一部分, 在錯誤發生之後, 系統將會立即地對其進行處理, 並將繼續以相同的服務能力繼續執行。

失敗的例子有: 硬體故障、 由於致命的資源耗盡而引起的程式意外終止,以及導致系統內部狀態損壞的程式缺陷。

C.2.8 隔離(和“遏制”)

隔離可以定義為在時間和空間上的解耦。 在時間上解耦意味著傳送者和接收者可以擁有獨立的生命週期—— 它們不需要在同時存在,從而使得相互通訊成為可能。 通過在元件(參見 C.2.4)之間新增非同步(參見 C.2.1)邊界, 以及通過訊息傳遞(參見 C.2.10)來實現了這一點。 在空間上解耦(定義為位置透明性(參見 C.2.9))意味著傳送者和接收者不必執行在同一個程式中。 不管運維部門或者執行時本身決策的部署結構是多麼的高效——但是在應用程式的生命週期之內,這一切都可能會發生改變。

真正的隔離超出了大多數物件導向的程式語言中的常見的封裝概念, 並使得我們可以劃分和遏制:

  • 狀態和行為:它支援無共享的設計,並最大限度地減少了競爭和一致性成本(如通用伸縮性原則(Universal Scalability Law)中所定義的);
  • 失敗:它支援在細粒度上捕獲、發出失敗訊號以及管理失敗(參見 C.2.1), 而不是將其級聯擴散(cascade)到其它元件。

元件之間的強隔離性是建立在明確定義的協議(參見 C.2.12)的通訊之上的, 並支援解耦, 從而使得系統更加容易被理解、擴充套件、測試和演化。

C.2.9 位置透明性

適應性/彈性(參見 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.10 訊息驅動(與事件驅動對照)

訊息是傳送到特定目的地的資料項, 事件是元件(參見 C.2.4)在達到了某個給定狀態時所發出的訊號。 在訊息驅動的系統中, 可定址的接收者等待訊息的到來, 並對訊息做出反應, 否則只是休眠(即非同步非阻塞地等待訊息的到來)。 而在事件驅動的系統中, 通知監聽器被附加到了事件源, 以便在事件被髮出時呼叫它們(指回撥)。 這也就意味著, 事件驅動的系統關注於可定址的事件源, 而訊息驅動的系統則著重於可定址的接收者。 訊息可以包含編碼為它的有效載荷的事件。

由於事件消耗鏈的短暫性, 所以在事件驅動的系統中很難實現回彈性/韌性: 當處理過程已經就緒,監聽器已經設定好, 以便於響應結果並對結果進行變換時, 這些監聽器通常都將直接地處理成功或者失敗(參見 C.2.7), 並向原始的客戶端報告執行結果。(這些監聽器)響應元件的失敗, 以便於恢復它(指失敗的元件)的正常功能,而在另外一方面, 需要處理的是那些並沒有與短暫的客戶端請求捆綁在一起的, 但是影響了整個元件的健康狀況的失敗。

C.2.11 非阻塞的

在併發程式設計中, 如果爭奪資源的執行緒並沒有被保護該資源的互斥所無限期地推遲執行, 那麼該演算法則被認為是非阻塞的。 在實踐中, 這通常縮影為一個 API, 當資源可用時, 該API將允許訪問該資源(參見 C.2.14), 否則它將會立即地返回, 並通知呼叫者該資源當前不可用, 或者該操作已經啟動了,但是尚未完成。 某個資源的非阻塞 API 使得其呼叫者可以進行其它操作, 而不是被阻塞以等待該資源變為可用。 此外,還可以通過允許資源的客戶端註冊, 以便讓其在資源可用時,或者操作已經完成時獲得通知。

C.2.12 協議

協議定義了在元件(參見 C.2.4)之間交換或者傳輸訊息的方法與規範。 協議由會話參與者之間的關係、 協議的累計狀態以及允許傳送的訊息集所構成。 這意味著, 協議描述了會話參與者在何時可以傳送什麼樣的訊息給另外一個會話參與者。 協議可以按照其訊息交換的形式進行分類, 一些常見的型別是:請求——響應模式、 重複的請求——響應模式(如 HTTP 中)、 釋出——訂閱模式、 以及(反應式)流模式(同時包含(動態地)推送和拉取)。

和本地程式設計介面相比, 協議則更加通用, 因為它可以包含兩個以上的參與者, 並且可以預見到訊息交換的進展, 而介面僅僅指定了呼叫者和接收者之間每次一次的互動過程。

需要注意的是, 這裡所定義的協議只指定了可能會傳送什麼樣的訊息, 而不是它們應該如何被編碼、解碼(即編解碼), 而且傳輸機制對於使用該協議的元件來說是透明的。

C.2.13 複製

在不同的地方同時地執行一個元件(參見 C.2.4)被稱為複製。 這可能意味著在不同的執行緒或者執行緒池、 程式、 網路節點或者計算中心中執行。 複製提供了可伸縮性(參見 C.2.15), 其中傳入的工作負載將會被分發到跨元件的多個例項中, 以及回彈性/韌性, 其中傳入的工作負載將會被複制到多個並行地處理相同請求的多個例項中。 這些方式可以結合使用, 例如, 在確保該元件的某個確定使用者的所有相關事務都將由兩個例項執行的同時, 例項的總數則又根據傳入的負載而變化,(參見適應性/彈性 ,C.2.6節)。

在複製有狀態的元件時,必須要小心同步副本之間的狀態資料,否則該元件的客戶則需要知道同步的模式,並且還違反了封裝的目的。通常,同步方案的選擇需要在一致性和可用性之間進行權衡,如果允許被複制的副本可以在有限的時間段內不一致(最終一致性),那麼將會得到最佳的可用性,同時,完美的一致性則要求所有的複製副本以一種步調一致(lock-step)的方式推進它們的狀態。在這兩種“極端”之間存在著一系列的可能解決方案,所以每個元件都應該選擇最適合於其需要的方式。

C.2.14 資源

元件(參見 C.2.4)執行其功能所依賴的一切都是資源, 資源必須要根據元件的需要而進行調配。 這包括 CPU 的分配、 記憶體以及持久化儲存以及網路頻寬、 記憶體頻寬、 CPU 快取、 內部插座的 CPU 連結、 可靠的計時器以及任務排程服務、 其它的輸入和輸出裝置、 外部服務(如資料庫或者網路檔案系統等)等等。 所有的這些資源都必須要考慮到適應性/彈性(參見 C.2.6)和回彈性/韌性, 因為缺少必需的資源將妨礙元件在被需要時發揮正常作用。

C.2.15 可伸縮性

一個系統(參見 C.2.16)通過利用更多的計算資源(參見 C.2.14)來提升其效能的能力, 是通過系統吞吐量的提升比上資源所增加的比值來衡量的。 一個完美的可伸縮性系統的特點是這兩個數字是成正比的。 所分配的資源加倍也將使得吞吐量翻倍。 可伸縮性通常受限於系統中所引入的瓶頸或者同步點, 參見Amdahl 定律以及 Gunther 的通用可伸縮模型( Amdahl’s Law and Gunther’s Universal Scalability Model)。

C.2.16 系統

系統為它的使用者(參見 C.2.17)或者客戶端提供服務。 系統可大可小, 它們可以包含許多元件或者只有少數幾個元件(參見 C.2.4)。 系統中的所有元件相互協作,從而提供這些服務。 在很多情況下, 位於相同系統中的多個元件具有某種客戶端——伺服器關係(例如,考慮一下,前端元件依賴於後端元件)。 一個系統將共享了一個通用的回彈性/韌性模型, 我們的意思是, 某個元件的失敗(參見 C.2.7)將會在該系統的內部得到處理, 並由一個元件委派(參見 C.2.5)給另外一個元件。 如果系統中的一組元件的功能、資源(參見 C.2.14)或者失敗模型都和系統中的其餘部分相互隔離, 那麼將這一組元件看作是系統的子系統將有所脾益。

C.2.17 使用者

我們使用這個術語來非正式地指代服務的任何消費者,可以是人或者其它服務。

結語

本文旨在快速推廣Reactive/反應式 的概念,及其背後的思考,希望能夠有更多的人看到、進而思考並且一起推動Reactive/反應式架構、設計、程式設計的落地。


相關文章