キーポイント
- Based on interaction and communication style, we can group microservices into two groups: external-facing microservices and internal microservices.
- RESTful APIs are the de facto communication technology for external-facing microservices (REST’s ubiquity and rich supporting ecosystem play a vital role in its continued success).
- gRPC is a relatively new implementation of the Remote Procedure Call (RPC) API paradigm. It can play a major role in all synchronous communications between internal microservices
- Here we examine key gRPC concepts, their usage and benefits of having gRPC as an inter-service communication by using a real-world microservice use case.
- gRPC is supported by many major programming languages. We will discuss sample implementations by using Ballerinalang and Golang as the programming languages.
現代的なマイクロサービスアーキテクチャにおけるマイクロサービスは、そのインタラクションとコミュニケーションの方法に基づいて、2つの主要なグループに分類することができます。最初のグループは外部に向けられたマイクロサービスで、コンシューマに対して直接公開されるものです。従来型のテキストベースのメッセージペイロード(JSON、XMLなど)を外部の開発者用に合わせて使用する、HTTPベースのAPIを中心とするもので、デファクトの通信テクノロジとしてREST(Representational State Transfer)が使われています。
これら外部公開されるマイクロサービスの成功には、RESTの遍在性とリッチなエコシステムが大きな役割を果たしています。OpenAPIは、このようなREST APIを記述し、開発し、運用し、可視化するための、明確に定義された仕様を提供します。これらのAPIにはAPI管理システムが有効で、セキュリティ、帯域制限、キャッシュ、ビジネス要件に沿った課金処理などを行うことができます。HTTPベースのREST APIに代替するものとしてGraphQLもありますが、この記事では取り上げません。
もうひとつのグループは内部的なもので、外部のシステムや開発者とコミュニケーションすることはありません。これらマイクロサービス間で相互にインタラクションを行い、一連のタスクを実行するのです。内部マイクロサービスでは、同期および非同期通信のいずれかを使用します。HTTP上のREST APIを同期モードで使用するケースが多いのですが、最適なテクノロジとして使用されている訳ではありません。この記事では、サービス間通信の最適化が可能なgRPCなどのバイナリプロトコルを活用する方法について、詳しく見ていきたいと思います。
gRPCとは何か
gRPCはサービス間通信を目的とした、比較的新しいRPC (Remote Procedure Call)パラダイムです。他のRPCと同じように、異なるマシンで動作するサーバアプリケーション上のメソッドを、ローカルオブジェクトのように直接起動することができます。また、ThriftやArvoなどのバイナリプロトコルと同じように、サービスコントラクト(contract)の定義にはIDL(Interface Description Language)を使用します。gRPCは、デフォルトのトランスポートプロトコルとして、最新のネットワークトランスポートプロトコルであるHTTP/2を使用します。このためにgRPCは、HTTP/1.1上のRESTよりも高速かつ堅牢になっています。
gRPCサービスのコントラクトはProtocol Buffersを使って定義することができます。各サービス定義では、メソッドの数、それぞれのメソッドが期待する入力メッセージと出力メッセージ、パラメータのデータ型と戻り値の型を指定します。主要なプログラミング言語の提供するツールを使えば、サービスコントラクトの定義するものと同じProtocol Buffersを使ったサーバ側のスケルトンとクライアント側のコード(スタブ)を生成することができます。
gRPCを使用した実用的マイクロサービスの例
図1: オンラインリテールショップ用マイクロサービスアーキテクチャの一部
マイクロサービスアーキテクチャのおもなメリットのひとつに、ひとつのプログラミング言語ですべてを開発するのではなく、最も適した言語を使って個々のサービスを開発できる、ということがあります。図1はオンラインリテールショップのマイクロサービスアーキテクチャから抜粋したものですが、ここではBallerina(本記事ではこれ以降Ballerinaと呼びます)とGo言語で実装された4つのマイクロサービスが、オンラインリテールショップの機能を提供しています。gRPCは多くの主要なプログラミング言語でサポートされているので、サービスコントラクトさえ定義すれば、実装には最も適したプログラミング言語を使うことができるのです。
各サービスのサービスコントラクトを定義しましょう。
syntax="proto3";
package retail_shop;
service OrderService {
rpc UpdateOrder(Item) returns (Order);
}
message Item {
string itemNumber = 1;
int32 quantity = 2;
}
message Order {
string itemNumber = 1;
int32 totalQuantity = 2;
float subTotal = 3;
リスト1: Orderマイクロサービスのサービスコントラクト(order.proto)
Orderサービスは購入品と購入数を取得して、その小計を返します。ここでBallerina gRPCツールを使って、gRPCサービスのボイラプレートコードとスタブ/クライアントをそれぞれ生成します。
$ ballerina grpc --mode service --input proto/order.proto --output gen_code
これを実行すると、OrderServiceサーバのボイラプレートコードが生成されます。
import ballerina/grpc;
listener grpc:Listener ep = new (9090);
service OrderService on ep {
resource function UpdateOrder(grpc:Caller caller, Item value) {
// Implementation goes here.
// You should return an Order
}
}
public type Order record {|
string itemNumber = "";
int totalQuantity = 0;
float subTotal = 0.0;
|};
public type Item record {|
string itemNumber = "";
int quantity = 0;
|};
リスト2: 生成されたボイラプレートコード(OrderService_sample_service.bal)のコード
gRPCサービスはBallerinaのservice
型に完璧にマップされています。gRPC rpcはBallerinaのresource
関数に、gRPCメッセージはBallerinaのrecord
型に、それぞれマップされます。
今回はOrderマイクロサービス用に独立したBallerinaプロジェクトを作成し、OrderServiceのボイラプレートコードを使ってgRPC Unaryサービスを実装しました。
Unaryブロッキング
OrderServiceはCartマイクロサービス内でコールされます。次のBallerinaコマンドを使って、クライアントのスタブとクライアントのコードを生成することができます。
$ ballerina grpc --mode client --input proto/order.proto --output gen_code
生成されたクライアントスタブには、ブロッキングとノンブロッキングの両方のリモートメソッドが用意されています。このサンプルコードは、gRPC UnaryサービスがgRPCブロッキングクライアントと対話する方法を示しています。
public remote function UpdateOrder(Item req, grpc:Headers?headers = ()) returns ([Order, grpc:Headers]|grpc:Error) {
var payload = check self.grpcClient->blockingExecute("retail_shop.OrderService/UpdateOrder", req, headers);
grpc:Headers resHeaders = new;
anydata result = ();
[result, resHeaders] = payload;
return [<Order>result, resHeaders];
}
};
リスト3: 生成されたブロッキングモード用のリモートオブジェクトコード
Ballerinaのリモートメソッドの抽象化はgRPCのクライアントスタブに非常に適しているため、UpdateOrder
の呼び出しコードはクリーンで整然としたものになっています。
Checkoutマイクロサービスは、Cartマイクロサービスから受信した暫定的なオーダをすべて集約することで、最終的な請求書を発行します。今回の例では、すべての暫定的オーダをstream Order
メッセージとして送信することにします。
syntax="proto3";
package retail_shop;
service CheckoutService {
rpc Checkout(stream Order) returns (FinalBill) {}
}
message Order {
string itemNumber = 1;
int32 totalQuantity = 2;
float subTotal = 3;
}
message FinalBill {
float total = 1;
}
リスト4: Checkoutマイクロサービス用のサービスコントラクト(checkout.proto)
ballerina grpc
コマンドを使えば、checkout.protoのボイラプレートコードを生成することができます。
$ ballerina grpc --mode service --input proto/checkout.proto --output gen_code
gRPCクライアントストリーミング
Cartマイクロサービス(クライアント)のストリームメッセージはStreamオブジェクト引数として提供されるので、ループを使ったイテレーションを行うことによって、クライアントから送信されたメッセージを順次処理することができます。以下のサンプル実装を見てください。
service CheckoutService on ep {
resource function Checkout(grpc:Caller caller, stream<Order,error> clientStream) {
float totalBill = 0;
//Iterating through streamed messages here
error?e = clientStream.forEach(function(Order order) {
totalBill += order.subTotal;
});
//Once the client completes stream, a grpc:EOS error is returned to indicate it
if (e is grpc:EOS) {
FinalBill finalBill = {
total:totalBill
};
//Sending the total bill to the client
grpc:Error?result = caller->send(finalBill);
if (result is grpc:Error) {
log:printError("Error occurred when sending the Finalbill: " +
result.message() + " — " + <string>result.detail()["message"]);
} else {
log:printInfo ("Sending Final Bill Total: " +
finalBill.total.toString());
}
result = caller->complete();
if (result is grpc:Error) {
log:printError("Error occurred when closing the connection: " +
result.message() +" — " + <string>result.detail()["message"]);
}
}
//If the client sends an error instead it can be handled here
else if (e is grpc:Error) {
log:printError("An unexpected error occured: " + e.message() + " — " +
<string>e.detail()["message"]);
}
}
}
リスト5: CheckoutService
サンプル実装のService
コード
クライアントのストリームが完了するとgrpc:EOSエラーが戻るので、callerオブジェクトを使って最後の応答メッセージ(合計)を送信するタイミングを知ることができます。
CheckoutServiceのサンプルクライアントコードとクライアントスタブは、次のコマンドを使って生成することが可能です。
$ ballerina grpc --mode client --input proto/checkout.proto --output gen_code
Cartマイクロサービスの実装を見ていきましょう。Cartマイクロサービスには2つのREST APIがあります — アイテムをカートに追加するものと、最終的なチェックアウトを行うためのものです。カートにアイテムを追加すると、CartサービスがOrderマイクロサービスをgRPCコールし、アイテム毎の暫定的なオーダと小計を取得して、それをメモリ内に保存します。Checkoutマイクロサービスをコールすると、メモリに保存されたすべての暫定オーダがgRPCストリームとしてCheckoutマイクロサービスに送られ、支払い総額が返されます。Ballerinaは、gRPCクライアントストリーミングの実装に、組み込みのStream型とClient Object抽象化を使用します。図2は、Ballerinaのクライアントストリーミングの動作方法を示したものです。
図2: Ballerina gRPCクライアントストリーミング
CheckoutServiceクライアントストリーミングの完全な実装は、Cartマイクロサービスのresource関数checkoutの中にあります。最後に、checkout処理の中からGo言語で実装されているStockマイクロサービスをgRPCコールして、在庫数から販売したアイテム数を差し引いて更新します。
grpc-gateway
syntax="proto3";
package retail_shop;
option go_package = "../stock;gen";
import "google/api/annotations.proto";
service StockService {
rpc UpdateStock(UpdateStockRequest) returns (Stock) {
option (google.api.http) = {
// Route to this method from POST requests to /api/v1/stock
put: "/api/v1/stock"
body: "*"
};
}
}
message UpdateStockRequest {
string itemNumber = 1;
int32 quantity = 2;
}
message Stock {
string itemNumber = 1;
int32 quantity = 2;
リスト6: Stockマイクロサービスのサービスコントラクト(stock.proto)
このシナリオでは、外部公開APIとしてREST APIを使用することによる呼び出しと、gRPCを使用したサービス間呼び出しによって、同じUpdateStockサービスが呼び出されます。grpc-gatewayはprotocのプラグインで、gRPCサービス定義を読み込み、RESTful JSONからgRPCに変換するリバースプロキシサーバを生成します。
図3: grpc-proxy
grpc-gatewayは、APIをgRPCとREST形式の両方で同時に提供する場合に有効です。
次のコマンドは、Go言語のgRPCスタブを生成します。
protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=plugins=grpc:. \
stock.proto
次のコマンドは、Go言語のgrpc-gatewayコードを生成します。
protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:. \
stock.proto
次のコマンドは、stock.swagger.jsonを生成します。
protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
-I$GOROOT/src \
--swagger_out=logtostderr=true:../stock/gen/. \
./stock.proto
サンプルの実行
microservices-with-grpc gitリポジトリをクローンして、README.mdのインストラクションに従ってください。
まとめ
gRPCは比較的新しいプロトコルですが、そのエコシステムとコミュニティの急速な成長によって、マイクロサービス開発に大きな影響を与えることは間違いありません。gRPCはオープンスタンダードであり、主要なプログラミング言語をすべてサポートしていることから、多言語マイクロサービス環境で使用するには理想的です。一般的なプラクティスとしてのgRPCは、内部のマイクロサービス間におけるすべての同期通信に使用可能であると同時に、grpc-gatewayなどの新たなテクノロジを使うことで、RESTスタイルのAPIとして外部公開することも可能です。この記事で議論した内容に加えて、DeadlinesやCancellation、Channels、xDSのサポートといったgRPCの機能は、効率的なマイクロサービスの開発に計り知れないパワーとフレキシビリティを提供してくれるものです。
その他のリソース
BallerinaのgRPCサポートについてさらに詳しく知るためには、以下のリンクをチェックしてください。
Go言語には包括的なgRPCサポートがあるので、gRPC Interceptor、Deadlines、Cancellation、Channelなどを使用することによって、今回紹介したマイクロサービスのセキュリティ、堅牢性、レジリエンスを拡張することが可能になります。grpc-go gitリポジトリには、これらコンセプトの実行可能なサンプルが豊富に用意されているので、ぜひチェックしてみてください。
推奨ビデオ:
- "Generating Unified APIs with Protocol Buffers and gRPC"
- "Writing REST Services for the gRPC curious"
- "Using gRPC for Long-lived and Streaming RPCs"
著者について
Lakmal Warusawithana氏はWSO2のシニアディレクタ-デベロッパリレーションです。2005年には、通信企業に特化した次世代型コラボレーティブ・クラウドコンピューティングプロダクト開発のパイオニアである、thinkCubeの共同設立者になりました。氏はエンジニアリングプロセス全般を監督していましたが、特にthinkCubeソリューションのスケーラビリティとサービスデリバリに注意を払いました。thinkCubeを設立する前は、サーバ管理用の使いやすいカスタムインターフェースを備えたLinuxベースサーバの提供を専門とする、ITABSという企業に4年間在籍していました。