gRPC 是什麼?
gRPC 是 Google 開源的 RPC(Remote Procedure Call)框架。核心想法是讓你呼叫另一台服務的方法時,寫起來像呼叫本地方法,網路傳輸、序列化和反序列化都由框架處理。
傳統的服務間傳輸資料大致要像這樣子
// curl版本
curl -X POST localhost:8080/api/orders \
-H 'Content-Type: application/json' \
-d '{"userId":"u123","amount":3000}'
// java code
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
這種方式是我們熟悉的,我們要發送請求前必須要知道對象的Domain、甚麼Path、甚麼HTTP方法、以及規範上要帶的HTTP Header、RequestBody等等。
但gRPC不這樣搞。
要使用gRPC之前,我們需要先寫一個 .proto 檔定義介面,這是一份請求方與回應方的規格書,又叫Interface Definition Language(IDL)。內容如下:
syntax = "proto3";
service WalletService {
rpc GetBalance (BalanceRequest) returns (BalanceResponse);
}
message BalanceRequest {
string user_id = 1;
}
message BalanceResponse {
int64 balance = 1;
string currency = 2;
}
用 Java 後端的概念來理解的話
- syntax > 指定了語言/規格版本
- service > 雙方溝通的方式,看起來就像是本地的Class但實際上是另外一台Server上面的,這就是像一個溝通的框架 interface那樣。
- rpc > 在一個gRPC的Service中可以包含多個方法,像是interface定義的方法
- message > DTO / POJO 就是請求與回傳的物件格式
- field = 1 > 是欄位編號,傳輸時只會送編號,不會送名稱
完成之後,我們需要利用protobuf-maven-plugin這個工具產生(我使用Java舉例),放入pom.xml之後,啟動compile之後,就會自動產生以下檔案:
當中 WalletServiceGrpc.java 的用途是 提供server用的 ImplBase,以及提供client用的 Stub。BalanceRequest與BalanceResponse就是請求與回復DTO
接著,Server端需要撰寫一個WalletServiceImpl去繼承WalletServiceGrpc.WalletServiceImplBase,然後我們可以在子類別中撰寫商業邏輯,詳細的怎麼傳輸、細節部份我們全部都交給框架就好。這是範例的code
public class WalletServiceImpl extends WalletServiceGrpc.WalletServiceImplBase {
@Override
public void getBalance(BalanceRequest request, StreamObserver<BalanceResponse> responseObserver) {
BalanceResponse response = BalanceResponse.newBuilder()
.setBalance(3000L)
.setCurrency("TWD")
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
Server端寫好實作之後,我們還需要建立一個專用的port去專門服務gRPC的請求。這是範例的code。
int port = 9090;
Server server = ServerBuilder.forPort(port)
.addService(new WalletServiceImpl())
.build();
server.start();
System.out.println("gRPC server started, listening on port " + port);
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown));
server.awaitTermination();
如此一來Server的宣告就完成了,接下來我們來看Client端吧。
Client 端要做的事很少,建立連線、拿到 Stub,就可以像呼叫本地方法一樣使用了。
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 9090)
.usePlaintext() // 本機測試不加密
.build();
WalletServiceGrpc.WalletServiceBlockingStub stub = WalletServiceGrpc.newBlockingStub(channel);
BalanceResponse response = stub.getBalance(
BalanceRequest.newBuilder().setUserId("u123").build());
跟一開始的 HTTP 版本比較,這裡完全沒有 Path、HTTP Method、Header,也沒有自己寫 DTO。我們只是呼叫了 getBalance(),剩下的都交給框架處理。
結論
目前gRPC的使用主要還是在後端微服務之間的資料溝通,它的好處是對延遲敏感,吞吐量大的系統來說比較合適,且可以跨語言,例如說你可以跟Python Team的人員共用一個protobuf,降低一些理解的歧異。
至於不適合的點就是,它不適合對外,對外服務主流還是REST API的方式,它也不適合直接給瀏覽器呼叫,例如說需要 HTTP cache 的場景,由於gRPC都是使用POST方法,瀏覽器快取會用不上,在使用起來比較麻煩。
還有一點就是Load balancing 要注意:它使用的 HTTP/2 是長連線,傳統 L4 load balancer 會把所有 request 導到同一台機器,簡單來說 L4 LB 可能會在gRPC傳輸上失效,實際要多留意這一部分。它需要 L7 LB(Envoy、Istio)或 client-side load balancing。
除此之外,gRPC也帶來了除錯比較難的議題,因為gRPC會將請求轉為二進位內容傳輸,這些工具的學習成本也是一個問題。
好,這樣一來對於gRPC大家應該有一個基礎的認識了,下一輪我要接著進入測試,測試gRPC這種犧牲可讀性換取效能的方法,到底可以換取多少的優勢。
那麼這次的文章就到這裡,讓我們在下一個猴年馬月再見吧。