NHN Cloud Meetup 編集部
gRPCの概要
2020.12.29
2,510
はじめに
先日、コンピュータービジョンチームと共に試験用のOMR認識およびリーダー機能を新しく開発しました。コンピュータービジョンチームではディープラーニングを活用してPythonで開発し、筆者の所属するチームでは馴染みのあるJava言語でOMRドキュメントのスキャン/アップロードする武運を開発しました。読み取りはPythonサーバーで行い、エクセルファイルの作成をJavaで開発し、読み取りリクエストをPythonサーバーに転送するため、アプリケーション間の通信方式をgRPCに定めて使用することにしました。
このような目的でチーム内で共有するため、gRPCについて調べた内容を紹介したいと思います。
RPC
まず、RPCについて簡単におさらいして本題に進みたいと思います。
RPCは、Remote Procedure Callの略で、分散ネットワーク環境で簡単にプログラミングできるように開発されました。
開発者が各ロジックに集中できるように、クライアント-サーバー間通信に必要な詳しい情報は可能な限り隠されており、クライアント/サーバーで一般的なメソッドを呼び出すように開発すれば済むようになりました。
- Caller / Callee
開発者が必要なビジネスロジックを作成し、定義されたIDL(interface definition language)を作成してStubを呼び出します。 - Stub
StubコンパイラがIDLファイルを読み込み、任意の言語(language)で作成したパラメータをマーシャリング/アンマーシャリング処理して、RPCプロトコルに転送します。 - RPC Runtime
通信して、各メッセージを配信します。
gRPC
それでは、gRPCについて調べてみましょう。
公式ページの説明を基に簡単に要約してみました。(https://grpc.io/)
gRPCは、Googleがマイクロサービスで使用していた単一汎用RPCインフラであるStubbyから始まり、Stubbyの次バージョンを計画中に外部公開されました。高い生産性と効率的なメンテナンス、さまざまな言語やプラットフォームに対応し、メッセージの圧縮率と性能に秀でた特徴を持つ、高性能のオープンソース汎用RPCフレームワークです。
それぞれの特徴について、さらに調べてみましょう。
生産性が高く、効率的なメンテナンスが可能
IDL(Identity Definition Language)でprotocol buffers(protobuf)を使用し、IDLを定義するだけで高性能なサービスとメッセージのソースコードが各言語に合わせて自動的に作成されます。
これにより、開発者は作成されたコードをクライアントとサーバー間の使用言語に関係なく使用することができ、定義された規則を共通して使用できるため、コミュニケーションコストを削減することができます。
さまざまな言語やプラットフォームに対応
前述のように、IDLの定義だけでさまざまな言語やプラットフォームで動作するサーバーとクライアントのコードが作成されます。
現在の公式サポート言語は、以下のとおりです。
メッセージの圧縮率が高く、すぐれた通信性能
gRPCは内部的にHTTP/2を使用するためヘッダー圧縮率が高く、プロトコルバッファにより通信時点ではバイナリデータで通信するため、メッセージサイズが小さくなります。
さらに、HTTP/2であるため、双方向のストリーム通信が可能です。
デメリット
このようなメリットがありますが、単純なREST APIの提供を目的とした場合には適していません。また、プロトコルバッファとHTTP/2に対してわずかなラーニングカーブが存在します。
一般的なREST APIとは異なり、メッセージがバイナリに伝達されるため、開発者が簡単にテストすることができません。これについては、PostmanのようなBloomRPC(https://github.com/uw-labs/bloomrpc)ツールを使ってgRPCテストを試みることができます。
プロトコルバッファ
プロトコルバッファについても簡単に確認してみましょう。
gRPCでIDLとして使用している言語で、Googleで作成して使用するデータのシリアル化ライブラリにおいて、次のような文法を用います。
このように作成されたprotoファイルからprotocコンパイラを使って、各言語のソースコードが作成されます。
下記のサンプルでプロトコルバッファが宣言され、通信の時点で下のイメージのようにエンコードされて送受信されます。したがって、本文のサイズがはるかに簡潔になります。
message Person { required string user_name = 1; optional int64 favourite_number = 2; repeated string interests = 3; }
サンプル
それでは簡単なサンプルコードを用いて、gRPCの使い方をみてみましょう。
Javaでサーバーを構成し、Pythonでクライアントを構成して、単純な会員登録機能を作ります。
1. proto作成
まず、protoファイルを作成します。
syntax = "proto3"; option java_multiple_files = true; option java_package = "com.ks.grpc"; option java_outer_classname = "Member"; package grpc; // enum定義 enum Gender { MALE = 0; FEMALE = 1; } // message = object定義 message Class { // int32 -> int(java) int32 classNo = 1; // string -> String(java) string className = 2; } message MemberRequest { string name = 1; // 上で定義したenum値を活用 Gender gender = 2; string phoneNo = 3; repeated Class classList = 4; } message MemberResponse { // int64 -> long(java) int64 memberNo = 1; } // 自動作成されるクラス service RegisterMember { // クラスメソッド作成 //unary : 一度の呼び出しで一度応答 rpc Register(MemberRequest) returns (MemberResponse); //client stream : クライアントからストリームで配信、サーバーからは一度応答 rpc RegisterClientStream(stream MemberRequest) returns (MemberResponse); //server stream : クライアントから一度配信、サーバーからストリームで応答 rpc RegisterServerStream(MemberRequest) returns (stream MemberResponse); //bi stream : クライアント/サーバーともにストリームで応答 rpc RegisterBiStream(stream MemberRequest) returns (stream MemberResponse); }
syntaxで使用する文法を宣言します。
enum、messageでオブジェクトを定義し、serviceでクラスが作成されます。
serviceの中にはメソッドを宣言すればよいのですが、ここでstreamを使ってストリームの使用を定義することができます。
それに応じてメソッド通信方式が一部異なる方法で適用されます。
2. ソースの作成
先ほど作成したprotoファイルを使って、サーバーサイドのJavaとPythonで使用するソースを作成します。
各言語別にコンパイラが存在し、そのコンパイラを使ってソースを作成します。
Java
JavaではMaven基盤でソースを作成します。
protoファイルをsrc/main/protoフォルダに置いて、grpcのdependencyを追加すると、ビルド設定によってコンパイル時点でソースが作成されます。
<dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.18.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.18.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.18.0</version> </dependency>
ソースは、targetフォルダ下位に作成されます。
<build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.4.1.Final</version> </extension> </extensions> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.5.0</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Python
クライアントサンプルとして使用するPythonでもソースを作成するための準備が必要です。
Python 3.5以降のバージョンと、pip 9.0.1以降のバージョンが必要です。
ライブラリのインストールが優先された後
$ python -m pip install grpcio $ python -m pip install grpcio-tools
ライブラリのコマンドを使ってソースを作成します。
$ python -m grpc_tools.protoc -I./ --python_out=. --grpc_python_out=. ../sample.proto
3.ビジネスロジックの実装
これで通信を行う準備はすべて完了しました。あとは各アプリケーションでビジネスロジックを実装するだけです。
クライアントからはメソッドを呼び出すだけでよく、サーバーは作成されたサービスを継承しロジックを実装すれば完了します。
Javaサーバー
ビルドして作成されたサービスクラスを継承し、各メソッドをオーバーライドして実装すると、サーバーロジックは完了します。
package com.ks.grpc.demo; import com.ks.grpc.MemberRequest; import com.ks.grpc.MemberResponse; import com.ks.grpc.RegisterMemberGrpc; import io.grpc.stub.StreamObserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class MemberService extends RegisterMemberGrpc.RegisterMemberImplBase { public static Logger logger = LoggerFactory.getLogger(MemberService.class); //unary @Override public void register(MemberRequest request, StreamObserver<MemberResponse> responseObserver) { logger.info("### unary Stream : {} ", request.toString()); MemberResponse response = MemberResponse.newBuilder() .setMemberNo(1) .build(); responseObserver.onNext(response); responseObserver.onCompleted(); } //client stream @Override public StreamObserver<MemberRequest> registerClientStream(StreamObserver<MemberResponse> responseObserver) { StreamObserver<MemberRequest> stream = new StreamObserver<MemberRequest>() { @Override public void onNext(MemberRequest memberRequest) { logger.info("### Client Stream : {} ", memberRequest.toString()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { MemberResponse response = MemberResponse.newBuilder() .setMemberNo(1) .build(); responseObserver.onNext(response); responseObserver.onCompleted(); } }; return stream; } //server stream @Override public void registerServerStream(MemberRequest request, StreamObserver<MemberResponse> responseObserver) { logger.info("### Server Stream : {} ", request.toString()); MemberResponse response = MemberResponse.newBuilder() .setMemberNo(1) .build(); responseObserver.onNext(response); responseObserver.onNext(response.toBuilder().setMemberNo(2).build()); responseObserver.onCompleted(); } //bi stream @Override public StreamObserver<MemberRequest> registerBiStream(StreamObserver<MemberResponse> responseObserver) { StreamObserver<MemberRequest> stream = new StreamObserver<MemberRequest>() { @Override public void onNext(MemberRequest memberRequest) { logger.info("### Bi Stream : {} ", memberRequest.toString()); } @Override public void onError(Throwable throwable) { } @Override public void onCompleted() { MemberResponse response = MemberResponse.newBuilder() .setMemberNo(1) .build(); responseObserver.onNext(response); responseObserver.onNext(response.toBuilder().setMemberNo(2).build()); responseObserver.onCompleted(); } }; return stream; } }
次にgRPCで提供されるサーバーオブジェクトを作成し、ポートとサービスを登録します。
Pythonクライアント
Pythonでチャネルを作成し、作成されたStubソースを使ってメソッドを呼び出すだけで、リクエストが完了します。
import grpc import sample_pb2 import sample_pb2_grpc def run(): channel = grpc.insecure_channel('localhost:8877') stub = sample_pb2_grpc.RegisterMemberStub(channel) response = stub.Register(sample_pb2.MemberRequest(name="python",gender=0,phoneNo="01028406735")) print("### Response : " + str(response.memberNo)) if __name__ == '__main__': run()
まとめ
以前の会社では、プロジェクト進行時にgRPCを適用して使用したことありました。アプリクライアント-サーバー間通信だったので、アプリ開発者とサーバー開発者が同じprotoファイルを共有して使用していましたが、当時はprotoファイルを共有する際にメールでやり取りをしていた記憶があります。このような場合は、どちらかのパートでprotoの適用が漏れるリスクがあり、最終的には、protoファイルを一元管理して、変更部分が抜け落ちないようにする作業が必要になります。
ここで調べた内容をチームメンバーと共有し、現在のサービスを交換する必要があるか検討したところ、ネットワーク通信のコストが削減でき、各モジュールもprotoファイルから定義されたとおりに開発すれば済むのでコミュニケーションコストが削減できると考えました。したがって、新しく構築することになる新規サービスでは活用すべきですが、ほとんどの場合、私たちがサービスしているアプリケーションではすでにjson-rpcを使用しており、これをgRPCに置き換えても大きなメリットがないと判断しました。
MSA環境を構築して新しいモジュールを開発することになれば、適用すればよいでしょう。
最後に、gRPCだけではウェブブラウザクライアントでは使用できないため、gRPC-webという追加のプロジェクトが計画されているそうです。JavaScriptで使用でき、単項演算(Unary)とサーバーサイドストリーミング(Server-side Streaming)方式も引き続きサポートされるそうです。
https://github.com/grpc/grpc-web