深入淺出 gRPC 04:gRPC 服務調用原理

目錄

1. 常用的服務調用方式

1.1 同步服務調用

1.2 並行服務調用

1.3 異步服務調用

2. 服務調用的一些誤區和典型問題

2.1 理解誤區

2.1.1 I/O 異步服務就是異步

2.1.2 服務調用天生就是同步的

2.1.3 異步服務調用性能更高

2.2 Restful API 的潛在性能風險

2.2.1 HTTP1.X 的性能問題

2.2.2 異步非阻塞 I/O 的 HTTP 協議棧

3. gRPC 服務調用

3.1 普通 RPC 調用

3.1.1 同步阻塞式 RPC 調用

3.1.2 基於 Future 的異步 RPC 調用

3.1.3 Reactive 風格異步 RPC 調用

3.2 Streaming 模式服務調用

3.2.1 服務端 streaming

3.2.2 客戶端 streaming

3.2.3 雙向 streaming

3.3 總結


1. 常用的服務調用方式

無論是 RPC 框架,還是當前比較流行的微服務框架,通常都會提供多種服務調用方式,以滿足不同業務場景的需求,比較常用的服務調用方式如下:

  • 同步服務調用:最常用的服務調用方式,開發比較簡單,比較符合編程人員的習慣,代碼相對容易維護些;
  • 並行服務調用:對於無上下文依賴的多個服務,可以一次並行發起多個調用,這樣可以有效降低服務調用的時延
  • 異步服務調用:客戶端發起服務調用之後,不同步等待響應,而是註冊監聽器或者回調函數,待接收到響應之後發起異步回調,驅動業務流程繼續執行,比較常用的有 Reactive 響應式編程和 JDK 的 Future-Listener 回調機制

下面我們分別對上述幾種服務調用方式進行講解。

1.1 同步服務調用

同步服務調用是最常用的一種服務調用方式,它的工作原理和使用都非常簡單,RPC/ 微服務框架默認都支持該調用形式。

同步服務調用的工作原理如下:客戶端發起 RPC 調用,將請求消息路由到 I/O 線程,無論 I/O 線程是同步還是異步發送消息,發起調用的業務線程都會同步阻塞,等待服務端的應答,由 I/O 線程喚醒同步等待的業務線程,獲取應答,然後業務流程繼續執行。它的工作原理圖如下所示:

  • 第 1 步,消費者調用服務端發佈的接口,接口調用由服務框架包裝成動態代理,發起遠程服務調用。
  • 第 2 步,通信框架的 I/O 線程通過網絡將請求消息發送給服務端。
  • 第 3 步,消費者業務線程調用通信框架的消息發送接口之後,直接或者間接調用 wait() 方法,同步阻塞等待應答。(備註:與步驟 2 無嚴格* 的順序先後關閉,不同框架實現策略不同)。
  • 第 4 步,服務端返回應答消息給消費者,由通信框架負責應答消息的反序列化。
  • 第 5 步,I/O 線程獲取到應答消息之後,根據消息上下文找到之前同步阻塞的業務線程,notify() 阻塞的業務線程,返回應答給消費者,完成服務調用。

同步服務調用會阻塞調用方的業務線程,爲了防止服務端長時間不返回應答消息導致客戶端用戶線程被掛死,業務線程等待的時候需要設置超時時間,超時時間的取值需要綜合考慮業務端到端的時延控制目標,以及自身的可靠性,超時時間不宜設置過大或者過小, 通常在幾百毫秒到幾秒之間。

1.2 並行服務調用

在大多數業務應用中,服務總是被串行的調用和執行,例如 A 業務調用 B 服務,B 服務又調用 C 服務,最後形成一個串行的服務調用鏈:A 業務 — B 服務 — C 服務…

串行服務調用代碼比較簡單,便於開發和維護,但在一些時延敏感型的業務場景中,需要採用並行服務調用來降低時延,比較典型的場景如下:

  • 多個服務之間邏輯上不存在上下文依賴關係,執行先後順序沒有嚴格的要求,邏輯上可以被並行執行;
  • 長流程業務,調用多個服務,對時延比較敏感,其中有部分服務邏輯上無上下文關聯,可以被並行調用。

並行服務調用的主要目標有兩個:

  • 降低業務 E2E 時延。
  • 提升整個系統的吞吐量。

以遊戲業務中購買道具流程爲例,對並行服務調用的價值進行說明:

在購買道具時,三個鑑權流程實際可以並行執行,最終執行結果做個 Join 即可。如果採用傳統的串行服務調用,耗時將是三個鑑權服務時延之和,顯然是沒有必要的。

計費之後的通知類服務亦如此(注意:通知服務也可以使用 MQ 做訂閱 / 發佈),單個服務的串行調用會導致購買道具時延比較長,影響遊戲玩家的體驗。

要解決串行調用效率低的問題,有兩個解決對策:

  • 並行服務調用,一次 I/O 操作,可以發起批量調用,然後同步等待響應;
  • 異步服務調用,在同一個業務線程中異步執行多個服務調用,不阻塞業務線程。

採用並行服務調用的僞代碼示例:

ParallelFuture future = ParallelService.invoke(serviceName [], methodName[], args []);
List<Object> results = future.get(timeout);// 同步阻塞式獲取批量服務調用的響應列表

採用並行服務調用之後,它的總耗時爲並行服務調用中耗時最大的服務的執行時間,即 T = Max(T(服務 A),T(服務 B),T(服務 C)),如果採用同步串行服務調用,則總耗時爲並行調用的各個服務耗時之和,即:T = T(服務 A) + T(服務 B) + T(服務 C)。服務調用越多,並行服務調用的優勢越明顯。

並行服務調用的一種實現策略如下所示:
 

  • 第 1 步,服務框架提供批量服務調用接口供消費者使用,它的定義樣例如下:ParallelService.invoke(serviceName [], methodName[], args []);
  • 第 2 步,平臺的並行服務調用器創建並行 Future,緩存批量服務調用上下文信息;
  • 第 3 步,並行服務調用器循環調用普通的 Invoker,通過循環的方式執行單個服務調用,獲取到單個服務的 Future 之後設置到 Parallel Future 中;
  • 第 4 步,返回 Parallel Future 給消費者;
  • 第 5 步,普通 Invoker 調用通信框架的消息發送接口,發起遠程服務調用;
  • 第 6 步,服務端返回應答,通信框架對報文做反序列化,轉換成業務對象更新 Parallel Future 的結果列表;
  • 第 7 步,消費者調用 Parallel Future 的 get(timeout) 方法, 同步阻塞,等待所有結果全部返回;
  • 第 8 步,Parallel Future 通過對結果集進行判斷,看所有服務調用是否都已經完成(包括成功、失敗和異常);

第 9 步,所有批量服務調用結果都已經返回,notify 消費者線程,消費者獲取到結果列表,完成批量服務調用,流程繼續執行。

通過批量服務調用 + Future 機制,可以實現並行服務調用,由於在調用過程中沒有創建新的線程,用戶就不需要擔心依賴線程上下文的功能發生異常。

1.3 異步服務調用

JDK 原生的 Future 主要用於異步操作,它代表了異步操作的執行結果,用戶可以通過調用它的 get 方法獲取結果。如果當前操作沒有執行完,get 操作將阻塞調用線程:

在實際項目中,往往會擴展 JDK 的 Future,提供 Future-Listener 機制,它支持主動獲取和被動異步回調通知兩種模式,適用於不同的業務場景。

異步服務調用的工作原理如下:

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

  • 第 1 步,消費者調用服務端發佈的接口,接口調用由服務框架包裝成動態代理,發起遠程服務調用;
  • 第 2 步,通信框架異步發送請求消息,如果沒有發生 I/O 異常,返回;
  • 第 3 步,請求消息發送成功後,I/O 線程構造 Future 對象,設置到 RPC 上下文中;
  • 第 4 步,業務線程通過 RPC 上下文獲取 Future 對象;
  • 第 5 步,構造 Listener 對象,將其添加到 Future 中,用於服務端應答異步回調通知;
  • 第 6 步,業務線程返回,不阻塞等待應答;
  • 第 7 步,服務端返回應答消息,通信框架負責反序列化等;
  • 第 8 步,I/O 線程將應答設置到 Future 對象的操作結果中;
  • 第 9 步,Future 對象掃描註冊的監聽器列表,循環調用監聽器的 operationComplete 方法,將結果通知給監聽器,監聽器獲取到結果之後,繼續後續業務邏輯的執行,異步服務調用結束。

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

  • 化串行爲並行,提升服務調用效率,減少業務線程阻塞時間。
  • 化同步爲異步,避免業務線程阻塞。

基於 Future-Listener 的純異步服務調用代碼示例如下:

xxxService1.xxxMethod(Req);
Future f1 = RpcContext.getContext().getFuture();
Listener l = new xxxListener();
f1.addListener(l);

class xxxListener {
public void operationComplete(F future) { 
    // 判斷是否執行成功,執行後續業務流程
}

2. 服務調用的一些誤區和典型問題

對於服務調用方式的理解,容易出現各種誤區,例如把 I/O 異步與服務調用的異步混淆起來,認爲異步服務調用一定性能更高等。

另外,由於 Restful 風格 API 的盛行,很多 RPC/ 微服務框架開始支持 Restful API,而且通常都是基於 HTTP/1.0/1.1 協議實現的。對於內部的 RPC 調用,使用 HTTP/1.0/1.1 協議代替 TCP 私有協議,可能會帶來一些潛在的性能風險,需要在開放性、標準性以及性能成本上綜合考慮,謹慎選擇。

2.1 理解誤區

2.1.1 I/O 異步服務就是異步

實際上,通信框架基於 NIO 實現,並不意味着服務框架就支持異步服務調用了,兩者本質上不是同一個層面的事情。在 RPC/ 微服務框架中,引入 NIO 帶來的好處是顯而易見的:

  • 所有的 I/O 操作都是非阻塞的,避免有限的 I/O 線程因爲網絡、對方處理慢等原因被阻塞;
  • 多路複用的 Reactor 線程模型:基於 Linux 的 epoll 和 Selector,一個 I/O 線程可以並行處理成百上千條鏈路,解決了傳統同步 I/O 通信線程膨脹的問題。

NIO 只解決了通信層面的異步問題,跟服務調用的異步沒有必然關係,也就是說,即便採用傳統的 BIO 通信,依然可以實現異步服務調用,只不過通信效率和可靠性比較差而已。

對異步服務調用和通信框架的關係進行說明:

用戶發起遠程服務調用之後,經歷層層業務邏輯處理、消息編碼,最終序列化後的消息會被放入到通信框架的消息隊列中。業務線程可以選擇同步等待、也可以選擇直接返回,通過消息隊列的方式實現業務層和通信層的分離是比較成熟、典型的做法,目前主流的 RPC 框架或者 Web 服務器很少直接使用業務線程進行網絡讀寫。

通過上圖可以看出,採用 NIO 還是 BIO 對上層的業務是不可見的,雙方的匯聚點就是消息隊列,在 Java 實現中它通常就是個 Queue。業務線程將消息放入到發送隊列中,可以選擇主動等待或者立即返回,跟通信框架是否是 NIO 沒有任何關係。因此不能認爲 I/O 異步就代表服務調用也是異步的。

2.1.2 服務調用天生就是同步的

RPC/ 微服務框架的一個目標就是讓用戶像調用本地方法一樣調用遠程服務,而不需要關心服務提供者部署在哪裏,以及部署形態(透明化調用)。由於本地方法通常都是同步調用,所以服務調用也應該是同步的。

從服務調用形式上看,主要包含 3 種:

  • one way 方式:只有請求,沒有應答。例如通知消息。
  • 請求 - 響應方式:一請求一應答,這種方式比較常用。
  • 請求 - 響應 - 異步通知方式:客戶端發送請求之後,服務端接收到就立即返回應答,類似 TCP 的 ACK。業務接口層面的響應通過異步通知的方式告知請求方。例如電商類的支付接口、充值繳費接口等。

OneWay 方式的調用示意圖如下:

請求 - 應答模式最常用,例如 HTTP 協議,就是典型的請求 - 應答方式:

請求 - 響應 - 異步通知方式流程:通過流程設計,將執行時間可能較長的服務接口從流程上設計成異步。通常在服務調用時請求方攜帶回調的通知地址,服務端接收到請求之後立即返回應答,表示該請求已經被接收處理。當服務調用真正完成之後,再通過回調地址反向調用服務消費端,將響應通知異步返回。通過接口層的異步,來實現整個服務調用流程的異步,降低了異步調用的開發難度。

One way 方式的服務調用由於不需要返回應答,因此也很容易被設計成異步的:消費者發起遠程服務調用之後,立即返回,不需要同步阻塞等待應答。

對於請求 - 響應方式,一般的觀點都認爲消費者必需要等待服務端響應,拿到結果之後才能返回,否則結果從哪裏取?即便業務線程不阻塞,沒有獲取到結果流程還是無法繼續執行下去。

從邏輯上看,上述觀點沒有問題。但實際上,同步阻塞等待應答並非是唯一的技術選擇,我們也可以利用 Java 的 Future-Listener 機制來實現異步服務調用。從業務角度看,它的效果與同步等待等價,但是從技術層面看,卻是個很大的進步,它可以保證業務線程在不同步阻塞的情況下實現同步等待的效果,服務執行效率更高。

即接口層面請求 - 響應式定義與具體的技術實現無關,選擇同步還是異步服務調用,取決於技術實現。當然,異步通知類接口,從技術實現上做異步更容易些。

2.1.3 異步服務調用性能更高

對於 I/O 密集型,資源不是瓶頸,大部分時間都在同步等應答的場景,異步服務調用會帶來巨大的吞吐量提升,資源使用率也可以提高,更加充分的利用硬件資源提升性能。

另外,對於時延不穩定的接口,例如依賴第三方服務的響應速度、數據庫操作類等,通常異步服務調用也會帶來性能提升。

但是,如果接口調用時延本身都非常小(例如毫秒級),內存計算型,不依賴第三方服務,內部也沒有 I/O 操作,則異步服務調用並不會提升性能。能否提升性能,主要取決於業務的應用場景。

2.2 Restful API 的潛在性能風險

使用 Restful API 可以帶來很多收益:

  • API 接口更加規範和標準,可以通過 Swagger API 規範來描述服務接口,並生成客戶端和服務端代碼;
  • Restful API 可讀性更好,也更容易維護;
  • 服務提供者和消費者基於 API 契約,雙方可以解耦,不需要在客戶端引入 SDK 和類庫的直接依賴,未來的獨立升級也更方便;
  • 內外可以使用同一套 API,非常容易開放給外部或者合作伙伴使用,而不是對內和對外維護兩套不同協議的 API。

通常,對外開放的 API 使用 Restful 是通用的做法,但是在系統內部,例如商品中心和訂單中心,RPC 調用使用 Restful 風格的 API 作爲微服務的 API,卻可能存在性能風險。

2.2.1 HTTP1.X 的性能問題

如果 HTTP 服務器採用同步阻塞 I/O,例如 Tomcat5.5 之前的 BIO 模型,如下圖所示:
 

 

就會存在如下幾個問題:

  • 性能問題:一連接一線程模型導致服務端的併發接入數和系統吞吐量受到極大限制;
  • 可靠性問題:由於 I/O 操作採用同步阻塞模式,當網絡擁塞或者通信對端處理緩慢會導致 I/O 線程被掛住,阻塞時間無法預測;
  • 可維護性問題:I/O 線程數無法有效控制、資源無法有效共享(多線程併發問題),系統可維護性差。

顯然,如果採用的 Restful API 底層使用的 HTTP 協議棧是同步阻塞 I/O,則服務端的處理性能將大打折扣。

2.2.2 異步非阻塞 I/O 的 HTTP 協議棧

如果 HTTP 協議棧採用了異步非阻塞 I/O 模型(例如 Netty、Servlet3.X 版本),則可以解決同步阻塞 I/O 的問題,帶來如下收益:

  • 同一個 I/O 線程可以並行處理多個客戶端鏈接,有效降低了 I/O 線程數量,提升了資源調度利用率;
  • 讀寫操作都是非阻塞的,不會因爲對端處理慢、網絡時延大等導致的 I/O 線程被阻塞。

相比於 TCP 類協議,例如 Thrift, 採用了非阻塞 I/O 的 HTTP/1.X 協議仍然存在性能問題,原因如下所示:

由於 HTTP 協議是無狀態的,客戶端發送請求之後,必須等待接收到服務端響應之後,才能繼續發送請求(非 websocket、pipeline 等模式)。

在某一個時刻,鏈路上只存在單向的消息流,實際上把 TCP 的雙工變成了單工模式。如果服務端響應耗時較大,則單個 HTTP 鏈路的通信性能嚴重下降,只能通過不斷的新建連接來提升 I/O 性能。

但這也會帶來很多副作用,例如句柄數的增加、I/O 線程的負載加重等。顯而易見,修 8 條單向車道的成本遠遠高於修一條雙向 8 車道的成本。

除了無狀態導致的鏈路傳輸性能差之外,HTTP/1.X 還存在如下幾個影響性能的問題:

  • HTTP 客戶端超時之後,由於協議是無狀態的,客戶端無法對請求和響應進行關聯,只能關閉鏈路重連,反覆的建鏈會增加成本開銷和時延(如果客戶端選擇不關閉鏈路,繼續發送新的請求,服務端可能會把上一條客戶端認爲超時的響應返回回去,也可能按照 HTTP 協議規範直接關閉鏈路,無路哪種處理,都會導致鏈路被關閉)。如果採用傳統的 RPC 私有協議,請求和響應可以通過消息 ID 或者會話 ID 做關聯,某條消息的超時並不需要關閉鏈路,只需要丟棄該消息重發即可。
  • HTTP 本身包含文本類型的協議消息頭,佔用一些字節,另外,採用 JSON 類文本的序列化方式,報文相比於傳統的私有 RPC 協議也大很多,降低了傳輸性能。
  • 服務端無法主動推送響應。

如果業務對性能和資源成本要求非常苛刻,在選擇使用基於 HTTP/1.X 的 Restful API 代替私有 RPC API(通常是基於 TCP 的二進制私有協議)時就要三思。反之,如果業務對性能要求較低,或者在硬件成本和開放性、規範性上更看重後者,則使用 Restful API 也無妨。

2.2.3 推薦解決方案

如果選擇 Restful API 作爲內部 RPC 或者微服務的接口協議,則建議使用 HTTP/2.0 協議來承載,它的優點如下:支持雙向流、消息頭壓縮、單 TCP 的多路複用、服務端推送等特性。可以有效解決傳統 HTTP/1.X 協議遇到的問題,效果與 RPC 的 TCP 私有協議接近。

3. gRPC 服務調用

gRPC 的通信協議基於標準的 HTTP/2 設計,主要提供了兩種 RPC 調用方式:

  • 普通 RPC 調用方式,即請求 - 響應模式。
  • 基於 HTTP/2.0 的 streaming 調用方式。

3.1 普通 RPC 調用

普通的 RPC 調用提供了三種實現方式:

  • 同步阻塞式服務調用,通常實現類是 xxxBlockingStub(基於 proto 定義生成)。

  • 異步非阻塞調用,基於 Future-Listener 機制,通常實現類是 xxxFutureStub。

  • 異步非阻塞調用,基於 Reactive 的響應式編程模式,通常實現類是 xxxStub。

3.1.1 同步阻塞式 RPC 調用

同步阻塞式服務調用,代碼示例如下(HelloWorldClient 類):

 blockingStub = GreeterGrpc.newBlockingStub(channel);
 ...
 HelloRequest request = HelloRequest.newBuilder().setName(name).build();
 HelloReply response;
 try {
   response = blockingStub.sayHello(request);
...

創建 GreeterBlockingStub,然後調用它的 sayHello,此時會阻塞調用方線程(例如 main 函數),直到收到服務端響應之後,業務代碼繼續執行,打印響應日誌。

實際上,同步服務調用是由 gRPC 框架的 ClientCalls 在框架層做了封裝,異步發起服務調用之後,同步阻塞調用方線程,直到收到響應再喚醒被阻塞的業務線程,源碼如下(ClientCalls 類):

try {
      ListenableFuture<RespT> responseFuture = futureUnaryCall(call, param);
      while (!responseFuture.isDone()) {
        try {
          executor.waitAndDrain();
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw Status.CANCELLED.withCause(e).asRuntimeException();
        }

判斷當前操作是否完成,如果沒完成,則在 ThreadlessExecutor 中阻塞(阻塞調用方線程,ThreadlessExecutor 不是真正的線程池),代碼如下(ThreadlessExecutor 類):

Runnable runnable = queue.take();
      while (runnable != null) {
        try {
          runnable.run();
        } catch (Throwable t) {
          log.log(Level.WARNING, "Runnable threw exception", t);
        }
        runnable = queue.poll();
      }

調用 queue 的 take 方法會阻塞,直到隊列中有消息 (響應),纔會繼續執行(BlockingQueue 類):

/**
     * Retrieves and removes the head of this queue, waiting if necessary
     * until an element becomes available.
     *
     * @return the head of this queue
     * @throws InterruptedException if interrupted while waiting
     */
    E take() throws InterruptedException;

3.1.2 基於 Future 的異步 RPC 調用

業務調用代碼示例如下(HelloWorldClient 類):

HelloRequest request 
= HelloRequest.newBuilder().setName(name).build();
    try {
   com.google.common.util.concurrent.ListenableFuture<io.grpc.examples.helloworld.HelloReply>
              listenableFuture = futureStub.sayHello(request);
      Futures.addCallback(listenableFuture, new FutureCallback<HelloReply>() {
        @Override
        public void onSuccess(@Nullable HelloReply result) {
          logger.info("Greeting: " + result.getMessage());
        }

調用 GreeterFutureStub 的 sayHello 方法返回的不是應答,而是 ListenableFuture,它繼承自 JDK 的 Future,接口定義如下:

@GwtCompatible
public interface ListenableFuture<V> extends Future<V> 

將 ListenableFuture 加入到 gRPC 的 Future 列表中,創建一個新的 FutureCallback 對象,當 ListenableFuture 獲取到響應之後,gRPC 的 DirectExecutor 線程池會調用新創建的 FutureCallback,執行 onSuccess 或者 onFailure,實現異步回調通知。

接着我們分析下 ListenableFuture 的實現原理,ListenableFuture 的具體實現類是 GrpcFuture,代碼如下(ClientCalls 類):

public static <ReqT, RespT> ListenableFuture<RespT> futureUnaryCall(
      ClientCall<ReqT, RespT> call,
      ReqT param) {
    GrpcFuture<RespT> responseFuture = new GrpcFuture<RespT>(call);
    asyncUnaryRequestCall(call, param, new UnaryStreamToFuture<RespT>(responseFuture), false);
    return responseFuture;
  }

獲取到響應之後,調用 complete 方法(AbstractFuture 類):

private void complete() {
    for (Waiter currentWaiter = clearWaiters();
        currentWaiter != null;
        currentWaiter = currentWaiter.next) {
      currentWaiter.unpark();
    }

將 ListenableFuture 加入到 Future 列表中之後,同步獲取響應(在 gRPC 線程池中阻塞,非業務調用方線程)(Futures 類):

public static <V> V getUninterruptibly(Future<V> future)
      throws ExecutionException {
    boolean interrupted = false;
    try {
      while (true) {
        try {
          return future.get();
        } catch (InterruptedException e) {
          interrupted = true;
        }
      }

獲取到響應之後,回調 callback 的 onSuccess,代碼如下(Futures 類):

 value = getUninterruptibly(future);
        } catch (ExecutionException e) {
          callback.onFailure(e.getCause());
          return;
        } catch (RuntimeException e) {
          callback.onFailure(e);
          return;
        } catch (Error e) {
          callback.onFailure(e);
          return;
        }
        callback.onSuccess(value);

除了將 ListenableFuture 加入到 Futures 中由 gRPC 的線程池執行異步回調,也可以自定義線程池執行異步回調,代碼示例如下(HelloWorldClient 類):

listenableFuture.addListener(new Runnable() {
        @Override
        public void run() {
          try {
            HelloReply response = listenableFuture.get();
            logger.info("Greeting: " + response.getMessage());
          }
          catch(Exception e)
          {
            e.printStackTrace();
          }
        }
      }, Executors.newFixedThreadPool(1));

3.1.3 Reactive 風格異步 RPC 調用

業務調用代碼示例如下(HelloWorldClient 類):

HelloRequest request = HelloRequest.newBuilder().setName(name).build();
    io.grpc.stub.StreamObserver<io.grpc.examples.helloworld.HelloReply> responseObserver =
            new io.grpc.stub.StreamObserver<io.grpc.examples.helloworld.HelloReply>()
            {
                public  void onNext(HelloReply value)
               {
                   logger.info("Greeting: " + value.getMessage());
               }
                public void onError(Throwable t){
                    logger.warning(t.getMessage());
                }
                public void onCompleted(){}
            };
           stub.sayHello(request,responseObserver);

構造響應 StreamObserver,通過響應式編程,處理正常和異常回調,接口定義如下:

將響應 StreamObserver 作爲入參傳遞到異步服務調用中,該方法返回空,程序繼續向下執行,不阻塞當前業務線程,代碼如下所示(GreeterGrpc.GreeterStub):

public void sayHello(io.grpc.examples.helloworld.HelloRequest request,
        io.grpc.stub.StreamObserver<io.grpc.examples.helloworld.HelloReply> responseObserver) {
      asyncUnaryCall(
          getChannel().newCall(METHOD_SAY_HELLO, getCallOptions()), request, responseObserver);
    }

下面分析下基於 Reactive 方式異步調用的代碼實現,把響應 StreamObserver 對象作爲入參傳遞到異步調用中,代碼如下 (ClientCalls 類):

 private static <ReqT, RespT> void asyncUnaryRequestCall(
      ClientCall<ReqT, RespT> call, ReqT param, StreamObserver<RespT> responseObserver,
      boolean streamingResponse) {
    asyncUnaryRequestCall(call, param,
        new StreamObserverToCallListenerAdapter<ReqT, RespT>(call, responseObserver,
            new CallToStreamObserverAdapter<ReqT>(call),
            streamingResponse),
        streamingResponse);
  }

當收到響應消息時,調用 StreamObserver 的 onNext 方法,代碼如下(StreamObserverToCallListenerAdapter 類):

public void onMessage(RespT message) {
      if (firstResponseReceived && !streamingResponse) {
        throw Status.INTERNAL
            .withDescription("More than one responses received for unary or client-streaming call")
            .asRuntimeException();
      }
      firstResponseReceived = true;
      observer.onNext(message);

當 Streaming 關閉時,調用 onCompleted 方法,如下所示(StreamObserverToCallListenerAdapter 類):

 public void onClose(Status status, Metadata trailers) {
      if (status.isOk()) {
        observer.onCompleted();
      } else {
        observer.onError(status.asRuntimeException(trailers));
      }
    }

通過源碼分析可以發現,Reactive 風格的異步調用,相比於 Future 模式,沒有任何同步阻塞點,無論是業務線程還是 gRPC 框架的線程都不會同步等待,相比於 Future 異步模式,Reactive 風格的調用異步化更徹底一些。

3.2 Streaming 模式服務調用

基於 HTTP/2.0,gRPC 提供了三種 streaming 模式:

  • 服務端 streaming
  • 客戶端 streaming
  • 服務端和客戶端雙向 streaming

3.2.1 服務端 streaming

服務端 streaming 模式指客戶端 1 個請求,服務端返回 N 個響應,每個響應可以單獨的返回,它的原理如下所示:

適用的場景主要是客戶端發送單個請求,但是服務端可能返回的是一個響應列表,服務端不想等到所有的響應列表都組裝完成才返回應答給客戶端,而是處理完成一個就返回一個響應,直到服務端關閉 stream,通知客戶端響應全部發送完成。

在實際業務中,應用場景還是比較多的,最典型的如 SP 短信羣發功能,如果不使用 streaming 模式,則原羣發流程如下所示:

採用 gRPC 服務端 streaming 模式之後,流程優化如下:
 

實際上,不同用戶之間的短信下發和通知是獨立的,不需要互相等待,採用 streaming 模式之後,單個用戶的體驗會更好。

服務端 streaming 模式的本質就是如果響應是個列表,列表中的單個響應比較獨立,有些耗時長,有些耗時短,爲了防止快的等慢的,可以處理完一個就返回一個,不需要等所有的都處理完才統一返回響應。可以有效避免客戶端要麼在等待,要麼需要批量處理響應,資源使用不均的問題,也可以壓縮單個響應的時延,端到端提升用戶的體驗(時延敏感型業務)。

像請求 - 響應 - 異步通知類業務,也比較適合使用服務端 streaming 模式。它的 proto 文件定義如下所示:

 rpc ListFeatures(Rectangle) returns (stream Feature) {}

下面一起看下業務示例代碼:

服務端 Sreaming 模式也支持同步阻塞和 Reactive 異步兩種調用方式,以 Reactive 異步爲例,它的代碼實現如下(RouteGuideImplBase 類):

public void listFeatures(io.grpc.examples.routeguide.Rectangle request,
        io.grpc.stub.StreamObserver<io.grpc.examples.routeguide.Feature> responseObserver) {
      asyncUnimplementedUnaryCall(METHOD_LIST_FEATURES, responseObserver);
    }

構造 io.grpc.stub.StreamObserver responseObserver,實現它的三個回調接口,注意由於是服務端 streaming 模式,所以它的 onNext(Feature value) 將會被回調多次,每次都代表一個響應,如果所有的響應都返回,則會調用 onCompleted() 方法。

3.2.2 客戶端 streaming

與服務端 streaming 類似,客戶端發送多個請求,服務端返回一個響應,多用於匯聚和彙總計算場景,proto 文件定義如下:

rpc RecordRoute(stream Point) returns (RouteSummary) {}

業務調用代碼示例如下(RouteGuideClient 類):

StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);
    try {
      // Send numPoints points randomly selected from the features list.
      for (int i = 0; i < numPoints; ++i) {
        int index = random.nextInt(features.size());
        Point point = features.get(index).getLocation();
        info("Visiting point {0}, {1}", RouteGuideUtil.getLatitude(point),
            RouteGuideUtil.getLongitude(point));
        requestObserver.onNext(point);
...

異步服務調用獲取請求 StreamObserver 對象,循環調用 requestObserver.onNext(point),異步發送請求消息到服務端,發送完成之後,調用 requestObserver.onCompleted(),通知服務端所有請求已經發送完成,可以接收服務端的響應了。

響應接收的代碼如下所示:由於響應只有一個,所以 onNext 只會被調用一次(RouteGuideClient 類):

StreamObserver<RouteSummary> responseObserver = new StreamObserver<RouteSummary>() {
      @Override
      public void onNext(RouteSummary summary) {
        info("Finished trip with {0} points. Passed {1} features. "
            + "Travelled {2} meters. It took {3} seconds.", summary.getPointCount(),
            summary.getFeatureCount(), summary.getDistance(), summary.getElapsedTime());
        if (testHelper != null) {
          testHelper.onMessage(summary);
        }

異步服務調用時,將響應 StreamObserver 實例作爲參數傳入,代碼如下:

StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);

3.2.3 雙向 streaming

客戶端發送 N 個請求,服務端返回 N 個或者 M 個響應,利用該特性,可以充分利用 HTTP/2.0 的多路複用功能,在某個時刻,HTTP/2.0 鏈路上可以既有請求也有響應,實現了全雙工通信(對比單行道和雙向車道),示例如下:
 

proto 文件定義如下:

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

業務代碼示例如下(RouteGuideClient 類):

StreamObserver<RouteNote> requestObserver =
        asyncStub.routeChat(new StreamObserver<RouteNote>() {
          @Override
          public void onNext(RouteNote note) {
            info("Got message \"{0}\" at {1}, {2}", note.getMessage(), note.getLocation()
                .getLatitude(), note.getLocation().getLongitude());
            if (testHelper != null) {
              testHelper.onMessage(note);
            }
          }

構造 Streaming 響應對象 StreamObserver並實現 onNext 等接口,由於服務端也是 Streaming 模式,因此響應是多個的,也就是說 onNext 會被調用多次。

通過在循環中調用 requestObserver 的 onNext 方法,發送請求消息,代碼如下所示(RouteGuideClient 類):

for (RouteNote request : requests) {
        info("Sending message \"{0}\" at {1}, {2}", request.getMessage(), request.getLocation()
            .getLatitude(), request.getLocation().getLongitude());
        requestObserver.onNext(request);
      }
    } catch (RuntimeException e) {
      // Cancel RPC
      requestObserver.onError(e);
      throw e;
    }
    // Mark the end of requests
    requestObserver.onCompleted();

requestObserver 的 onNext 方法實際調用了 ClientCall 的消息發送方法,代碼如下(CallToStreamObserverAdapter 類):

private static class CallToStreamObserverAdapter<T> extends ClientCallStreamObserver<T> {
    private boolean frozen;
    private final ClientCall<T, ?> call;
    private Runnable onReadyHandler;
    private boolean autoFlowControlEnabled = true;
    public CallToStreamObserverAdapter(ClientCall<T, ?> call) {
      this.call = call;
    }
    private void freeze() {
      this.frozen = true;
    }
    @Override
    public void onNext(T value) {
      call.sendMessage(value);
    }

對於雙向 Streaming 模式,只支持異步調用方式。

3.3 總結

gRPC 服務調用支持同步和異步方式,同時也支持普通的 RPC 和 streaming 模式,可以最大程度滿足業務的需求。

對於 streaming 模式,可以充分利用 HTTP/2.0 協議的多路複用功能,實現在一條 HTTP 鏈路上並行雙向傳輸數據,有效的解決了 HTTP/1.X 的數據單向傳輸問題,在大幅減少 HTTP 連接的情況下,充分利用單條鏈路的性能,可以媲美傳統的 RPC 私有長連接協議:更少的鏈路、更高的性能:

gRPC 的網絡 I/O 通信基於 Netty 構建,服務調用底層統一使用異步方式,同步調用是在異步的基礎上做了上層封裝。因此,gRPC 的異步化是比較徹底的,對於提升 I/O 密集型業務的吞吐量和可靠性有很大的幫助。

發佈了410 篇原創文章 · 獲贊 1345 · 訪問量 208萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章