高性能RPC框架gRPC竟恐怖如斯~
RPC、gRPC、Thrift、HTTP,大家知道它們之間的聯系和區別么?這些都是面試??嫉膯栴},今天帶大家先搞懂 RPC 和 gRPC。
在講述 gRPC 之前,我們需要先搞懂什么是 RPC。
不 BB,直接上文章目錄:
?
?
什么是 RPC ?
RPC(Remote Procedure Call Protocol)遠程過程調用協議,目標就是讓遠程服務調用更加簡單、透明。
RPC 框架負責屏蔽底層的傳輸方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二進制)和通信細節,服務調用者可以像調用本地接口一樣調用遠程的服務提供者,而不需要關心底層通信細節和調用過程。
?
為什么要用 RPC ?
當我們的業務越來越多、應用也越來越多時,自然的,我們會發現有些功能已經不能簡單劃分開來或者劃分不出來。
此時可以將公共業務邏輯抽離出來,將之組成獨立的服務 Service 應用,而原有的、新增的應用都可以與那些獨立的 Service 應用 交互,以此來完成完整的業務功能。
所以我們急需一種高效的應用程序之間的通訊手段來完成這種需求,RPC 大顯身手的時候來了!
常用的 RPC 框架
- gRPC:一開始由 google 開發,是一款語言中立、平臺中立、開源的遠程過程調用(RPC)系統。
- Thrift:thrift 是一個軟件框架,用來進行可擴展且跨語言的服務的開發。它結合了功能強大的軟件堆棧和代碼生成引擎,以構建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些編程語言間無縫結合的、高效的服務。
- Dubbo:Dubbo 是一個分布式服務框架,以及 SOA 治理方案,Dubbo自2011年開源后,已被許多非阿里系公司使用。
- Spring Cloud:Spring Cloud 由眾多子項目組成,如 Spring Cloud Config、Spring Cloud Netflix、Spring Cloud Consul 等,提供了搭建分布式系統及微服務常用的工具。
RPC 的調用流程
要讓網絡通信細節對使用者透明,我們需要對通信細節進行封裝,我們先看下一個 RPC 調用的流程涉及到哪些通信細節:
?
RPC 的目標就是要 2~8 這些步驟都封裝起來,讓用戶對這些細節透明,下面是網上的另外一幅圖,感覺一目了然:
?
什么是 gRPC ?
gRPC 是一個高性能、通用的開源 RPC 框架,其由 Google 2015 年主要面向移動應用開發并基于 HTTP/2 協議標準而設計,基于 ProtoBuf 序列化協議開發,且支持眾多開發語言。
由于是開源框架,通信的雙方可以進行二次開發,所以客戶端和服務器端之間的通信會更加專注于業務層面的內容,減少了對由 gRPC 框架實現的底層通信的關注。
如下圖,DATA 部分即業務層面內容,下面所有的信息都由 gRPC 進行封裝。
?
?
gRPC 的特點
- 跨語言使用,支持 C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP 等編程語言;
- 基于 IDL 文件定義服務,通過 proto3 工具生成指定語言的數據結構、服務端接口以及客戶端 Stub;
- 通信協議基于標準的 HTTP/2 設計,支持雙向流、消息頭壓縮、單 TCP 的多路復用、服務端推送等特性,這些特性使得 gRPC 在移動端設備上更加省電和節省網絡流量;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一種語言無關的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 調用的高性能;
- 安裝簡單,擴展方便(用該框架每秒可達到百萬個RPC)。
gRPC 交互過程
?
- 交換機在開啟 gRPC 功能后充當 gRPC 客戶端的角色,采集服務器充當 gRPC 服務器角色;
- 交換機會根據訂閱的事件構建對應數據的格式(GPB/JSON),通過 Protocol Buffers 進行編寫 proto 文件,交換機與服務器建立 gRPC 通道,通過 gRPC 協議向服務器發送請求消息;
- 服務器收到請求消息后,服務器會通過 Protocol Buffers 解譯 proto 文件,還原出最先定義好格式的數據結構,進行業務處理;
- 數據處理完后,服務器需要使用 Protocol Buffers 重編譯應答數據,通過 gRPC 協議向交換機發送應答消息;
- 交換機收到應答消息后,結束本次的 gRPC 交互。
簡單地說,gRPC 就是在客戶端和服務器端開啟 gRPC 功能后建立連接,將設備上配置的訂閱數據推送給服務器端。
我們可以看到整個過程是需要用到 Protocol Buffers 將所需要處理數據的結構化數據在 proto 文件中進行定義。
Protocol Buffers
你可以理解?ProtoBuf 是一種更加靈活、高效的數據格式,與 XML、JSON 類似,在一些高性能且對響應速度有要求的數據傳輸場景非常適用。
ProtoBuf 在 gRPC 的框架中主要有三個作用:定義數據結構、定義服務接口,通過序列化和反序列化方式提升傳輸效率。
為什么 ProtoBuf 會提高傳輸效率呢?
我們知道使用 XML、JSON 進行數據編譯時,數據文本格式更容易閱讀,但進行數據交換時,設備就需要耗費大量的 CPU 在 I/O 動作上,自然會影響整個傳輸速率。
Protocol Buffers 不像前者,它會將字符串進行序列化后再進行傳輸,即二進制數據。
?
?
可以看到其實兩者內容相差不大,并且內容非常直觀,但是 Protocol Buffers 編碼的內容只是提供給操作者閱讀的,實際上傳輸的并不會以這種文本形式,而是序列化后的二進制數據,字節數會比 JSON、XML 的字節數少很多,速率更快。
gPRC 如何支撐跨平臺,多語言呢 ?
Protocol Buffers 自帶一個編譯器也是一個優勢點,前面提到的 proto 文件就是通過編譯器進行編譯的,proto 文件需要編譯生成一個類似庫文件,基于庫文件才能真正開發數據應用。
具體用什么編程語言編譯生成這個庫文件呢?由于現網中負責網絡設備和服務器設備的運維人員往往不是同一組人,運維人員可能會習慣使用不同的編程語言進行運維開發,那么 Protocol Buffers 其中一個優勢就能發揮出來——跨語言。
從上面的介紹,我們得出在編碼方面 Protocol Buffers 對比 JSON、XML 的優點:
- 標準的 IDL 和 IDL 編譯器,這使得其對工程師非常友好;
- 序列化數據非常簡潔,緊湊,與 XML 相比,其序列化之后的數據量約為 1/3 到 1/10;
- 解析速度非???#xff0c;比對應的 XML 快約 20-100 倍;
- 提供了非常友好的動態庫,使用非常簡單,反序列化只需要一行代碼。
Protobuf 也有其局限性:
- 由于 Protobuf 產生于 Google,所以目前其僅支持 Java、C++、Python 三種語言;
- Protobuf 支持的數據類型相對較少,不支持常量類型;
- 由于其設計的理念是純粹的展現層協議(Presentation Layer),目前并沒有一個專門支持 Protobuf 的 RPC 框架。
Protobuf 適用場景:
- Protobuf 具有廣泛的用戶基礎,空間開銷小以及高解析性能是其亮點,非常適合于公司內部的對性能要求高的 RPC 調用;
- 由于 Protobuf 提供了標準的 IDL 以及對應的編譯器,其 IDL 文件是參與各方的非常強的業務約束;
- Protobuf 與傳輸層無關,采用 HTTP 具有良好的跨防火墻的訪問屬性,所以 Protobuf 也適用于公司間對性能要求比較高的場景;
- 由于其解析性能高,序列化后數據量相對少,非常適合應用層對象的持久化場景;
- 主要問題在于其所支持的語言相對較少,另外由于沒有綁定的標準底層傳輸層協議,在公司間進行傳輸層協議的調試工作相對麻煩。
基于 HTTP 2.0 標準設計
除了 Protocol Buffers 之外,從交互圖中和分層框架可以看到, gRPC 還有另外一個優勢——它是基于 HTTP 2.0 協議的。
由于 gRPC 基于 HTTP 2.0 標準設計,帶來了更多強大功能,如多路復用、二進制幀、頭部壓縮、推送機制。
這些功能給設備帶來重大益處,如節省帶寬、降低 TCP 連接次數、節省 CPU 使用等,gRPC 既能夠在客戶端應用,也能夠在服務器端應用,從而以透明的方式實現兩端的通信和簡化通信系統的構建。
HTTP 1.X 定義了四種與服務器交互的方式,分別為 GET、POST、PUT、DELETE,這些在 HTTP 2.0 中均保留,我們看看 HTTP 2.0 的新特性:雙向流、多路復用、二進制幀、頭部壓縮。
性能對比
與采用文本格式的 JSON 相比,采用二進制格式的 protobuf 在速度上可以達到前者的 5 倍!
Auth0 網站所做的性能測試結果顯示,protobuf 和 JSON 的優勢差異在 Java、Python 等環境中尤為明顯,下圖是 Auth0 在兩個 Spring Boot 應用程序間所做的對比測試結果。
?
?
結果顯示,protobuf 所需的請求時間最多只有 JSON 的 20% 左右,即速度是其 5 倍!
下面看一下性能和空間開銷對比。
?
從上圖可得出如下結論:
- XML序列化(Xstream)無論在性能和簡潔性上比較差。
- Thrift 與 Protobuf 相比在時空開銷方面都有一定的劣勢。
- Protobuf 和 Avro 在兩方面表現都非常優越。
gRPC 實戰
1. 項目結構
我們先看一下項目結構:
?
?
?
2. 生成 protobuf 文件
文件 helloworld.proto:
syntax = "proto3";option java_multiple_files = true; option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; option objc_class_prefix = "HLW";package helloworld;// The greeting service definition. service Greeter {// Sends a greetingrpc SayHello (HelloRequest) returns (HelloReply) {} }// The request message containing the user's name. message HelloRequest {string name = 1; }// The response message containing the greetings message HelloReply {string message = 1; }?
這里提供了一個 SayHello() 方法,然后入參為 HelloRequest,返回值為 HelloReply,可以看到 proto 文件只定義了入參和返回值的格式,以及調用的接口,至于接口內部的實現,該文件完全不用關心。
文件 pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>rpc-study</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>grpc-demo</artifactId><dependencies><dependency><groupId>io.grpc</groupId><artifactId>grpc-netty-shaded</artifactId><version>1.14.0</version></dependency><dependency><groupId>io.grpc</groupId><artifactId>grpc-protobuf</artifactId><version>1.14.0</version></dependency><dependency><groupId>io.grpc</groupId><artifactId>grpc-stub</artifactId><version>1.14.0</version></dependency></dependencies><build><extensions><extension><groupId>kr.motd.maven</groupId><artifactId>os-maven-plugin</artifactId><version>1.5.0.Final</version></extension></extensions><plugins><plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.5.1</version><configuration><protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact><pluginId>grpc-java</pluginId><pluginArtifact>io.grpc:protoc-gen-grpc-java:1.14.0:exe:${os.detected.classifier}</pluginArtifact></configuration><executions><execution><goals><goal>compile</goal><goal>compile-custom</goal></goals></execution></executions></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>6</source><target>6</target></configuration></plugin></plugins></build> </project>?
這里面的 build 其實是為了安裝 protobuf 插件,里面其實有 2 個插件我們需要用到,分別為 protobuf:compile 和 protobuf:compile-javanano,當我們直接執行時,會生成左側文件,其中 GreeterGrpc 提供調用接口,Hello 開頭的文件功能主要是對數據進行序列化,然后處理入參和返回值。
可能有同學會問,你把文件生成到 target 中,我想放到 main.src 中,你可以把這些文件 copy 出來,或者也可以通過工具生成:
- 下載 protoc.exe 工具 ,下載地址:Releases · protocolbuffers/protobuf · GitHub
- 下載 protoc-gen-grpc 插件, 下載地址:?http://jcenter.bintray.com/io/grpc/protoc-gen-grpc-java/
?
?
3. 服務端和客戶端
文件 HelloWorldClient.java:
public class HelloWorldClient {private final ManagedChannel channel;private final GreeterGrpc.GreeterBlockingStub blockingStub;private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName());public HelloWorldClient(String host,int port){channel = ManagedChannelBuilder.forAddress(host,port).usePlaintext(true).build();blockingStub = GreeterGrpc.newBlockingStub(channel);}public void shutdown() throws InterruptedException {channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);}public void greet(String name){HelloRequest request = HelloRequest.newBuilder().setName(name).build();HelloReply response;try{response = blockingStub.sayHello(request);} catch (StatusRuntimeException e){logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());return;}logger.info("Message from gRPC-Server: "+response.getMessage());}public static void main(String[] args) throws InterruptedException {HelloWorldClient client = new HelloWorldClient("127.0.0.1",50051);try{String user = "world";if (args.length > 0){user = args[0];}client.greet(user);}finally {client.shutdown();}} }?
這個太簡單了,就是連接服務端口,調用 sayHello() 方法。
文件 HelloWorldServer.java:
public class HelloWorldServer {private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());private int port = 50051;private Server server;private void start() throws IOException {server = ServerBuilder.forPort(port).addService(new GreeterImpl()).build().start();logger.info("Server started, listening on " + port);Runtime.getRuntime().addShutdownHook(new Thread() {@Overridepublic void run() {System.err.println("*** shutting down gRPC server since JVM is shutting down");HelloWorldServer.this.stop();System.err.println("*** server shut down");}});}private void stop() {if (server != null) {server.shutdown();}}// block 一直到退出程序private void blockUntilShutdown() throws InterruptedException {if (server != null) {server.awaitTermination();}}public static void main(String[] args) throws IOException, InterruptedException {final HelloWorldServer server = new HelloWorldServer();server.start();server.blockUntilShutdown();}// 實現 定義一個實現服務接口的類private class GreeterImpl extends GreeterGrpc.GreeterImplBase {@Overridepublic void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {HelloReply reply = HelloReply.newBuilder().setMessage(("Hello " + req.getName())).build();responseObserver.onNext(reply);responseObserver.onCompleted();System.out.println("Message from gRPC-Client:" + req.getName());System.out.println("Message Response:" + reply.getMessage());}} }?
主要是實現 sayHello() 方法,里面對數據進行了簡單處理,入參為 “W orld”,返回的是 “Hello World”。
4. 啟動服務
先啟動 Server,返回如下:
再啟動 Client,返回如下:?
?同時 Server返回如下:
?
源碼地址:GitHub - lml200701158/rpc-study
寫在最后
這篇文章其實是我去年寫的,這次是重新整理,文章詳細講解了 RPC 和 gRPC,以及 gRPC 的應用示例,非常全面,后面會再把 Thrift 整理出來。
這個 Demo 看起來很簡單,我 TM 居然搞了大半天,一開始是因為不知道需要執行 2 個不同的插件來生成 protobuf,以為只需要點擊 protobuf:compile 就可以,結果發現 protobuf:compile-javanano 也需要點一下。
總結
以上是生活随笔為你收集整理的高性能RPC框架gRPC竟恐怖如斯~的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为什么说 HashMap 是无序的
- 下一篇: 指针 转 智能指针_智能指针-它们真的那