做者:李林鋒 連接:https://www.infoq.cn/article/q3iPeYQv-uF5YsISq62c
1. 異步 RPC 調用的應用場景react
1.1 縮短長流程的調用時延數據庫
隨着業務分佈式架構的發展,系統間的系統調用日趨複雜,以電商的商品購買爲例,前臺界面的購買操做涉及到底層上百次服務調用,造成複雜的調用鏈,示例以下:編程
圖 1 分佈式消息調用鏈緩存
對於一些邏輯上不存在互相依賴關係的服務,能夠經過異步 RPC 調用,實現服務的並行調用,經過並行調用來下降服務調用總耗時,以手遊購買道具流程爲例,消費次數限制鑑權、帳戶餘額鑑權和下載記錄鑑權三個服務能夠經過異步的方式並行調用,來下降遊戲道具購買的耗時:網絡
圖 2 購買道具異步 RPC 調用流程架構
1.2 服務調用耗時波動較大場景併發
對於一些業務場景,服務調用耗時與消息自己、調用的資源對象有關係,例如上傳和下載接口,若是下載的資源較多則耗時就會相應的增長。對於這類場景,接口的調用超時時間比較難配置,若是配置過大,服務端自身響應慢以後會拖垮調用方,若是配置太小,萬一遇到一個須要較長耗時的 RPC 調用就會超時。經過異步 RPC 調用,就不用再擔憂調用方業務線程被阻塞,超時時間能夠相應配置大一些,減小超時致使的失敗。框架
1.3 第三方接口調用異步
對於大部分的第三方服務調用,都須要採用防護性編程,防止由於第三方故障致使自身不能正常工做。若是採用同步 RPC 方式調用第三方服務,一旦第三方服務的處理耗時增長,就會致使客戶端調用線程被阻塞,當超時時間配置不合理時,系統很容易被阻塞。經過異步化的 RPC 調用,能夠防止被第三方服務端阻塞,Hystrix 的第三方故障隔離就是採用相似機制,只不過它底層建立了線程池,經過 Hystrix 的線程池將第三方服務調用與業務線程作了隔離,實現了非侵入式的故障隔離。async
1.4 性能和資源利用率提高
對於一個同步串行化調用的系統,大量的業務線程都在等待服務端返回響應,系統的 CPU 使用率很低,可是性能卻沒法有效提高,這個問題幾乎是全部採用同步 RPC 調用的業務都遇到的一個通病。要想充分利用 CPU 資源,須要讓業務線程儘量的跑滿 CPU,而不是常常性的處於同步等待狀態。採用異步 RPC 調用以後,在單位時間內業務線程能夠接收並處理更多的請求消息,更充分的利用 CPU 資源,提高系統的吞吐量。
根據一些公開的測試數據,一些業務採用異步 RPC 替換同步 RPC 調用以後,綜合性能提高 2-3 倍 +。
2. 異步 RPC 調用實踐
2.1 Tomcat + Servlet3.X 的異步化
2.1.1 工做原理
Servlet 異步是指 Servlet 3 規範中提供了對異步處理 Servlet 請求的支持,能夠把 HTTP 協議處理線程和業務邏輯執行線程隔離開:
1.Servlet3.0 對異步的支持:Servlet3 以前一個 HTTP 請求消息的處理流程,包括:HTTP 請求消息的解析、Read Body、Response Body,以及後續的業務邏輯處理都是由 Tomcat 線程池中的工做線程處理。Servlet3 以後可讓 I/O 線程和業務處理線程分開,進而對業務作隔離和異步化處理。還能夠根據業務重要性進行業務分級,同時把業務線程池分類,實現業務的優先級處理,隔離核心業務和普通業務,提高應用可靠性。
2.Servlet3.1 對非阻塞 I/O 的支持:Servlet3.1 之後增長了對非阻塞 I/O 的支持,根據 Servlet3.1 規範中描述:非阻塞 I/O 僅對在 Servlet 中的異步處理請求有效,不然,當調用 ServletInputStream.setReadListener 或 ServletOutputStream.setWriteListener 方法時拋出 IllegalStateException 異常。Servlet3.1 對非阻塞 I/O 的支持是對以前異步化版本的加強,配套 Tomcat8.X 版本。
Tomcat + Servlet3 的異步化處理原理以下所示:
圖 3 Tomcat + Servlet3 異步處理原理
2.1.2 異步化處理流程
關鍵處理流程以下:
1.聲明 Servlet,增長 asyncSupported 屬性,開啓異步支持,例如 @WebServlet(urlPatterns=「/AsyncLongRunningServlet」,asyncSupported=true)。
2.經過 request 獲取異步上下文 AsyncContext, AsyncContext context = request.startAsync(), 相關接口定義以下:
3.啓動業務邏輯處理線程,並將 AsyncContext 對象傳遞給業務線程。例如:Executor.execute(()->{context, request, response…})。
4.在業務線程中,經過獲取 request 進行業務邏輯處理,完成以後填充 response 對象。
5.業務邏輯處理完成以後,調用 AsyncContext 的 complete() 方法完成響應消息的發送。
2.2 Spring MVC 異步化
2.2.1 工做原理
SpringMVC 3.2+ 版本基於 Servlet3 作了封裝,以簡化業務使用。它的工做原理以下所示:
圖 4 SpringMVC 異步工做原理
2.2.2 異步的幾種實現方式
SpringMVC 支持多種異步化模式,經常使用的有兩種:
1.Controller 的返回值爲 DeferredResult,在業務 Controller 方法中構造 DeferredResult 對象,而後將請求封裝成 Task 投遞到業務線程池中異步執行,業務執行完成以後,構造 ModelAndView,調用 deferredResult.setResult(ModelAndView) 完成異步化處理和響應消息的發送。
2.Controller 的返回值爲 WebAsyncTask,實現 Callable, 在 call 方法中完成業務邏輯處理,由 SpringMVC 框架的線程池來異步執行業務邏輯(非 Tomcat 工做線程)。
以 DeferredResult 爲例,它的異步處理流程以下所示:
圖 5 SpringMVC DeferredResult 工做原理
2.3 Apache ServiceComb 的異步化服務調用
Apache ServiceComb 是一個開箱即用、高性能、兼容流行生態、支持多語言的一站式開源微服務解決方案。它同時支持同步和異步服務調用,下面一塊兒分析下它的異步化服務調用機制。
2.3.1 純 Reactive 模式
純 Reactive 模式的特色是:
1.異步化接口,消費端不須要同步等待服務提供端返回響應,不會產生阻塞。
2.與傳統流程不一樣的,全部功能都在 eventloop 中執行,並不會進行線程切換。
3.只要有任務,線程就不會中止,會一直執行任務,能夠充分利用 cpu 資源,也不會產生多餘的線程切換,去無謂地消耗 cpu。
它的處理流程以下所示:
圖 6 ServiceComb 的 Reactive 工做模式
關鍵流程解讀:
1.異步:橙色箭頭走完後,對本線程的佔用即完成了,不會阻塞等待應答,該線程能夠處理其餘任務。
2.當收到遠端應答後,由網絡數據驅動開始走紅色箭頭的應答流程。
對應的代碼示例以下所示:
經過代碼示例能夠看出,ServiceComb 的 Reactive 工做模式採用了 JDK8 的 CompletableFuture 做爲異步編程模型,利用 CompletableFuture 能夠方便的對多個異步操做結果作編排,以及作級聯異步操做,功能強大,使用靈活。
純 Reactive 模式的使用約束:全部在 eventloop 中執行的邏輯,不容許有任何的阻塞動做,包括不限於 wait、sleep、巨大循環、同步查詢 DB 等等。實際上就是若是業務的微服務採用了 Reactive,則須要作全棧異步,不然會阻塞 eventloop 線程,致使消息收發出現問題。若是業務的微服務想作異步化,可是因爲數據庫、緩存等緣由沒法實現全棧異步,則能夠採用後面介紹的混合 Reactive 模式。
2.3.2 混合 Reactive 模式
混合 Reactive 模式的實現策略以下:
1.服務端接口返回值爲 CompletableFuture,這樣採用透明 RPC 調用時就能夠實現異步化。
2.對於可能產生同步阻塞的業務邏輯代碼,採用獨立線程池的方式進行處理,防止阻塞平臺的 eventloop 線程。
混合 Reactive 模式與純 Reactive 模式相比,主要有兩點差別:
1.存在線程切換。
2.可能致使同步阻塞的業務邏輯放到獨立的線程池中執行,純 Reactive 模式全部業務邏輯都在 eventloop 線程中執行(與 I/O 線程相同)。
它的處理流程以下所示:
圖 7 ServiceComb 的混合 Reactive 工做模式
2.3.3 異步模式的幾個特色
相比於其它的微服務框架(RPC 框架),ServiceComb 的 Reactive 有以下幾個特色:
對於微服務提供端:
1.producer 是否使用 reactive 與 consumer 如何調用,沒有任何聯繫。
2.當 operation 返回值爲 CompletableFuture 類型時,默認此 operation 工做於 reactive 模式,此時若是須要強制此 operation 工做於線程池模式,須要在微服務的配置文件中(microservice.yaml)中明確配置,指定業務線程池。這樣業務邏輯的執行就能夠由 eventloop 線程(I/O 線程)切換到業務線程。
對於微服務消費端:
1.consumer 是否使用 reactive 與 producer 如何實現,沒有任何聯繫。
2.當前只支持透明 RPC 模式,使用 JDK 原生的 CompletableFuture 來承載此功能 ompletableFuture 的 when、then 等等功能均可直接使用。
對於 ServiceComb,不管服務端定義的接口是同步仍是異步的,消費端均可以採用異步的方式調用它,對具體細節感興趣的讀者能夠到 ServiceComb 官網下載 Demo 示例學習。
2.3.4 I/O 線程和業務線程的交互優化
ServiceComb 微服務的完整線程模型以下圖所示:
圖 8 I/O 線程和業務線程交互
ServiceComb 經過線程綁定技術來減小鎖競爭,提高性能:
1.業務線程在第一次調用時會綁定某一個網絡線程, 避免在不一樣網絡線程之間切換, 無謂地增長線程衝突的機率。
2.業務線程綁定網絡線程後, 會再綁定該網絡線程內部的某個鏈接, 一樣是爲了不線程衝突。
2.4 gRPC 的異步化
gRPC 的服務調用有三種方式:
2.4.1 基於 Future 的異步 RPC 調用
業務調用代碼示例以下:
調用 GreeterFutureStub 的 sayHello 方法返回的不是應答,而是 ListenableFuture,它繼承自 JDK 的 Future,接口定義以下:
將 ListenableFuture 加入到 gRPC 的 Future 列表中,建立一個新的 FutureCallback 對象,當 ListenableFuture 獲取到響應以後,gRPC 的 DirectExecutor 線程池會調用新建立的 FutureCallback,執行 onSuccess 或者 onFailure,實現異步回調通知。
接着咱們分析下 ListenableFuture 的實現原理,ListenableFuture 的具體實現類是 GrpcFuture,代碼以下:
獲取到響應以後,調用 complete 方法:
將 ListenableFuture 加入到 Future 列表中以後,同步獲取響應(在 gRPC 線程池中阻塞,非業務調用方線程):
獲取到響應以後,回調 callback 的 onSuccess,代碼以下:
除了將 ListenableFuture 加入到 Futures 中由 gRPC 的線程池執行異步回調,也能夠自定義線程池執行異步回調,代碼示例以下:
2.4.2.Reactive 風格異步 RPC 調用
業務調用代碼示例以下:
構造響應 StreamObserver,經過響應式編程,處理正常和異常回調,接口定義以下:
將響應 StreamObserver 做爲入參傳遞到異步服務調用中,該方法返回空,程序繼續向下執行,不阻塞當前業務線程,代碼以下所示:
下面分析下基於 Reactive 方式異步調用的代碼實現,把響應 StreamObserver 對象做爲入參傳遞到異步調用中,代碼以下:
當收到響應消息時,調用 StreamObserver 的 onNext 方法,代碼以下:
當 Streaming 關閉時,調用 onCompleted 方法,以下所示:
經過源碼分析能夠發現,Reactive 風格的異步調用,相比於 Future 模式,沒有任何同步阻塞點,不管是業務線程仍是 gRPC 框架的線程都不會同步等待,相比於 Future 異步模式,Reactive 風格的調用異步化更完全一些。
2.4.3 異步雙向 streaming 調用
gRPC 的通訊協議基於標準的 HTTP/2 設計,除了普通的 RPC 調用,還支持 streaming 調用。
客戶端發送 N 個請求,服務端返回 N 個或者 M 個響應,利用該特性,能夠充分利用 HTTP/2.0 的多路複用功能,在某個時刻,HTTP/2.0 鏈路上能夠既有請求也有響應,實現了全雙工通訊(對比單行道和雙向車道),示例以下:
圖 9 雙向 streaming 模式
proto 文件定義以下:
業務代碼示例以下:
構造 Streaming 響應對象 StreamObserver並實現 onNext 等接口,因爲服務端也是 Streaming 模式,所以響應是多個的,也就是說 onNext 會被調用屢次。
經過在循環中調用 requestObserver 的 onNext 方法,發送請求消息,代碼以下所示:
requestObserver 的 onNext 方法實際調用了 ClientCall 的消息發送方法,代碼以下:
對於雙向 Streaming 模式,只支持異步調用方式。
2.4.4 總結
gRPC 服務調用支持同步和異步方式,同時也支持普通的 RPC 和 streaming 模式,能夠最大程度的知足業務的需求。
對於 streaming 模式,能夠充分利用 HTTP/2.0 協議的多路複用功能,實如今一條 HTTP 鏈路上並行雙向傳輸數據,有效的解決了 HTTP/1.X 的數據單向傳輸問題,在大幅減小 HTTP 鏈接的狀況下,充分利用單條鏈路的性能,能夠媲美傳統的 RPC 私有長鏈接協議:更少的鏈路、更高的性能:
圖 10 傳統 RPC 和雙向 streaming 模式的對比
gRPC 的網絡 I/O 通訊基於 Netty 構建,服務調用底層統一使用異步方式,同步調用是在異步的基礎上作了上層封裝。所以,gRPC 的異步化是比較完全的,對於提高 I/O 密集型業務的吞吐量和可靠性有很大的幫助。
3. 異步化的一些技術難點
3.1.1 異步異常傳遞
當採用異步編程以後,異步拋出的異常傳遞給調用方會變得很是困難,例如 Runnable, 當異步執行它時,異常須要在 run 方法中捕獲和處理,不然會致使線程跑飛,run 方法中的異常是沒法回傳到調用方的。
使用 JDK8 的 CompletableFuture 以後,它的經常使用方法參數基本是 Lambda 表達式,因爲函數接口中的方法一般不容許檢查期異常,在表達式中發生的異常沒法回傳給調用方,相比於之前同步調用能夠將異常拋給調用方處理的方式有很大差別。
異步異常的解決策略:
1.若是異步的編程模型基於 JDK8 的 CompletableFuture,能夠經過 whenComplete 對返回值的異常進行非空判斷,當異常非空時,進行異常邏輯處理,相關接口以下:
也能夠經過 exceptionally 方法來處理異步執行發生的異常,相關接口以下所示:
2.異步回調(Lambda 表達式)代碼塊中的異常處理有兩種策略:1)必定要經過 exceptionally 方法或者 whenComplete 對異常進行捕獲處理,不然會致使 Lambda 表達式異常退出,後續操做被忽略,最終致使業務邏輯跑飛。2)運行期異常,一般是沒法拋出來由調用方處理的,須要在發生異常的地方就地捕獲和處理。
3.1.2 超時控制
異步代碼塊(Lambda 表達式)中可能會涉及到多種業務邏輯操做,例如:
1.數據庫、緩存、MQ 等中間件平臺調用。
2.第三方接口調用。
3.級聯嵌套其它微服務調用。
對於異步的超時控制,建議策略以下:
1.對單個原子的中間件、第三方接口、微服務作超時控制。
2.不建議直接對異步代碼塊(Lambda 表達式)總體作超時控制,例如包裝出一個支持異步超時的 CompletableFuture,主要緣由以下:
沒有超時控制以後,要確保 CompletableFuture 可以正常或者異常的結束,不然會致使 CompletableFuture 積壓,最終發生 OOM。
3.1.3 上下文傳遞
在傳統的同步 RPC 調用時,業務每每經過線程變量來傳遞上下文,例如:TraceID、會話 Session、IP 等信息。異步化以後,因爲潛在的線程切換和線程被多個消息交叉複用,一般不建議繼續使用線程變量傳遞上下文。
想學習Java工程化、分佈式架構、高併發、高性能、深刻淺出、微服務架構、Spring,MyBatis,Netty源碼分析等技術能夠加羣:479499375,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們,歡迎進羣一塊兒深刻交流學習。
異步化以後,上下文傳遞的建議策略:
1.若是是 Lambda 表達式,能夠直接引用局部變量,經過變量引用的方式將上下文信息傳遞到 Lambda 表達式中,後續能夠經過方法傳參等層層傳遞下去。
2.在全部發生線程切換的地方,顯式的進行上下文信息的拷貝和清理,特別須要注意的是隱式線程切換,例如 Hystrix,底層會本身啓線程池。
3.建議經過調用級的消息上下文來作參數傳遞,每一個上下文都關聯一次 RPC 調用,調用完成以後自動清理掉。
4.異步化以後,須要排重點查全部使用 ThreadLocal 的地方,一般狀況下都會存在問題,須要作改造。
3.1.4 異步回調地獄問題
若是使用的是 JDK8 的 CompletableFuture,它支持對異步操做結果作編排以及級聯操做,可以比較好的解決相似 JS 和傳統 Future-Listener 的回調地域問題,感興趣的讀者能夠體會下 CompletableFuture 的異步化接口。