Edited at

gRPCとREST APIでスループットを比較する


概要

表題の通り、gRPCのパフォーマンスを下記の方法で比較しました。

gRPC

gRPCの特徴については本家以外にもすでに多くの記載がございますのでここでは割愛させていただきます。

ここでは下記の注目しました。


  • データ転送の標準フォーマットがProtocol Buffers

  • HTTP/2通信


比較パターン


  1. gRPC Unary RPC

  2. gRPC Bidirectional streaming RPC

  3. REST API (フォーマット: Protocol Buffers)

  4. 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

Unary.png


  • 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

BiStream.png


  • 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

RESTAPI.png


  • レスポンスのデータフォーマットが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);
// レスポンス受取後、特に何もしない
}
}
}


スループットの結果


計測処理フロー


  1. 時間計測開始

  2. job count分の送受信

  3. 時間計測終了


計測結果

通信回数(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よりパフォーマンスが良いので使い勝手が良さそうです。