Reactive編程即反應式編程,隨着這些年的發展已經逐步的進入了開發者的視野當中。早在2014年社區就有人發起響應式宣言,推動着Reactive的發展:
響應式宣言
Published on September 16 2014. (v2.0) 來自不同領域的組織正在不約而同地發現一些看起來如出一轍的軟件構建模式。它們的系統更加穩健,更加有可回復性,更加靈活,並且以更好的定位來滿足現代的需求。 這些變化之所以會發生,是因為近幾年的應用需求出現了戲劇性的變化。僅僅在幾年之前,大型應用意味着數十台服務器,數秒的響應時間,數小時的離線維護時間以及若干 GB 的數據。而在今天,應用被部署在一切場合,從移動設備到基於雲的集群,這些集群運行在數以千計的多核心處 理器的之上。用戶期望毫秒級的響應時間以及 100% 的正常運行時間。數據則以 PB 為單位來衡 量。昨天的軟件架構已經完全無法地滿足今天的需求。 我們相信,一種條理分明的系統架構方法是必要的,而且我們相信關於這種方法的所有必要方面 已經逐一地被人們認識到:我們需要的系統是響應式的,具有可回復性的,可伸縮的,以及以消息驅動的。我們將這樣的系統之為響應式系統。 以響應式系統方式構建的系統更加靈活,松耦合和可擴展。這使得它們更容易被開發,而且經得起變化的考驗。它們對於系統失敗表現出顯著的包容性,並且當失敗真的發生時,它們能用優雅 的方式去應對,而不是放任災難的發生。響應式系統是高度靈敏的,能夠給用戶以有效的交互式的反饋。
Reactive構建的程序代表的是異步非阻塞、函數式編程、事件驅動的思想。
異步非阻塞
相比同步,異步的目的是提高硬件資源的利用率。同步模式下線程等待I/O時進入阻塞狀態(Blocked)相當於閑置,異步則可以利用CPU同步等待I/O返回的時間以避免資源消耗,從而達到更大的並發量以及更低的響應時延。
在原生JDK中,juc包提供Future、Callable、FutureTask等相關類讓我們完成最基本的異步編程。但其存在如下兩個典型問題:
- 獲得返回結果依然需要阻塞get。
- 大量應用Future寫出類似如下代碼,可讀性差且不優雅。
public Future<List<Future<T>>> getData();
為了避免阻塞我們可以引入回調(Callback),從而達到真正的異步,解決上面提出的第一個問題。但應用回調的同時也容易產生回調地獄(Callback Hell),層層嵌套的回調函數往往讓人產生困惑,極大的降低了代碼的可讀性和可維護性,仍然不完美。
函數式編程
為了解決上文例子的第二個問題——異步帶來的可讀性差,可以用函數式編程思想:把函數邏輯組織成事件流,通過對事件的編排和組合,可以用清晰的代碼接口來達到對事件返回的立即響應和對失敗的立即響應(Fail Fast),構建起事件驅動(Event Driven)的程序。
假設模擬一個訂單的場景,我們需要連續調用三個服務的接口:用戶服務,訂單服務,庫存服務,並且每次調用都依賴於上次調用的返回結果。如果是傳統的同步阻塞代碼,我們需要連續等待每個接口的I/O調用返回。而應用Vert.x等Reactive框架我們可以寫出類似如下偽代碼:
1 //下訂單調用 2 public Result order(long uid, Product product) { 3 userServer.getUserInfo(uid) //用戶服務,查詢用戶信息 4 .compose(user->asyncAddOrder(user, product)) //訂單服務,生成訂單 5 .compose(order->asyncWmsOccupy(order)) //庫存服務,庫存占用 6 .setHandler(result->handle(result)); //處理結果 7 }
應用函數式編程范式,每個compose方法異步調用,結束后都會自動回調下一個compose,而任何錯誤都會立即響應到setHandler中同一處理,保證我們寫出優雅的異步代碼。
Vert.x
隨着Reactive思想這些年來的發展,社區中逐漸涌現出一批優秀的Reactive開源框架:如RxJava,Vert.x等。包括最近比較熱的Rsocket,在協議層面上進行reactive的實踐。
2011年,在VMware工作的 Tim Fox 開始開發Vert.x,后來貢獻給了Eclipse基金會。Vert.x提供了一系列的異步、流式工具,它更像一個工具包,使用Vert.x可以輕松的構建輕量級的Reactive web服務器。
Vert.x采用Actor的模型簡化了多線程的編程。圖靈獎得主、面向對象編程之父Alan Kay曾經這樣描述面向對象:我在描述“面向對象編程”時使用了“對象”這個概念。很抱歉這個概念讓許多人誤入歧途,他們將學習的重心放在了“對象”這個次要的方面。真正主要的方面是“消息”。 幾十年前, Carl Hewitt提出了 Actor 模型,將其作為在高性能網絡中處理並行任務的一種方法。如今隨着硬件條件的發展,產生了很多面向對象無法解決的挑戰,Actor模型的思想重新進入人們視野,並以一種新的思路解決了多線程多cpu環境下的並發問題。
Vert.x是一個事件驅動的框架,底層使用Netty作為I/O庫保證性能 。Vert.x采用了簡單的並發模型,所有代碼都運行在單線程中,避免多線程編程可能出現的並發問題、狀態共享、以及各種鎖的處理,同時每個actor模型獨立天然符合分布式的部署和支持高可用(HA)。
與Node.js的Eventloop類似,在Vert.x的線程模型中,同樣也有Eventloop的概念。但相比於單線程的Node.js,Vert.x設置了一個Eventloop線程池,來發揮多核處理器的性能。通過在代碼中定義Verticle——一種actor的實現,使業務代碼和Verticle綁定,而每個Verticle會綁定到一個Eventloop線程上,只要不阻塞Eventloop線程,就可以源源不斷的處理新事件從而最大程度利用計算資源。當遇到耗時的長任務時則可以交給額外的Worker線程執行,避免Eventloop線程阻塞。基於Verticle來編寫的業務代碼始終運行在單線程中,加上基於回調機制可以實現天然的異步無鎖。
Figure 1vert.x的actor模型
Figure 2tomcat的同步線程模型
Java Chassis 的Reactive實踐
Java Chassis 是 Apache Service Comb 的一個子項目,提供了開箱即用的Java微服務開發框架SDK,並在通信部分采用了基於Vert.x的Reactive架構。目前已經在華為應用市場業務中廣泛應用,在生產實踐中為高達60w tps的業務並發保駕護航。通過性能對比測試,業務采用Reactive異步模式之后,TPS提升 43% 左右、RT時延降低 28% 左右,CPU占用降低 56% 左右,性能收益很大。
使用Spring MVC作為web框架的Spring cloud,其底層實際上基於 Servlet 即采用DipatcherServlet來分發請求,需要運行在Tomcat等web容器中,性能受web容器限制。雖然 Servlet 3.1以后的版本提供了對異步和NIO的支持,但也阻止不了最近幾年出現的一些新的Reactive web服務器試圖取代Servlet的地位,它們往往更加輕量級和靈活。
Spring也在去年推出了Spring WebFlux作為新一代的Reactive web框架以期望在異步編程中代替Spring MVC,但WebFlux目前的主要應用場景還是在網關。雖然WebFlux拋棄了Servlet,自己基於netty重寫了通信部分,但在實測中其性能提升並不明顯。WebFlux與Spring cloud集成構建的微服務系統需要構建起完整的流式的應用才能發揮出其優勢,代表着整個系統都要求是流式編程,對開發者的要求高,且系統重構的代價大。
Java Chassis 則采用基於Vert.x的reactive服務器以及客戶端,更加輕量級、線程模型更加友好,同時提供了大量天然的異步API、以及更多的協議支持。同時基於actor的線程模型在簡單的單服務間調用就可以表現出明顯的性能優勢,不需要開發人員構建起難以調試的流式系統,就可以享受Reactive帶來的性能提升。
Java Chassis原生支持純Reactive異步模型,在異步模式下同一個Enventloop線程中完成I/O以及業務的處理。同時,actor的模型下代表着更少的線程數量可以做到更優的性能:與Tomcat的默認200個線程相比,Vert.x默認只開啟兩倍cpu核數的Eventloop線程。從操作系統的計算成本角度上講,更少的線程數量意味着更少的上下文切換時間開銷——在線程切換時,操作系統從用戶態切換為內核態再切回用戶態。同時更少的線程數讓內存中的線程私有棧信息也更少。
另外,Java Chassis也為同時提供對Tomcat的兼容,通過簡單配置即可部署到Tomcat。
Figure 3基於vert.x的線程模型
筆者通過在4核8g的機器壓測對比測試,數據結果顯示:
微服務框架\客戶端 |
RestTemplete |
rpc |
spring cloud huawei(webflux reactive) |
rt:12.75ms/qps:18.20k |
\ |
spring cloud huawei(tomcat) |
rt:13.93ms/qps:14.53k |
\ |
spring cloud 原生(tomcat) |
rt:14.09ms/qps:14.30k |
\ |
java chassis(tomcat) |
rt:13.67ms/qps:15.34k |
rt:11.88ms/qps:17.56k |
java chassis(vertx reactive) |
rt:6.77ms/qps:33.62k |
rt:5.67ms/qps:42.73k |
在調用鏈為單服務A->B時,對比Tomcat同步模式和切換到基於Vert.x的Reactive模式,平均時延和並發處理能力都有大幅度的提升。理論上在實際生產環境中如果服務調用鏈更長,服務中的同步操作越多,采用Reactive模式的優勢會更大。詳細測試代碼以及數據已經開源 https://github.com/GuoYL123/ReactiveBenchmark,有興趣的讀者可以自行了解。
結論
Reactive模式構建的程序,不僅僅代表的是高TPS低RT(時延),其帶來的更直接的受益就是同等條件下更低廉的硬件成本。Java Chassis 作為Reactive在微服務領域的先行者,已經通過在生產環境中的應用積累了大量經驗。
但同時構建Reactive模式的程序也為開發者帶來更高的要求,面臨比同步更為復雜的編程模型,需要更好的處理好阻塞和寫出更優秀的異步代碼。