響應式編程與響應式系統
下載 Konrad Malawski 的免費電子書《為什麼選擇響應式?企業應用中的基本原則》,深入了解更多響應式技術的知識與好處。
自從 2013 年一起合作寫了《響應式宣言》之後,我們看著響應式從一種幾乎無人知曉的軟體構建技術——當時只有少數幾個公司的邊緣項目使用了這一技術——最後成為 中間件領域 大佬們全平台戰略中的一部分。本文旨在定義和澄清響應式各個方面的概念,方法是比較在響應式編程風格下和把響應式系統視作一個緊密整體的設計方法下編寫代碼的不同之處。
響應式是一組設計原則
響應式技術目前成功的標誌之一是「 響應式 」成為了一個熱詞,並且跟一些不同的事物與人聯繫在了一起——常常伴隨著像「 流 」、「 輕量級 」和「 實時 」這樣的詞。
舉個例子:當我們看到一支運動隊時(像棒球隊或者籃球隊),我們一般會把他們看成一個個單獨個體的組合,但是當他們之間碰撞不出火花,無法像一個團隊一樣高效地協作時,他們就會輸給一個「更差勁」的隊伍。從這篇文章的角度來看,響應式是一組設計原則,一種關於系統架構與設計的思考方式,一種關於在一個分散式環境下,當 實現技術 、工具和設計模式都只是一個更大系統的一部分時如何設計的思考方式。
這個例子展示了不經考慮地將一堆軟體拼揍在一起——儘管單獨來看,這些軟體都很優秀——和響應式系統之間的不同。在一個響應式系統中,正是不同 組件 間的相互作用讓響應式系統如此不同,它使得不同組件能夠獨立地運作,同時又一致協作從而達到最終想要的結果。
一個響應式系統 是一種 架構風格 ,它允許許多獨立的應用結合在一起成為一個單元,共同響應它們所處的環境,同時保留著對單元內其它應用的「感知」——這能夠表現為它能夠做到 放大/縮小規模 ,負載平衡,甚至能夠主動地執行這些步驟。
以響應式的風格(或者說,通過響應式編程)寫一個軟體是可能的;然而,那也不過是拼圖中的一塊罷了。雖然在上面的提到的各個方面似乎都足以稱其為「響應式的」,但僅就其它們自身而言,還不足以讓一個系統成為響應式的。
當人們在軟體開發與設計的語境下談論「響應式」時,他們的意思通常是以下三者之一:
- 響應式系統(架構與設計)
- 響應式編程(基於聲明的事件的)
- 函數響應式編程(FRP)
我們將調查這些做法與技術的意思,特別是前兩個。更明確地說,我們會在使用它們的時候討論它們,例如它們是怎麼聯繫在一起的,從它們身上又能到什麼樣的好處——特別是在為多核、雲或移動架構搭建系統的情境下。
讓我們先來說一說函數響應式編程吧,以及我們在本文後面不再討論它的原因。
函數響應式編程(FRP)
函數響應式編程 ,通常被稱作 FRP,是最常被誤解的。FRP 在二十年前就被 Conal Elliott 精確地定義過了了。但是最近這個術語卻被錯誤地 腳註1 用來描述一些像 Elm、Bacon.js 的技術以及其它技術中的響應式插件(RxJava、Rx.NET、 RxJS)。許多的 庫 聲稱他們支持 FRP,事實上他們說的並非響應式編程,因此我們不會再進一步討論它們。
響應式編程
響應式編程 ,不要把它跟函數響應式編程混淆了,它是非同步編程下的一個子集,也是一種範式,在這種範式下,由新信息的 有效性 推動邏輯的前進,而不是讓 一條執行線程 去推動 控制流 。
它能夠把問題分解為多個獨立的步驟,這些獨立的步驟可以以非同步且 非阻塞 的方式被執行,最後再組合在一起產生一條 工作流 ——它的輸入和輸出可能是 非綁定的 。
「非同步地」 被牛津詞典定義為「不在同一時刻存在或發生」,在我們的語境下,它意味著一條消息或者一個事件可發生在任何時刻,也有可能是在未來。這在響應式編程中是非常重要的一項技術,因為響應式編程允許[ 非阻塞式 ]的執行方式——執行線程在競爭一塊共享資源時不會因為 阻塞 而陷入等待(為了防止執行線程在當前的工作完成之前執行任何其它操作),而是在共享資源被佔用的期間轉而去做其它工作。 阿姆達爾定律 腳註2 告訴我們,競爭是 可伸縮性 最大的敵人,所以一個響應式系統應當在極少數的情況下才不得不做阻塞工作。
響應式編程一般是 事件驅動 ,相比之下,響應式系統則是 消息驅動 的——事件驅動與消息驅動之間的差別會在文章後面闡明。
響應式編程庫的應用程序介面(API)一般是以下二者之一:
- 基於回調的 —匿名的 間接作用 回調函數被綁定在 事件源 上,當事件被放入 數據流 中時,回調函數被調用。
- 聲明式的 ——通過函數的組合,通常是使用一些固定的函數,像 map、 filter、 fold 等等。
大部分的庫會混合這兩種風格,一般還帶有 基於流 的 操作符 ,像 windowing、 counts、 triggers。
說響應式編程跟 數據流編程 有關是很合理的,因為它強調的是數據流而不是控制流。
舉幾個為這種編程技術提供支持的的編程抽象概念:
- Futures/Promises——一個值的容器,具有 讀共享/寫獨佔 的語義,即使變數尚不可用也能夠添加非同步的值轉換操作。
- 流 - 響應式流——無限制的數據處理流,支持非同步,非阻塞式,支持多個源與目的的 反壓轉換管道 。
- 數據流變數——依賴於輸入、 過程 或者其它單元的 單賦值變數 (存儲單元),它能夠自動更新值的改變。其中一個應用例子是表格軟體——一個單元的值的改變會像漣漪一樣盪開,影響到所有依賴於它的函數,順流而下地使它們產生新的值。
在 JVM 中,支持響應式編程的流行庫有 Akka Streams、Ratpack、Reactor、RxJava 和 Vert.x 等等。這些庫實現了響應式編程的規範,成為 JVM 上響應式編程庫之間的 互通標準 ,並且根據它自身的敘述是「……一個為如何處理非阻塞式反壓非同步流提供標準的倡議」。
響應式編程的基本好處是:提高多核和多 CPU 硬體的計算資源利用率;根據阿姆達爾定律以及引申的 Günther 的通用可伸縮性定律 腳註3 ,通過減少 序列化點 來提高性能。
另一個好處是開發者生產效率,傳統的編程範式都儘力想提供一個簡單直接的可持續的方法來處理非同步非阻塞式計算和 I/O。在響應式編程中,因活動(active)組件之間通常不需要明確的協作,從而也就解決了其中大部分的挑戰。
響應式編程真正的發光點在於組件的創建跟工作流的組合。為了在非同步執行上取得最大的優勢,把 反壓 加進來是很重要,這樣能避免過度使用,或者確切地說,避免無限度的消耗資源。
儘管如此,響應式編程在搭建現代軟體上仍然非常有用,為了在更高層次上 理解 一個系統,那麼必須要使用到另一個工具: 響應式架構 ——設計響應式系統的方法。此外,要記住編程範式有很多,而響應式編程僅僅只是其中一個,所以如同其它工具一樣,響應式編程並不是萬金油,它不意圖適用於任何情況。
事件驅動 vs. 消息驅動
如上面提到的,響應式編程——專註於短時間的數據流鏈條上的計算——因此傾向於事件驅動,而響應式系統——關注於通過分散式系統的通信和協作所得到的彈性和韌性——則是消息驅動的 腳註4(或者稱之為 消息式 的)。
一個擁有 長期存活的可定址 組件的消息驅動系統跟一個事件驅動的數據流驅動模型的不同在於,消息具有固定的導向,而事件則沒有。消息會有明確的(一個)去向,而事件則只是一段等著被 觀察 的信息。另外, 消息式 更適用於非同步,因為消息的發送與接收和發送者和接收者是分離的。
響應式宣言中的術語表定義了兩者之間概念上的不同:
一條消息就是一則被送往一個明確目的地的數據。一個事件則是達到某個給定狀態的組件發出的一個信號。在一個消息驅動系統中,可定址到的接收者等待消息的到來然後響應它,否則保持休眠狀態。在一個事件驅動系統中,通知的監聽者被綁定到消息源上,這樣當消息被發出時它就會被調用。這意味著一個事件驅動系統專註於可定址的事件源而消息驅動系統專註於可定址的接收者。
分散式系統需要通過消息在網路上傳輸進行交流,以實現其溝通基礎,與之相反,事件的發出則是本地的。在底層通過發送包裹著事件的消息來搭建跨網路的事件驅動系統的做法很常見。這樣能夠維持在分散式環境下事件驅動編程模型的相對簡易性,並且在某些特殊的和合理的範圍內的使用案例上工作得很好。
然而,這是有利有弊的:在編程模型的抽象性和簡易性上得一分,在控制上就減一分。消息強迫我們去擁抱分散式系統的真實性和一致性——像 局部錯誤 , 錯誤偵測 , 丟棄/複製/重排序 消息,最後還有一致性,管理多個並發真實性等等——然後直面它們,去處理它們,而不是像過去無數次一樣,藏在一個蹩腳的抽象面罩後——假裝網路並不存在(例如EJB、 RPC、 CORBA 和 XA)。
這些在語義學和適用性上的不同在應用設計中有著深刻的含義,包括分散式系統的 複雜性 中的 彈性 、 韌性 、 移動性 、 位置透明性 和 管理 ,這些在文章後面再進行介紹。
在一個響應式系統中,特別是使用了響應式編程技術的,這樣的系統中就即有事件也有消息——一個是用於溝通的強大工具(消息),而另一個則呈現現實(事件)。
響應式系統和架構
響應式系統 —— 如同在《響應式宣言》中定義的那樣——是一組用於搭建現代系統——已充分準備好滿足如今應用程序所面對的不斷增長的需求的現代系統——的架構設計原則。
響應式系統的原則決對不是什麼新東西,它可以被追溯到 70 和 80 年代 Jim Gray 和 Pat Helland 在 串級系統 上和 Joe aomstrong 和 Robert Virding 在 Erland 上做出的重大工作。然而,這些人在當時都超越了時代,只有到了最近 5 - 10 年,技術行業才被不得不反思當前企業系統最好的開發實踐活動並且學習如何將來之不易的響應式原則應用到今天這個多核、雲計算和物聯網的世界中。
響應式系統的基石是 消息傳遞 ,消息傳遞為兩個組件之間創建一條暫時的邊界,使得它們能夠在 時間 上分離——實現並發性——和 空間 ——實現 分散式 與 移動性 。這種分離是兩個組件完全 隔離 以及實現 彈性 和 韌性 基礎的必需條件。
從程序到系統
這個世界的連通性正在變得越來越高。我們不再構建 程序 ——為單個操作子來計算某些東西的端到端邏輯——而更多地在構建 系統 了。
系統從定義上來說是複雜的——每一部分都包含多個組件,每個組件的自身或其子組件也可以是一個系統——這意味著軟體要正常工作已經越來越依賴於其它軟體。
我們今天構建的系統會在多個計算機上操作,小型的或大型的,或少或多,相近的或遠隔半個地球的。同時,由於人們的生活正變得越來越依賴於系統順暢運行的有效性,用戶的期望也變得越得越來越難以滿足。
為了實現用戶——和企業——能夠依賴的系統,這些系統必須是 靈敏的 ,這樣無論是某個東西提供了一個正確的響應,還是當需要一個響應時響應無法使用,都不會有影響。為了達到這一點,我們必須保證在錯誤( 彈性 )和欠載( 韌性 )下,系統仍然能夠保持靈敏性。為了實現這一點,我們把系統設計為 消息驅動的 ,我們稱其為 響應式系統 。
響應式系統的彈性
彈性是與 錯誤下 的 靈敏性 有關的,它是系統內在的功能特性,是需要被設計的東西,而不是能夠被動的加入系統中的東西。彈性是大於容錯性的——彈性無關於 故障退化 ——雖然故障退化對於系統來說是很有用的一種特性——與彈性相關的是與從錯誤中完全恢復達到 自愈 的能力。這就需要組件的隔離以及組件對錯誤的包容,以免錯誤散播到其相鄰組件中去——否則,通常會導致災難性的連鎖故障。
因此構建一個彈性的、 自愈 系統的關鍵是允許錯誤被:容納、具體化為消息,發送給其他(擔當 監管者 )的組件,從而在錯誤組件之外修復出一個安全環境。在這,消息驅動是其促成因素:遠離高度耦合的、脆弱的深層嵌套的同步調用鏈,大家長期要麼學會忍受其煎熬或直接忽略。解決的想法是將調用鏈中的錯誤管理分離,將客戶端從處理服務端錯誤的責任中解放出來。
響應式系統的韌性
韌性 是關於 欠載下的 靈敏性 的——意味著一個系統的吞吐量在資源增加或減少時能夠自動地相應 增加或減少 (同樣能夠 向內或外擴展 )以滿足不同的需求。這是利用雲計算承諾的特性所必需的因素:使系統利用資源更加有效,成本效益更佳,對環境友好以及實現按次付費。
系統必須能夠在不重寫甚至不重新設置的情況下,適應性地——即無需介入自動伸縮——響應狀態及行為,溝通負載均衡, 故障轉移 ,以及升級。實現這些的就是 位置透明性 :使用同一個方法,同樣的編程抽象,同樣的語義,在所有向度中 伸縮 系統的能力——從 CPU 核心到數據中心。
如同《響應式宣言》所述:
一個極大地簡化問題的關鍵洞見在於意識到我們都在使用分散式計算。無論我們的操作系統是運行在一個單一結點上(擁有多個獨立的 CPU,並通過 QPI 鏈接進行交流),還是在一個 節點集群 (獨立的機器,通過網路進行交流)上。擁抱這個事實意味著在垂直方向上多核的伸縮與在水平方面上集群的伸縮並無概念上的差異。在空間上的解耦 [...],是通過非同步消息傳送以及運行時實例與其引用解耦從而實現的,這就是我們所說的位置透明性。
因此,不論接收者在哪裡,我們都以同樣的方式與它交流。唯一能夠在語義上等同實現的方式是消息傳送。
響應式系統的生產效率
既然大多數的系統生來即是複雜的,那麼其中一個最重要的點即是保證一個系統架構在開發和維護組件時,最小程度地減低生產效率,同時將操作的 偶發複雜性 降到最低。
這一點很重要,因為在一個系統的生命周期中——如果系統的設計不正確——系統的維護會變得越來越困難,理解、定位和解決問題所需要花費時間和精力會不斷地上漲。
響應式系統是我們所知的最具 生產效率 的系統架構(在多核、雲及移動架構的背景下):
- 錯誤的隔離為組件與組件之間裹上艙壁(LCTT 譯註:當船遭到損壞進水時,艙壁能夠防止水從損壞的船艙流入其他船艙),防止引發連鎖錯誤,從而限制住錯誤的波及範圍以及嚴重性。
- 監管者的層級制度提供了多個等級的防護,搭配以自我修復能力,避免了許多曾經在偵查(inverstigate)時引發的操作 代價 ——大量的 瞬時故障 。
- 消息傳送和位置透明性允許組件被卸載下線、代替或 重新布線 同時不影響終端用戶的使用體驗,並降低中斷的代價、它們的相對緊迫性以及診斷和修正所需的資源。
- 複製減少了數據丟失的風險,減輕了數據 檢索 和存儲的有效性錯誤的影響。
- 韌性允許在使用率波動時保存資源,允許在負載很低時,最小化操作開銷,並且允許在負載增加時,最小化 運行中斷 或 緊急投入 伸縮性的風險。
因此,響應式系統使 生成系統 很好的應對錯誤、隨時間變化的負載——同時還能保持低運營成本。
響應式編程與響應式系統的關聯
響應式編程是一種管理 內部邏輯 和 數據流轉換 的好技術,在本地的組件中,做為一種優化代碼清晰度、性能以及資源利用率的方法。響應式系統,是一組架構上的原則,旨在強調分散式信息交流並為我們提供一種處理分散式系統彈性與韌性的工具。
只使用響應式編程常遇到的一個問題,是一個事件驅動的基於回調的或聲明式的程序中兩個計算階段的 高度耦合 ,使得 彈性 難以實現,因此時它的轉換鏈通常存活時間短,並且它的各個階段——回調函數或 組合子 ——是匿名的,也就是不可定址的。
這意味著,它通常在內部處理成功與錯誤的狀態而不會向外界發送相應的信號。這種定址能力的缺失導致單個 階段 很難恢復,因為它通常並不清楚異常應該,甚至不清楚異常可以,發送到何處去。
另一個與響應式系統方法的不同之處在於單純的響應式編程允許 時間 上的 解耦 ,但不允許 空間 上的(除非是如上面所述的,在底層通過網路傳送消息來 分發 數據流)。正如敘述的,在時間上的解耦使 並發性 成為可能,但是是空間上的解耦使 分布 和 移動性 (使得不僅僅靜態拓撲可用,還包括了動態拓撲)成為可能的——而這些正是 韌性 所必需的要素。
位置透明性的缺失使得很難以韌性方式對一個基於適應性響應式編程技術的程序進行向外擴展,因為這樣就需要分附加工具,例如 消息匯流排 , 數據網格 或者在頂層的 定製網路協議 。而這點正是響應式系統的消息驅動編程的閃光的地方,因為它是一個包含了其編程模型和所有伸縮向度語義的交流抽象概念,因此降低了複雜性與認知超載。
對於基於回調的編程,常會被提及的一個問題是寫這樣的程序或許相對來說會比較簡單,但最終會引發一些真正的後果。
例如,對於基於匿名回調的系統,當你想理解它們,維護它們或最重要的是在 生產供應中斷 或錯誤行為發生時,你想知道到底發生了什麼、發生在哪以及為什麼發生,但此時它們只提供極少的內部信息。
為響應式系統設計的庫與平台(例如 Akka 項目和 Erlang 平台)學到了這一點,它們依賴於那些更容易理解的長期存活的可定址組件。當錯誤發生時,根據導致錯誤的消息可以找到唯一的組件。當可定址的概念存在組件模型的核心中時, 監控方案 就有了一個 有意義 的方式來呈現它收集的數據——利用 傳播 的身份標識。
一個好的編程範式的選擇,一個選擇實現像可定址能力和錯誤管理這些東西的範式,已經被證明在生產中是無價的,因它在設計中承認了現實並非一帆風順,接受並擁抱錯誤的出現 而不是毫無希望地去嘗試避免錯誤。
總而言之,響應式編程是一個非常有用的實現技術,可以用在響應式架構當中。但是記住這隻能幫助管理一部分:非同步且非阻塞執行下的數據流管理——通常只在單個結點或服務中。當有多個結點時,就需要開始認真地考慮像 數據一致性 、 跨結點溝通 、 協調 、 版本控制 、 編製 、 錯誤管理 、 關注與責任 分離等等的東西——也即是:系統架構。
因此,要最大化響應式編程的價值,就把它作為構建響應式系統的工具來使用。構建一個響應式系統需要的不僅是在一個已存在的遺留下來的 軟體棧 上抽象掉特定的操作系統資源和少量的非同步 API 和 斷路器 。此時應該擁抱你在創建一個包含多個服務的分散式系統這一事實——這意味著所有東西都要共同合作,提供一致性與靈敏的體驗,而不僅僅是如預期工作,但同時還要在發生錯誤和不可預料的負載下正常工作。
總結
企業和中間件供應商在目睹了應用響應式所帶來的企業利潤增長後,同樣開始擁抱響應式。在本文中,我們把響應式系統做為企業最終目標進行描述——假設了多核、雲和移動架構的背景——而響應式編程則從中擔任重要工具的角色。
響應式編程在內部邏輯及數據流轉換的組件層次上為開發者提高了生產率——通過性能與資源的有效利用實現。而響應式系統在構建 原生雲 和其它大型分散式系統的系統層次上為架構師及 DevOps 從業者提高了生產率——通過彈性與韌性。我們建議在響應式系統設計原則中結合響應式編程技術。
腳註
via: https://www.oreilly.com/ideas/reactive-programming-vs-reactive-systems
作者:Jonas Bonér, Viktor Klang 譯者:XLCYun 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive