検証目的
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;
}
