検証目的
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が速くなる…ってことはないよね?たぶん。。間違ってたらすみません)
検証結果
検証結果は以下のとおりとなりました。
まとめると、以下のとおりです。
- ①:大量データを1リクエストでやり取り
- 結果:「Json API」のほうが速い。
- ②:軽量データを大多数リクエストでやり取り
- 結果:「gRPC API」のほうが速い。
HTTP/2は複数のリクエストを同時に処理できるだけあって、②はgRPCのほうが速いようですね。
でも正直なところ、業務アプリで同時にデータ取得するマスタ/トランザクションの数って、せいぜい4つくらいな気がする(=それぞれのマスタ/トランザクションがマイクロサービス化されているとしたら4リクエストくらいな気がする)ので、そう考えると上記のgRPCが速さを発揮するケースって業務アプリ開発上だとほとんど無いんじゃ…。
「20,000件データを4リクエストでやり取り」が現実的に有り得そうなケースなので、ちょっと計測してみました。結果は以下のとおりです。
※後述のソースコードでは計測できません。ちょっとイジって計測しました。
意外な結果になりました。HTTP/2は複数のリクエストを同時に処理できるので、gRPCのほうが速い結果になると予想していたのですが、遅い結果になりましたね。。しかも結構の差がある。。
結論
業務アプリ開発ではgRPCは使わず、Json API を使うことにします。
(速さ以外にgRPCのメリットはあるかと思いますが、やはりユーザが一番嬉しいのは「速いこと」だと思うので)
(速さ以外に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; }