一、gRPC介紹
gRPC 是在 HTTP/2 之上實現的 RPC 框架,HTTP/2 是第 7 層(應用層)協議,它運行在 TCP(第 4 層 - 傳輸層)協議之上,相比于傳統的 REST/JSON 機制有諸多的優點:
- 基于 HTTP/2 之上的二進制協議(Protobuf 序列化機制);
- 一個連接上可以多路復用,并發處理多個請求和響應;
- 多種語言的類庫實現;
- 服務定義文件和自動代碼生成(.proto 文件和 Protobuf 編譯工具)。
此外,gRPC 還提供了很多擴展點,用于對框架進行功能定制和擴展,例如,通過開放負載均衡接口可以無縫的與第三方組件進行集成對接(Zookeeper、域名解析服務、SLB 服務等)。
二、gRPC服務調用原理
一個完整的 RPC 調用流程示例如下:
RPC 請求消息發送流程
gRPC 默認基于 Netty HTTP/2 + PB 進行 RPC 調用,請求消息發送流程如下所示:
RPC 響應接收和處理流程
gRPC 客戶端響應消息的接收入口是 NettyClientHandler,它的處理流程如下所示:
并行調用和異步調用
要解決串行調用效率低的問題,有兩個解決對策:
- 并行服務調用,一次 I/O 操作,可以發起批量調用,然后同步等待響應;
- 異步服務調用,在同一個業務線程中異步執行多個服務調用,不阻塞業務線程。
采用并行服務調用的偽代碼示例:
ParallelFuture future = ParallelService.invoke(serviceName [], methodName[], args []);
List<Object> results = future.get(timeout);// 同步阻塞式獲取批量服務調用的響應列表
并行服務調用的一種實現策略如下所示:
異步服務調用的工作原理如下:
異步服務調用相比于同步服務調用有兩個優點:
- 化串行為并行,提升服務調用效率,減少業務線程阻塞時間。
- 化同步為異步,避免業務線程阻塞。
基于 Future-Listener 的純異步服務調用代碼示例如下:
xxxService1.xxxMethod(Req);
Future f1 = RpcContext.getContext().getFuture();
Listener l = new xxxListener();
f1.addListener(l);
class xxxListener{
public void operationComplete(F future)
{ // 判斷是否執行成功,執行后續業務流程}
}
理解誤區
1、異步服務就是異步嗎?
實際上,通信框架基于 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、異步服務調用性能肯定更高嗎?
對于 I/O 密集型,資源不是瓶頸,大部分時間都在同步等應答的場景,異步服務調用會帶來巨大的吞吐量提升,資源使用率也可以提高,更加充分的利用硬件資源提升性能。
另外,對于時延不穩定的接口,例如依賴第三方服務的響應速度、數據庫操作類等,通常異步服務調用也會帶來性能提升。
但是,如果接口調用時延本身都非常小(例如毫秒級),內存計算型,不依賴第三方服務,內部也沒有 I/O 操作,則異步服務調用并不會提升性能。能否提升性能,主要取決于業務的應用場景。
普通 RPC 調用
普通的 RPC 調用提供了三種實現方式:
- 同步阻塞式服務調用,通常實現類是 xxxBlockingStub(基于 proto 定義生成)。
- 異步非阻塞調用,基于 Future-Listener 機制,通常實現類是 xxxFutureStub。
- 異步非阻塞調用,基于 Reactive 的響應式編程模式,通常實現類是 xxxStub。
Streaming 模式服務調用
gRPC 服務調用支持同步和異步方式,同時也支持普通的 RPC 和 streaming 模式,可以最大程度滿足業務的需求。
對于 streaming 模式,可以充分利用 HTTP/2.0 協議的多路復用功能,實現在一條 HTTP 鏈路上并行雙向傳輸數據,有效的解決了 HTTP/1.X 的數據單向傳輸問題,在大幅減少 HTTP 連接的情況下,充分利用單條鏈路的性能,可以媲美傳統的 RPC 私有長連接協議:更少的鏈路、更高的性能:
gRPC 的網絡 I/O 通信基于 Netty 構建,服務調用底層統一使用異步方式,同步調用是在異步的基礎上做了上層封裝。因此,gRPC 的異步化是比較徹底的,對于提升 I/O 密集型業務的吞吐量和可靠性有很大的幫助。
三、gRPC線程模型
影響 RPC 框架性能的三個核心要素如下:
- I/O 模型:用什么樣的通道將數據發送給對方,BIO、NIO 或者 AIO,IO 模型在很大程度上決定了框架的性能;
- 協議:采用什么樣的通信協議,Rest+ JSON 或者基于 TCP 的私有二進制協議,協議的選擇不同,性能模型也不同,相比于公有協議,內部私有二進制協議的性能通常可以被設計的更優;
- 線程:數據報如何讀取?讀取之后的編解碼在哪個線程進行,編解碼后的消息如何派發,通信線程模型的不同,對性能的影響也非常大。
gRPC 線程模型
消息的序列化和反序列化均由 gRPC 線程負責,而沒有在 Netty 的 Handler 中做 CodeC,原因如下:Netty4 優化了線程模型,所有業務 Handler 都由 Netty 的 I/O 線程負責,通過串行化的方式消除鎖競爭,原理如下所示:
如果大量的 Handler 都在 Netty I/O 線程中執行,一旦某些 Handler 執行比較耗時,則可能會反向影響 I/O 操作的執行,像序列化和反序列化操作,都是 CPU 密集型操作,更適合在業務應用線程池中執行,提升并發處理能力。因此,gRPC 并沒有在 I/O 線程中做消息的序列化和反序列化。
改進點思考
1、時間可控的接口調用直接在 I/O 線程上處理
gRPC 采用的是網絡 I/O 線程和業務調用線程分離的策略,大部分場景下該策略是最優的。但是,對于那些接口邏輯非常簡單,執行時間很短,不需要與外部網元交互、訪問數據庫和磁盤,也不需要等待其它資源的,則建議接口調用直接在 Netty /O 線程中執行,不需要再投遞到后端的服務線程池。避免線程上下文切換,同時也消除了線程并發問題。
例如提供配置項或者接口,系統默認將消息投遞到后端服務調度線程,但是也支持短路策略,直接在 Netty 的 NioEventLoop 中執行消息的序列化和反序列化、以及服務接口調用。
2、減少鎖競爭
當前 gRPC 的線程切換策略如下:
優化之后的 gRPC 線程切換策略:
通過線程綁定技術(例如采用一致性 hash 做映射), 將 Netty 的 I/O 線程與后端的服務調度線程做綁定,1 個 I/O 線程綁定一個或者多個服務調用線程,降低鎖競爭,提升性能。
四、gRPC 安全性設計
RPC 調用安全主要涉及如下三點:
- 個人 / 企業敏感數據加密:例如針對個人的賬號、密碼、手機號等敏感信息進行加密傳輸,打印接口日志時需要做數據模糊化處理等,不能明文打印;
- 對調用方的身份認證:調用來源是否合法,是否有訪問某個資源的權限,防止越權訪問;
- 數據防篡改和完整性:通過對請求參數、消息頭和消息體做簽名,防止請求消息在傳輸過程中被非法篡改。
敏感數據加密傳輸
1. 基于 SSL/TLS 的通道加密
2. 針對敏感數據的單獨加密
有些 RPC 調用并不涉及敏感數據的傳輸,或者敏感字段占比較低,為了最大程度的提升吞吐量,降低調用時延,通常會采用 HTTP/TCP + 敏感字段單獨加密的方式,既保障了敏感信息的傳輸安全,同時也降低了采用 SSL/TLS 加密通道帶來的性能損耗,對于 JDK 原生的 SSL 類庫,這種性能提升尤其明顯。
它的工作原理如下所示:
通常使用 Handler 攔截機制,對請求和響應消息進行統一攔截,根據注解或者加解密標識對敏感字段進行加解密,這樣可以避免侵入業務。
認證和鑒權
1. 身份認證
2. 權限管控
在 RPC 調用領域比較流行的是基于 OAuth2.0 的權限認證機制,它的工作原理如下:
數據完整性和一致性
利用消息摘要可以保障數據的完整性和一致性,它的特點如下:
- 單向 Hash 算法,從明文到密文的不可逆過程,即只能加密而不能解密;
- 無論消息大小,經過消息摘要算法加密之后得到的密文長度都是固定的;
- 輸入相同,則輸出一定相同。
目前常用的消息摘要算法是 SHA-1、MD5 和 MAC,MD5 可產生一個 128 位的散列值。 SHA-1 則是以 MD5 為原型設計的安全散列算法,可產生一個 160 位的散列值,安全性更高一些。MAC 除了能夠保證消息的完整性,還能夠保證來源的真實性。