概要
表題の通り、gRPCのパフォーマンスを下記の方法で比較しました。
gRPCの特徴については本家以外にもすでに多くの記載がございますのでここでは割愛させていただきます。
ここでは下記の注目しました。
- データ転送の標準フォーマットがProtocol Buffers
- HTTP/2通信
比較パターン
- gRPC Unary RPC
- gRPC Bidirectional streaming RPC
- REST API (フォーマット: Protocol Buffers)
- REST API (フォーマット: Json)
type | data format | server | client | client package |
---|---|---|---|---|
gRPC Unary RPC | Protocol Buffers | Go | C# | Google.Protobuf |
gRPC Bidirectional streaming RPC | Protocol Buffers | Go | C# | Google.Protobuf |
REST API (Protocol Buffers) | Protocol Buffers | Go | C# | Google.Protobuf |
REST API (Json) | Json | Go | C# | Json.NET |
gRPCのRPCの種類は4つ存在します。
こちらがわかりやすいです。
その中でここではストリームを不使用の送受信のUnary RPC、
HTTP/2のストリームを使用しかつ複数回の送受信が可能なBidirectional streaming RPC、
それにREST API(フォーマット形式はProtocol Buffers)を比較対象に選びました。
ここまではフォーマット形式に依存しない純粋なgRPCとREST APIの比較が目的です。
また、わかりやすいさのためJson形式のREST APIも加えました。(※Json形式のREST APIの詳細説明は割愛します)
仕様
仕様は下記の通りとてもシンプルなものにしました。
- クライアントからサーバーへ中身が文字列のcontentパラメータを投げる
- サーバーはクライアントから受け取ったcontentパラメータの中身をそのままクライアントへ返す
#gRPC Unary RPC
- HTTP/2を使用
- 1リクエスト-1レスポンスの方式
proto定義
Protocol Buffers定義
syntax = "proto3";
package proto.data;
service DataManager{
rpc UnaryTest (RequestMessage) returns (ResponseMessage) {}
rpc BiStreamTest (stream RequestMessage) returns (stream ResponseMessage) {}
}
message RequestMessage {
string content = 1;
}
message ResponseMessage {
string content = 1;
}
ServerSide (Go)
ServerにHTTPリクエストが来たときに実行されるメソッドになります。
func (s *server) UnaryTest(ctx context.Context, in *pb.RequestMessage) (*pb.ResponseMessage, error) {
return &pb.ResponseMessage{Content: in.Content}, nil
}
ClientSide (C#)
実行時に1回呼ばれるメソッドになります。引数のjobCountはスループットのカウント数になります。
public static void DoUnaryTest(int jobCount)
{
var channel = new Channel(Host, Port, ChannelCredentials.Insecure);
var client = new DataManager.DataManagerClient(channel);
for (int i = 0; i < jobCount; i++)
{
var res = client.UnaryTest(new RequestMessage { Content = "TestContent" + i });
// レスポンス受取後、特に何もしない
}
channel.ShutdownAsync().Wait();
}
gRPC Bidirectional streaming RPC
- HTTP/2を使用
- 1つのHTTP/2ストリームコネクション内で任意の数のリクエストとレスポンスの送受信を行える
proto定義
gRPC Unary RPCと同様のものを使用します
ServerSide (Go)
上記同様にServerにHTTPリクエストが来たときに実行されるメソッドになります。
func (s *server) BiStreamTest(stream pb.DataManager_BiStreamTestServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
stream.Send(&pb.ResponseMessage{Content: in.Content})
}
return nil // RPC終了
}
ClientSide (C#)
実行時に1回呼ばれるメソッドになります。引数のjobCountはスループットのカウント数になります。
public static async Task DoBiStreamTest(int jobCount)
{
var channel = new Channel(Host, Port, ChannelCredentials.Insecure);
var client = new DataManager.DataManagerClient(channel);
using (var call = client.BiStreamTest())
{
// Get ResponseMessage
var responseTask = Task.Run(async () =>
{
while (await call.ResponseStream.MoveNext(CancellationToken.None))
{
var res = call.ResponseStream.Current;
// レスポンス受取後、特に何もしない
}
});
// Send RequestMessage
for (int i = 0; i < jobCount; i++)
{
var req = new RequestMessage { Content = "TestContent" + i };
await call.RequestStream.WriteAsync(req);
}
await call.RequestStream.CompleteAsync();
await responseTask;
}
await channel.ShutdownAsync();
}
REST API
- レスポンスのデータフォーマットがProtocol BuffersのREST API
- HTTP/1.1
proto定義
ほとんどgRPCと同じですが、RequestMessageは使用ません。
syntax = "proto3";
package proto.data.rest;
message ResponseMessage {
string content = 1;
}
ServerSide (Go)
上記同様にServerにHTTPリクエストが来たときに実行されるメソッドになります。
func RestProtoTest(w http.ResponseWriter, r *http.Request) {
p := &proto1.ResponseMessage{Content:r.URL.Query().Get("content")}
b, err := proto.Marshal(p)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
pt := fmt.Sprint(reflect.TypeOf(p))
pt = strings.Replace(pt, "*", "", -1)
w.Header().Set("Content-Type", "application/protobuf")
w.Header().Set("Proto-Type", pt)
w.WriteHeader(http.StatusOK)
w.Write(b)
}
ClientSide (C#)
実行時に1回呼ばれるメソッドになります。引数のjobCountはスループットのカウント数になります。
public static void DoRestApiTest(int jobCount)
{
using (var client = new WebClient()) {
for (int i = 0; i < jobCount; i++)
{
var url = string.Format("{0}?content=TestContent{1}", Url, i);
var bytes = client.DownloadData(url);
var res = ResponseMessage.Parser.ParseFrom(bytes);
// レスポンス受取後、特に何もしない
}
}
}
スループットの結果
計測処理フロー
- 時間計測開始
- job count分の送受信
- 時間計測終了
計測結果
通信回数(job count) = 10000の結果
type | job count | elapsed time (sec) | throughput (jobs/sec) |
---|---|---|---|
Unary RPC (gRPC) | 10000 | 1.983 | 5042.9971 |
BiStream RPC (gRPC) | 10000 | 1.131 | 8843.38124 |
REST API (Protocol Buffers) | 10000 | 3.329 | 3003.56168 |
REST API (Json.Net) | 10000 | 4.221 | 2369.08799 |
- 結果、BiStream RPC(gRPC)が一番パフォーマンスがいい
- gRPCにてchannel接続・切断の処理時間はBiStream RPCの方がUnary RPCと比べて長い
- HTTP/2のストリームが存在する分クローズ時に処理時間がかかるが、通信回数が増えるほど影響は小さくなる
Unity上で使用
こちらをご参照ください
最後に
実業務では1ストリーム中に10000回という通信は存在しないかもしれませんが、Unary RPCとして利用してもREST APIよりパフォーマンスが良いので使い勝手が良さそうです。