1. 程式人生 > >gRPC初探——概念介紹以及如何構建一個簡單的gRPC服務

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