gRPC初探——概念介紹以及如何構建一個簡單的gRPC服務
目錄
引言
對於分散式系統而言,不同的服務分佈在不同的節點上,一個服務要完成自己的功能經常需要呼叫其他服務的介面,比如典型的微服務架構。通常這種服務呼叫方式有兩種,一種是傳送HTTP請求的方式,另一種則是RPC的方式,RPC是Remote Procedure Call(遠端過程呼叫)的簡稱,可以讓我們像呼叫本地介面一樣使用遠端服務。相比HTTP呼叫,RPC的方式至少在以下幾個方面有優勢
- 傳輸效率
RPC可以自定義TCP報文,基於TCP協議進行通訊,比如dubbo;同時也支援使用HTTP2協議進行通訊,比如gRPC。這相比傳統的HTTP1.1協議報文體積會更小,傳輸效率會更高。 - 效能消耗
RPC框架通常自帶高效的序列化機制,序列化和反序列化耗時更低,序列化後的位元組數通常也更小。 - 負責均衡
RPC框架通常自帶負載均衡策略,而HTTP請求要做負載均衡需要外部應用如Nginx的支援。 - 服務治理
下游服務新增,重啟,下線時能自動通知上游使用者,而HTTP的方式需要事先通知並修改相關配置。
正因為基於RPC方式的服務呼叫有著效能消耗低,傳輸效率高,更容易做負載均衡和服務治理的優點,所以分散式系統內部大多采用這種方式進行分散式服務呼叫。可供選擇的RPC框架很多,比如Hession,Dubbo,Thrift這些很早就開源,平時專案中使用也很多。不過最近有一個叫gRPC的RPC框架很火,被使用在很多微服務相關的開源專案中,比如華為的Apache ServiceComb Saga。這篇部落格作為我學習gRPC的入門筆記,只對它的核心概念和簡單用法做些介紹
1. gRPC簡介
gRPC是由Google開發並開源的RPC框架,它具有以下特點
- 語言中立
支援C,Java,Go等多種語言來構建RPC服務,這是gRPC被廣泛的應用在微服務專案中的重要原因,因為不同的微服務可能用不同的語言構建。 - 基於HTTP/2協議
支援雙向流,訊息頭壓縮,單TCP的多路複用,服務端推送等,這些特性使得gRPC更加適用於移動場景下的客戶端和服務端之間的通訊。 - 基於IDL定義服務
編寫.proto檔案即可生成特定語言的資料結構、服務端介面和客戶端Stub。 - 支援Protocol Buffer序列化
Protocol Buffer是由Google開發的一種資料序列化協議(類似於XML、JSON、Hession),平臺無關,壓縮和傳輸效率高,語法簡單,表達能力強。
一個gRPC服務的大體架構可以用官網上的一幅圖表示
gRPC服務端使用C++構建,客戶端可以使用Ruby或者Java構建,客戶端通過一個Stub存根(代理)物件發起RPC呼叫,請求和響應訊息都使用Protocol Buffer進行序列化。
當我們在微服務中使用gRPC時,整個服務呼叫過程如下所示(圖片來自網路)
通過gRPC,遠端服務的呼叫對使用者更加簡單和透明,底層的傳輸方式,序列化方式,通訊細節等統統不需要關係,當然這些對其他RPC框架而言也適用。
2. 使用Protocol Buffers進行服務定義
一個直觀的想法,在客戶端呼叫服務端提供的遠端介面前,雙方必須進行一些約定,比如介面的方法簽名,請求和響應的資料結構等,這個過程稱為服務定義。服務定義需要特定的介面定義語言(IDL)來完成,gRPC中預設使用protocol buffers。它是google很早就開源的一款序列化框架,其定義了一種資料序列化協議,獨立於語言和平臺,提供了多種語言的實現:Java,C++,Go等,每一種實現都包含了相應語言的編譯器和庫檔案。使用它進行服務定義需要編寫.proto字尾的IDL檔案,並通過其編譯器生成特定語言的資料結構、服務端介面和客戶端Stub程式碼。
2.1 定義訊息
訊息是表示RPC介面的請求引數和響應結果的資料結構。如下定義了一個請求訊息和響應訊息
//定義請求訊息的結構
message SearchResponse {
// repeated表示該欄位可以重複任意次,等價於陣列:Result[]
repeated Result result = 1;
}
//定義響應訊息的結構
message Result {
//required表示該欄位的值恰好為1個
required string url = 1;
//optional表示該欄位的值為0或1個
optional string title = 2;
repeated string snippets = 3;
}
定義訊息的關鍵字為message,相當於java中的class關鍵字,一個訊息就相當於java中的一個類。訊息內可以有多個欄位,欄位的型別可以分類如下
基本資料型別
int32表示java中的int,int64表示java中的long,string表示java中的string,具體的對應關係如下表所示
- 複雜資料型別
列舉,map等。
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
map<key_type, value_type> map_field = N;
和java中類中可以定義類一樣,Protocol Buffers中訊息內也可以定義訊息,形成多層的巢狀結構
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
關於訊息定義,有幾點需要注意的地方
1.訊息中的欄位前可以有修飾符,修飾符主要有三種
required
required int64 ival = 1;
該欄位的值恰好只有一個,沒有或傳入多個都將報錯。optional
optional int32 result_per_page = 3 [default = 10];
該欄位的值有0個或1個,傳入多個將報錯。且以optional修飾的欄位可以設定預設值,若沒有設定,則編譯器會根據型別自動設定一個預設值,比如string設定為空字串,bool型別設定為false等。repeated
repeated int32 samples = 4
該欄位相當於java中的陣列,可以有0個或多個值。
2.訊息中的欄位有唯一編號,如下所示
這個唯一編號用來在訊息的二進位制格式中進行欄位的區分,範圍從1-229 - 1,其中19000-19999是保留編號不能使用。這些欄位編號在使用過程中不能進行修改,否則會出現問題。
2.2 定義服務介面
標題中的介面可以類比java中的Interface,內部可以有多個方法。gRPC中使用service關鍵定義服務介面
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
該服務介面HelloService內部只有一個rpc方法SayHello,請求引數為HelloRequest,響應結果為HelloResponse。
grpc中可以定義4中型別的rpc方法
- 1.簡單rpc方法
rpc SayHello(HelloRequest) returns (HelloResponse){
}
客戶端傳送一個請求,從服務端獲得一個響應,整個過程就像一個本地的方法呼叫。
- 2.服務端流式響應的rpc方法
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
客戶端傳送一個請求,並從服務端獲得一個流(stream)。服務端可以往流中寫入N個訊息作為響應,並且每個訊息可以單獨傳送,客戶端可以從流中按順序讀取這些訊息,如下圖所示(圖片來自網路)
- 3.客戶端流式請求的rpc方法
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
客戶端通過流傳送一連串的多個請求,並等待從服務端返回的一個響應。
- 4.雙向流式rpc方法
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
客戶端通過流傳送N個請求,服務端通過流傳送N個響應,彼此相互獨立,並且讀寫沒有特定的次序要求,比如服務端可以收到所有請求後再返回響應,也可以每讀取一個或K個請求會返回響應。
該特性可以充分利用HTTP/2.0的多路複用功能,實現了服務端和客戶端的全雙工通訊,如下圖所示(圖片來自網路)
3.構建簡單的gRPC服務
按照慣例,編寫一個gRPC版本的hello world來講解如何構建一個簡單的gRPC服務——客戶端傳送一個請求,服務端返回一個響應。
比如
客戶端:takumiCX
服務端:Hello takumiCX
3.1 編寫proto檔案,定義訊息和介面
建立proto檔案
- 定義訊息和介面
//Protocal Buffers的版本有v2和v3之分,語法有較多變化,且相互不相容
//這裡使用的v3版本的
syntax = "proto3";
//編譯後生成的訊息類HelloRequest和HelloReply是否分別放在單獨的class檔案中
option java_multiple_files = true;
//生成程式碼的包路徑
option java_package = "com.takumiCX.greeter";
//最外層的類名稱
option java_outer_classname = "HelloWorldProto";
//包名稱空間
package helloworld;
// 服務介面
service Greeter {
// 一個簡單的rpc方法
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 請求訊息
message HelloRequest {
string name = 1;
}
// 響應訊息
message HelloReply {
string message = 1;
}
3.2 通過maven外掛生成相應程式碼
- pom檔案配置如下
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.16.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.16.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.16.1</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.16.1:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
在target目錄下可以看到編譯器通過編譯proto檔案為我們生成了對應的類,如下圖所示
3.3 gRPC服務端建立
- 第一步:首先要建立一個具體的服務介面實現類GreeterImpl,通過擴充套件gRPC為我們自動生成的服務抽象類GreeterGrpc.GreeterImplBase
/**
* <pre>
* 服務介面
* </pre>
*/
public static abstract class GreeterImplBase implements io.grpc.BindableService {
/**
* <pre>
* 一個簡單的rpc方法
* </pre>
*/
public void sayHello(com.takumiCX.greeter.HelloRequest request,
io.grpc.stub.StreamObserver<com.takumiCX.greeter.HelloReply> responseObserver) {
asyncUnimplementedUnaryCall(getSayHelloMethod(), responseObserver);
}
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
.addMethod(
getSayHelloMethod(),
asyncUnaryCall(
new MethodHandlers<
com.takumiCX.greeter.HelloRequest,
com.takumiCX.greeter.HelloReply>(
this, METHODID_SAY_HELLO)))
.build();
}
}
- 建立服務端物件,監聽特定埠,註冊具體的服務實現類並啟動
//服務要監聽的埠
int port=50051;
//建立服務物件,監聽埠,註冊服務並啟動
Server server = ServerBuilder.
forPort(port) //監聽50051埠
.addService(new GreeterImpl()) //註冊服務
.build() //建立Server物件
.start(); //啟動
log.info("Server started,listening on "+port);
server.awaitTermination();
完整程式碼如下
/**
* @author: takumiCX
* @create: 2018-12-01
**/
public class HelloWorldServer {
private static final Logger log=Logger.getLogger(HelloWorldServer.class.getName());
//擴充套件gRPC自動生成的服務介面,實現業務功能
static class GreeterImpl extends GreeterGrpc.GreeterImplBase{
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
//構建響應訊息,從請求訊息中獲取姓名,在前面拼接上"Hello "
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build();
//在流關閉或丟擲異常前可以呼叫多次
responseObserver.onNext(reply);
//關閉流
responseObserver.onCompleted();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
//服務要監聽的埠
int port=50051;
//建立服務物件,監聽埠,註冊服務並啟動
Server server = ServerBuilder.
forPort(port) //監聽50051埠
.addService(new GreeterImpl()) //註冊服務
.build() //建立Server物件
.start(); //啟動
log.info("Server started,listening on "+port);
server.awaitTermination();
}
}
gRPC的服務端建立過程如下所示(圖片來自網路)
3.5 gRPC客戶端建立
整個過程可以分為3步
- 1.根據服務端的ip和埠號,建立ManagedChannel
- 2.建立供客戶端使用的stub物件,可以建立兩種型別的stub,一種進行同步呼叫,一種進行非同步呼叫,後者發起呼叫的業務執行緒不會同步阻塞。
- 3.通過stub物件發起rpc呼叫,獲取服務端響應。
完整程式碼如下:
/**
* @author: takumiCX
* @create: 2018-12-01
**/
public class HelloWorldClient {
private static final Logger log=Logger.getLogger(HelloWorldClient.class.getName());
public static void main(String[] args) {
String host="localhost";
int port=50051;
//1.建立ManagedChannel,繫結服務端ip地址和埠
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build();
//2.獲得同步呼叫的stub物件
GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel);
// //獲得非同步呼叫的stub物件
// GreeterGrpc.GreeterFutureStub futureStub = GreeterGrpc.newFutureStub(channel);
Scanner scanner = new Scanner(System.in);
while (true){
//從控制檯讀取使用者輸入
String name = scanner.nextLine().trim();
//構建請求訊息
HelloRequest helloRequest = HelloRequest.newBuilder().setName(name).build();
//通過stub代理物件進行服務呼叫,獲取服務端響應
HelloReply helloReply = stub.sayHello(helloRequest);
final String message = helloReply.getMessage();
log.warning("Greeting: "+message);
}
}
}
gRPC客戶端的呼叫流程如下所示
3.6 測試
先啟動gRPC服務端,然後啟動gRPC客戶單。客戶端傳送gRPC請求takumiCX
,收到了來自服務端的響應Hello takumiCX
4. 總結
gRPC作為開源RPC框架的新勢力,基於HTTP/2.0協議進行設計,使用高效能的Protocol Buffer進行訊息的序列化,因而效能非常好,而且提供了完整的負載均衡和服務治理能力,加上其和語言無關、平臺無關的特點,非常適合作為微服務內部服務間呼叫的選型。
5. 參考資料
《深入淺出gRPC》
https://grpc.io
https://grpc.io/docs/guides/concepts.html#service-definition