1. 程式人生 > >深入剖析通信層和 RPC 調用的異步化:一

深入剖析通信層和 RPC 調用的異步化:一

facebook 編寫 免費學習 說明文檔 connector neu images star 場景

  • 異步的一些常見誤區
  • 1.1. 常見的理解誤區

    在將近 10 年的平臺中間件研發歷程中,我們的平臺和業務經歷了從 C++ 到 Java,從同步的 BIO 到非阻塞的 NIO,以及純異步的事件驅動 I/O(AIO)。服務器也從 Web 容器逐步遷移到了內部更輕量、更高性能的微容器。服務之間的 RPC 調用從最初的同步阻塞式調用逐步升級到了全棧異步非阻塞調用。

    每次的技術演進都會涉及到大量底層平臺技術以及上層編程模型的切換,在實際工作中,我發現很多同學對通信框架的異步和 RPC 調用的異步理解有誤,比較典型的錯誤理解包括:

    1.我使用的是 Tomcat8,因為 Tomcat8 支持 NIO,所以我基於 Tomcat 開發的 HTTP 調用都是異步的。

    2.因為我們的 RPC 框架底層使用的是 Netty、Vert.X 等異步框架,所以我們的 RPC 調用天生就是異步的。

    3.因為我們底層的通信框架不支持異步,所以 RPC 調用也無法異步化。

    1.2. 混淆 Tomcat NIO 與 HTTP 服務的異步化

    1.2.1. Tomcat 的 BIO 和 NIO

    在 Tomcat6.X 版本對 NIO 提供比較完善的支持之前,作為 Web 服務器,Tomcat 以 BIO 的方式接收並處理客戶端的 HTTP 請求,當並發訪問量比較大時,就容易發生擁塞等性能問題,它的工作原理示意如下所示:

    技術分享圖片

    圖 1 采用 BIO 做 HTTP 服務器的 Web 容器

    傳統同步阻塞通信(BIO)面臨的主要問題如下:

    1.性能問題:一連接一線程模型導致服務端的並發接入數和系統吞吐量受到極大限制。

    2.可靠性問題:由於 I/O 操作采用同步阻塞模式,當網絡擁塞或者通信對端處理緩慢會導致 I/O 線程被掛住,阻塞時間無法預測。

    3.可維護性問題:I/O 線程數無法有效控制、資源無法有效共享(多線程並發問題),系統可維護性差。

    從上圖我們可以看出,每當有一個新的客戶端接入,服務端就需要創建一個新的線程(或者重用線程池中的可用線程),每個客戶端鏈路對應一個線程。當客戶端處理緩慢或者網絡有擁塞時,服務端的鏈路線程就會被同步阻塞,也就是說所有的 I/O 操作都可能被掛住,這會導致線程利用率非常低,同時隨著客戶端接入數的不斷增加,服務端的 I/O 線程不斷膨脹,直到無法創建新的線程。

    同步阻塞 I/O 導致的問題無法在業務層規避,必須改變 I/O 模型,才能從根本上解決這個問題。

    Tomcat 6.X 提供了對 NIO 的支持,通過指定 Connector 的 protocol=“org.apache.coyote.http11.Http11NioProtocol”,就可以開啟 NIO 模式,采用 NIO 之後,利用 Selector 的輪詢以及 I/O 操作的非阻塞特性,可以實現使用更少的 I/O 線程處理更多的客戶端連接,提升吞吐量和主機的資源利用率。Tomcat 8.X 之後提供了對 NIO2.0 的支持,默認也開啟了 NIO 通信模式。

    1.2.2. Tomcat NIO 與 Servlet 異步

    事實上,Tomcat 支持 NIO,與 Tomcat 的 HTTP 服務是否是異步的,沒有必然關系,這個可以從兩個層面理解:

    1.HTTP 消息的讀寫:即便采用了 NIO,HTTP 請求和響應的消息處理仍然可能是同步阻塞的,這與協議棧的具體策略有關系。從 Tomcat 官方文檔可以看到,Tomcat 6.X 版本即便采用 Http11NioProtocol,HTTP 請求消息和響應消息的讀寫仍然是 Blocking 的。

    2.HTTP 請求和響應的生命周期管理:本質上就是 Servlet 是否支持異步,如果 Servlet 是 3.X 之前的版本,則 HTTP 協議的處理仍然是同步的,這就意味著 Tomcat 的 Connector 線程需要同時處理 HTTP 請求消息、執行 Servlet Filter、以及業務邏輯,然後將業務構造的 HTTP 響應消息發送給客戶端,整個 HTTP 消息的生命周期都采用了同步處理方式。

    Tomcat 與 Servlet 的版本配套關系如下所示:

    Servlet 規範版本Tomcat 版本JDK**** 版本4.09.0.X8+3.18.0.X7+3.07.0.X6+2.56.0.X5+2.45.5.X1.4+2.34.1.X1.3+

    表 1 Tomcat 與 Servlet 的版本配套關系

    1.2.3. Tomcat NIO 與 HTTP 服務調用

    以 Tomcat 6.X 版本為例,Tomcat HTTP 協議消息和後續的業務邏輯處理如下所示(Tomcat HTTP 協議處理非常復雜,為了便於理解,圖示做了簡化):

    技術分享圖片

    圖 2 Tomcat 6.X 的 HTTP 消息接入和處理原理

    從上圖可以看出,HTTP 請求消息的讀取、Servlet Filter 的執行、業務 Servlet 的邏輯處理,以及 HTTP 響應都是由 Tomcat 的 NIO 線程(Processor,實際更復雜些,這裏做了簡化處理)做處理,即 HTTP 消息的處理周期中都是串行同步執行的,盡管 Tomcat 使用 NIO 做接入,HTTP 服務端的處理仍然是同步的。它的弊端很明顯,如果 Servlet 中的業務邏輯處理比較復雜,則會導致 Tomcat 的 NIO 線程被阻塞,無法讀取其它 HTTP 客戶端發送的 HTTP 請求消息,導致客戶端讀響應超時。

    可能有讀者會有疑問,途中標識?處,為什麽不能創建一個業務線程池,由業務線程池異步處理業務邏輯,處理完成之後再填充 HttpServletResponse,發送響應。實際上在 Servlet 支持異步之前是無法實現的,原因是每個響應對象只有在 Servlet 的 service 方法或 Filter 的 doFilter 方法範圍內有效,該方法一旦調用完成,Tomcat 就認為本次 HTTP 消息處理完成,它會回收 HttpServletRequest 和 HttpServletResponse 對象再利用,如果業務異步化之後再處理 HttpServletResponse,拿到的實際就不是之前請求消息對應的響應,會發生各種非預期問題,因此,業務邏輯必須在 service 方法結束前執行,無法做異步化處理。

    如果使用的是支持 Servlet3.0+ 版本的 Tomcat,通過開啟異步處理模式,就能解決同步調用面臨的各種問題,在後續章節中會有詳細介紹。

    1.2.4. 總結

    通過以上分析我們可以看出,除了將 Tomcat 的 Connector 配置成 NIO 模式之外,還需要 Tomcat 配套的 Servlet 版本支持異步化(3.0+),同時還需要在業務 Servlet 的代碼中開啟異步模式,HTTP 服務端才能夠實現真正的異步化:I/O 異步以及業務邏輯處理的異步化。

    1.3. 混淆 RPC 異步與 I/O 異步

    1.3.1. Java 的各種 I/O 模型

    很多人喜歡將 JDK 1.4 提供的 NIO 框架稱為異步非阻塞 I/O,但是,如果嚴格按照 UNIX 網絡編程模型和 JDK 的實現進行區分,實際上它只能被稱為非阻塞 I/O,不能叫異步非阻塞 I/O。在早期的 JDK 1.4 和 1.5 update10 版本之前,JDK 的 Selector 基於 select/poll 模型實現,它是基於 I/O 復用技術的非阻塞 I/O,不是異步 I/O。在 JDK 1.5 update10 和 Linux core2.6 以上版本,Sun 優化了 Selctor 的實現,它在底層使用 epoll 替換了 select/poll,上層的 API 並沒有變化,可以認為是 JDK NIO 的一次性能優化,但是它仍舊沒有改變 I/O 的模型。相關優化的官方說明如下圖所示:

    技術分享圖片

    圖 3 JDK1.5_update10 支持 epoll

    由 JDK1.7 提供的 NIO 2.0 新增了異步的套接字通道,它是真正的異步 I/O,在異步 I/O 操作的時候可以傳遞信號變量,當操作完成之後會回調相關的方法,異步 I/O 也被稱為 AIO。NIO 類庫支持非阻塞讀和寫操作,相比於之前的同步阻塞讀和寫,它是異步的,因此很多人仍然習慣於稱 NIO 為異步非阻塞 I/O,在此不需要太咬文嚼字。

    不同的 I/O 模型由於線程模型、API 等差別很大,所以用法的差異也非常大。各種 I/O 模型的優缺點對比如下:

    同步阻塞 I/O(BIO)非阻塞 I/O(NIO)異步 I/O(AIO)客戶端個數:I/O 線程1:1M:1(1 個 I/O 線程處理多個客戶端連接)M:0(不需要用戶啟動額外的 I/O 線程,被動回調)I/O 類型(阻塞)阻塞 I/O非阻塞 I/O非阻塞 I/OI/O 類型(同步)同步 I/O同步 I/O(I/O 多路復用)異步 I/OAPI 使用難度簡單非常復雜復雜調試難度簡單復雜復雜可靠性非常差高高吞吐量低高高

    表 2 Java 各種 I/O 模型優缺點對比

    1.3.2. RPC 工作原理

    RPC 的全稱是 Remote Procedure Call,它是一種進程間通信方式。允許像調用本地服務一樣調用遠程服務,它的具體實現方式可以不同,例如 Spring 的 HTTP Invoker,Facebook 的 Thrift 二進制私有協議通信。

    RPC 框架的目標就是讓遠程過程(服務)調用更加簡單、透明,RPC 框架負責屏蔽底層的傳輸方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二進制)和通信細節。框架使用者只需要了解誰在什麽位置提供了什麽樣的遠程服務接口即可,開發者不需要關心底層通信細節和調用過程。

    RPC 框架的調用原理圖如下所示:

    技術分享圖片

    圖 4 RPC 框架原理圖

    RPC 框架實現的幾個核心技術點總結如下:

    1.遠程服務提供者需要以某種形式提供服務調用相關的信息,包括但不限於服務接口定義、數據結構,或者中間態的服務定義文件,例如 Thrift 的 IDL 文件,WS-RPC 的 WSDL 文件定義,甚至也可以是服務端的接口說明文檔;服務調用者需要通過一定的途徑獲取遠程服務調用相關信息,例如服務端接口定義 Jar 包導入,獲取服務端 IDL 文件等。

    2.遠程代理對象:服務調用者調用的服務實際是遠程服務的本地代理,對於 Java 語言,它的實現就是 JDK 的動態代理,通過動態代理的攔截機制,將本地調用封裝成遠程服務調用。

    3.通信:RPC 框架與具體的協議無關,例如 Spring 的遠程調用支持 HTTP Invoke、RMI Invoke,MessagePack 使用的是私有的二進制壓縮協議。

    4.序列化:遠程通信,需要將對象轉換成二進制碼流進行網絡傳輸,不同的序列化框架,支持的數據類型、數據包大小、異常類型以及性能等都不同。不同的 RPC 框架應用場景不同,因此技術選擇也會存在很大差異。一些做的比較好的 RPC 框架,可以支持多種序列化方式,有的甚至支持用戶自定義序列化框架(Hadoop Avro)。

    1.3.3. RPC 異步與 I/O 的異步

    RPC 異步與 I/O 的異步沒有必然關系,當然,在大多數場景下,RPC 框架底層會使用異步 I/O,實現全棧異步。

    RPC 框架異步調度模型如下所示:

    技術分享圖片

    圖 5 異步 RPC 調用原理

    異步 RPC 調用的關鍵點有 2 個:

    1.不能阻塞調用方線程:接口調用通常會返回 Future 或者 Promise 對象,代表異步操作的一個回調對象,當異步操作完成之後,由 I/O 線程回調業務註冊的 Listener,繼續執行業務邏輯。

    2.請求和響應的上下文關聯:除了 HTTP/1.X 協議,大部分二進制協議的 TCP 鏈路都是多路復用的,請求和響應消息的發送和接收順序是無序的。所以,異步 RPC 調用需要緩存請求和響應的上下文關聯關系,以及響應需要使用到的消息上下文。

    正如上圖所示,當 RPC 調用請求消息發送到 I/O 線程的消息隊列之後,業務線程就可以返回,至於 I/O 線程采用同步還是異步的方式讀寫消息,與 RPC 調用的同步和異步沒必然的關聯關系,當然,采用異步 I/O, 整體性能和可靠性會更好一些,所以現在大部分的 RPC 框架底層采用的都是異步 / 非阻塞 I/O。以 Netty 為例,無論 RPC 調用是同步還是異步,只要調用消息發送接口,Netty 都會將發送請求封裝成 Task,加入到 I/O 線程的消息隊列中統一處理,相關代碼如下所示:

    技術分享圖片

    異步回調的一些實現策略:

    1.Future/Promise:比較常用的有 JDK8 之前的 Future,通過添加 Listener 來做異步回調,JDK8 之後通常使用 CompletableFuture,它支持各種復雜的異步處理策略,例如自定義線程池、多個異步操作的編排、有返回值和無返回值異步、多個異步操作的級聯操作等。

    2.線程池 +RxJava: 最經典的實現就是 Netflix 開源的 Hystrix 框架,使用 HystrixCommand(創建線程池)做一層異步封裝,將同步調用封裝成異步調用,利用 RxJava API,通過訂閱的方式對結果做異步處理,它的工作原理如下所示:

    技術分享圖片

    圖 6 利用 Hystix 做異步化封裝

    1.3.4. 總結

    通過以上分析可以得出如下結論:

    1.RPC 異步指的是業務線程發起 RPC 調用之後,不用同步等待服務端返回應答,而是立即返回,當接收到響應之後,回調執行業務的後續邏輯。

    2.I/O 的異步是通信層的具體實現策略,使用異步 I/O 會帶來性能和可靠性提升,但是與 RPC 調用是同步還是異步沒必然關系。

    1. RPC 同步與異步調用

    很多 RPC 框架同時支持同步和異步調用,下面對同步和異步 RPC 調用的工作原理以及優缺點進行分析。

    2.1. 同步 RPC 調用

    2.1.1. 同步 RPC 調用流行的原因

    在傳統的單體架構中,以 Spring + Struts + MyBatis + Tomcat 為例,業務邏輯通常由各種 Controller(Spring Bean)來實現,它的邏輯架構如下所示:

    技術分享圖片

    圖 7 基於 MVC 的傳統單體架構

    在單體架構中,本地方法調用都是同步方式,而且定義形式往往都是如下形式(請求參數 + 方法返回值):

    String sayHello(String hello);

    切換到 RPC 框架之後,很多都支持通過 XML 引用或者代碼註解的方式引用遠端的 RPC 服務,可以像使用本地接口一樣調用遠程的服務,這種開發模式與傳統單體應用開發模式相似,編程簡單,學習和切換成本低,調試也比較方便,因此,同步 RPC 調用成為大部分項目的首選。

    以 XML 方式導入遠端服務提供者的 API 接口示例如下:

    <xxx:reference id="echoService" interface="edu.neu.EchoService" />
    
    <bean class="edu.neu.xxxAction" init-method="start">
    
    ? <property name="echoService" ref="echoService" />
    
    </bean>

    導入之後業務就可以直接在代碼中調用 echoService 接口,與傳統單體應用調用本地 Spring Bean 一樣,無需感知遠端服務接口的具體部署位置信息。

    2.1.2. 同步 RPC 調用工作原理

    同步 RPC 調用是最常用的一種服務調用方式,它的工作原理如下:客戶端發起遠程 RPC 調用請求,用戶線程完成消息序列化之後,將消息投遞到通信框架,然後同步阻塞,等待通信線程發送請求並接收到應答之後,喚醒同步等待的用戶線程,用戶線程獲取到應答之後返回。它的工作原理圖如下所示:

    它的工作原理圖如下所示:

    技術分享圖片

    圖 8 同步 RPC 調用

    主要流程如下:

    1.消費者調用服務端發布的接口,接口調用由 RPC 框架包裝成動態代理,發起遠程 RPC 調用。

    2.消費者線程調用通信框架的消息發送接口之後,直接或者間接調用 wait() 方法,同步阻塞等待應答。

    3.通信框架的 I/O 線程通過網絡將請求消息發送給服務端。

    4.服務端返回應答消息給消費者,由通信框架負責應答消息的反序列化。

    5.I/O 線程獲取到應答消息之後,根據消息上下文找到之前同步阻塞的業務線程,notify() 阻塞的業務線程,返回應答給消費者,完成 RPC 調用。

    2.1.3. 同步 RPC 調用面臨的挑戰

    同步 RPC 調用的主要缺點如下:

    1.線程利用率低:線程資源是系統中非常重要的資源,在一個進程中線程總數是有限制的,提升線程使用率就能夠有效提升系統的吞吐量,在同步 RPC 調用中,如果服務端沒有返回響應,客戶端業務線程就會一直阻塞,無法處理其它業務消息。

    2.糾結的超時時間:RPC 調用的超時時間配置是個比較棘手的問題。如果配置的過大,一旦服務端返回響應慢,就容易把客戶端掛死。如果配置的過小,則超時失敗率會增加。即便參考測試環境的平均和最大時延來設置,由於生產環境數據、硬件等與測試環境的差異,也很難一次設置的比較合理。另外,考慮到客戶端流量的變化、服務端依賴的數據庫、緩存、第三方系統等的性能波動,這都會導致服務調用時延發生變化,因此,依靠超時時間來保障系統的可靠性,難度很大。

    3.雪崩效應:在一個同步調用鏈中,只要下遊某個服務返回響應慢,會導致故障沿著調用鏈向上遊蔓延,最終把整個系統都拖垮,引起雪崩,示例如下:

    技術分享圖片

    圖 9 同步 RPC 調用級聯故障

    2.2. 異步 RPC 調用

    2.2.1. 異步 RPC 調用工作原理

    JDK 原生的 Future 主要用於異步操作,它代表了異步操作的執行結果,用戶可以通過調用它的 get 方法獲取結果。如果當前操作沒有執行完,get 操作將阻塞調用線程。在實際項目中,往往會擴展 JDK 的 Future,提供 Future-Listener 機制,它支持主動獲取和被動異步回調通知兩種模式,適用於不同的業務場景。

    基於 JDK 的 Future-Listener 機制,可以實現異步 RPC 調用,它的工作原理如下所示:
    技術分享圖片
    深入剖析通信層和 RPC 調用的異步化:一

    圖 10 異步 RPC 調用原理圖

    異步 RPC 調用的工作流程如下:

    1.消費者調用 RPC 服務端發布的接口,接口調用由 RPC 框架包裝成動態代理,發起遠程 RPC 調用。

    2.通信框架異步發送請求消息,如果沒有發生 I/O 異常,返回。

    3.請求消息發送成功後,I/O 線程構造 Future 對象,設置到 RPC 上下文中。

    4.用戶線程通過 RPC 上下文獲取 Future 對象。

    5.構造 Listener 對象,將其添加到 Future 中,用於服務端應答異步回調通知。

    6.用戶線程返回,不阻塞等待應答。

    7.服務端返回應答消息,通信框架負責反序列化等。

    8.I/O 線程將應答設置到 Future 對象的操作結果中。

    9.Future 對象掃描註冊的監聽器列表,循環調用監聽器的 operationComplete 方法,將結果通知給監聽器,監聽器獲取到結果之後,繼續後續業務邏輯的執行,異步 RPC 調用結束。

    2.2.2. 異步 RPC 調用編程模型的優化

    Java8 的 CompletableFuture 提供了非常豐富的異步功能,它可以幫助用戶簡化異步編程的復雜性,通過 Lambda 表達式可以方便的編寫異步回調邏輯,除了普通的異步回調接口,它還提供了多個異步操作結果轉換以及與或等條件表達式的編排能力,方便對多個異步操作結果進行邏輯編排。

    CompletableFuture 提供了大約 20 類比較實用的異步 API,接口定義示例如下:

    技術分享圖片

    圖 11 CompletableFuture 異步 API 定義

    利用 JDK 的 CompletableFuture 與 Netty 的 NIO,可以非常方便的實現異步 RPC 調用,設計思路如下所示:
    技術分享圖片

    圖 12 異步 RPC 調用設計原理

    異步 RPC 調用的工作流程如下:

    1.消費者通過 RPC 框架調用服務端。

    2.Netty 異步發送 HTTP 請求消息,如果沒有發生 I/O 異常就正常返回。

    3.HTTP 請求消息發送成功後,I/O 線程構造 CompletableFuture 對象,設置到 RPC 上下文中。

    4.用戶線程通過 RPC 上下文獲取 CompletableFuture 對象。

    5.不阻塞用戶線程,立即返回 CompletableFuture 對象。

    6.通過 CompletableFuture 編寫 Function 函數,在 Lambda 表達式中實現異步回調邏輯。

    7.服務端返回 HTTP 響應,Netty 負責反序列化工作。

    8.Netty I/O 線程通過調用 CompletableFuture 的 complete 方法將應答設置到 CompletableFuture 對象的操作結果中。

    9.CompletableFuture 通過 whenCompleteAsync 等接口異步執行業務回調邏輯,實現 RPC 調用的異步化。

    2.2.3. 異步 RPC 調用的優勢

    異步 RPC 調用相比於同步調用有兩個優點:

    1.化串行為並行,提升 RPC 調用效率,減少業務線程阻塞時間。

    2.化同步為異步,避免業務線程阻塞。

    假如一次閱讀首頁訪問需要調用多個服務接口,采用同步調用方式,它的調用流程如下所示:

    技術分享圖片

    圖 13 同步調用多個服務場景

    由於每次 RPC 調用都是同步阻塞,三次調用總耗時為 T = T1 + T2 + T3。下面看下采用異步 RPC 調用之後的優化效果:
    技術分享圖片

    圖 14 異步多服務調用場景

    采用異步 RPC 調用模式,最後調用三個異步操作結果 Future 的 get 方法同步等待應答,它的總執行時間 T = Max(T1, T2,T3),相比於同步 RPC 調用,性能提升效果非常明顯。

    2.3. 總結

    2.3.1. 異步 RPC 調用性能未必會更高

    通常在實驗室環境中測試,由於網絡時延小、模擬業務又通常比較簡單,所以異步 RPC 調用並不一定性能更高,但在生產環境中,異步 RPC 調用往往性能更高、可靠性也更好。主要原因是網絡環境相對惡劣、真實的 RPC 調用耗時更多等,這種惡劣的運行環境正好可以發揮異步 RPC 調用的優勢。

    2.3.2. 最佳實踐

    服務框架支持多種 RPC 調用方式,在實際項目中如何選擇呢?建議從以下幾個角度進行考慮:

    1.降低業務 E2E 時延:業務調用鏈是否太長、某些服務是否不太可靠,需要對服務調用流程進行梳理,看是否可以通過異步並行 RPC 調用來提升調用效率,降低 RPC 調用時延。

    2.可靠性角度:某些業務調用鏈上的關鍵服務不太可靠,一旦出故障會導致大量線程資源被掛住,可以考慮使用異步 RPC 調用防止故障擴散。

    3.傳統的 RPC 調用:服務調用比較簡單,對時延要求不高的場景,則可以考慮同步 RPC 調用,降低編程復雜度,以及調試難度,提升開發效率。
    技術分享圖片
    如果想免費學習Java工程化、高性能及分布式、深入淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java架構交流群:714526711,群裏有阿裏大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給大家。

    深入剖析通信層和 RPC 調用的異步化:一