gRPC 服務端創建和調用原理解析

gRPC 是一個高性能、開源和通用的 RPC 框架,面向服務端和移動端,基於 HTTP/2 設計。由 Google 開發並開源,語言中立,當前支持 C、Java 和 Go 語言,其中 C 版本支持 C、C++、Node.js、C# 等。

RPC 入門

RPC 框架原理

RPC 框架的目標就是讓遠程服務調用更加簡單、透明,RPC 框架負責屏蔽底層的傳輸方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二進制)和通信細節。服務調用者可以像調用本地接口一樣調用遠程的服務提供者,而不需要關心底層通信細節和調用過程。

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

RPC 框架調用原理

業界主流的 RPC 框架

業界主流的 RPC 框架整體上分爲三類:

  1. 支持多語言的 RPC 框架,比較成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift。

  2. 只支持特定語言的 RPC 框架,例如新浪微博的 Motan。

  3. 支持服務治理等服務化特性的分佈式服務框架,其底層內核仍然是 RPC 框架。例如阿里的 Dubbo。

隨着微服務的發展,基於語言中立性原則構建微服務,逐漸成爲一種主流模式,例如對於後端併發處理要求高的微服務,比較適合採用 GO 語言構建,而對於前端的 Web 界面,則更適合 Java 和 JS。

因此,基於多語言的 RPC 框架來構建微服務,是一種比較好的技術選擇。例如 Netflix,API 服務編排層和後端的微服務之間就採用 gRPC 進行通信。

gRPC 簡介

gRPC 是一個高性能、開源和通用的 RPC 框架,面向服務端和移動端,基於 HTTP/2 設計。

gRPC 概覽

gRPC 是由 Google 開發並開源的一種語言中立的 RPC 框架,當前支持 C、Java 和 Go 語言,其中 C 版本支持 C、C++、Node.js、C# 等。當前 Java 版本最新 Release 版爲 1.5.0,Git 地址如下:

https://github.com/grpc/grpc-java

gRPC 的調用示例如下所示:

gRPC 調用示例

gRPC 特點

  1. 語言中立,支持多種語言。

  2. 基於 IDL 文件定義服務,通過 proto3 工具生成指定語言的數據結構、服務端接口以及客戶端 Stub。

  3. 通信協議基於標準的 HTTP/2 設計,支持雙向流、消息頭壓縮、單 TCP 的多路複用、服務端推送等特性,這些特性使得 gRPC 在移動端設備上更加省電和節省網絡流量。

  4. 序列化支持 PB(Protocol buffer)和 JSON,PB 是一種語言無關的高性能序列化框架,基於 HTTP/2 + PB, 保障了 RPC 調用的高性能。

gRPC 服務端創建

以官方的 helloworld 爲例,介紹 gRPC 服務端創建以及 service 調用流程(採用簡單 RPC 模式)。

服務端創建業務代碼

服務定義如下(helloworld.proto):

服務端創建代碼如下:

其中,服務端接口實現類(GreeterImpl)如下所示:

服務端創建流程

gRPC 服務端創建採用 Build 模式,對底層服務綁定、transportServer 和 NettyServer 的創建和實例化做了封裝和屏蔽,讓服務調用者不用關心 RPC 調用細節,整體上分爲三個過程:

  1. 創建 Netty HTTP/2 服務端;

  2. 將需要調用的服務端接口實現類註冊到內部的 Registry 中,RPC 調用時,可以根據 RPC 請求消息中的服務定義信息查詢到服務接口實現類;

  3. 創建 gRPC Server,它是 gRPC 服務端的抽象,聚合了各種 Listener,用於 RPC 消息的統一調度和處理。

下面我們看下 gRPC 服務端創建流程:

gRPC 服務端創建流程

gRPC 服務端創建關鍵流程分析:

  1. NettyServer 實例創建:gRPC 服務端創建,首先需要初始化 NettyServer,它是 gRPC 基於 Netty4.1 HTTP/2 協議棧之上封裝的 HTTP/2 服務端。NettyServer 實例由 NettyServerBuilder 的 buildTransportServer 方法構建。NettyServer 構建完成之後,監聽指定的 Socket 地址,即可實現基於 HTTP/2 協議的請求消息接入。

  2. 綁定 IDL 定義的服務接口實現類:gRPC 與其它一些 RPC 框架的差異點是服務接口實現類的調用並不是通過動態代理和反射機制,而是通過 proto 工具生成代碼。在服務端啓動時,將服務接口實現類實例註冊到 gRPC 內部的服務註冊中心上。請求消息接入之後,可以根據服務名和方法名,直接調用啓動時註冊的服務實例,而不需要通過反射的方式進行調用,性能更優。

  3. gRPC 服務實例(ServerImpl)構建:ServerImpl 負責整個 gRPC 服務端消息的調度和處理,創建 ServerImpl 實例過程中,會對服務端依賴的對象進行初始化。例如 Netty 的線程池資源、gRPC 的線程池、內部的服務註冊類(InternalHandlerRegistry)等。ServerImpl 初始化完成之後,就可以調用 NettyServer 的 start 方法啓動 HTTP/2 服務端,接收 gRPC 客戶端的服務調用請求。

服務端 service 調用流程

gRPC 的客戶端請求消息由 Netty Http2ConnectionHandler 接入,由 gRPC 負責將 PB 消息(或者 JSON)反序列化爲 POJO 對象,然後通過服務定義查詢到該消息對應的接口實例,發起本地 Java 接口調用,調用完成之後,將響應消息反序列化爲 PB(或者 JSON),通過 HTTP2 Frame 發送給客戶端。

流程並不複雜,但是細節卻比較多,整個 service 調用可以劃分爲如下四個過程:

  1. gRPC 請求消息接入

  2. gRPC 消息頭和消息體處理

  3. 內部的服務路由和調用

  4. 響應消息發送

gRPC 請求消息接入

gRPC 的請求消息由 Netty HTTP/2 協議棧接入,通過 gRPC 註冊的 Http2FrameListener,將解碼成功之後的 HTTP Header 和 HTTP Body 發送到 gRPC 的 NettyServerHandler 中,實現基於 HTTP/2 的 RPC 請求消息接入。

gRPC 請求消息接入流程如下:

gRPC 請求消息接入

關鍵流程解讀如下:

  1. Netty4.1 提供了 HTTP/2 底層協議棧,通過 Http2ConnectionHandler 及其依賴的其它類庫,實現了 HTTP/2 消息的統一接入和處理。通過註冊 Http2FrameListener 監聽器,可以回調接收 HTTP2 協議的消息頭、消息體、優先級、Ping、SETTINGS 等。gRPC 通過 FrameListener 重載 Http2FrameListener 的 onDataRead、onHeadersRead 等方法,將 Netty 的 HTTP/2 消息轉發到 gRPC 的 NettyServerHandler 中。

  2. Netty 的 HTTP/2 協議接入仍然是通過 ChannelHandler 的 CodeC 機制實現,它並不影響 NIO 線程模型。因此,理論上各種協議、以及同一個協議的多個服務端實例可以共用同一個 NIO 線程池(NioEventLoopGroup), 也可以獨佔。在實踐中獨佔模式普遍會存在線程資源佔用過載問題,很容易出現句柄等資源泄漏。在 gRPC 中,爲了避免該問題,默認採用共享池模式創建 NioEventLoopGroup,所有的 gRPC 服務端實例,都統一從 SharedResourceHolder 分配 NioEventLoopGroup 資源,實現 NioEventLoopGroup 的共享。

gRPC 消息頭和消息體處理

gRPC 消息頭的處理入口是 NettyServerHandler 的 onHeadersRead(),處理流程如下所示:

gRPC 消息頭處理

處理流程如下:

  1. 對 HTTP Header 的 Content-Type 校驗,此處必須是“application/grpc”;

  2. 從 HTTP Header 的 URL 中提取接口和方法名,以 HelloWorldServer 爲例,它的 method 爲:“helloworld.Greeter/SayHello”;

  3. 將 Netty 的 HTTP Header 轉換成 gRPC 內部的 Metadata,Metadata 內部維護了一個鍵值對的二維數組 namesAndValues,以及一系列的類型轉換方法:

  4. 創建 NettyServerStream 對象,它持有了 Sink 和 TransportState 類,負責將消息封裝成 GrpcFrameCommand,與底層 Netty 進行交互,實現協議消息的處理。

  5. 創建 NettyServerStream 之後,會觸發 ServerTransportListener 的 streamCreated 方法,在該方法中,主要完成了消息上下文和 gRPC 業務監聽器的創建。

  6. gRPC 上下文創建:CancellableContext 創建之後,支持超時取消。如果 gRPC 客戶端請求消息在 Http Header 中攜帶了“grpc-timeout”,系統在創建 CancellableContext 的同時會啓動一個延時定時任務,延時週期爲超時時間,一旦該定時器成功執行,就會調用 CancellableContext.CancellationListener 的 cancel 方法,發送 CancelServerStreamCommand 指令。

  7. JumpToApplicationThreadServerStreamListener 的創建:它是 ServerImpl 的內部類,從命名上基本可以看出它的用途,即從 ServerStream 跳轉到應用線程中進行服務調用。gRPC 服務端的接口調用主要通過 JumpToApplicationThreadServerStreamListener 的 messageRead 和 halfClosed 方法完成。

  8. 將 NettyServerStream 的 TransportState 緩存到 Netty 的 Http2Stream 中;當處理請求消息體時,可以根據 streamId 獲取到 Http2Stream,進而根據“streamKey”還原 NettyServerStream 的 TransportState,進行後續處理。

gRPC 消息體的處理入口是 NettyServerHandler 的 onDataRead(),處理流程如下所示:

gRPC 消息體處理

消息體處理比較簡單,下面就關鍵技術點進行講解:

  1. 因爲 Netty HTTP/2 協議 Http2FrameListener 分別提供了 onDataRead 和 onHeadersRead 回調方法,所以 gRPC NettyServerHandler 在處理完消息頭之後需要緩存上下文,以便後續處理消息體時使用。

  2. onDataRead 和 onHeadersRead 方法都是由 Netty 的 NIO 線程負責調度,但是在執行 onDataRead 的過程中發生了線程切換,如下所示:

因此,實際上它們是並行 + 交叉串行實行的,後續章節介紹線程模型時會介紹切換原則。

內部的服務路由和調用

內部的服務路由和調用,主要包括如下幾個步驟:

  1. 將請求消息體反序列爲 Java 的 POJO 對象,即 IDL 中定義的請求參數對象;

  2. 根據請求消息頭中的方法名到註冊中心查詢到對應的服務定義信息;

  3. 通過 Java 本地接口調用方式,調用服務端啓動時註冊的 IDL 接口實現類。

具體流程如下所示:

gRPC 服務調用流程

中間的交互流程比較複雜,涉及的類較多,但是關鍵步驟主要有三個:

  1. 解碼:對 HTTP/2 Body 進行應用層解碼,轉換成服務端接口的請求參數。解碼的關鍵就是調用 requestMarshaller.parse(input),將 PB 碼流轉換成 Java 對象。

  2. 路由:根據 URL 中的方法名從內部服務註冊中心查詢到對應的服務實例。路由的關鍵是調用 registry.lookupMethod(methodName) 獲取到 ServerMethodDefinition 對象。

  3. 調用:調用服務端接口實現類的指定方法,實現 RPC 調用。與一些 RPC 框架不同的是,此處調用是 Java 本地接口調用,非反射調用,性能更優,它的實現關鍵是 UnaryRequestMethod.invoke(request, responseObserver) 方法。

響應消息發送

響應消息的發送由 StreamObserver 的 onNext 觸發,流程如下所示:

gRPC 響應消息發送流程

響應消息的發送原理如下:

  1. 分別發送 gRPC HTTP/2 響應消息頭和消息體,由 NettyServerStream 的 Sink 將響應消息封裝成 SendResponseHeadersCommand 和 SendGrpcFrameCommand,加入到 WriteQueue 中。

  2. WriteQueue 通過 Netty 的 NioEventLoop 線程進行消息處理,NioEventLoop 將 SendResponseHeadersCommand 和 SendGrpcFrameCommand 寫入到 Netty 的Channel 中,進而觸發 DefaultChannelPipeline 的write(Object msg,    ChannelPromise promise) 操作。

  3. 響應消息通過 ChannelPipeline 職責鏈進行調度,觸發 NettyServerHandler 的 sendResponseHeaders 和 sendGrpcFrame 方法。調用 Http2ConnectionEncoder 的 writeHeaders 和 writeData 方法,將響應消息通過 Netty 的 HTTP/2 協議棧發送給客戶端。

需要指出的是,請求消息的接收、服務調用以及響應消息發送,多次發生 NIO 線程和應用線程之間的互相切換,以及並行處理。因此上述流程中的一些步驟,並不是嚴格按照圖示中的順序執行的,後續線程模型章節,會做分析和介紹。

服務端創建和 service 調用源碼分析

主要類和功能交互流程

gRPC 請求消息頭處理

gRPC 請求消息頭處理類

gRPC 請求消息頭處理涉及的主要類庫如下:

  1. NettyServerHandler:gRPC Netty Server 的 ChannelHandler 實現,負責 HTTP/2 請求消息和響應消息的處理。

  2. SerializingExecutor: 應用調用線程池,負責 RPC 請求消息的解碼、響應消息編碼以及服務接口的調用等。

  3. MessageDeframer:負責請求 Framer 的解析,主要用於處理 HTTP/2 Header 和 Body 的讀取。

  4. ServerCallHandler:真正的服務接口處理類,提供 onMessage(ReqT request) 和 onHalfClose() 方法,用於服務接口的調用。

gRPC 請求消息體處理和服務調用

請求消息體處理和服務調用

gRPC 響應消息處理

gRPC 響應消息處理

需要說明的是,響應消息的發送由調用服務端接口的應用線程執行,在本示例中,由 SerializingExecutor 進行調用。當請求消息頭被封裝成 SendResponseHeadersCommand 並被插入到 WriteQueue 之後,後續操作由 Netty 的 NIO 線程 NioEventLoop 負責處理。應用線程繼續發送響應消息體,將其封裝成 SendGrpcFrameCommand 並插入到 WriteQueue 隊列中,由 Netty 的 NIO 線程 NioEventLoop 處理。響應消息的發送嚴格按照順序:即先消息頭,後消息體。

源碼分析

瞭解 gRPC 服務端消息接入和 service 調用流程之後,針對主要的流程和類庫,進行源碼分析,以加深對 gRPC 服務端工作原理的瞭解。

Netty 服務端創建

基於 Netty 的 HTTP/2 協議棧,構建 gRPC 服務端,Netty HTTP/2 協議棧初始化代碼如下所示(創建 NettyServerHandler):

創建 gRPC FrameListener,作爲 Http2FrameListener,監聽 HTTP/2 消息的讀取,回調到 NettyServerHandler 中:

將 NettyServerHandler 添加到 Netty 的 ChannelPipeline 中,接收和發送 HTTP/2 消息:

gRPC 服務端請求和響應消息統一由 NettyServerHandler 攔截處理,相關方法如下:

NettyServerHandler 是 gRPC 應用側和底層協議棧的橋接類,負責將原生的 HTTP/2 消息調度到 gRPC 應用側;同時將應用側的消息發送到協議棧。

服務實例創建和綁定

gRPC 服務端啓動時,需要將調用的接口實現類實例註冊到內部的服務註冊中心,用於後續的接口調用,關鍵代碼如下:

服務接口綁定時,由 Proto3 工具生成代碼,重載 bindService() 方法:

service 調用

1、gRPC 消息的接收:

gRPC 消息的接入由 Netty HTTP/2 協議棧回調 gRPC 的 FrameListener,進而調用 NettyServerHandler 的 onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers) 和 onDataRead(int streamId, ByteBuf data, int padding, boolean endOfStream),代碼如下所示:

消息頭和消息體的處理,主要由 MessageDeframer 的 deliver 方法完成,相關代碼如下:

gRPC 請求消息(PB)的解碼由 PrototypeMarshaller 負責,代碼如下:

2、gRPC 響應消息發送:

響應消息分爲兩部分發送:響應消息頭和消息體,分別被封裝成不同的 WriteQueue.AbstractQueuedCommand,插入到 WriteQueue 中。

消息頭封裝代碼:

消息體封裝代碼:

Netty 的 NioEventLoop 將響應消息發送到 ChannelPipeline,最終被 NettyServerHandler 攔截並處理。

響應消息頭處理代碼如下:

響應消息體處理代碼如下:

3、服務接口實例調用:

經過一系列預處理,最終由 ServerCalls 的 ServerCallHandler 調用服務接口實例,代碼如下:

最終的服務實現類調用如下:

服務端線程模型

gRPC 的線程由 Netty 線程 + gRPC 應用線程組成,它們之間的交互和切換比較複雜,下面做下詳細介紹。

Netty Server 線程模型

Netty Server 線程模型

它的工作流程總結如下:

  1. 從主線程池(bossGroup)中隨機選擇一個 Reactor 線程作爲 Acceptor 線程,用於綁定監聽端口,接收客戶端連接;

  2. Acceptor 線程接收客戶端連接請求之後創建新的 SocketChannel,將其註冊到主線程池(bossGroup)的其它 Reactor 線程上,由其負責接入認證、握手等操作;

  3. 步驟 2 完成之後,應用層的鏈路正式建立,將 SocketChannel 從主線程池的 Reactor 線程的多路複用器上摘除,重新註冊到 Sub 線程池(workerGroup)的線程上,用於處理 I/O 的讀寫操作。

Netty Server 使用的 NIO 線程實現是 NioEventLoop,它的職責如下:

  1. 作爲服務端 Acceptor 線程,負責處理客戶端的請求接入;

  2. 作爲客戶端 Connecor 線程,負責註冊監聽連接操作位,用於判斷異步連接結果;

  3. 作爲 I/O 線程,監聽網絡讀操作位,負責從 SocketChannel 中讀取報文;

  4. 作爲 I/O 線程,負責向 SocketChannel 寫入報文發送給對方,如果發生寫半包,會自動註冊監聽寫事件,用於後續繼續發送半包數據,直到數據全部發送完成;

  5. 作爲定時任務線程,可以執行定時任務,例如鏈路空閒檢測和發送心跳消息等;

  6. 作爲線程執行器可以執行普通的任務 Task(Runnable)。

gRPC service 線程模型

gRPC 服務端調度線程爲 SerializingExecutor,它實現了 Executor 和 Runnable 接口,通過外部傳入的 Executor 對象,調度和處理 Runnable,同時內部又維護了一個任務隊列 ConcurrentLinkedQueue,通過 run 方法循環處理隊列中存放的 Runnable 對象,代碼示例如下:

線程調度和切換策略

Netty Server I/O 線程的職責:

  1. gRPC 請求消息的讀取、響應消息的發送

  2. HTTP/2 協議消息的編碼和解碼

  3. NettyServerHandler 的調度

gRPC service 線程的職責:

  1. 將 gRPC 請求消息(PB 碼流)反序列化爲接口的請求參數對象

  2. 將接口響應對象序列化爲 PB 碼流

  3. gRPC 服務端接口實現類調用

gRPC 的線程模型遵循 Netty 的線程分工原則,即:協議層消息的接收和編解碼由 Netty 的 I/O(NioEventLoop) 線程負責;後續應用層的處理由應用線程負責,防止由於應用處理耗時而阻塞 Netty 的 I/O 線程。

基於上述分工原則,在 gRPC 請求消息的接入和響應發送過程中,系統不斷的在 Netty I/O 線程和 gRPC 應用線程之間進行切換。明白了分工原則,也就能夠理解爲什麼要做頻繁的線程切換。

gRPC 線程模型存在的一個缺點,就是在一次 RPC 調用過程中,做了多次 I/O 線程到應用線程之間的切換,頻繁切換會導致性能下降,這也是爲什麼 gRPC 性能比一些基於私有協議構建的 RPC 框架性能低的一個原因。儘管 gRPC 的性能已經比較優異,但是仍有一定的優化空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章