"reactive programming"的概念

devos發表於2014-12-18

下面的內容大多是翻譯來的。

Reactive Programming?

What is Reactive Programming?

為了瞭解Reactive——從程式設計正規化至其背後的動機,有必要了解現在的開發者和公司在十年前不曾面對的挑戰。

遊戲的改變主要有兩大方面:

  • 硬體的提升
  • 因特網

Why things are different now?

(原文對比了一遍2005年和2014年因特使用者的增長:10億至29.5億;Facebook使用者的增長:550萬至13億; twitter從無至有,0至2.7億使用者)

時代改變了,現在一個網站就需要處理十年前整個因特網的流量。實下,容易發現軟體對於日常生活越來越重要。也容易發現,以前的一些正規化並不再適用於現在,更不適用於將來。

The four Reactive principles

Reactive applications 構建於四個指導性的原則。

 

  • 目標是一個responsive的程式
  • 一個responsive的程式一定既scalable又resilent.responsiveness離開了scalability和resilence是不可能實現的
  • 一個message-driven的架構是scalale, resilent以及最終實現responsive系統的基礎

讓我們在高層瞭解下每一個原則,理解為了開發一個適用於現在環境的軟體,為什麼它們是缺一不可的。

Responsive

當我們說一個程式是"responsive"的時候,是在說什麼?

A responsive system is quick to react to all users — under blue skies and grey skies—in order to ensure a consistently positive user experience.

一個responsive的系統是對所有使用者都能快速反應的——不管晴天陰天——來保證一個始終積極的使用者體驗。

在各種條件下都“快速”而“積級”的使用者體驗,比如外部系統出問題或者流量激增,依賴於Reactive application的兩個特質:resilence,以及scalability。一個訊息驅動架構提供了一個responsive系統的基礎。

為什麼訊息驅動架構對於responsiveness如此重要?

這個世界是非同步的。

 假如你現在來杯咖啡,卻發現糖和奶油沒了。

一種做法是:

  • 開始煮咖啡
  • 去商店買糖和奶沒
  • 回家
  • 啥都齊了,開始喝咖啡
  • 享受生活

另一種做法是:

  • 去商店買糖和奶油
  • 回家
  • 煮咖啡
  • 等著咖啡煮好
  • 忍受咖啡因短缺
  • 崩潰

就像你看到的,一個訊息驅動的架構提供給你非同步邊界(asynchronous boundary),使你從時間和空間中解放出來。在這個文章的其餘部分我們將繼續深入非同步邊界的概念。(這段舉的例子挺好,可是看不出這個例子跟訊息驅動有啥關係呀)

Consistency at Walmart Canada 一致性之於加拿大亞馬遜

(譯註:這個一致性和分散式系統的一致性不一樣……)

在加入Typesafe公司之前,我是負責構建亞馬遜加拿大的新電商平臺的Play和Scala團隊的技術leader。

我們的目標是提供一致的積極的使用者體驗,不管:

  • 無論什麼型別的物理裝置被用來訪問walmart.ca,不設它是桌面電腦,平板還是移動裝置
  • 當前的流量峰值,不管它是由於激增才出來的,還是持久的
  • 一個主要的基礎設施不可用,比如一整個資料中心。

響應時間和整體的使用者體驗必須保持一致,不管上面的情況是否發生。一致性對於你的網站是至關重要的,因為當下,這就是你的品牌。一個不好的使用者體驗是難以被忘記或者忽略的,不管是發生線上上還是在一個線下的商店。

Responsive retail at Gilt 

Gilt的Responsive零售。電商領域的一致性(的重要性)並不是碰巧出現的。比如Gilt也是一樣,Gilt是一個閃購網站,每天中午當他們釋出了當日的銷售商品時流量就會激增。

讓我考慮下一個閃購網站的使用者體驗。如果你在11:58訪問了Gilt,12:01又訪問了它。那麼你希望兩次訪問都有積極的體驗,不管是不是Gilt在12:01那時候流量突增。

Gilt提供始終積極、responsive的使用者體驗,並且通過Reactive來實現這一切。通過這篇ReadWrite interview with Eric Bowman of Gilt 瞭解Gilt向基於Scala的微服務架構(Scala-based microservices architecture)的遷移。

Resilient

大部分的程式都被設計和開發成適用於“晴天”,但是事情可能往壞的方面發展。每隔幾天就會有報告說一個主要的應用不可用,或者由於黑客的攻擊造成服務不可用、資料丟失、或者儲譽受損。

A resilient system applies proper design and architecture principles in order to ensure responsiveness under grey skies as well as blue.

一個彈性的系統採用了一些設計和架構的原則,使得不管外部情況的好壞,都是responsive的

Java和JVM都是用來把一個程式無縫地佈署到多個作業系統,類似的是,201x年的內部互聯的應用程式都是關於應用程式層級的組合、連線和安全。(注:這句有點繞……忽略,看下邊的)

現在的程式大都由一定數量的其它程式組裝而成,通過web service和其它網路協議整合在一起。現在一個應用可能依賴於大量的其它服務——10,20或者更多,它所依賴的這些服務在自己的信賴防火牆(trust firewall)之外。它可能同時在為大量外部使用者提供服務——既包含人,也包括其它系統。

考慮到整合的複雜性,有多少開發人員:

  • 分析和建模所有外在的依賴
  • 把整合進的每個服務的理想響應時間整理成文件,進行效能測試——在峰值和平常——來全面評估最初的期望
  • 把所有的預期,失敗情況和其它非功能的需求編碼到系統的核心邏輯中
  • 分析和測試每個服務的失敗情形的影響
  • 分析每個外部依賴的安全,識別是否整合這個外部依賴是否給系統帶來最的安全隱患

Resiliency is one of the weakest links of even the most sophisticated application, 但是Resilency作為一種事後的思考的情況很快就會終結。現代的程式必須在其核心就是resilent的,以保持在現實世界多變的環境下,而不只是理想情況下,保持responsive。 效能、永續性、安全都是resilency的方面。你的應用必須在各個層次都是resilent的。

 Message-driven resiliency

在一個訊息驅動的核心之上構建應用的美妙之處在於你自然地得到了很多有價值的構造模組。

Isolation 隔離性是一個自愈(self-heal)的系統所需要的。當隔離性在被恰當地使用,我們可以依據一些因素把不同型別的工作進行分離,比如失效的風險、不同的效能、CPU和記憶體的使用,等。一個isolation的元件的失效並不會影響整個系統的responsiveness, 並且給予了失效的那個系統一個被治癒的機會。

Location transparency 位置透明性使得我們與不同叢集結點上的不同程式間的通訊就像與同一個VM的程式內通訊一樣。

A dedicated separate error channel 一個精心設計的分離的錯誤資訊通道使得我們可以把錯誤訊號重定向到其它地方,而不是拋回到呼叫者面前。(注:這個作用應該型別於Akka中的event stream這種東西,使得某些失敗的資訊可以被訂閱,使得不光在當前是呼叫了某個失敗元件的元件可以知道它失敗了,而是所有相關的元件都可以瞭解到這個情況,便於他們即是作出響應)

這些因素幫助我們把健壯的錯誤處理和容錯合併進我們的系統中。Akka的supervisor hierarchies的實現展示了這點。

訊息驅動架構提供的這些核心的構建模組有助於resiliency,進而有助於responsiveness——不僅在理想情況下,而且在各種並非理想的、現實世界的情況下。

 

The 440 million dollar resiliency mistake

來見識下騎士資本集團在2012年發生軟體故障的經歷(software glitch experienced by Knight Capital Group)。在一次軟體升級中,另一個休眠的、被整合在一起程式被不經意地喚醒,並且開始放大交易量。

接下來的45分鐘發生的事情是一場惡夢。

騎士資本的自動交易系統洪水般地向NASDAQ(注:納斯達克)進行錯誤交易,使得公司進入了一個需要付出數十億美元代價的位置。事後,這家公司花了4.4億美元來扭轉局勢。在這個故障中,騎士資本不能停止泛水般的錯誤交易,NASDAQ不得不終止騎士資本來停止這一切。騎士資本的股票在一天跌了63%, 幾乎不能做為一個公司存活了下來,只能等著它的股票恢復了一些價值之後被一些投資者接管。

騎士資本的系統是高效能的,但是不是resilent的。高效能但是沒有resilience使得問題被放大,就像騎士資本發現的那樣。他們甚至沒有一個停止開關來使得自己的系統在出現嚴重問題時停止交易, 所以,當真的出了問題時,他們的自動交易系統在45分鐘內耗盡了整個公司的資本。

這就是設計和開發只能用於"晴天“的程式的結果。如今,軟體是我們的私人生活和生意的關鍵組成。如果我們沒有為”陰天“的情況提前設計和準備,那麼即使這種情況只存在了不到一個小時,也會給我們造成巨大的損失。

 Scalable

Resililency和Scalability手拉手,一起共建始終responsive的程式。

A scalable system is easily upgraded on demand in order to ensure responsiveness under varioius load conditions.

一個可擴充套件的系統可以在面對不同的負載時輕鬆地進行升級,來保證respnsiveness

任何一個在網上賣過東西的人都明白一個事實:你的流量最大的時候就是你賣的東西最多的時候。除非流量的突增是由於故意的黑客襲擊,不然期望流量突增是挺好的一件事。在一個突增的流量中,人們願意花錢給你。

那麼如何面對這種流量突增,或者是一個長久的但是大量的流量增長。

首先選擇你的正規化(paradigm),然後選擇擁抱這種正規化的語言和工具。很多情況下,開發者只是簡單地選擇語言和框架……。一旦選擇了工具,就很難改回來. 所以,就像你在處理任何一個重大的投資一樣進行這些決定。如果你的技術選擇基於一些原則和分析,那麼你就已經領先一步了。

 Thread-based limitations to concurrency 基於執行緒的併發的不足

一個至關重要的技術選擇就是框架的併發模型。在較高的層次,有兩種併發模型:

  • 傳統的基於執行緒的併發模型,基於呼叫棧和共享記憶體
  • 訊息驅動的併發

一些流行的MVC框架,比如Rails,是蔡於執行緒的。這類框架的典型特點包括:

  • 共享的可變狀態
  • 每個請求一個執行緒
  • 對於可變狀態的併發訪問——變數以及物件例項——使用鎖和其它複雜的同步結構來進行管理

這些特點結合動態型別、解釋型語言比如Ruby,會迅速達到效能和擴充套件性的上限。你可以認為所有本質是指令碼語言的語言都是如此。

Out or up?

讓我們考慮擴充套件一個應用的不同方式。

向上擴充套件(scaling up)是關於最大化的利用一個CPU/伺服器的資源,通常需要購買更強大的,專用的、更貴的機器。

向外擴充套件(scaling out)是關於把計算分佈到由廉價的commdity機器組成的叢集中。這樣在成本上更划算,但是如果你的系統是基於時間和空間概念的(注:”基於時間的“可以認為程式的各個動作是同步的,需要阻塞,等待,”基於空間的“可以認為是基於一個程式內部的記憶體共享,比如不執行緒間共享的變數),那麼向外擴充套件就非常困難。 就像我們之前提到的那樣,一個訊息驅動的架構提供了與時間和空間解耦合所需要的非同步邊界(asynchronous boundary),提供了按需向外擴充套件(scale out on demand)的能力,這種能力也被叫做elasticity. Scaling up是關於更高效地利用已有的資源,而elasticity是關於在系統需要改變時按需增加新資源。能夠”scale out, on demand"的能力,就是Reactive programming在擴充套件性方面的終極目標。

Reactive應用程式很難構建於基於執行緒的框架之上,想想就知道把一個基於可變狀態,執行緒和鎖的的程式進行scale out有多困難。開發人員不僅需要利用單個機器上多核的優劣,在某些時候,開發者也需要利用計算機叢集的力量。共享可變狀態也使得scale up變得困難,即使不是不可能。任何曾嘗試操作兩個執行緒共享的可變狀態的人都能夠理解保證執行緒安全有多複雜,並且也能夠理解為了確保執行緒安全所需要花費的多餘努力所帶來的效能上的懲罰。

Message-driven

一個訊息驅動的架構是Reactive應用程式的基礎。一個訊息驅動的程式可以是事件驅動的(event-driven),基於actor的(actor-based),或者兩者的結合。

一個事件驅動的系統基於事件,這些事件被一個或更多監聽者監聽。這和imperative programming不同,因此呼叫者不需要阻塞以等待被呼叫的例程的響應。事件並不是被導向特定的地址,而是被監聽,我們將會更深入地討論其隱含的意義。

基於actor的併發是訊息傳遞架構的一種延伸,在基於actor的併發架構中,訊息可以跨越執行緒的邊界或者被傳遞到不同物理機器上的另一個actor的郵箱(mailbox)中。這使得elasticity——scale out on demand——變得可能,因為actor可以被分佈到網路的各處,但是仍然可以像他們在同一個VM裡一樣通訊。

訊息和事件的不同在於訊息是有方向的,而事件只是”發生“並沒有方向。訊息有一個清晰的目的的,而event可以被一個或者多的監聽者監聽。

讓我們深入探討下事件驅動和基於訊息的併發。

Evnt-driven concurrency

典型的程式用命令的風格開發—一個命令的線性序列—並且是基於呼叫棧(call stack)的。呼叫棧的主要功能是追蹤例程(routine,感覺應該是函式呀)的呼叫者,在阻塞呼叫者的同時執行被呼叫的例程,當執行完畢後把控制流交還給呼叫者,並且返回執行結果的值(或者不返回)

事件驅動的程式並不關注於呼叫棧,而是關注於觸發事件。事件可以被編碼成訊息,放在佇列裡,被一個或者更多監聽者監聽。事件驅動和命令式的巨大區別在於呼叫者不被阻塞,不用在等待響應的時候佔據一個執行緒(caller does not block and hold onto a thread while waiting for a response)。事件迴圈本身可以是單執行緒的,但是仍然可以實現併發,因為可以在(有時是單執行緒 的)事件迴圈處理新的請求的同時呼叫例程來完成工作(注:因為對例程的呼叫被搞成了非阻塞的,即不會阻塞event-loop)。不再是阻塞於一個請求,等待它被處理完,現在呼叫者的標識可以和請求訊息本身一起被傳遞,使得(如果被呼叫的例程選擇這麼做)呼叫者可以被回撥,在回撥時傳遞響應(the caller can be called back with a response)。

選擇一個事件驅動架構的主要結果是他們可有會遇到一個現象,叫做回撥地獄(callback hell)http://callbackhell.com 。 回撥地獄之所以發生,是因為訊息的接受者是匿名函式而不是可定址的接受者(addressable recipients).通常的解決方案僅僅關注於句法—aka, the Pyramid of Doom—而忽略了推導和除錯程式碼中表達的event所產生的後續事件的困難。

Actor-based concurrency

基於actor的程式以在多個actor之間非同步的訊息傳遞為中心。

一個Actor由以下的屬性組成:

  • 用於接收訊息的mailbox
  • actor的邏輯,它依賴於模式匹配(pattern matching)來決定這個actor如何處理不同型別的訊息
  • 隔離的狀態(isolated state),而不是共享的狀態。使用隔離的狀態來儲存請求之間的上下文。

像事件驅動的併發一樣,基於actor的併發也避開了呼叫棧,轉向輕量級的訊息傳遞。actor可以返回以及傳送訊息,甚至傳遞訊息給自己。一個訊息可以傳遞給自己訊息,使得它先處理佇列裡的其它訊息,然後再去結束一個長時間執行的請求。基於actor的併發的一個巨大好處是,它不僅可以有事件驅動的併發的優點,而且可以讓計算跨越網路邊界變得更容易,並且可以避免callback hell,因為訊息被定向到actor。這是一個強大的概念,有助於構造易於設計、實施、維護的高可擴充套件性的程式。不用考慮“空間”和“時間”,也不用考慮深度巢狀的回撥函式,你只需要考慮actor之間的訊息流動。

另一個基於actor的架構的主要好處是元件之間解耦合。呼叫者不用阻塞自己的執行緒等待響應,因此呼叫者可以迅速地轉去進行其它工作。被呼叫的例程,封裝在一個actor中,只需在必須的回覆呼叫者。這開啟了很多可能性,比如分佈例程(routines)到一個叢集中,因為不再有呼叫棧把程式耦合在單一的記憶體中,actor模型可以使佈署一個拓撲(topology)只是一個虛擬的配置而不再是編碼在程式裡。

Akka是一個基於actor的工具箱和執行時- Typesafe Reactive Platform的一部分,用來構造併發的、分散式的、容錯的,基於actor的程式,執行於JVM。Akka還有其它的一些用於構造Reactive application的不可思憶的特性,像是用於rsilience的supervisor hierarchies,以及可以把分佈工作來實現擴充套件性。對Akka的深入研究超出了本文的範圍,但是我強烈推薦訪問Let it Crash blog 獲取更多關於Akka的內容。

我也強烈推薦閱讀Benjamin Erb’s Diploma Thesis,Concurrent Programming for Scalable Web Architectures, 被用於本節的參考。

Conclusion

以上的內容描述了時下軟體開發的的表面,引導我們認識到為什麼Reactive programming不僅僅是一個潮流,而且是現在的軟體開發者需要學習的正規化。不管你選擇什麼語言和工具,優先考慮scalability和resilient來實現responsiveness是唯一能滿足使用者需求的方式。每過一年,這點就會變得更加重要。

想要手把手的學習吧?下載Typesafe Activator ,在今天開始構建你的Reactive applications. (廣告呀……)

 


 

下面是另一篇文章

  • Reactive

    is readily responsive to a stimulus

    • react to events (event-driven)
    • react to load (scalable)
    • react to failures (resilient)
    • react to users (responsive)

     

    • Event-Driven

      Systems are composed from loosely coupled event handlers.

      • Events can be handled asynchronously, without blocking.
    • Scalable

      An application is scalable if it is able to be expanded according to its
      usage.

      • scale up: make use of parallelism in multi-core systems
      • scale out: make use of multiple server nodes

      Important for scalability: Minimize shared mutable state.
      Important for scale out: Location transparency, resilience.

    • Resilient

      An application is resilient if it can recover quickly from failures.
      Failures can be:

      • software failures
      • hardware failures
      • connection failures

      Typically, resilience cannot be added as an afterthought; it needs to be
      part of the design from the beginning.
      Needed:

      • loose coupling
      • strong encapsulation of state
      • pervasive supervisor hierarchies
    • Responsive

      An application is responsive if it provides rich, real-time interaction with
      its users even under load and in the presence of failures.
      Responsive applications can be built on an event-driven, scalable, and
      resilient architecture. Still need careful attention to algorithms, system design, back-pressure, and many other details.

 

相關文章