概要

表題の通り、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;
}

ServerSilde (Go)

ServerにHTTPリクエストが来たときに実行されるメソッドになります。

func (s *server) UnaryTest(ctx context.Context, in *pb.RequestMessage) (*pb.ResponseMessage, error) {
    return &pb.ResponseMessage{Content: in.Content}, nil
}

ClientSilde (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と同様のものを使用します

ServerSilde (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終了
}

ClientSilde (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;
}

ServerSilde (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)
}

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.