《LINUX教學:gRPC客戶端創建和調用原理解析》要點:
本文介紹了LINUX教學:gRPC客戶端創建和調用原理解析,希望對您有用。如果有疑問,可以聯系我們。
gRPC是在HTTP/2之上實現的RPC框架,HTTP/2是第7層(應用層)協議,它運行在TCP(第4層 - 傳輸層)協議之上,相比于傳統的REST/JSON機制有諸多的優點:
此外,gRPC還提供了很多擴展點,用于對框架進行功能定制和擴展,例如,通過開放負載均衡接口可以無縫的與第三方組件進行集成對接(Zookeeper、域名解析服務、SLB服務等).
一個完整的RPC挪用流程示例如下:
(點擊放年夜圖像)
圖1-1 通用RPC挪用流程
gRPC的RPC調用與上述流程相似,下面我們一起學習下gRPC的客戶端創立和服務調用流程.
以gRPC入門級的helloworld Demo為例,客戶端發起RPC調用的代碼主要包含如下幾部分:
1) 根據hostname和port創立ManagedChannelImpl
2) 根據helloworld.proto文件生成的GreeterGrpc創立客戶端Stub,用來發起RPC調用
3) 使用客戶端Stub(GreeterBlockingStub)發起RPC挪用,獲取響應.
相關示例代碼如下所示:
(點擊放年夜圖像)
gRPC的客戶端調用主要包括基于Netty的HTTP/2客戶端創建、客戶端負載均衡、哀求消息的發送和響應接收處理四個流程.
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)仍然是非阻塞的.
ManagedChannel是對Transport層SocketChannel的抽象,Transport層負責協議消息的序列化和反序列化,以及協議消息的發送和讀取.ManagedChannel將處理后的哀求和響應傳遞給與之相關聯的ClientCall進行上層處理,同時,ManagedChannel提供了對Channel的生命周期管理(鏈路創建、空閑、關閉等).
ManagedChannel提供了接口式的切面ClientInterceptor,它可以攔截RPC客戶端調用,注入擴展點,以及功能定制,便利框架的使用者對gRPC進行功能擴展.
ManagedChannel的主要實現類ManagedChannelImpl創立流程如下:
(點擊放年夜圖像)
圖1-3 ManagedChannelImpl創立流程
流程癥結技術點解讀:
ManagedChannel實例構造完成之后,即可創建ClientCall,發起RPC調用.
完成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創立流程
流程癥結技術點解讀:
ClientCallImpl實例創建完成之后,就可以調用ClientTransport,創建HTTP/2 Client,向gRPC服務端發起遠程服務調用.
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來初始化并啟動客戶端,但是有兩個細節必要注意:
3. WriteQueue創建:Netty的NioSocketChannel初始化并向Selector注冊之后(發起HTTP連接之前),立即由NettyClientHandler創建WriteQueue,用于接收并處理gRPC內部的各種Command,例如鏈路關閉指令、發送Frame指令、發送Ping指令等.
HTTP/2 Client創建完成之后,即可由客戶端根據協商策略發起HTTP/2連接.如果連接創建勝利,后續即可復用該HTTP/2連接,進行RPC調用.
HTTP/2在TCP連接之初通過協商的方式進行通信,只有協商成功,能力進行后續的業務層數據發送和接收.
HTTP/2的版本標識分為兩類:
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.0\r\n\r\nSM\r\n\r\n SETTINGS幀
客戶端序言發送完成之后,可以不需要等待服務端的SETTINGS幀,而直接發送業務哀求Frame.
假如客戶端和服務端已經約定使用HTTP/2,則可以免去101協商和切換流程,直接提議HTTP/2連接,具體流程如下所示:
(點擊放年夜圖像)
圖1-6 HTTP/2 直接連接進程
幾個癥結點:
gRPC支持三種Protocol Negotiator策略:
下面我們以PlaintextNegotiator為例,了解下基于Netty的HTTP/2連接創建流程:
(點擊放年夜圖像)
圖1-7 基于Netty的HTTP/2直連流程
總體上看,RPC的負載均衡策略有兩年夜類:
外部負載均衡模式如下所示:
(點擊放年夜圖像)
圖1-8 代理經銷負載均衡模式示意圖
以代理LB模式為例:RPC客戶端向負載均衡代理發送哀求,負載均衡代理按照指定的路由策略,將哀求消息轉發到后端可用的服務實例上.負載均衡代理負責維護后端可用的服務列表,如果發現某個服務不可用,則將其剔除出路由表.
代理LB模式的優點是客戶端不需要實現負載均衡策略算法,也不需要維護后端的服務列表信息,不直接跟后端的服務進行通信,在做網絡平安邊界隔離時,非常實用.例如通過Ngix做L7層負載均衡,將互聯網前端的流量平安的接入到后端服務中.
代理經銷LB模式通常支持L4(Transport)和L7(Application)層負載均衡,兩者各有優缺點,可以根據RPC的協議特點靈活選擇.L4/L7層負載均衡對應場景如下:
客戶端負載均衡策略由客戶端內置負載均衡能力,通過靜態配置、域名解析服務(例如DNS服務)、訂閱發布(例如Zookeeper服務注冊中心)等方式獲取RPC服務端地址列表,并將地址列表緩存到客戶端內存中.每次RPC調用時,根據客戶端配置的負載均衡策略由負載均衡算法從緩存的服務地址列表中選擇一個服務實例,發起RPC調用.
客戶端負載平衡策略工作原理示例如下:
(點擊放年夜圖像)
圖1-9 客戶端負載平衡策略示意圖
gRPC默認采納客戶端負載均衡策略,同時提供了擴展機制,使用者通過自定義實現NameResolver和LoadBalancer,即可覆蓋gRPC默認的負載均衡策略,實現自定義路由策略的擴展.
gRPC提供的負載平衡策略實現類如下所示:
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的分布式配置服務中心計劃.
gRPC默認基于Netty HTTP/2 + PB進行RPC調用,哀求消息發送流程如下所示:
(點擊放年夜圖像)
圖1-11 gRPC哀求消息發送流程圖
流程癥結技術點解讀:
gRPC客戶端響應消息的接收入口是NettyClientHandler,它的處理流程如下所示:
(點擊放年夜圖像)
圖1-12 gRPC響應消息接管流程圖
流程癥結技術點解讀:
gRPC客戶端調用原理并不復雜,但是代碼卻相對比較繁雜.下面圍繞關鍵的類庫,對主要功能點進行源碼分析.
NettyClientTransport的主要功能如下:
以啟動HTTP/2客戶端為例進行講解:
(點擊放年夜圖像)
根據啟動時配置的HTTP/2協商策略,以NettyClientHandler為參數創立ProtocolNegotiator.Handler.
創建Bootstrap,并設置EventLoopGroup,必要指出的是,此處并沒有使用EventLoopGroup,而是它的一種實現類EventLoop,原因在前文中已經說明,相關代碼示例如下:
(點擊放年夜圖像)
創立WriteQueue并設置到NettyClientHandler中,用于接收內部的各種QueuedCommand,初始化完成之后,發起HTTP/2連接,代碼如下:
(點擊放年夜圖像)
NettyClientHandler繼承自Netty的Http2ConnectionHandler,是gRPC接收和發送HTTP/2消息的關鍵實現類,也是gRPC和Netty的交互橋梁,它的主要功能如下所示:
協議消息的發送:無論是業務哀求消息,還是協議指令消息,都統一封裝成QueuedCommand,由NettyClientHandler攔截并處理,相關代碼如下所示:
(點擊放年夜圖像)
協議消息的接管:NettyClientHandler通過向Http2ConnectionDecoder注冊FrameListener來監聽RPC響應消息和協議指令消息,相關接口如下:
(點擊放年夜圖像)
FrameListener回調NettyClientHandler的相關辦法,實現協議消息的接收和處理:
(點擊放年夜圖像)
需要指出的是,NettyClientHandler并沒有實現所有的回調接口,對于需要特殊處理的幾個辦法進行了重載,例如onDataRead和onHeadersRead.
ProtocolNegotiator用于HTTP/2連接創建的協商,gRPC支持三種策略并有三個實現子類:
(點擊放年夜圖像)
gRPC的ProtocolNegotiator實現類完全遵循HTTP/2相關規范,以PlaintextUpgradeNegotiator為例,通過設置Http2ClientUpgradeCodec,用于101協商和協議進級,相關代碼如下所示:
(點擊放年夜圖像)
LoadBalancer負責客戶端負載均衡,它是個抽象類,gRPC框架的使用者可以通過繼承的方式進行擴展.
gRPC當前已經支持PickFirstBalancer和RoundRobinLoadBalancer兩種負載均衡策略,將來不排除會提供更多的策略.
以RoundRobinLoadBalancer為例,它的工作原理如下:依據PickSubchannelArgs來選擇一個Subchannel:
(點擊放年夜圖像)
再看下Subchannel的選擇算法:
(點擊放年夜圖像)
即通過次序的方式從服務端列表中獲取一個Subchannel.
如果用戶必要定制負載均衡策略,則可以在RPC調用時,使用如下代碼:
(點擊放年夜圖像)
ClientCalls提供了各種RPC調用方式,包括同步、異步、Streaming和Unary方式等,相關辦法如下所示:
(點擊放年夜圖像)
下面一起看下RPC哀求消息的發送和應答接收相關代碼.
哀求調用主要有兩步:哀求Frame構造和Frame發送,哀求Frame構造代碼如下所示:
(點擊放年夜圖像)
使用PB對哀求消息做序列化,生成InputStream,構造哀求Frame:
(點擊放年夜圖像)
Frame發送代碼如下所示:
(點擊放年夜圖像)
NettyClientHandler接收到發送變亂之后,調用Http2ConnectionEncoder將Frame寫入Netty HTTP/2協議棧:
(點擊放年夜圖像)
響應消息的接收入口是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調用,代碼如下:
(點擊放年夜圖像)
李林鋒,華為軟件平臺開放實驗室架構師,有多年Java NIO、平臺中間件、PaaS平臺、API網關設計和開發經驗.精曉Netty、Mina、分???式服務框架、云計算等,目前從事軟件公司的API開放相關的架構和設計工作.
接洽方式:新浪微博 Nettying 微信:Nettying
Email:neu_lilinfeng@sina.com
本文永遠更新鏈接地址:
更多LINUX教程,盡在維易PHP學院專欄。歡迎交流《LINUX教學:gRPC客戶端創建和調用原理解析》!