1. 程式人生 > >Reactive Stack系列(一):響應式程式設計從入門到放棄

Reactive Stack系列(一):響應式程式設計從入門到放棄

為了詳細介紹下基於Spring Framework 5 & Spring Boot 2 的WebFlux的響應式程式設計,先畫下如下邏輯圖,後文將以邏輯圖箭頭方向逐一解釋關於響應式程式設計的點點滴滴。

roadmap.png


1. Spring Framework5

自 2013 年12月Spring Framework4.0.0釋出以後,時隔接近4年Spring才迎來了下一個大版本,這其中引入的新特性中, 最受人關注的主要圍繞在兩個方面,即響應式程式設計+全非同步非阻塞,知乎上的回答有人戲稱這是Spring堵上未來的一擊,react spring+JDK9像是一種新的Java語言。

1.1 The Reactive Manifesto/Rx Java

自從大名鼎鼎的響應式宣言/The Reactive Manifestohttps://www.reactivemanifesto.org)提出以來,現代web應用構建在沿著其提出的思路一步一步演進,並且不是一時趨勢。其主要內容翻譯成中文如下圖:

響應式宣言.jpg

我們需要系統具備以下特質:即時響應性(Responsive)、回彈性(Resilient)、彈性(Elastic)以及訊息驅動(Message Driven)。 對於這樣的系統,我們稱之為反應式系統(Reactive System)。


閱讀對應的響應式宣言,我們會發現核心思想就是通過回彈性(Resilient)、彈性(Elastic)以及訊息驅動(Message Driven)三種手段來實現系統高可用的健壯易維護系統。


其中對三種形式手段做了詳細介紹:

回彈性:系統在出現失敗時依然保持即時響應性。 這不僅適用於高可用的、 任務關鍵型系統——任何不具備回彈性的系統都將會在發生失敗之後丟失即時響應性。 回彈性是通過複製、 遏制、 隔離以及委託來實現的。 失敗的擴散被遏制在了每個[元件](/glossary.zh-cn.md#元件)內部, 與其他元件相互隔離, 從而確保系統某部分的失敗不會危及整個系統,並能獨立恢復。 每個元件的恢復都被委託給了另一個(外部的)元件, 此外,在必要時可以通過複製來保證高可用性。 (因此)元件的客戶端不再承擔元件失敗的處理。

彈性: 系統在不斷變化的工作負載之下依然保持即時響應性。 反應式系統可以對輸入(負載)的速率變化做出反應,比如通過增加或者減少被分配用於服務這些輸入(負載)的

資源。 這意味著設計上並沒有爭用點和中央瓶頸, 得以進行元件的分片或者複製, 並在它們之間分佈輸入(負載)。 通過提供相關的實時效能指標, 反應式系統能支援預測式以及反應式的伸縮演算法。 這些系統可以在常規的硬體以及軟體平臺上實現成本高效的彈性

訊息驅動:反應式系統依賴非同步的訊息傳遞,從而確保了鬆耦合、隔離、位置透明的元件之間有著明確邊界。 這一邊界還提供了將失敗作為訊息委託出去的手段。 使用顯式的訊息傳遞,可以通過在系統中塑造並監視訊息流佇列, 並在必要時應用回壓, 從而實現負載管理、 彈性以及流量控制。 使用位置透明的訊息傳遞作為通訊的手段, 使得跨叢集或者在單個主機中使用相同的結構成分和語義來管理失敗成為了可能。 非阻塞的通訊使得接收者可以只在活動時才消耗資源, 從而減少系統開銷。


ReactiveX專案(Reactive Extension)是基於響應式非同步程式設計的一個跨語言專案,其中Rx Java為Java版本的對應實現,在Spring專案中,其基於Reative Streams 實現了一套對應的響應式程式設計實現(Flux/Mono).


1.2 None-Blcoking/Tomcat 8 & Netty

非阻塞的概念由來已久,其不僅僅在HTTP通訊中涉及,在其他讀寫中也普遍涉及,Netty給出了一個非常優雅的實現方式,其隱藏了我們在JDK7種常見的一些類似Selector/Channel等複雜概念,可以快速簡單的構建出一個非阻塞Web伺服器。對於Tomcat而言,其非阻塞模型主要針對我們常見的大量連線情況,傳統的BIO效能拖累主要集中在大量的連線請求和工作執行緒資料繫結導致無法彈性削峰填谷,而最新的Tomcat版本是用輪詢的方式減少對連線請求的資源消耗的問題,其把對應的請求接收後放入佇列中,等待後續處理。以Tomcat為例,其NIO效能提升關鍵如下圖所示(基於Servlet3.1),分別是BIO和NIO效能區別關鍵:

BIO.png

NIO.png

CP.png

2. Spring WebFlux

WebFlux,簡而言之,是一套Spring Team認為未來需要代替Spring MVC體系的web框架,其構建於響應式程式設計規範之上,提供流式非阻塞具體實現。在官網提供的各種資料中,目前我們可以認為Spring MVC和Spring WebFlux是兩套平行的架構體系。在Spring Boot2.0.0版本以及後續中,我們通過Spring Initializer引入對應的web模組或者web reactive 模組,即可發現其分別對應著Spring MVC和Spring WebFlux。

MVC_WebFlux.png

2.1 Spring MVC VS Spring WebFlux

首選放出官方對比圖:

MVC_VS_WebFlux.png

這張圖代表了以MVC的Servlet Stack和以WebFlux為代表的Reactive Stack的框架對比圖,從各個層面做了對比,具體解釋如下:

@Controller/@RequestMapping VS Router Functions:我們知道MVC體系在SpringBoot中, 只要在Controller層加標註@RestController以及對應方法加上@GetMapping/@PostMapping方法即可在啟動Tomcat容器以後自動根據Annotation來載入對應的mapping關係。在Reactive Stack中,我們需要在啟動的時候通過Lambda風格的Router Functions統一把所有的Web入口註冊一遍(雖然我覺得很怪,但是目前就是這麼實現的)。

spring-webmvc VS spring-webflux模組:MVC體系中我們通常是命令式語法,WebFlux體系中,我們需要結合流式寫法加上基於對應Router Functions註冊的方法對流式處理對返回結果做一定的轉化(詳細見後續例子)。

Servlet API VS HTTP/Reactive Streams:MVC體系目前可以基於Servlet3.1實現非同步通訊,但是實現流式通訊的實現較複雜。WebFlux天生基於流式非同步通訊,程式設計方式較友好。

Servlet Container Vs Reactive Stream Contrainers:WebFlux天生構建於非同步流式非阻塞模式,所以它適用於對應的特定支援Web容器。

具體寫法舉例如下截圖解釋:

Router Functions.png

以查詢某人繼續解釋,根據上圖我們會去PersonHandler裡面的getPerson方法構建返回結果。

Hander.png

可以看出,Handler類相當於我們MVC體系中的Service層,MVC中的Controller層集合相當於上面舉例的RounterFunctions中不斷構建的入口列表。


根據gradle.build中引入的starter元件,我們分別註釋掉對應的一部分starter元件,如下:

implementation()
implementation()


可以分別嘗試用Tomcat和Netty啟動伺服器,reactive stack預設使用netty啟動,我們可以觀察對應的Idea啟動日誌,以及啟動對應的Visual VM來觀察對應的執行緒,截圖如下:需要注意的是,其中,reactor-http-nio執行緒池是由程式根據系統的CPU數量來決定的。

Tomcat如下:

Tomcat.png

tomcat_vm.png


Reactive Stack中,Netty如下:

Netty.png

netty_vm.png


從截圖中我們可以清楚看到對應的執行緒工作狀態。看到這裡,大部分人應該認識到,對於Reactive Stack來說,我們並不需要去管理執行緒池,程式是在根據系統資源在決定我們應該建立多少執行緒,替我們管理執行緒的生命週期,執行緒的排程。也就是說,Reactive Stack中,我們基本可以不用去管ThreadPoolExecutor。站在更高的角度看,這一層是很有深意的,需要我們去仔細思考這樣到底帶來了哪些好處。


2.2 Spring WebFlux的幾個特性介紹以及和Spring MVC對應特性的橫向對比。

通過2.1小節,我們基本知道了在SpringBoot中,如何引入WebFlux,以及對應的寫法實踐和Spring MVC的對比,接下來介紹下對應的響應式特性(及優缺點)。

由於不用關心執行緒池,加上Reactive模式的核心是沒有全域性單點,這決定了一件事,任何一個請求在WebFlux框架中走一圈,耗時應該是相同的(假設我們所有的元件都是None-Blocking的,尤其是資料庫層),如下圖:

ReactiveX.png

這是ReactiveX官網上的一張圖,它的寓意就是:假設我們現在看到每個顏色的圓點都是一個HTTP請求,那麼從他們落入我們的網絡卡開始到離開我們的網絡卡,對於開發者來說,我們如果正常編碼,這些請求的耗時是完全相同的,這一點很重要,其實是對應著響應式宣言的“彈性(Elastic)”,這進而可以讓我們確定一點:系統的效能瓶頸是可預測的。這點的重要性在於,當我們預測可預見的未來訪問請求增加1000倍,我們的部署資源增加1000倍即可,這即對應了響應式宣言中的彈性。由於執行緒資源的固定,不存在頻繁的執行緒切換/生成/銷燬等等,所有的效能類似於固定成通過水流的管道,那麼根據水流的大小,我們就可以確定管道的數量。


根據上一段,進而要提到一個概念;背壓(BackPressure),如果當前流入管道的水流速率超過了管道規定的上限,那麼上游的傳送源會收到反向的傳送壓力,網上有一張形象的圖片就是消防員拿著消防栓噴水滅火時,被反作用力壓的往後退。“背壓”是一種現象,通常來說,解決背壓的方式有兩種:一種是返回源頭錯誤告警,一種是忽略請求。


流:在WebFlux中,我們的請求可以通過Flux<T>型別來返回一系列的資料,而通過我們的WebClient客戶端 + application/json+stream 模式,我們完成資料的持續流式傳輸,例如客戶端請求1W個客戶的具體資訊->服務端通過Luttuce每秒去Redis獲取100個客戶,那麼對應的返回資料會在100秒內持續的返回對應的客戶端。通過這種方式,我們可以做到網路均衡的傳輸。


完全非阻塞呼叫鏈:

DATA_Blocking.png

根據最新的GA版本,我們的系統呼叫大部分時間是可以實現完全的非阻塞系統呼叫的-->關係型資料庫(MySQL)除外。因為原來的 Spring 事務管理(Spring Data JPA)都是基於 ThreadLocal 傳遞事務的,其本質是基於阻塞IO模型,不是非同步的。但Reactive是要求非同步的,不同執行緒裡面 ThreadLocal 肯定取不到值了。根據最新的進展,好訊息是目前的R2DBC專案已經實現postgresql的非阻塞實現,相信在不久的將來,MySQL也會最終加入Reactive Stack的拼圖。


2.3 Reactive Stack 效能測試

先想一想,結果會是怎樣?答案是,和Servlet Stack(MVC)沒多大差距,甚至可能還弱一點。Google上給出的大部分測試結果已經證明了這一點,為什麼呢?想了一下,應該是因為以下幾點:

  • Spring MVC是一個經過時間和實踐檢驗的成熟體系,沒有短板,在相同的把CPU,記憶體,IO等資源吃滿的情況下,只要合理的控制好Thread對CPU時間片的浪費,MVC完全是可以做到最優解的。

  • WebFlux的執行緒管理模式,包括背壓在內的一系列特性,其實熟悉執行緒管理來說,就相當於是ScheduledThreadPoolEXecutor根據對應引數設定,並且對應的設定好rejectHandler policy。

  • DefferredResult,ResponseBodyEmmiter,SseEmmiter等類讓MVC體系也可以毫無壓力完成非同步以及流等高效能響應方式,從終極實踐方式來說,兩者並無本質上的差異。


3 總結與展望

說了這麼多,好像發現WebFlux也沒啥?Spring 堵上未來的一擊會成功嗎?誠然,就官方目前的說法也是,不太適合大規模應用,但是小範圍的改造是OK的。


但是,回到最開始,讓我們再看看響應式宣言。Spring WebFlux費盡心思搞了這麼一套和MVC平行的體系架構,其本質思想已經基本默默地在反映著響應式宣言。即時響應性,彈性,回彈性,訊息驅動。這整個一套技術棧,其實是在構建一套可預測的,更加健壯的,開發人員更加少干預的Web框架,從而讓大家專注於業務層面的實現,進一步忽略/遮蔽底層系統的細節。


如果說Spring Cloud是從【巨集觀系統層面的開發】角度在實踐健壯的高可用系統+系統運維,K8S在【DEV OPS】層面實踐更好的系統運維,Service Mesh在【基礎設施層(infra)】實踐健壯的高可用系統+系統運維,那麼WebFlux(包括整個Reactive Stack體系的其他成員)就是從【微觀專案層面的開發】角度在實踐健壯的高可用系統+系統運維。或多或少,它們都從各個維度在朝著“更少的人治”角度去努力。