gRPCサーバーの動作確認、evansとBloomRPCどっちなんだい
前提
grpcに関しては下記のスクラップにメモ中
grpc
- サービス間通信のための新技術「gRPC」入門 | さくらのナレッジ
- ymmt2005/grpc-tutorial: gRPC tutorial for Japanese readers
- goでgRPCの4つの通信方式やってみた(Dockerのサンプルあり) - Qiita
evans
BloomRPC
その他
RPCとは
RPCはいわゆる「クライアント−サーバー」型の通信プロトコルであり、サーバー上で実装されている関数(Procedure、プロシージャ)をクライアントからの呼び出しに応じて実行する技術だ。クライアントはサーバーに対し実行する処理を指定するパラメータや引数として与えるデータを送信し、それに対しサーバーはパラメータに応じた処理を実行してその結果をクライアントに返す、というのがRPCの基本的な流れだ。
gRPC
データの転送効率や散発的なデータのやり取りの面でこれまでの技術に対して優位性がある(らしい)
多数のコンポーネントを組み合わせてサービスを実現する、いわゆる「マイクロサービスアーキテクチャ」でサービス間の通信を行うために用いられてたり。
「オブジェクトではなくサービス、リファレンス(参照)ではなくメッセージ」
関連用語
-
Protocol Buffers
サービス定義に用いられる。型付けされたデータや構造化されたデータをネットワーク経由でやり取りできるフォーマットに変換する。
構造化されたデータをバイト列に変換する処理を「シリアル化」もしくは「シリアライズ」などと呼ぶ。 -
リモートプロシージャ
公開された関数。リモートプロシージャも引数、戻り値の型も全て事前に定義しておく。
今回はproto3のフォーマットを勉強していく。(proto2って古いのもある)
Unary RPC(Python gRPC)
unary(単一の要素)くらいの意味。1つのデータ受け取り=>1つのデータを返す。
「サービスはオブジェクトではなく、メッセージはリファレンス(参照)ではない」
分散オブジェクトでは、そのオブジェクトにアクセスする側がそのオブジェクトについての知識を事前に十分に知り得ている必要があり、そのためサービス同士が密に結合してしまう。一方、マイクロサービスアーキテクチャではこのようなサービス間での密な結合は避けるべきとされており、そのコンセプトに従ってgRPCは設計されている。
Protocol Buffers
デフォルトではトランスポートにHTTP/2が、データのシリアライズにはProtocol Buffersという技術を使用するようになっており(省略...)Protocol BuffersはGoogleが開発したデータフォーマットで、バイナリデータを含むデータでも効率的に扱えるのが特徴だ。このProtocol Buffersについても、さまざまなプラットフォーム・プログラミング言語から利用できるライブラリが提供されている。
自動生成ツール:protoc?
プロトコル定義ファイルから各プログラミング言語に定義されたクラス定義ファイルを生成するツール。
書き方
// Protocol Buffersバージョン3での記述であることを宣言する
syntax = "proto3";
// message = やり取りするデータのこと
message <CamelCase定義するメッセージ型の名前> {
<型> <snake_caseフィールド名1> = <そのフィールドに紐づける一意なフィールド番号>;
<型> <snake_caseフィールド名2> = <そのフィールドに紐づける一意なフィールド番号>;
<型> <snake_caseフィールド名3> = <そのフィールドに紐づける一意なフィールド番号>;
:
:
}
/*
プロシージャ名は実行する処理(リモートプロシージャ)を識別するための文字列で、
いわゆる関数名に相当
*/
service <サービス名> {
rpc <プロシージャ名1> (<引数として受け取るメッセージ型>) returns (<戻り値として返すメッセージ型>) {}
rpc <プロシージャ名2> (<引数として受け取るメッセージ型>) returns (<戻り値として返すメッセージ型>) {}
:
:
}
※serviceも参照先では従ってないがCamelCaseに従うこと
If your .proto defines an RPC service, you should use CamelCase (with an initial capital) for both the service name and any RPC method names:
フィールド番号はメッセージをシリアル化する際にフィールドを識別するために用いられる。
フィールド番号は連続していなくてOKだが1~15に収まると1バイトで済む。
※フィールド名やその型を変更した場合でも、フィールド番号が一致していれば同じとして扱われる。
フィールド番号の予約はreserved
出来る。
データの型
その他、列挙型(enum)、配列(repeated)、複数の中の1つ(oneof)、キーと値の組み合わせ(map)なども
Protocol Buffersでは、一定の条件を満たした上でのメッセージ型定義の変更であれば、互換性を保つことができるような仕組みが備えられている。
データ型としてint32、uint32、int64、uint64、boolを使用していた場合、これらの間であればデータ型を変更しても互換性は保たれる
同様にsint32とsint64、fixed32とsfixed32、fixed64とsfixed64には互換性がある
データの型ごとのデフォルト値
double/floatといった浮動小数点数型やint32、int64などの整数型:0
bool:false
string:空文字列
bytes:空バイト列
enumで定義された列挙型:フィールド番号が0に相当する値
メッセージ型:値はセットされない(実装依存)
インポート機能、モジュール機能
import "<ファイルパス>";
やimport public "foo.proto";
インポート対象のプロトコル定義ファイル内で定義されているメッセージ型は、デフォルトではインポートしたファイル内でのみ参照が可能。publicをつけると自分をimportした先でも自分がimportしたのを使える。
名前空間はpackage 名前;
でおk。パッケージ名は.
で連結させて階層構造にも出来る。
gRPCを使ったアプリケーション開発の流れ
1. gPRCの利用に必要なツール・ライブラリのインストール
2. Protocol Buffersを使ったサービスの定義
3. サービス定義ファイルからコード生成
4. サーバー(叩かれる側)の実装
5. クライアント(叩く側)の実装
1. gRPCの利用に必要なツール・ライブラリのインストール
今回はPythonを用いて行うのでpip install grpcio
でパッケージをインストール。
サービス定義ファイルからコードを自動生成するためのツールが「grpcio-tools」などの別パッケージで提供されている。
pip3 install grpcio-tools
2. Protocol Buffersを使ったサービスの定義
syntax = "proto3";
// ユーザー情報を表すメッセージ型
message User {
uint32 id = 1;
string nickname = 2;
string mail_address = 3;
enum UserType {
NORMAL = 0;
ADMINISTRATOR = 1;
GUEST = 2;
DISABLED = 3;
}
UserType user_type = 4;
}
// ユーザー情報のリクエストに使用するメッセージ型
message UserRequest {
uint32 id = 1;
}
// ユーザー情報を返す際に使用するメッセージ型
message UserResponse {
bool error = 1;
string message = 2;
User user = 3;
}
// ユーザー管理を行うサービス
service UserManager {
// ユーザー情報を取得する
rpc GetUser (UserRequest) returns (UserResponse) {}
}
3. サービス定義ファイルからコード生成
python3 -m grpc_tools.protoc -I<プロトコル定義ファイルが格納されているディレクトリ> --python_out=<コード出力先ディレクトリ> --grpc_python_out=<コード出力先ディレクトリ> <プロトコル定義ファイルのパス名>
$ python -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/user.proto
$ ls
user_pb2.py user_pb2_grpc.py protos
「_pb2.py」で終わるファイルと、「_pb2_grpc.py」で終わるファイルの2つが生成される。
_pb2.py
「_pb2.py」で終わるファイル(今回の例では「user_pb2.py」)では、プロトコル定義ファイル内での
メッセージ型定義に対応したクラスが実装されている。
_pb2_grpc.py
「_pb2_grpc.py」で終わるファイル(今回の例では「user_pb2_grpc.py」)はサービス定義に対応するコードで、サービスを実装するための基底クラスや、gRPCのサーバークラスにサービスを追加するために使用する関数などが定義されている
4. サーバー(叩かれる側)の実装
request引数にはクライアントが引数として与えたメッセージに対応するオブジェクトが、context引数にはRPCに関する情報を含むオブジェクトが渡される。
# gRPCのサーバー実装ではThreadPoolを利用するので、そのためのモジュールをimportしておく
from concurrent.futures import ThreadPoolExecutor
import json
# 「grpc」パッケージと、grpc_tools.protocによって生成したパッケージをimportする
import grpc
import user_pb2
import user_pb2_grpc
# ユーザー情報の読み込み
with open("./users.json") as fp:
users = json.load(fp)
# サービス定義から生成されたクラスを継承して、定義したリモートプロシージャに対応するメソッドを実装する
class UserManager(user_pb2_grpc.UserManagerServicer):
def GetUser(self, request, context):
"""
ユーザー情報を取得する
"""
# クライアントが送信した引数はrequest引数に格納され、
# このオブジェクトに対しては一般的なPythonオブジェクトと
# 同様の形でプロパティにアクセスできる
user_id = request.id
# ユーザー情報はユーザーIDを文字列に変換したものをキーとする辞書型データ
# なので、適宜文字列型に変換して使用している
if str(user_id) not in users:
# 該当するユーザーが存在しない場合エラーを返す
return user_pb2.UserResponse(error=True,
message="not found")
user = users[str(user_id)]
# 戻り値として返すUserオブジェクトを作成する
result = user_pb2.User()
result.id = user["id"]
result.nickname = user["nickname"]
result.mail_address = user["mail_address"]
result.user_type = user_pb2.User.UserType.Value(user["user_type"])
# UserResponseオブジェクトを返す
return user_pb2.UserResponse(error=False,
user=result)
def main():
# Serverオブジェクトを作成する
server = grpc.server(ThreadPoolExecutor(max_workers=2))
# Serverオブジェクトに定義したServicerクラスを登録する
user_pb2_grpc.add_UserManagerServicer_to_server(UserManager(), server)
# 1234番ポートで待ち受けするよう指定する
server.add_insecure_port('[::]:1234')
# 待ち受けを開始する
server.start()
# 待ち受け終了後の後処理を実行する
server.wait_for_termination()
if __name__ == '__main__':
main()
5. クライアント側の実装
evans
動作中なら
$ evans --host localhost -p 1234 ./protos/user.proto
______
| ____|
| |__ __ __ __ _ _ __ ___
| __| \ \ / / / _. | | '_ \ / __|
| |____ \ V / | (_| | | | | | \__ \
|______| \_/ \__,_| |_| |_| |___/
more expressive universal gRPC client
UserManager@localhost:1234>
service一覧を表示・呼び出し(gRPCサーバーを立ち上げておくこと)
UserManager@localhost:1234> show service
+-------------+---------+--------------+---------------+
| SERVICE | RPC | REQUEST TYPE | RESPONSE TYPE |
+-------------+---------+--------------+---------------+
| UserManager | GetUser | UserRequest | UserResponse |
+-------------+---------+--------------+---------------+
UserManager@localhost:1234> call GetUser
id (TYPE_UINT32) => 1
{
"user": {
"id": 1,
"mailAddress": "admin@example.com",
"nickname": "admin",
"userType": "ADMINISTRATOR"
}
}
動作中でなくても
-- サービス一覧
$ evans --proto ./protos/user.proto cli list
UserManager
bloomrpc 基礎
特徴
サービス一覧がGUIベースで見やすく表示される。
型を見て自動的に適当なパラメーターを埋めてくれるのが便利。
セットアップ
Mac
Homebrewで簡単に入れることが出来る。
brew install --cask bloomrpc
Linux勢
AppImage形式[1]でファイルが配布されている。
リンク先から拡張子が.AppImage
になっているファイルをダウンロードした上で実行権限を付与してやる。
-- ダウンロードしたディレクトリに移動
$ cd {BloomRPCのAppImageダウンロード先パス}
-- 実行権限を付与
$ chmod +x ./BloomRPC-{バージョン}.AppImage
-- 実行
$ ./BloomRPC-{バージョン}.AppImage
記事の構成案
gRPCの動作確認
クライアントを作る?=>面倒
evans
MacならHomebrewで簡単に入れることが出来ます。
brew tap ktr0731/evans
brew install evans
$ evans --proto api.proto cli list # サービスの列挙
api.Example
$ evans --proto api.proto cli list api.Example # メソッドの列挙
api.Example.Unary
$ evans --proto api.proto cli desc api.Example.Unary # Unary というメソッドの定義を表示
api.Example.Unary:
rpc Unary ( .api.Request ) returns ( .api.Response );
$ evans --proto api.proto cli desc api.Request # Request というメッセージの定義の表示
api.Request:
message Request {
string name = 1;
}
$ echo '{ "name": "ktr" }' | evans --proto api.proto cli call api.Example.Unary # Unary の呼び出し
{
"message": "hello, ktr"
}
bloomRPC
gRPCリフレクション(Server Reflection)とは?
gRPCリフレクションを設定しておくと、IDLで書かれたファイル(例えばProtocol Buffersを使っているならproto
ファイル)それぞれを直接的に読み込まずに全てのサービサーのメソッドを一元的に呼び出すことが出来ます。[2]
-
AppImageについてはこちらのLinuxでAppImage形式のアプリを使う方法と注意点のまとめ | virtualiment
の記事の解説が分かりやすい ↩︎ -
ただし一部の言語(Ruby等)では対応がまだのようです。 ↩︎
gRPCの4つの通信方式
クライアント-サーバーでUnaryかStreamingで
1. Unary RPCs(Simple RPC)
2. Server streaming RPC
3. Client streaming RPC
4.Bidirectional streaming RPC
grpcリフレクション
pip install grpcio-reflection
いままでと違い(protoのような)IDLのファイルを指定しなくてもサービスが取得できる。
evans --host localhost -p 1234 -r cli list
gRPCの動作確認にはBloomRPCとevansが便利!という話
1. はじめに
gRPCの動作確認、みなさんはどうされてますか?
サービスのメソッド1つ1つの確認のためにクライアントをわざわざ実装するのは面倒ですよね。
今回はクライアントの実装よりもずっと楽かつ便利にgRPCの動作確認を出来るBloomRPCとのevansそれぞれの良さ・使い分けについて自分の考えを言語化してまとめてみました。
2. BloomRPC
BloomRPCはGUIで直感的に操作可能な作りになっています。
インストールも簡単でMacならHomebrewで簡単に入れることが出来ます。
$ brew install --cask bloomrpc
またLinuxでもAppImage形式[1]でファイルが配布されています。
リンク先のReleasesから拡張子が.AppImage
になっているファイルをダウンロードした上で実行権限を付与してやるだけで良いので、非常にお手軽に使い始められます。
-- ダウンロードしたディレクトリに移動
$ cd {BloomRPCのAppImageダウンロード先パス}
-- 実行権限を付与
$ chmod +x ./BloomRPC-{バージョン}.AppImage
-- 実行
$ ./BloomRPC-{バージョン}.AppImage
BloomRPCの良さ
IDLの型から良い感じにパラメーターを埋めておいてくれる
BloomRPCはproto等のIDLのファイルのmessageの型などからパラメーターを良い感じに埋めておいてくれる機能があります。
これのおかげで特に重要でない項目に関してはBloomRPCの埋めてくれた値をそのまま流用して動作確認を行えるので非常に楽です。
WebフレームワークでのテストなどでRailsのFactory GirlやDjangoのfactory_boyがテストデータを良い感じに用意してくれる良さに近いです。
streamingでの通信方式の確認やポート番号変更、metadataの設定等の操作性がシンプル
双方向通信(Bidirectional streaming RPC)[2]を例にとってやると、
上記のようにrequest、responseそれぞれのstreamのデータをタブ形式で切り替えて見れるのが非常に手軽です。
またタブ切り替えで別のポート番号のgRPCの確認なども容易で切り替えが出来ます。
さらにmetadata[3]の設定も下のタブから簡単に行うことが出来ます。
3. evans
evansはCLIのgRPCクライアントツールの1つです。REPL(対話)モードとCLIモードの2つを切り替えることが出来ます。
evansは基本的にBloomRPCに出来ることは何でも出来る(+BloomRPCには出来ないgRPCリフレクションにも対応出来る)ので、CLIツールの方がスキな方にはevansがオススメです。
MacならHomebrewで簡単に導入できます。
$ brew tap ktr0731/evans
$ brew install evans
LinuxだとRelasesからファイルをダウンロードして解凍してやるとすぐ使えます。
-- ダウンロード後
$ tar -zxvf evans_linux_amd64.tar.gz
$ mv evans ~/.local/bin/
evansの良さ
REPLモードでの補完機能が優秀
evansは対話モードで実行した際、かなり気の利いた感じで補完を提案してくれるのでかなり楽にツール操作を行うことが出来ます。
gRPCリフレクション(Server Reflection)への対応
BloomRPC[4]及びgRPC公式のCLIツールのgprc_cliが未対応のgRPCリフレクションもevansなら対応済です。
gRPCリフレクションを利用する際は-r
オプション(--reflection
)を付けます。
CLIモードでの例を下記に示します。
-- サービスの列挙
$ evans -r cli list --port 1234
ClubManager
UserManager
grpc.reflection.v1alpha.ServerReflection
-- メソッドの列挙
$ evans -r cli list --port 1234 UserManager
UserManager.AddUser
UserManager.CountAlreadyUsers
UserManager.GetUser
UserManager.GetUsersByIds
UserManager.GetUsersByType
-- 特定のメソッドの定義確認
$ evans -r cli desc --port 1234 UserManager.AddUser
UserManager.AddUser:
rpc AddUser ( .User ) returns ( .UserResponse );
REPLモードでも同様のことが行えます。
4. 参考
- Core concepts, architecture and lifecycle | gRPC
- gRPC Reflection — gRPC Python 1.46.2 documentation
- gRPC と gRPC クライアントツール Evans | メルカリエンジニアリング
- LinuxでAppImage形式のアプリを使う方法と注意点のまとめ | virtualiment
- Add support for GRPC Server Reflection Protocol · Issue #1 · bloomrpc/bloomrpc
-
AppImageについてはこちらのLinuxでAppImage形式のアプリを使う方法と注意点のまとめ | virtualiment
の記事の解説が分かりやすく、参考になります。 ↩︎ -
gRPCではデータのやり取りをunary(単一)でやるかstreamingでやるかの2種類あり、それをclientとserverそれぞれがどちらを選択するかによって4つの通信方式のどれに分類されるかが決まります。今回例にしているBidirectional streaming RPCという通信方式の場合、client(request)もserver(response)も両方streamingの通信を行います。 ↩︎
-
特定の RPC 呼び出しに関する情報(認証の詳細など)。キーと値のペアのリスト形式で提供される。 ↩︎
-
2022年11月16日現在も対応継続中のよう。Add support for GRPC Server Reflection Protocol · Issue #1 · bloomrpc/bloomrpc ↩︎
下記で記事にまとめたのでClose