Closed17

gRPCサーバーの動作確認、evansとBloomRPCどっちなんだい

Kumamoto-HamachiKumamoto-Hamachi

RPCとは

RPCはいわゆる「クライアント−サーバー」型の通信プロトコルであり、サーバー上で実装されている関数(Procedure、プロシージャ)をクライアントからの呼び出しに応じて実行する技術だ。クライアントはサーバーに対し実行する処理を指定するパラメータや引数として与えるデータを送信し、それに対しサーバーはパラメータに応じた処理を実行してその結果をクライアントに返す、というのがRPCの基本的な流れだ。

https://knowledge.sakura.ad.jp/24059/#HTTPRPCgRPC

gRPC

データの転送効率や散発的なデータのやり取りの面でこれまでの技術に対して優位性がある(らしい)
多数のコンポーネントを組み合わせてサービスを実現する、いわゆる「マイクロサービスアーキテクチャ」でサービス間の通信を行うために用いられてたり。
オブジェクトではなくサービス、リファレンス(参照)ではなくメッセージ

関連用語

  • Protocol Buffers
    サービス定義に用いられる。型付けされたデータや構造化されたデータをネットワーク経由でやり取りできるフォーマットに変換する。
    構造化されたデータをバイト列に変換する処理を「シリアル化」もしくは「シリアライズ」などと呼ぶ。

  • リモートプロシージャ
    公開された関数。リモートプロシージャも引数、戻り値の型も全て事前に定義しておく。
    今回はproto3のフォーマットを勉強していく。(proto2って古いのもある)

Kumamoto-HamachiKumamoto-Hamachi

「サービスはオブジェクトではなく、メッセージはリファレンス(参照)ではない」

分散オブジェクトでは、そのオブジェクトにアクセスする側がそのオブジェクトについての知識を事前に十分に知り得ている必要があり、そのためサービス同士が密に結合してしまう。一方、マイクロサービスアーキテクチャではこのようなサービス間での密な結合は避けるべきとされており、そのコンセプトに従ってgRPCは設計されている。

https://knowledge.sakura.ad.jp/24059/#HTTPRPCgRPC

Kumamoto-HamachiKumamoto-Hamachi

Protocol Buffers

デフォルトではトランスポートにHTTP/2が、データのシリアライズにはProtocol Buffersという技術を使用するようになっており(省略...)Protocol BuffersはGoogleが開発したデータフォーマットで、バイナリデータを含むデータでも効率的に扱えるのが特徴だ。このProtocol Buffersについても、さまざまなプラットフォーム・プログラミング言語から利用できるライブラリが提供されている。

自動生成ツール:protoc?

プロトコル定義ファイルから各プログラミング言語に定義されたクラス定義ファイルを生成するツール。

https://knowledge.sakura.ad.jp/24059/#HTTPRPCgRPC

書き方

// 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。パッケージ名は.で連結させて階層構造にも出来る。

Kumamoto-HamachiKumamoto-Hamachi

gRPCを使ったアプリケーション開発の流れ

1. gPRCの利用に必要なツール・ライブラリのインストール

2. Protocol Buffersを使ったサービスの定義

3. サービス定義ファイルからコード生成

4. サーバー(叩かれる側)の実装

5. クライアント(叩く側)の実装

Kumamoto-HamachiKumamoto-Hamachi

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) {}
}
Kumamoto-HamachiKumamoto-Hamachi

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のサーバークラスにサービスを追加するために使用する関数などが定義されている

Kumamoto-HamachiKumamoto-Hamachi

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()
Kumamoto-HamachiKumamoto-Hamachi

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
Kumamoto-HamachiKumamoto-Hamachi

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]

脚注
  1. AppImageについてはこちらのLinuxでAppImage形式のアプリを使う方法と注意点のまとめ | virtualiment
    の記事の解説が分かりやすい ↩︎

  2. ただし一部の言語(Ruby等)では対応がまだのようです。 ↩︎

Kumamoto-HamachiKumamoto-Hamachi

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. 参考

脚注
  1. AppImageについてはこちらのLinuxでAppImage形式のアプリを使う方法と注意点のまとめ | virtualiment
    の記事の解説が分かりやすく、参考になります。 ↩︎

  2. gRPCではデータのやり取りをunary(単一)でやるかstreamingでやるかの2種類あり、それをclientとserverそれぞれがどちらを選択するかによって4つの通信方式のどれに分類されるかが決まります。今回例にしているBidirectional streaming RPCという通信方式の場合、client(request)もserver(response)も両方streamingの通信を行います。 ↩︎

  3. 特定の RPC 呼び出しに関する情報(認証の詳細など)。キーと値のペアのリスト形式で提供される。 ↩︎

  4. 2022年11月16日現在も対応継続中のよう。Add support for GRPC Server Reflection Protocol · Issue #1 · bloomrpc/bloomrpc ↩︎

このスクラップは2022/11/16にクローズされました