首頁 > 軟體

gRPC用戶端建立和呼叫原理解析

2020-06-16 17:07:57

1. gRPC用戶端建立流程

1.1. 背景

gRPC是在HTTP/2之上實現的RPC框架,HTTP/2是第7層(應用層)協定,它執行在TCP(第4層 - 傳輸層)協定之上,相比於傳統的REST/JSON機制有諸多的優點:

  • 基於HTTP/2之上的二進位制協定(Protobuf序列化機制)
  • 一個連線上可以多路複用,並行處理多個請求和響應
  • 多種語言的類庫實現
  • 服務定義檔案和自動程式碼生成(.proto檔案和Protobuf編譯工具)

此外,gRPC還提供了很多擴充套件點,用於對框架進行功能客製化和擴充套件,例如,通過開放負載均衡介面可以無縫的與第三方元件進行整合對接(Zookeeper、域名解析服務、SLB服務等)。

一個完整的RPC呼叫流程範例如下:

(點選放大影象)

圖1-1 通用RPC呼叫流程

gRPC的RPC呼叫與上述流程相似,下面我們一起學習下gRPC的用戶端建立和服務呼叫流程。

1.2. 業務程式碼範例

以gRPC入門級的helloworld Demo為例,用戶端發起RPC呼叫的程式碼主要包括如下幾部分:

1) 根據hostname和port建立ManagedChannelImpl

2) 根據helloworld.proto檔案生成的GreeterGrpc建立用戶端Stub,用來發起RPC呼叫

3) 使用用戶端Stub(GreeterBlockingStub)發起RPC呼叫,獲取響應。

相關範例程式碼如下所示:

(點選放大影象)

1.3. RPC呼叫流程

gRPC的用戶端呼叫主要包括基於Netty的HTTP/2用戶端建立、用戶端負載均衡、請求訊息的傳送和響應接收處理四個流程。

1.3.1. 用戶端呼叫總體流程

gRPC的用戶端呼叫總體流程如下圖所示:

(點選放大影象)

圖1-2 gRPC總體呼叫流程

gRPC的用戶端呼叫流程如下:

1) 用戶端Stub(GreeterBlockingStub)呼叫sayHello(request),發起RPC呼叫

2) 通過DnsNameResolver進行域名解析,獲取伺服器端的地址資訊(列表),隨後使用預設的LoadBalancer策略,選擇一個具體的gRPC伺服器端範例

3) 如果與路由選中的伺服器端之間沒有可用的連線,則建立NettyClientTransport和NettyClientHandler,發起HTTP/2連線

4) 對請求訊息使用PB(Protobuf)做序列化,通過HTTP/2 Stream傳送給gRPC伺服器端

5) 接收到伺服器端響應之後,使用PB(Protobuf)做反序列化

6) 回撥GrpcFuture的set(Response)方法,喚醒阻塞的用戶端呼叫執行緒,獲取RPC響應

需要指出的是,用戶端同步阻塞RPC呼叫阻塞的是呼叫方執行緒(通常是業務執行緒),底層Transport的I/O執行緒(Netty的NioEventLoop)仍然是非阻塞的。

1.3.2. ManagedChannel建立流程

ManagedChannel是對Transport層SocketChannel的抽象,Transport層負責協定訊息的序列化和反序列化,以及協定訊息的傳送和讀取。ManagedChannel將處理後的請求和響應傳遞給與之相關聯的ClientCall進行上層處理,同時,ManagedChannel提供了對Channel的生命週期管理(鏈路建立、空閒、關閉等)。

ManagedChannel提供了介面式的切面ClientInterceptor,它可以攔截RPC用戶端呼叫,注入擴充套件點,以及功能客製化,方便框架的使用者對gRPC進行功能擴充套件。

ManagedChannel的主要實現類ManagedChannelImpl建立流程如下:

(點選放大影象)

圖1-3 ManagedChannelImpl建立流程

流程關鍵技術點解讀:

  1. 使用builder模式建立ManagedChannelBuilder實現類NettyChannelBuilder,NettyChannelBuilder提供了buildTransportFactory工廠方法建立NettyTransportFactory,最終用於建立NettyClientTransport。
  2. 初始化HTTP/2連線方式:採用plaintext協商模式還是預設的TLS模式,HTTP/2的連線有兩種模式:h2(基於TLS之上構建的HTTP/2)和h2c(直接在TCP之上構建的HTTP/2)。
  3. 建立NameResolver.Factory工廠類,用於伺服器端URI的解析,gRPC預設採用DNS域名解析方式。

ManagedChannel範例構造完成之後,即可建立ClientCall,發起RPC呼叫。

1.3.3. ClientCall建立流程

完成ManagedChannelImpl建立之後,由ManagedChannelImpl發起建立一個新的ClientCall範例。ClientCall的用途是業務應用層的訊息排程和處理,它的典型用法如下:

 call = channel.newCall(unaryMethod, callOptions);
 call.start(listener, headers);
 call.sendMessage(message);
 call.halfClose();
 call.request(1);
 // wait for listener.onMessage()

ClientCall範例的建立流程如下所示:

(點選放大影象)

圖1-4 ClientCallImpl建立流程

流程關鍵技術點解讀:

  1. ClientCallImpl的主要構造引數是MethodDescriptor和CallOptions,其中MethodDescriptor存放了需要呼叫RPC服務的介面名、方法名、服務呼叫的方式(例如UNARY型別)以及請求和響應的序列化和反序列化實現類。CallOptions則存放了RPC呼叫的其它附加資訊,例如超時時間、鑑權資訊、訊息長度限制和執行用戶端呼叫的執行緒池等。
  2. 設定壓縮和解壓縮的註冊類(CompressorRegistry和DecompressorRegistry),以便可以按照指定的壓縮演算法對HTTP/2訊息做壓縮和解壓縮。

ClientCallImpl範例建立完成之後,就可以呼叫ClientTransport,建立HTTP/2 Client,向gRPC伺服器端發起遠端服務呼叫。

1.3.4. 基於Netty的HTTP/2 Client建立流程

gRPC用戶端底層基於Netty4.1的HTTP/2協定棧框架構建,以便可以使用HTTP/2協定來承載RPC訊息,在滿足標準化規範的前提下,提升通訊效能。

gRPC HTTP/2協定棧(用戶端)的關鍵實現是NettyClientTransport和NettyClientHandler,用戶端初始化流程如下所示:

(點選放大影象)

圖1-5 HTTP/2 Client建立流程

流程關鍵技術點解讀:

1.NettyClientHandler的建立:級聯建立Netty的Http2FrameReader、Http2FrameWriter和Http2Connection,用於構建基於Netty的gRPC HTTP/2用戶端協定棧。

2.HTTP/2 Client啟動:仍然基於Netty的Bootstrap來初始化並啟動用戶端,但是有兩個細節需要注意:

  • NettyClientHandler(實際被包裝成ProtocolNegotiator.Handler,用於HTTP/2的握手協商)建立之後,不是由傳統的ChannelInitializer在初始化Channel時將NettyClientHandler加入到pipeline中,而是直接通過Bootstrap的handler方法直接加入到pipeline中,以便可以立即接收傳送任務。
  • 用戶端使用的work執行緒組並非通常意義的EventLoopGroup,而是一個EventLoop:即HTTP/2用戶端使用的work執行緒並非一組執行緒(預設執行緒數為CPU核心 * 2),而是一個EventLoop執行緒。這個其實也很容易理解,一個NioEventLoop執行緒可以同時處理多個HTTP/2用戶端連線,它是多路複用的,對於單個HTTP/2用戶端,如果預設獨佔一個work執行緒組,將造成極大的資源浪費,同時也可能會導致控制代碼溢位(並行啟動大量HTTP/2用戶端)。

3. WriteQueue建立:Netty的NioSocketChannel初始化並向Selector註冊之後(發起HTTP連線之前),立即由NettyClientHandler建立WriteQueue,用於接收並處理gRPC內部的各種Command,例如鏈路關閉指令、傳送Frame指令、傳送Ping指令等。

HTTP/2 Client建立完成之後,即可由用戶端根據協商策略發起HTTP/2連線。如果連線建立成功,後續即可複用該HTTP/2連線,進行RPC呼叫。

1.3.5. HTTP/2連線建立流程

HTTP/2在TCP連線之初通過協商的方式進行通訊,只有協商成功,才能進行後續的業務層資料傳送和接收。

HTTP/2的版本標識分為兩類:

  • 基於TLS之上構架的HTTP/2, 即HTTPS,使用h2表示(ALPN):0x68與0x32
  • 直接在TCP之上構建的HTTP/2,即HTTP,使用h2c表示

HTTP/2連線建立,分為兩種:通過協商升級協定方式和直接連線方式。

假如不知道伺服器端是否支援HTTP/2,可以先使用HTTP/1.1進行協商,用戶端傳送協商請求訊息(只含訊息頭),報文範例如下:

GET / HTTP/1.1
Host: 127.0.0.1
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

伺服器端接收到協商請求之後,如果不支援HTTP/2,則直接按照HTTP/1.1響應返回,雙方通過HTTP/1.1進行通訊,報文範例如下:

HTTP/1.1 200 OK
Content-Length: 28
Content-Type: text/css

body...

如果伺服器端支援HTTP/2,則協商成功,返回101結果碼,通知用戶端一起升級到HTTP/2進行通訊,範例報文如下:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c

[ HTTP/2 connection...

101響應之後,服務需要傳送SETTINGS幀作為連線序言,用戶端接收到101響應之後,也必須傳送一個序言作為回應,範例如下:

PRI * HTTP/2.0rnrnSMrnrn
SETTINGS幀

用戶端序言傳送完成之後,可以不需要等待伺服器端的SETTINGS幀,而直接傳送業務請求Frame。

假如用戶端和伺服器端已經約定使用HTTP/2,則可以免去101協商和切換流程,直接發起HTTP/2連線,具體流程如下所示:

(點選放大影象)

圖1-6 HTTP/2 直接連線過程

幾個關鍵點:

  • 如果已經明確知道伺服器端支援HTTP/2,則可免去通過HTTP/1.1 101協定切換方式進行升級。TCP連線建立之後即可傳送序言,否則只能在接收到伺服器端101響應之後傳送序言
  • 針對一個連線,伺服器端第一個要傳送的幀必須是SETTINGS幀,連線序言所包含的SETTINGS幀可以為空
  • 用戶端可以在傳送完序言之後傳送應用幀資料,不用等待來自伺服器端的序言SETTINGS幀

gRPC支援三種Protocol Negotiator策略:

  • PlaintextNegotiator:明確伺服器端支援HTTP/2,採用HTTP直接連線的方式與伺服器端建立HTTP/2連線,省去101協定切換過程。
  • PlaintextUpgradeNegotiator:不清楚伺服器端是否支援HTTP/2,採用HTTP/1.1協商模式切換升級到HTTP/2。
  • TlsNegotiator:在TLS之上構建HTTP/2,協商採用ALPN擴充套件協定,以”h2”作為協定識別符號。

下面我們以PlaintextNegotiator為例,了解下基於Netty的HTTP/2連線建立流程:

(點選放大影象)

圖1-7 基於Netty的HTTP/2直連流程

1.3.6. 負載均衡策略

總體上看,RPC的負載均衡策略有兩大類:

  • 伺服器端負載均衡(例如代理模式、外部負載均衡服務)
  • 用戶端負載均衡(內建負載均衡策略和演算法,用戶端實現)

外部負載均衡模式如下所示:

(點選放大影象)

圖1-8 代理負載均衡模式示意圖

以代理LB模式為例:RPC用戶端向負載均衡代理傳送請求,負載均衡代理按照指定的路由策略,將請求訊息轉發到後端可用的服務範例上。負載均衡代理負責維護後端可用的服務列表,如果發現某個服務不可用,則將其剔除出路由表。

代理LB模式的優點是用戶端不需要實現負載均衡策略演算法,也不需要維護後端的服務列表資訊,不直接跟後端的服務進行通訊,在做網路安全邊界隔離時,非常實用。例如通過Ngix做L7層負載均衡,將網際網路前端的流量安全的接入到後端服務中。

代理LB模式通常支援L4(Transport)和L7(Application)層負載均衡,兩者各有優缺點,可以根據RPC的協定特點靈活選擇。L4/L7層負載均衡對應場景如下:

  1. L4層:對時延要求苛刻、資源損耗少、RPC本身採用私有TCP協定
  2. L7層:有對談狀態的連線、HTTP協定簇(例如Restful)

用戶端負載均衡策略由用戶端內建負載均衡能力,通過靜態設定、域名解析服務(例如DNS服務)、訂閱發布(例如Zookeeper服務註冊中心)等方式獲取RPC伺服器端地址列表,並將地址列表快取到用戶端記憶體中。每次RPC呼叫時,根據用戶端設定的負載均衡策略由負載均衡演算法從快取的服務地址列表中選擇一個服務範例,發起RPC呼叫。

用戶端負載均衡策略工作原理範例如下:

(點選放大影象)

圖1-9 用戶端負載均衡策略示意圖

gRPC預設採用用戶端負載均衡策略,同時提供了擴充套件機制,使用者通過自定義實現NameResolver和LoadBalancer,即可覆蓋gRPC預設的負載均衡策略,實現自定義路由策略的擴充套件。

gRPC提供的負載均衡策略實現類如下所示:

  • PickFirstBalancer:無負載均衡能力,即使有多個伺服器端地址可用,也只選擇第一個地址。
  • RoundRobinLoadBalancer:“RoundRobin”負載均衡策略。

gRPC負載均衡流程如下所示:

(點選放大影象)

圖1-10 gRPC用戶端負載均衡流程圖

流程關鍵技術點解讀:

1.負載均衡功能模組的輸入是用戶端指定的hostName、需要呼叫的介面名和方法名等引數,輸出是執行負載均衡演算法後獲得的NettyClientTransport。通過NettyClientTransport可以建立基於Netty HTTP/2的gRPC用戶端,發起RPC呼叫。

2.gRPC系統預設提供的是DnsNameResolver,它通過InetAddress.getAllByName(host)獲取指定host的IP地址列表(本地DNS服務)。

對於擴充套件者而言,可以繼承NameResolver實現自定義的地址解析服務,例如使用Zookeeper替換DnsNameResolver,把Zookeeper作為動態的服務地址設定中心,它的虛擬碼範例如下:

第一步:繼承NameResolver,實現start(Listener listener)方法:

void start(Listener listener)
{
     //獲取ZooKeeper地址,並連線
     //建立Watcher,並實現process(WatchedEvent event),監聽地址變更
 //根據介面名和方法名,呼叫getChildren方法,獲取發布該服務的地址列表
//將地址列表加到List中
// 呼叫NameResolver.Listener.onAddresses(),通知地址解析完成

第二步:建立ManagedChannelBuilder時,指定Target的地址為Zookeeper伺服器端地址,同時設定nameResolver為Zookeeper NameResolver,範例程式碼如下所示:

 this(ManagedChannelBuilder.forTarget(zookeeperAddr)
        .loadBalancerFactory(RoundRobinLoadBalancerFactory.getInstance())
        .nameResolverFactory(new ZookeeperNameResolverProvider())
        .usePlaintext(false));

3. LoadBalancer負責從nameResolver中解析獲得的伺服器端URL中按照指定路由策略,選擇一個目標伺服器端地址,並建立ClientTransport。同樣,可以通過覆蓋handleResolvedAddressGroups實現自定義負載均衡策略。

通過LoadBalancer + NameResolver,可以實現靈活的負載均衡策略擴充套件。例如基於Zookeeper、etcd的分散式設定服務中心方案。

1.3.7. RPC請求訊息傳送流程

gRPC預設基於Netty HTTP/2 + PB進行RPC呼叫,請求訊息傳送流程如下所示:

(點選放大影象)

圖1-11 gRPC請求訊息傳送流程圖

流程關鍵技術點解讀:

  1. ClientCallImpl的sendMessage呼叫,主要完成了請求物件的序列化(基於PB)、HTTP/2 Frame的初始化。
  2. ClientCallImpl的halfClose呼叫將用戶端準備就緒的請求Frame封裝成自定義的SendGrpcFrameCommand,寫入到WriteQueue中。
  3. WriteQueue執行flush()將SendGrpcFrameCommand寫入到Netty的Channel中,呼叫Channel的write方法,被NettyClientHandler攔截到,由NettyClientHandler負責具體的傳送操作。
  4. NettyClientHandler呼叫Http2ConnectionEncoder的writeData方法,將Frame寫入到HTTP/2 Stream中,完成請求訊息的傳送。

1.3.8. RPC響應接收和處理流程

gRPC用戶端響應訊息的接收入口是NettyClientHandler,它的處理流程如下所示:

(點選放大影象)

圖1-12 gRPC響應訊息接收流程圖

流程關鍵技術點解讀:

  1. NettyClientHandler的onHeadersRead(int streamId, Http2Headers headers, boolean endStream)方法會被呼叫兩次,根據endStream判斷是否是Stream結尾。
  2. 請求和響應的關聯:根據streamId可以關聯同一個HTTP/2 Stream,將NettyClientStream快取到Stream中,用戶端就可以在接收到響應訊息頭或訊息體時還原出NettyClientStream,進行後續處理。
  3. RPC用戶端呼叫執行緒的阻塞和喚醒使用到了GrpcFuture的wait和notify機制,來實現用戶端呼叫執行緒的同步阻塞和喚醒。
  4. 用戶端和伺服器端的HTTP/2 Header和Data Frame解析共用同一個方法,即MessageDeframer的deliver()。

2. 用戶端原始碼分析

gRPC用戶端呼叫原理並不複雜,但是程式碼卻相對比較繁雜。下面圍繞關鍵的類庫,對主要功能點進行原始碼分析。

2.1. NettyClientTransport功能和原始碼分析

NettyClientTransport的主要功能如下:

  • 通過start(Listener transportListener) 建立HTTP/2 Client,並連線gRPC伺服器端
  • 通過newStream(MethodDescriptor<?, ?> method, Metadata headers, CallOptions callOptions) 建立ClientStream
  • 通過shutdown() 關閉底層的HTTP/2連線

以啟動HTTP/2用戶端為例進行講解:

(點選放大影象)

根據啟動時設定的HTTP/2協商策略,以NettyClientHandler為引數建立ProtocolNegotiator.Handler。

建立Bootstrap,並設定EventLoopGroup,需要指出的是,此處並沒有使用EventLoopGroup,而是它的一種實現類EventLoop,原因在前文中已經說明,相關程式碼範例如下:

(點選放大影象)

建立WriteQueue並設定到NettyClientHandler中,用於接收內部的各種QueuedCommand,初始化完成之後,發起HTTP/2連線,程式碼如下:

(點選放大影象)

2.2. NettyClientHandler功能和原始碼分析

NettyClientHandler繼承自Netty的Http2ConnectionHandler,是gRPC接收和傳送HTTP/2訊息的關鍵實現類,也是gRPC和Netty的互動橋樑,它的主要功能如下所示:

  • 傳送各種協定訊息給gRPC伺服器端
  • 接收gRPC伺服器端返回的應答訊息頭、訊息體和其它協定訊息
  • 處理HTTP/2協定相關的指令,例如StreamError、ConnectionError等。

協定訊息的傳送:無論是業務請求訊息,還是協定指令訊息,都統一封裝成QueuedCommand,由NettyClientHandler攔截並處理,相關程式碼如下所示:

(點選放大影象)

協定訊息的接收:NettyClientHandler通過向Http2ConnectionDecoder註冊FrameListener來監聽RPC響應訊息和協定指令訊息,相關介面如下:

(點選放大影象)

FrameListener回撥NettyClientHandler的相關方法,實現協定訊息的接收和處理:

(點選放大影象)

需要指出的是,NettyClientHandler並沒有實現所有的回撥介面,對於需要特殊處理的幾個方法進行了過載,例如onDataRead和onHeadersRead。

2.3. ProtocolNegotiator功能和原始碼分析

ProtocolNegotiator用於HTTP/2連線建立的協商,gRPC支援三種策略並有三個實現子類:

(點選放大影象)

gRPC的ProtocolNegotiator實現類完全遵循HTTP/2相關規範,以PlaintextUpgradeNegotiator為例,通過設定Http2ClientUpgradeCodec,用於101協商和協定升級,相關程式碼如下所示:

(點選放大影象)

2.4. LoadBalancer功能和原始碼分析

LoadBalancer負責用戶端負載均衡,它是個抽象類,gRPC框架的使用者可以通過繼承的方式進行擴充套件。

gRPC當前已經支援PickFirstBalancer和RoundRobinLoadBalancer兩種負載均衡策略,未來不排除會提供更多的策略。

以RoundRobinLoadBalancer為例,它的工作原理如下:根據PickSubchannelArgs來選擇一個Subchannel:

(點選放大影象)

再看下Subchannel的選擇演算法:

(點選放大影象)

即通過順序的方式從伺服器端列表中獲取一個Subchannel。

如果使用者需要客製化負載均衡策略,則可以在RPC呼叫時,使用如下程式碼:

(點選放大影象)

2.5. ClientCalls功能和原始碼分析

ClientCalls提供了各種RPC呼叫方式,包括同步、非同步、Streaming和Unary方式等,相關方法如下所示:

(點選放大影象)

下面一起看下RPC請求訊息的傳送和應答接收相關程式碼。

2.5.1. RPC請求呼叫原始碼分析

請求呼叫主要有兩步:請求Frame構造和Frame傳送,請求Frame構造程式碼如下所示:

(點選放大影象)

使用PB對請求訊息做序列化,生成InputStream,構造請求Frame:

(點選放大影象)

Frame傳送程式碼如下所示:

(點選放大影象)

NettyClientHandler接收到傳送事件之後,呼叫Http2ConnectionEncoder將Frame寫入Netty HTTP/2協定棧:

(點選放大影象)

2.5.2. RPC響應接收和處理原始碼分析

響應訊息的接收入口是NettyClientHandler,包括HTTP/2 Header和HTTP/2 DATA Frame兩部分,程式碼如下:

(點選放大影象)

如果引數endStream為True,說明Stream已經結束,呼叫transportTrailersReceived,通知Listener close,程式碼如下所示:

(點選放大影象)

讀取到HTTP/2 DATA Frame之後,呼叫MessageDeframer的deliver對Frame進行解析,程式碼如下:

(點選放大影象)

將Frame 轉換成InputStream之後,通知ClientStreamListenerImpl,呼叫messageRead(final InputStream message),將InputStream反序列化為響應物件,相關程式碼如下所示:

(點選放大影象)

當接收到endOfStream之後,通知ClientStreamListenerImpl,呼叫它的close方法,如下所示:

(點選放大影象)

最終呼叫UnaryStreamToFuture的onClose方法,set響應物件,喚醒阻塞的呼叫方執行緒,完成RPC呼叫,程式碼如下:

(點選放大影象)

3. 作者簡介

李林鋒,華為軟體平台開放實驗室架構師,有多年Java NIO、平台中間??、PaaS平台、API閘道器設計和開發經驗。精通Netty、Mina、分散式服務架構、雲端計算等,目前從事軟體公司的API開放相關的架構和設計工作。

聯絡方式:新浪微博 Nettying 微信:Nettying

Email:neu_lilinfeng@sina.com

本文永久更新連結地址http://www.linuxidc.com/Linux/2017-09/146887.htm


IT145.com E-mail:sddin#qq.com