響應式程式設計與響應式系統

Jonas Bonér, Viktor Klang發表於2017-08-12

在恆久的迷惑與過多期待的海洋中,登上一組簡單響應式設計原則的小島。

下載 Konrad Malawski 的免費電子書《為什麼選擇響應式?企業應用中的基本原則》,深入瞭解更多響應式技術的知識與好處。

自從 2013 年一起合作寫了《響應式宣言》之後,我們看著響應式從一種幾乎無人知曉的軟體構建技術——當時只有少數幾個公司的邊緣專案使用了這一技術——最後成為中介軟體領域middleware field大佬們全平臺戰略中的一部分。本文旨在定義和澄清響應式各個方面的概念,方法是比較在響應式程式設計風格下和把響應式系統視作一個緊密整體的設計方法下編寫程式碼的不同之處。

響應式是一組設計原則

響應式技術目前成功的標誌之一是“響應式reactive”成為了一個熱詞,並且跟一些不同的事物與人聯絡在了一起——常常伴隨著像“streaming”、“輕量級lightweight”和“實時real-time”這樣的詞。

舉個例子:當我們看到一支運動隊時(像棒球隊或者籃球隊),我們一般會把他們看成一個個單獨個體的組合,但是當他們之間碰撞不出火花,無法像一個團隊一樣高效地協作時,他們就會輸給一個“更差勁”的隊伍。從這篇文章的角度來看,響應式是一組設計原則,一種關於系統架構與設計的思考方式,一種關於在一個分散式環境下,當實現技術implementation techniques、工具和設計模式都只是一個更大系統的一部分時如何設計的思考方式。

這個例子展示了不經考慮地將一堆軟體拼揍在一起——儘管單獨來看,這些軟體都很優秀——和響應式系統之間的不同。在一個響應式系統中,正是不同元件parts間的相互作用讓響應式系統如此不同,它使得不同元件能夠獨立地運作,同時又一致協作從而達到最終想要的結果。

一個響應式系統 是一種架構風格architectural style,它允許許多獨立的應用結合在一起成為一個單元,共同響應它們所處的環境,同時保留著對單元內其它應用的“感知”——這能夠表現為它能夠做到放大/縮小規模scale up/down,負載平衡,甚至能夠主動地執行這些步驟。

以響應式的風格(或者說,透過響應式程式設計)寫一個軟體是可能的;然而,那也不過是拼圖中的一塊罷了。雖然在上面的提到的各個方面似乎都足以稱其為“響應式的”,但僅就其它們自身而言,還不足以讓一個系統成為響應式的。

當人們在軟體開發與設計的語境下談論“響應式”時,他們的意思通常是以下三者之一:

  • 響應式系統(架構與設計)
  • 響應式程式設計(基於宣告的事件的)
  • 函式響應式程式設計(FRP)

我們將調查這些做法與技術的意思,特別是前兩個。更明確地說,我們會在使用它們的時候討論它們,例如它們是怎麼聯絡在一起的,從它們身上又能到什麼樣的好處——特別是在為多核、雲或移動架構搭建系統的情境下。

讓我們先來說一說函式響應式程式設計吧,以及我們在本文後面不再討論它的原因。

函式響應式程式設計(FRP)

函式響應式程式設計Functional reactive programming,通常被稱作 FRP,是最常被誤解的。FRP 在二十年前就被 Conal Elliott 精確地定義過了了。但是最近這個術語卻被錯誤地腳註1 用來描述一些像 Elm、Bacon.js 的技術以及其它技術中的響應式外掛(RxJava、Rx.NET、 RxJS)。許多的libraries聲稱他們支援 FRP,事實上他們說的並非響應式程式設計,因此我們不會再進一步討論它們。

響應式程式設計

響應式程式設計Reactive programming,不要把它跟函式響應式程式設計混淆了,它是非同步程式設計下的一個子集,也是一種正規化,在這種正規化下,由新資訊的有效性availability推動邏輯的前進,而不是讓一條執行執行緒a thread-of-execution去推動控制流control flow

它能夠把問題分解為多個獨立的步驟,這些獨立的步驟可以以非同步且非阻塞non-blocking的方式被執行,最後再組合在一起產生一條工作流workflow——它的輸入和輸出可能是非繫結的unbounded

“非同步地”Asynchronous被牛津詞典定義為“不在同一時刻存在或發生”,在我們的語境下,它意味著一條訊息或者一個事件可發生在任何時刻,也有可能是在未來。這在響應式程式設計中是非常重要的一項技術,因為響應式程式設計允許[非阻塞式non-blocking]的執行方式——執行執行緒在競爭一塊共享資源時不會因為阻塞blocking而陷入等待(為了防止執行執行緒在當前的工作完成之前執行任何其它操作),而是在共享資源被佔用的期間轉而去做其它工作。阿姆達爾定律Amdahl's Law 腳註2 告訴我們,競爭是可伸縮性scalability最大的敵人,所以一個響應式系統應當在極少數的情況下才不得不做阻塞工作。

響應式程式設計一般是事件驅動event-driven,相比之下,響應式系統則是訊息驅動message-driven的——事件驅動與訊息驅動之間的差別會在文章後面闡明。

響應式程式設計庫的應用程式介面(API)一般是以下二者之一:

  • 基於回撥的Callback-based—匿名的間接作用side-effecting回撥函式被繫結在事件源event sources上,當事件被放入資料流dataflow chain中時,回撥函式被呼叫。
  • 宣告式的Declarative——透過函式的組合,通常是使用一些固定的函式,像 mapfilterfold 等等。

大部分的庫會混合這兩種風格,一般還帶有基於流stream-based運算子operators,像 windowing、 counts、 triggers。

說響應式程式設計跟 資料流程式設計dataflow programming有關是很合理的,因為它強調的是資料流而不是控制流

舉幾個為這種程式設計技術提供支援的的程式設計抽象概念:

  • Futures/Promises——一個值的容器,具有讀共享/寫獨佔many-read/single-write的語義,即使變數尚不可用也能夠新增非同步的值轉換操作。
  • streams - 響應式流——無限制的資料處理流,支援非同步,非阻塞式,支援多個源與目的的反壓轉換管道back-pressured transformation pipelines
  • 資料流變數——依賴於輸入、過程procedures或者其它單元的單賦值變數single assignment variables(儲存單元),它能夠自動更新值的改變。其中一個應用例子是表格軟體——一個單元的值的改變會像漣漪一樣盪開,影響到所有依賴於它的函式,順流而下地使它們產生新的值。

在 JVM 中,支援響應式程式設計的流行庫有 Akka Streams、Ratpack、Reactor、RxJava 和 Vert.x 等等。這些庫實現了響應式程式設計的規範,成為 JVM 上響應式程式設計庫之間的互通標準standard for interoperability,並且根據它自身的敘述是“……一個為如何處理非阻塞式反壓非同步流提供標準的倡議”。

響應式程式設計的基本好處是:提高多核和多 CPU 硬體的計算資源利用率;根據阿姆達爾定律以及引申的 Günther 的通用可伸縮性定律Günther’s Universal Scalability Law 腳註3 ,透過減少序列化點serialization points來提高效能。

另一個好處是開發者生產效率,傳統的程式設計正規化都盡力想提供一個簡單直接的可持續的方法來處理非同步非阻塞式計算和 I/O。在響應式程式設計中,因活動(active)元件之間通常不需要明確的協作,從而也就解決了其中大部分的挑戰。

響應式程式設計真正的發光點在於元件的建立跟工作流的組合。為了在非同步執行上取得最大的優勢,把 反壓back-pressure加進來是很重要,這樣能避免過度使用,或者確切地說,避免無限度的消耗資源。

儘管如此,響應式程式設計在搭建現代軟體上仍然非常有用,為了在更高層次上理解reason about一個系統,那麼必須要使用到另一個工具:響應式架構reactive architecture——設計響應式系統的方法。此外,要記住程式設計正規化有很多,而響應式程式設計僅僅只是其中一個,所以如同其它工具一樣,響應式程式設計並不是萬金油,它不意圖適用於任何情況。

事件驅動 vs. 訊息驅動

如上面提到的,響應式程式設計——專注於短時間的資料流鏈條上的計算——因此傾向於事件驅動,而響應式系統——關注於透過分散式系統的通訊和協作所得到的彈性和韌性——則是訊息驅動的 腳註4 (或者稱之為 訊息式messaging 的)。

一個擁有長期存活的可定址long-lived addressable元件的訊息驅動系統跟一個事件驅動的資料流驅動模型的不同在於,訊息具有固定的導向,而事件則沒有。訊息會有明確的(一個)去向,而事件則只是一段等著被觀察observe的資訊。另外,訊息式messaging更適用於非同步,因為訊息的傳送與接收和傳送者和接收者是分離的。

響應式宣言中的術語表定義了兩者之間概念上的不同

一條訊息就是一則被送往一個明確目的地的資料。一個事件則是達到某個給定狀態的元件發出的一個訊號。在一個訊息驅動系統中,可定址到的接收者等待訊息的到來然後響應它,否則保持休眠狀態。在一個事件驅動系統中,通知的監聽者被繫結到訊息源上,這樣當訊息被髮出時它就會被呼叫。這意味著一個事件驅動系統專注於可定址的事件源而訊息驅動系統專注於可定址的接收者。

分散式系統需要透過訊息在網路上傳輸進行交流,以實現其溝通基礎,與之相反,事件的發出則是本地的。在底層透過傳送包裹著事件的訊息來搭建跨網路的事件驅動系統的做法很常見。這樣能夠維持在分散式環境下事件驅動程式設計模型的相對簡易性,並且在某些特殊的和合理的範圍內的使用案例上工作得很好。

然而,這是有利有弊的:在程式設計模型的抽象性和簡易性上得一分,在控制上就減一分。訊息強迫我們去擁抱分散式系統的真實性和一致性——像區域性錯誤partial failures錯誤偵測failure detection丟棄/複製/重排序dropped/duplicated/reordered訊息,最後還有一致性,管理多個併發真實性等等——然後直面它們,去處理它們,而不是像過去無數次一樣,藏在一個蹩腳的抽象面罩後——假裝網路並不存在(例如EJB、 RPCCORBAXA)。

這些在語義學和適用性上的不同在應用設計中有著深刻的含義,包括分散式系統的複雜性complexity中的 彈性resilience韌性elasticity移動性mobility位置透明性location transparency管理management,這些在文章後面再進行介紹。

在一個響應式系統中,特別是使用了響應式程式設計技術的,這樣的系統中就即有事件也有訊息——一個是用於溝通的強大工具(訊息),而另一個則呈現現實(事件)。

響應式系統和架構

響應式系統 —— 如同在《響應式宣言》中定義的那樣——是一組用於搭建現代系統——已充分準備好滿足如今應用程式所面對的不斷增長的需求的現代系統——的架構設計原則。

響應式系統的原則決對不是什麼新東西,它可以被追溯到 70 和 80 年代 Jim Gray 和 Pat Helland 在 串級系統Tandem System上和 Joe aomstrong 和 Robert Virding 在 Erland 上做出的重大工作。然而,這些人在當時都超越了時代,只有到了最近 5 - 10 年,技術行業才被不得不反思當前企業系統最好的開發實踐活動並且學習如何將來之不易的響應式原則應用到今天這個多核、雲端計算和物聯網的世界中。

響應式系統的基石是訊息傳遞message-passing,訊息傳遞為兩個元件之間建立一條暫時的邊界,使得它們能夠在 時間 上分離——實現併發性——和 空間space ——實現分散式distribution移動性mobility。這種分離是兩個元件完全 隔離isolation以及實現 彈性resilience 韌性elasticity基礎的必需條件。

從程式到系統

這個世界的連通性正在變得越來越高。我們不再構建 程式 ——為單個操作子來計算某些東西的端到端邏輯——而更多地在構建 系統 了。

系統從定義上來說是複雜的——每一部分都包含多個元件,每個元件的自身或其子元件也可以是一個系統——這意味著軟體要正常工作已經越來越依賴於其它軟體。

我們今天構建的系統會在多個計算機上操作,小型的或大型的,或少或多,相近的或遠隔半個地球的。同時,由於人們的生活正變得越來越依賴於系統順暢執行的有效性,使用者的期望也變得越得越來越難以滿足。

為了實現使用者——和企業——能夠依賴的系統,這些系統必須是 靈敏的responsive ,這樣無論是某個東西提供了一個正確的響應,還是當需要一個響應時響應無法使用,都不會有影響。為了達到這一點,我們必須保證在錯誤( 彈性 )和欠載( 韌性 )下,系統仍然能夠保持靈敏性。為了實現這一點,我們把系統設計為 訊息驅動的 ,我們稱其為 響應式系統

響應式系統的彈性

彈性是與 錯誤下靈敏性responsiveness有關的,它是系統內在的功能特性,是需要被設計的東西,而不是能夠被動的加入系統中的東西。彈性是大於容錯性的——彈性無關於故障退化graceful degradation——雖然故障退化對於系統來說是很有用的一種特性——與彈性相關的是與從錯誤中完全恢復達到 自愈 的能力。這就需要元件的隔離以及元件對錯誤的包容,以免錯誤散播到其相鄰元件中去——否則,通常會導致災難性的連鎖故障。

因此構建一個彈性的、自愈self-healing系統的關鍵是允許錯誤被:容納、具體化為訊息,傳送給其他(擔當監管者supervisors)的元件,從而在錯誤元件之外修復出一個安全環境。在這,訊息驅動是其促成因素:遠離高度耦合的、脆弱的深層巢狀的同步呼叫鏈,大家長期要麼學會忍受其煎熬或直接忽略。解決的想法是將呼叫鏈中的錯誤管理分離,將客戶端從處理服務端錯誤的責任中解放出來。

響應式系統的韌性

韌性Elasticity是關於 欠載下的靈敏性responsiveness 的——意味著一個系統的吞吐量在資源增加或減少時能夠自動地相應增加或減少scales up or down(同樣能夠向內或外擴充套件scales in or out)以滿足不同的需求。這是利用雲端計算承諾的特性所必需的因素:使系統利用資源更加有效,成本效益更佳,對環境友好以及實現按次付費。

系統必須能夠在不重寫甚至不重新設定的情況下,適應性地——即無需介入自動伸縮——響應狀態及行為,溝通負載均衡,故障轉移failover,以及升級。實現這些的就是 位置透明性location transparency:使用同一個方法,同樣的程式設計抽象,同樣的語義,在所有向度中伸縮scaling系統的能力——從 CPU 核心到資料中心。

如同《響應式宣言》所述:

一個極大地簡化問題的關鍵洞見在於意識到我們都在使用分散式計算。無論我們的作業系統是執行在一個單一結點上(擁有多個獨立的 CPU,並透過 QPI 連結進行交流),還是在一個節點叢集cluster of nodes(獨立的機器,透過網路進行交流)上。擁抱這個事實意味著在垂直方向上多核的伸縮與在水平方面上叢集的伸縮並無概念上的差異。在空間上的解耦 [...],是透過非同步訊息傳送以及執行時例項與其引用解耦從而實現的,這就是我們所說的位置透明性。

因此,不論接收者在哪裡,我們都以同樣的方式與它交流。唯一能夠在語義上等同實現的方式是訊息傳送。

響應式系統的生產效率

既然大多數的系統生來即是複雜的,那麼其中一個最重要的點即是保證一個系統架構在開發和維護元件時,最小程度地減低生產效率,同時將操作的 偶發複雜性accidental complexity降到最低。

這一點很重要,因為在一個系統的生命週期中——如果系統的設計不正確——系統的維護會變得越來越困難,理解、定位和解決問題所需要花費時間和精力會不斷地上漲。

響應式系統是我們所知的最具 生產效率 的系統架構(在多核、雲及移動架構的背景下):

  • 錯誤的隔離為元件與元件之間裹上艙壁(LCTT 譯註:當船遭到損壞進水時,艙壁能夠防止水從損壞的船艙流入其他船艙),防止引發連鎖錯誤,從而限制住錯誤的波及範圍以及嚴重性。
  • 監管者的層級制度提供了多個等級的防護,搭配以自我修復能力,避免了許多曾經在偵查(inverstigate)時引發的操作代價cost——大量的瞬時故障transient failures
  • 訊息傳送和位置透明性允許元件被解除安裝下線、代替或重新佈線rerouted同時不影響終端使用者的使用體驗,並降低中斷的代價、它們的相對緊迫性以及診斷和修正所需的資源。
  • 複製減少了資料丟失的風險,減輕了資料檢索retrieval和儲存的有效性錯誤的影響。
  • 韌性允許在使用率波動時儲存資源,允許在負載很低時,最小化操作開銷,並且允許在負載增加時,最小化執行中斷outgae緊急投入urgent investment伸縮性的風險。

因此,響應式系統使生成系統creation systems很好的應對錯誤、隨時間變化的負載——同時還能保持低運營成本。

響應式程式設計與響應式系統的關聯

響應式程式設計是一種管理內部邏輯internal logic資料流轉換dataflow transformation的好技術,在本地的元件中,做為一種最佳化程式碼清晰度、效能以及資源利用率的方法。響應式系統,是一組架構上的原則,旨在強調分散式資訊交流併為我們提供一種處理分散式系統彈性與韌性的工具。

只使用響應式程式設計常遇到的一個問題,是一個事件驅動的基於回撥的或宣告式的程式中兩個計算階段的高度耦合tight coupling,使得 彈性 難以實現,因此時它的轉換鏈通常存活時間短,並且它的各個階段——回撥函式或組合子combinator——是匿名的,也就是不可定址的。

這意味著,它通常在內部處理成功與錯誤的狀態而不會向外界傳送相應的訊號。這種定址能力的缺失導致單個階段stages很難恢復,因為它通常並不清楚異常應該,甚至不清楚異常可以,傳送到何處去。

另一個與響應式系統方法的不同之處在於單純的響應式程式設計允許 時間 上的解耦decoupling,但不允許 空間 上的(除非是如上面所述的,在底層透過網路傳送訊息來分發distribute資料流)。正如敘述的,在時間上的解耦使 併發性 成為可能,但是是空間上的解耦使 分佈distribution 移動性mobility (使得不僅僅靜態拓撲可用,還包括了動態拓撲)成為可能的——而這些正是 韌性 所必需的要素。

位置透明性的缺失使得很難以韌性方式對一個基於適應性響應式程式設計技術的程式進行向外擴充套件,因為這樣就需要分附加工具,例如訊息匯流排message bus資料網格data grid或者在頂層的定製網路協議bespoke network protocol。而這點正是響應式系統的訊息驅動程式設計的閃光的地方,因為它是一個包含了其程式設計模型和所有伸縮向度語義的交流抽象概念,因此降低了複雜性與認知超載。

對於基於回撥的程式設計,常會被提及的一個問題是寫這樣的程式或許相對來說會比較簡單,但最終會引發一些真正的後果。

例如,對於基於匿名回撥的系統,當你想理解它們,維護它們或最重要的是在生產供應中斷production outages或錯誤行為發生時,你想知道到底發生了什麼、發生在哪以及為什麼發生,但此時它們只提供極少的內部資訊。

為響應式系統設計的庫與平臺(例如 Akka 專案和 Erlang 平臺)學到了這一點,它們依賴於那些更容易理解的長期存活的可定址元件。當錯誤發生時,根據導致錯誤的訊息可以找到唯一的元件。當可定址的概念存在元件模型的核心中時,監控方案monitoring solution就有了一個 有意義 的方式來呈現它收集的資料——利用傳播propagated的身份標識。

一個好的程式設計正規化的選擇,一個選擇實現像可定址能力和錯誤管理這些東西的正規化,已經被證明在生產中是無價的,因它在設計中承認了現實並非一帆風順,接受並擁抱錯誤的出現 而不是毫無希望地去嘗試避免錯誤。

總而言之,響應式程式設計是一個非常有用的實現技術,可以用在響應式架構當中。但是記住這隻能幫助管理一部分:非同步且非阻塞執行下的資料流管理——通常只在單個結點或服務中。當有多個結點時,就需要開始認真地考慮像資料一致性data consistency跨結點溝通cross-node communication協調coordination版本控制versioning編制orchestration錯誤管理failure management關注與責任concerns and responsibilities分離等等的東西——也即是:系統架構。

因此,要最大化響應式程式設計的價值,就把它作為構建響應式系統的工具來使用。構建一個響應式系統需要的不僅是在一個已存在的遺留下來的軟體棧software stack上抽象掉特定的作業系統資源和少量的非同步 API 和 斷路器circuit breakers。此時應該擁抱你在建立一個包含多個服務的分散式系統這一事實——這意味著所有東西都要共同合作,提供一致性與靈敏的體驗,而不僅僅是如預期工作,但同時還要在發生錯誤和不可預料的負載下正常工作。

總結

企業和中介軟體供應商在目睹了應用響應式所帶來的企業利潤增長後,同樣開始擁抱響應式。在本文中,我們把響應式系統做為企業最終目標進行描述——假設了多核、雲和移動架構的背景——而響應式程式設計則從中擔任重要工具的角色。

響應式程式設計在內部邏輯及資料流轉換的元件層次上為開發者提高了生產率——透過效能與資源的有效利用實現。而響應式系統在構建 原生雲cloud native 和其它大型分散式系統的系統層次上為架構師及 DevOps 從業者提高了生產率——透過彈性與韌性。我們建議在響應式系統設計原則中結合響應式程式設計技術。

腳註

  1. 參考 Conal Elliott,FRP 的發明者,見這個演示
  2. Amdahl 定律揭示了系統理論上的加速會被一系列的子部件限制,這意味著系統在新的資源加入後會出現收益遞減diminishing returns
  3. Neil Günter 的 通用可伸縮性定律Universal Scalability Law是理解併發與分散式系統的競爭與協作的重要工具,它揭示了當新資源加入到系統中時,保持一致性的開銷會導致不好的結果。
  4. 訊息可以是同步的(要求傳送者和接受者同時存在),也可以是非同步的(允許他們在時間上解耦)。其語義上的區別超出本文的討論範圍。

via: https://www.oreilly.com/ideas/reactive-programming-vs-reactive-systems

作者:Jonas BonérViktor Klang 譯者:XLCYun 校對:wxy

本文由 LCTT 組織編譯,Linux中國 榮譽推出

響應式程式設計與響應式系統

相關文章