このブログを検索

この記事の内容は、個人の見解、検証の範囲のものであり、誤りがある可能性があります。
個人の責任において情報活用をお願いします。


2022年3月25日金曜日

gRPCはどのくらい速いのか?

検証目的


gRPCは通信プロトコルにHTTP/2を使うため、高速化・双方向通信・streaming等を実現できる…というような記事をよく見るのですが、この「高速化」について、正直、普段の業務アプリ開発でHTTP/2を使った通信をしたことないため、どのくらい速くなるのかよくわかりません。。

そこで、実際にgRPCを使って速さを測定し、「gRPCは業務アプリ開発で使ったほうがいいのか?」を確認しようと思います。

※使うとしても、もちろん「ブラウザ↔サーバ」間ではなく、マイクロサービスを想定した「サーバ↔サーバ」間で使います。


検証内容


なるべく簡単にgRPCを使いたいなと思い、調べたところ、 NestJS でgRPCが使えるということを知ったので、今回は NestJS の gRPC を使って検証しようと思います。

比較対象が必要なので、普段の業務で使っている「Json API」を比較対象とし、「Json API」と「gRPC API」を作って比較してみます。

あと、やり取りするデータとリクエスト回数についてですが、今回は、

  • ①:大量データを1リクエストでやり取り
  • ②:軽量データを大多数リクエストでやり取り

を試そうと思います。
それぞれのイメージは以下のとおりです。

①のイメージ

以下のN件データを1リクエストでやり取りする
{
    {
      rowNum: 0,
      columnA: 'columnA0',
      columnB: 'columnB0',
      columnC: 'columnC0',
      columnD: 'columnD0',
      columnE: 'columnE0',
      columnF: 'columnF0',
      columnG: 'columnG0',
      columnH: 'columnH0',
      columnI: 'columnI0',
      columnJ: 'columnJ0'
    },
    {
      rowNum: 1,
      columnA: 'columnA1',
      columnB: 'columnB1',
      columnC: 'columnC1',
      columnD: 'columnD1',
      columnE: 'columnE1',
      columnF: 'columnF1',
      columnG: 'columnG1',
      columnH: 'columnH1',
      columnI: 'columnI1',
      columnJ: 'columnJ1'
    },
    …以下N件のデータが続く…
}

②のイメージ

以下の1件データをNリクエストでやり取りする
{
    {
      rowNum: 0,
      columnA: 'columnA0',
      columnB: 'columnB0',
      columnC: 'columnC0',
      columnD: 'columnD0',
      columnE: 'columnE0',
      columnF: 'columnF0',
      columnG: 'columnG0',
      columnH: 'columnH0',
      columnI: 'columnI0',
      columnJ: 'columnJ0'
    }
}


それと、「クライアントサーバ」「Json API サーバ」「gRPC API サーバ」の構成、および、計測箇所のイメージは以下のとおりです。



※補足1:検証の結果、最終的に「Json API」と「gRPC API」で若干のデータ構造の違いがでてしまいましたが、概ね同じ構造だと思ってください。

※補足2:今回はデータ量をバイト数で表記しません。普段の業務でデータの数を意識することはあっても、バイト数を意識することはあまり無いため、表記したところでピンと来ないと思ったためです。(意識しろよって話かもですが。。)

※補足3:今回はネットワークの遅延を含めたくなかったので、全サーバをローカルで起動して計測します。(ネットワークの通信速度によってgRPCが速くなる…ってことはないよね?たぶん。。間違ってたらすみません)

検証結果


検証結果は以下のとおりとなりました。

ケース番号データ件数リクエスト数API呼び出し回数計測時間(ms)平均時間(ms)
①ー1
1
1
Json API
1回目3.049
2.940
2回目2.756
3回目3.015
gRPC API
1回目2.766
2.830
2回目2.767
3回目2.958
①ー2
10,000
1
Json API
1回目56.332
47.220
2回目54.471
3回目30.857
gRPC API
1回目86.188
94.660
2回目106.468
3回目91.324
①ー3
1,000,000
1
Json API
1回目5,302.862
5,177.148
2回目4,803.492
3回目5,425.089
gRPC API
1回目12,201.268
12,075.561
2回目11,939.809
3回目12,085.605
②ー1
1
10
Json API
1回目9.551
9.758
2回目10.846
3回目8.878
gRPC API
1回目9.279
8.483
2回目8.422
3回目7.749
②ー2
1
100
Json API
1回目75.867
72.088
2回目74.990
3回目65.406
gRPC API
1回目56.728
50.794
2回目50.003
3回目45.650
②ー3
1
1,000
Json API
1回目655.518
551.685
2回目537.163
3回目462.374
gRPC API
1回目341.100
295.650
2回目315.221
3回目230.630

まとめると、以下のとおりです。

  • ①:大量データを1リクエストでやり取り
    • 結果:「Json API」のほうが速い。

  • ②:軽量データを大多数リクエストでやり取り
    • 結果:「gRPC API」のほうが速い。

HTTP/2は複数のリクエストを同時に処理できるだけあって、②はgRPCのほうが速いようですね。

でも正直なところ、業務アプリで同時にデータ取得するマスタ/トランザクションの数って、せいぜい4つくらいな気がする(=それぞれのマスタ/トランザクションがマイクロサービス化されているとしたら4リクエストくらいな気がする)ので、そう考えると上記のgRPCが速さを発揮するケースって業務アプリ開発上だとほとんど無いんじゃ…。

「20,000件データを4リクエストでやり取り」が現実的に有り得そうなケースなので、ちょっと計測してみました。結果は以下のとおりです。

※後述のソースコードでは計測できません。ちょっとイジって計測しました。

ケース番号データ件数リクエスト数API呼び出し回数計測時間(ms)平均時間(ms)
おまけ
20,000
4
Json API
1回目228.920
220.704
2回目219.529
3回目213.664
gRPC API
1回目677.273
700.893
2回目656.078
3回目769.329


意外な結果になりました。HTTP/2は複数のリクエストを同時に処理できるので、gRPCのほうが速い結果になると予想していたのですが、遅い結果になりましたね。。しかも結構の差がある。。


結論


業務アプリ開発ではgRPCは使わず、Json API を使うことにします。
(速さ以外にgRPCのメリットはあるかと思いますが、やはりユーザが一番嬉しいのは「速いこと」だと思うので)

どこか検証方法でまずいところあったんですかね。。勉強し直して、誤りがあればまたブログに書くかもです。


参考:ソースコード


NestJS CLI でプロジェクトを作ったあと、自分が追加・変更したソースだけ参考までに掲載しておきます。(NestJSのバージョンは「8.2.6」です)

「クライアントサーバ」「Json API サーバ」「gRPC API サーバ」それぞれでプロジェクトを作った形となります。

◆クライアントサーバ

事前に以下のnpmコマンドを実行してライブラリをインストールしました。
npm i --save @nestjs/axios
npm i --save axios

nest-cli.json (変更)
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": ["**/*.proto"],
    "watchAssets": true
  }
}


src/main.ts (変更)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();


src/app.module.ts (変更)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ClientController } from './client.controller';
import { AppService } from './app.service';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path/posix';
import { HttpModule } from '@nestjs/axios';

@Module({
  imports: [
    HttpModule,
    ClientsModule.register([
      {
        name: 'SAMPLE_PACKAGE',
        transport: Transport.GRPC,
        options: {
          package: 'sample',
          protoPath: join(__dirname, 'proto/sample.proto'),
          url: 'localhost:3002',
          maxSendMessageLength: 1024 * 1024 * 1024 * 7, // ★大量のデータをやり取りするため設定。
          maxReceiveMessageLength: 1024 * 1024 * 1024 * 7, // ★大量のデータをやり取りするため設定。
        },
      },
    ]),
  ],
  controllers: [AppController, ClientController],
  providers: [AppService],
})
export class AppModule {}


src/client.controller.ts (追加)
import { HttpService } from '@nestjs/axios';
import { Controller, Get, Inject, OnModuleInit, Param } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { AxiosResponse } from 'axios';
import { forkJoin, Observable } from 'rxjs';

@Controller('client')
export class ClientController implements OnModuleInit {
  private sampleService: any;

  constructor(
    private httpService: HttpService,
    @Inject('SAMPLE_PACKAGE') private client: ClientGrpc,
  ) {}

  onModuleInit() {
    this.sampleService = this.client.getService<any>('SampleService');
  }

  @Get('call-a/:count')
  callServerA(@Param() params): string {
    const count = Number(params.count);

    const startTime = performance.now();
    this.httpService
      .get('http://localhost:3001/server-a/' + count)
      .subscribe((res) => {
        // console.log(res.data.samples[count - 1]); // ★データを見たかったらコメントアウトを外してください。
        const endTime = performance.now();
        console.log(endTime - startTime + ' ms');
      });

    return 'success';
  }

  @Get('call-a-parallel/:count')
  callServerAParallel(@Param() params): string {
    const count = Number(params.count);

    const reqList: Observable<AxiosResponse<any>>[] = [];
    for (let i = 0; i < count; i++) {
      reqList.push(this.httpService.get('http://localhost:3001/server-a/' + 1));
    }

    const startTime = performance.now();
    forkJoin(reqList).subscribe((res) => {
      /* // ★データを見たかったらコメントアウトを外してください。
      for (let i = 0; i < count; i++) {
        console.log(res[i].data.samples[0]);
      }
      */
      const endTime = performance.now();
      console.log(endTime - startTime + ' ms');
    });

    return 'success';
  }

  @Get('call-b/:count')
  callServerB(@Param() params): string {
    const count = Number(params.count);

    const startTime = performance.now();
    this.sampleService.getSamples({ count: count }).subscribe((res) => {
      // console.log(res.samples[count - 1]); // ★データを見たかったらコメントアウトを外してください。
      const endTime = performance.now();
      console.log(endTime - startTime + ' ms');
    });

    return 'success';
  }

  @Get('call-b-parallel/:count')
  callServerBParallel(@Param() params): string {
    const count = Number(params.count);

    const reqList: Observable<any>[] = [];
    for (let i = 0; i < count; i++) {
      reqList.push(this.sampleService.getSamples({ count: 1 }));
    }

    const startTime = performance.now();
    forkJoin(reqList).subscribe((res) => {
      /* // ★データを見たかったらコメントアウトを外してください。
      for (let i = 0; i < count; i++) {
        console.log(res[i].samples[0]);
      }
      */
      const endTime = performance.now();
      console.log(endTime - startTime + ' ms');
    });

    return 'success';
  }
}


src/proto/sample.proto (追加)
// proto/sample.proto
syntax = "proto3";

package sample;

service SampleService {
  rpc GetSamples (GetSamplesParam) returns (Samples) {}
}

message GetSamplesParam {
  int32 count = 1;
}

message Samples {
  repeated Sample samples = 1;
}

message Sample {
  int32 rowNum = 1;
  string columnA = 2;
  string columnB = 3;
  string columnC = 4;
  string columnD = 5;
  string columnE = 6;
  string columnF = 7;
  string columnG = 8;
  string columnH = 9;
  string columnI = 10;
  string columnJ = 11;
}

◆Json API サーバ(サーバA)

src/main.ts (変更)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3001);
}
bootstrap();


src/app.module.ts (変更)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ServerAController } from './server-a.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController, ServerAController],
  providers: [AppService],
})
export class AppModule {}


src/server-a.controller.ts (追加)
import { Controller, Get, Param } from '@nestjs/common';

@Controller('server-a')
export class ServerAController {
  @Get(':count')
  getSamples(@Param() params): any {
    const count = Number(params.count);
    const samples = [];
    for (let i = 0; i < count; i++) {
      samples.push({
        rowNum: i,
        columnA: 'columnA' + i,
        columnB: 'columnB' + i,
        columnC: 'columnC' + i,
        columnD: 'columnD' + i,
        columnE: 'columnE' + i,
        columnF: 'columnF' + i,
        columnG: 'columnG' + i,
        columnH: 'columnH' + i,
        columnI: 'columnI' + i,
        columnJ: 'columnJ' + i,
      });
    }
    return { samples: samples };
  }
}


◆gRPC API サーバ(サーバB)

事前に以下のnpmコマンドを実行してライブラリをインストールしました。
npm i --save @grpc/grpc-js @grpc/proto-loader
npm i --save @nestjs/microservices


nest-cli.json (変更)
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": ["**/*.proto"],
    "watchAssets": true
  }
}


src/main.ts (変更)
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path/posix';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.GRPC,
      options: {
        package: 'sample',
        protoPath: join(__dirname, 'proto/sample.proto'),
        url: 'localhost:3002',
      },
    },
  );
  await app.listen();
}
bootstrap();


src/app.module.ts (変更)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ServerBController } from './server-b.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController, ServerBController],
  providers: [AppService],
})
export class AppModule {}


src/server-b.controller.ts (追加)
import { Metadata, ServerUnaryCall } from '@grpc/grpc-js';
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';

@Controller()
export class ServerBController {
  @GrpcMethod('SampleService', 'GetSamples')
  getSamples(
    data: any,
    metadata: Metadata,
    call: ServerUnaryCall<any, any>,
  ): any {
    const count = Number(data.count);
    const samples = [];
    for (let i = 0; i < count; i++) {
      samples.push({
        rowNum: i,
        columnA: 'columnA' + i,
        columnB: 'columnB' + i,
        columnC: 'columnC' + i,
        columnD: 'columnD' + i,
        columnE: 'columnE' + i,
        columnF: 'columnF' + i,
        columnG: 'columnG' + i,
        columnH: 'columnH' + i,
        columnI: 'columnI' + i,
        columnJ: 'columnJ' + i,
      });
    }
    return { samples: samples };
  }
}


src/proto/sample.proto (追加)
// proto/sample.proto
syntax = "proto3";

package sample;

service SampleService {
  rpc GetSamples (GetSamplesParam) returns (Samples) {}
}

message GetSamplesParam {
  int32 count = 1;
}

message Samples {
  repeated Sample samples = 1;
}

message Sample {
  int32 rowNum = 1;
  string columnA = 2;
  string columnB = 3;
  string columnC = 4;
  string columnD = 5;
  string columnE = 6;
  string columnF = 7;
  string columnG = 8;
  string columnH = 9;
  string columnI = 10;
  string columnJ = 11;
}