gRPC的技術棧
(1)遠程服務提供者需要以某種形式提供服務調用相關的信息,包括但不限於服務接口定義、數據結構,以及中間態的服務定義文件,例如gRPC 的 proto 文件、WS-RPC 的WSDL文件定義,甚至也可以是服務端的接口說明文檔。服務調用者需要通過一定的途徑獲取遠程服務調用相關信息,例如服務端接口定義Jar包導入、獲取服務端IDL文件等。
(2)遠程代理對象:服務調用者調用的服務實際是遠程服務的本地代理,對於 Java語言,它的實現就是 JDK 的動態代理,通過動態代理的攔截機制,將本地調用封裝成遠程服務調用。
(3)通信:RPC框架與具體的協議無關,例如Spring的遠程調用支持HTTP Invoke、RMI Invoke,MessagePack使用的是私有的二進制壓縮協議。
(4)序列化:遠程通信需要將對象轉換成二進制碼流進行網絡傳輸,不同的序列化框架支持的數據類型、數據包大小、異常類型及性能等都不同。不同的 RPC框架的應用場景不同,因此技術選擇也存在很大差異。一些做得比較好的 RPC框架,支持多種序列化方式,有的甚至支持用戶自定義
相比其他開源的RPC框架,gRPC有如下幾個特點
(1)支持多種語言。
(2)基於 IDL 文件定義服務,通過 proto3 工具生成指定語言的數據結構、服務端接口及客戶端Stub。
(3)通信協議基於標准的HTTP/2設計,支持雙向流、消息頭壓縮、單TCP的多路復用、服務端推送等特性。
(4)序列化支持Protocol buffer和JSON,Protocol buffer是一種語言無關的高性能序列化框架,基於“HTTP/2+Protocol buffer”,保障了RPC調用的高性能。
gRPC 底層的通信框架基於Netty 4.1構建,通過集成Netty的HTTP/2協議棧,支持雙向流、消息頭壓縮、單TCP的多路復用、服務端推送等特性,傳統的HTTP/1.0或者HTTP/1.1是無狀態的,創建HTTP連接之后,客戶端發送請求消息,然后等待服務端響應,接收到服務端響應之后,客戶端接着發送后續的請求消息,服務端再返回響應,周而復始。請求和響應消息都是成對出現的,采用的是“一請求對應一響應”模式。在某個時刻,一個HTTP連接上只能單向地處理一個消息,就像單行道,一個消息處理得慢,很容易導致后續消息阻塞。
采用該模式主要存在如下幾個缺點。
(1)單個連接的通信效率不高,無法多路復用。
(2)一個請求消息處理得慢,很容易阻塞后續其他請求消息。
(3)如果客戶端讀取響應超時,由於消息是無狀態的,只能關閉連接,重建連接之后再發送請求。如果頻繁地發生客戶端超時,就會發生大量的HTTP連接斷連和重連,如果采用HTTPS,SSL鏈路的重建成本很高,很容易導致服務端因負載過重而宕機。
(4)為了解決單個HTTP連接性能不足問題,只能創建一個大的連接池,在大規模集群組網場景下,HTTP連接數會非常多,額外占用大量句柄資源。
采用HTTP/2之后,可以實現多路復用,客戶端可以連續發送多個請求,服務端也可以返回一個或者多個響應,而且還可以主動推送數據到服務端,實現雙向通信,達到TCP協議的通信效果。
gRPC通過對Netty HTTP/2的封裝,向用戶屏蔽底層RPC通信的協議細節。
Netty HTTP/2服務端創建的主要流程:
1.NettyServer 具體管理 HTTP/2協議棧的啟動和停止,以及 HTTP/2協議相關的各種參數。
2.NettyServer 的 start 方法會創建 NettyServerTransport,通過NettyServerTransport來創建gRPC的Netty HTTP/2 ChannelHandler實例,並加入ChannelPipeline。
(NettyServerHandler主要負責HTTP/2消息的處理,例如HTTP/2請求消息體和消息頭的讀取、Frame消息的發送、Stream狀態消息的處理等)
3.創建 ProtocolNegotiator實例,用於 HTTP/2連接創建的協商。gRPC支持三種協商策略,分別是 PlaintextNegotiator、PlaintextUpgradeNegotiator和 TlsNegotiator。其中PlaintextUpgradeNegotiator通過設置Http2ClientUpgradeCodec,用於協議升級。
4.創建ServerBootstrap、bossGroup和workerGroup,啟動HTTP/2服務端,在NettyServer的start方法里
gRPC服務端的請求消息由Netty HTTP/2協議棧負責接入,gRPC通過繼承Http2FrameAdapter,將自定義的FrameListener添加到Netty的Http2ConnectionDecoder中,在HTTP/2請求消息頭和消息體被解析成功之后,回調 gRPC的FrameListener,接收並處理 HTTP/2請求消息,將 Netty的請求消息體 ByteBuf轉換成 gRPC內部的NettyReadableBuffer對象,調用deframe方法,完成請求消息體的解碼.
服務端發送HTTP/2 響應消息原理:
gRPC 服務端通過將響應消息封裝成WriteQueue.AbstractQueuedCommand,異步寫入WriteQueue,然后調用 WriteQueue 的 scheduleFlush 操作,將響應消息發送命令放到NioEventLoop 中執行,NioEventLoop調用 channel.write 方法將其發送到ChannelPipeline,由 gRPC 的NettyServerHandler 攔截 write 方法,按照命令的分類進行處理,最后調用 Netty Http2ConnectionEncoder的write方法完成響應消息的發送。
1.通過NettyServerStream的Sink類對需要發送的HTTP/2響應消息進行Task封裝,實現消息發送的異步化.
2.WriteQueue 將任務投遞到對應 Channel 的 NioEventLoop 線程異步執行消息發送操作,注意WriteQueue的scheduleFlush操作是在業務線程中的,會有多個業務線程,所以這里要加鎖,保證多線程的同步性,這里采用了無鎖化操作CAS,代碼如下
3.NioEventLoop線程循環處理待發送的響應消息,調用 Channel的write方法,將消息發送到ChannelPipeline,由gRPC ChannelHandler攔截處理
4.gRPC的NettyServerHandler攔截write方法,按照命令的類型進行分類處理,如果是 SendGrpcFrameCommand 和SendResponseHeadersCommand,則調用 Netty 的Http2ConnectionEncoder完成HTTP/2消息的發送。
即HTTP/2服務端創建、HTTP/2請求消息的接入和響應發送都由Netty NioEventLoop線程負責,gRPC消息的序列化和反序列化業務服務接口的調用由 gRPC的SerializingExecutor線程池負責。
Netty NIO線程和gRPC的SerializingExecutor之間沒有映射關系(M:N),當線程數量比較多時,鎖競爭會非常激烈,可以采用 I/O線程和 gRPC服務調用線程綁定的方式,降低出現鎖競爭的概率,提升並發性能,通過線程綁定技術降低鎖競爭的概率(1:1).