Bazel と rules_protobuf を使用したgRPCサービスの構築
gRPCは、さまざまな言語で生成されたサービスエントリポイントを提供することで、高性能なマイクロサービスの構築を容易にします。Bazelは、高機能で高速なポリグロットビルド環境でこれらの取り組みを補完します。
rules_protobufはbazelを拡張し、gRPCサービスの開発を容易にします。
これは以下のことを行うことで実現されます。
protoc(Protocol Bufferコンパイラ)と、必要なすべてのprotoc-gen-*プラグインをビルドします。- gRPC関連のコードをコンパイルするために必要なProtobufとgRPCライブラリをビルドします。
protocプラグインの呼び出しを抽象化します(protocの呼び出し方法を学習したり覚えたりする必要は必ずしもありません)。- Protobufソースファイルが変更されたときに、出力を再生成・再コンパイルします。
この記事では、bazelの仕組み(パート1)と、rules_protobufでgRPCサービスを構築する方法(パート2)について背景を説明します。すでにbazelの熱狂的なファンの方は、パート2に直接進むことができます。
より理解を深めるために、bazelをインストールし、rules_protobufリポジトリをクローンしてください。
git clone https://github.com/pubref/rules_protobuf
cd rules_protobuf
~/rules_protobuf$
さて、始めましょう!
1: Bazelについて
Bazelは、Googleの内部ビルドツール「Blaze」のオープンソース版です。Blazeは、さまざまな言語で記述されたコードを扱う大規模なモノレポを管理する課題から生まれました。Blazeは、PantsやBuckなど、他の高機能で高速なビルドツールのインスピレーションとなりました。Bazelは概念的にはシンプルですが、理解すべきいくつかのコアコンセプトと用語があります。
Bazelコマンド:コマンドラインから呼び出されたときに、何らかの種類の作業を実行する関数です。一般的なものには、
bazel build(ライブラリのコンパイル)、bazel run(実行可能バイナリの実行)、bazel test(テストの実行)、bazel query(ビルド依存関係グラフについて何か教えてください)があります。すべてbazel helpで確認できます。ビルドフェーズ:bazelコマンドを呼び出したときに、bazelが経由する3つのステージ(ロード、解析、実行)です。
WORKSPACEファイル:プロジェクトのルートを定義する必須ファイルです。主に外部依存関係(外部ワークスペース)を宣言するために使用されます。
BUILDファイル:ディレクトリ内に
BUILDファイルが存在すると、そのディレクトリはパッケージとして定義されます。BUILDファイルには、ターゲットパターン構文を使用して選択できるターゲットを定義するルールが含まれています。ルールは、skylarkと呼ばれるPythonライクな言語で記述されます。SyklarkはPythonよりも厳密な決定論的保証を持ちますが、意図的に最小限に抑えられており、再帰、クラス、ラムダなどの言語機能は除外されています。
1.1: パッケージ構造
これらの概念を説明するために、rules_protobufのexamplesサブディレクトリのパッケージ構造を見てみましょう。BUILDファイルを持つフォルダのみを表示したファイルツリーを見てみましょう。
tree -P 'BUILD|WORKSPACE' -I 'third_party|bzl' examples/
.
├── BUILD
├── WORKSPACE
└── examples
├── helloworld
│ ├── cpp
│ │ └── BUILD
│ ├── go
│ │ ├── client
│ │ │ └── BUILD
│ │ ├── greeter_test
│ │ │ └── BUILD
│ │ └── server
│ │ └── BUILD
│ ├── grpc_gateway
│ │ └── BUILD
│ ├── java
│ │ └── org
│ │ └── pubref
│ │ └── rules_protobuf
│ │ └── examples
│ │ └── helloworld
│ │ ├── client
│ │ │ └── BUILD
│ │ └── server
│ │ └── BUILD
│ └── proto
│ └── BUILD
└── proto
└── BUILD
1.2: ターゲット
examples/フォルダ内のターゲットのリストを取得するには、クエリを使用します。これは、「Bazel、examplesフォルダ内のすべてのパッケージ内の実行可能なターゲットをすべて表示し、そのパスラベルに加えてそれがどのような種類のものを教えてください」と言っているようなものです。
~/rules_protobuf$ bazel query //examples/... --output label_kind | sort | column -t
cc_binary rule //examples/helloworld/cpp:client
cc_binary rule //examples/helloworld/cpp:server
cc_library rule //examples/helloworld/cpp:clientlib
cc_library rule //examples/helloworld/proto:cpp
cc_library rule //examples/proto:cpp
cc_proto_compile rule //examples/helloworld/proto:cpp.pb
cc_proto_compile rule //examples/proto:cpp.pb
cc_test rule //examples/helloworld/cpp:test
filegroup rule //examples/helloworld/proto:protos
filegroup rule //examples/proto:protos
go_binary rule //examples/helloworld/go/client:client
go_binary rule //examples/helloworld/go/server:server
go_library rule //examples/helloworld/go/server:greeter
go_library rule //examples/helloworld/grpc_gateway:gateway
go_library rule //examples/helloworld/proto:go
go_library rule //examples/proto:go_default_library
go_proto_compile rule //examples/helloworld/proto:go.pb
go_proto_compile rule //examples/proto:go_default_library.pb
go_test rule //examples/helloworld/go/greeter_test:greeter_test
go_test rule //examples/helloworld/grpc_gateway:greeter_test
grpc_gateway_proto_compile rule //examples/helloworld/grpc_gateway:gateway.pb
java_binary rule //examples/helloworld/java/org/pubref/rules_protobuf/examples/helloworld/client:netty
java_binary rule //examples/helloworld/java/org/pubref/rules_protobuf/examples/helloworld/server:netty
java_library rule //examples/helloworld/java/org/pubref/rules_protobuf/examples/helloworld/client:client
java_library rule //examples/helloworld/java/org/pubref/rules_protobuf/examples/helloworld/server:server
java_library rule //examples/helloworld/proto:java
java_library rule //examples/proto:java
java_proto_compile rule //examples/helloworld/proto:java.pb
java_proto_compile rule //examples/proto:java.pb
js_proto_compile rule //examples/helloworld/proto:js
js_proto_compile rule //examples/proto:js
py_proto_compile rule //examples/helloworld/proto:py.pb
ruby_proto_compile rule //examples/proto:rb.pb
私たちのワークスペース内のターゲットに限定されるわけではありません。実は、Google Protobufリポジトリは外部リポジトリとして名前が付けられており(詳細は後述)、同様の方法でそのワークスペースのターゲットにもアドレス指定できます。以下はその一部リストです。
~/rules_protobuf$ bazel query @com_github_google_protobuf//... --output label_kind | sort | column -t
cc_binary rule @com_github_google_protobuf//:protoc
cc_library rule @com_github_google_protobuf//:protobuf
cc_library rule @com_github_google_protobuf//:protobuf_lite
cc_library rule @com_github_google_protobuf//:protoc_lib
cc_library rule @com_github_google_protobuf//util/python:python_headers
filegroup rule @com_github_google_protobuf//:well_known_protos
java_library rule @com_github_google_protobuf//:protobuf_java
objc_library rule @com_github_google_protobuf//:protobuf_objc
py_library rule @com_github_google_protobuf//:protobuf_python
...
これは、ProtobufチームがリポジトリのルートにBUILDファイルを提供しているため可能です。Protobufチームのみなさん、ありがとう!後ほど、すでにBUILDファイルを持っていないリポジトリに独自のBUILDファイルを「注入」する方法を学びます。
上のリストを調べると、protocという名前のcc_binaryルールが見つかります。そのターゲットをbazel runすると、bazelはProtobufリポジトリをクローンし、すべての依存ライブラリをビルドし、ソースからクリーンな実行可能バイナリをビルドし、それを呼び出します(バイナリルールにはダブルダッシュの後にコマンドライン引数を渡します)。
~/rules_protobuf$ bazel run @com_github_google_protobuf//:protoc -- --help
Usage: /private/var/tmp/_bazel_pcj/63330772b4917b139280caef8bb81867/execroot/rules_protobuf/bazel-out/local-fastbuild/bin/external/com_github_google_protobuf/protoc [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
-IPATH, --proto_path=PATH Specify the directory in which to search for
imports. May be specified multiple times;
directories will be searched in order. If not
given, the current working directory is used.
--version Show version info and exit.
-h, --help Show this text and exit.
...
後ほどわかるように、Protobufの外部依存関係に特定のコミットIDを指定して名前を付けることで、使用しているprotocのバージョンに曖昧さがなくなります。このようにして、バイナリをチェックインしたり、gitサブモジュールなどのハックに頼ったりすることなく、リポジトリを肥大化させることなく、信頼性が高く再現可能で安全な精度でプロジェクトにツールをベンダーインできます。非常にクリーンです!
注:gRPCリポジトリにもBUILDファイルがあります:
$ bazel query @com_github_grpc_grpc//... --output label_kind
1.3: ターゲットパターン構文
これらの例を踏まえて、ターゲット構文をもう少し詳しく見てみましょう。Bazelを使い始めた頃、ターゲットパターンの構文は少し威圧的に感じましたが、実際にはそれほど難しくありません。詳しく見てみましょう。


@(アットマーク)は、外部ワークスペースを選択します。これらは、ネットワーク(またはファイルシステム)から取得したものに名前をバインドするワークスペースルールによって確立されます。//(ダブルスラッシュ)は、ワークスペースのルートを選択します。:(コロン)は、パッケージ内のターゲット(ルールまたはファイル)を選択します。パッケージは、ワークスペースのサブフォルダにBUILDファイルが存在することによって確立されることを思い出してください。/(シングルスラッシュ)は、ワークスペースまたはパッケージ内のフォルダを選択します。
混乱の一般的な原因は、BUILDファイルが存在するだけで、そのファイルシステムサブツリーがパッケージとして定義されるため、常にそれを考慮する必要があるということです。たとえば、
foo/bar/baz/にqux.jsというファイルがあり、baz/にもBUILDファイルが存在する場合、ファイルはfoo/bar/baz:qux.jsで選択され、foo/bar/baz/quz.jsではありません。
一般的なショートカット:パッケージと同じ名前のルールが存在する場合、これは暗黙的なターゲットであり、省略できます。たとえば、外部ワークスペースcom_google_guava_guavaの//jarパッケージには:jarターゲットがあるため、以下は同等です。
deps = ["@com_google_guava_guava//jar:jar"]
deps = ["@com_google_guava_guava//jar"]
1.4: 外部依存関係:ワークスペースルール
多くの大企業では、正確で再現可能なビルドを保証するために、必要なツール、コンパイラ、リンカーなどをすべてチェックインしています。外部ワークスペースを使用すると、リポジトリを肥大化させることなく、実質的に同じことを達成できます。
注:bazelの慣例では、外部依存関係名には完全な名前空間識別子を使用します(特殊文字をアンダースコアに置き換えます)。たとえば、リモートリポジトリのURLはhttps://github.com/google/protobuf.gitです。これはワークスペース識別子com_github_google_protobufに単純化されます。同様に、慣例では、jarアーティファクト
io.grpc:grpc-netty:jar:1.0.0-pre1はio_grpc_grpc_nettyになります。
1.4.1: 事前にワークスペースが必要なワークスペースルール
これらのルールは、リモートリソースまたはURLにファイルツリーの先頭にWORKSPACEファイルと、ルールターゲットを定義するBUILDファイルが含まれていることを前提としています。これらはbazelリポジトリと呼ばれます。
git_repository:gitリポジトリからの外部bazel依存関係。ルールには
commit(またはtag)が必要です。http_archive:URLからの外部zipまたはtar.gz依存関係。セキュリティのためにsha265を指定することが強く推奨されます。
注:bazelのexecution_rootに直接アクセスすることはありませんが、これらの外部依存関係が
$(bazel info execution_root)/external/WORKSPACE_NAMEで展開されたときにどのようになるかを確認できます。
1.4.2: ワークスペースファイルを自動生成するワークスペースルール
これらのリポジトリルールの実装には、リソースを利用可能にするためのWORKSPACEファイルとBUILDファイルを自動生成するロジックが含まれています。常に、セキュリティのために既知のsha265を提供して、悪意のあるエージェントが侵害されたネットワーク経由で汚染されたコードを紛れ込ませるのを防ぐことをお勧めします。
http_jar:URLからの外部jar。jarファイルは
@WORKSPACE_NAME//jarとしてjava_library依存関係として利用可能です。maven_jar:URLからの外部jar。jarファイルは
@WORKSPACE_NAME//jarとしてjava_library依存関係として利用可能です。http_file:URLからの外部ファイル。リソースは
@WORKSPACE_NAME//fileを介してfilegroupとして利用可能です。
たとえば、maven_jar guava依存関係の生成されたBUILDファイルを次のように確認できます。
~/rules_protobuf$ cat $(bazel info execution_root)/external/com_google_guava_guava/jar/BUILD
# DO NOT EDIT: automatically generated BUILD file for maven_jar rule com_google_guava_guava
java_import(
name = 'jar',
jars = ['guava-19.0.jar'],
visibility = ['//visibility:public']
)
filegroup(
name = 'file',
srcs = ['guava-19.0.jar'],
visibility = ['//visibility:public']
)
注:外部ワークスペースディレクトリは、実際に必要になるまで存在しません。そのため、
bazel build examples/helloworld/java/org/pubref/rules_protobuf/examples/helloworld/clientのように、それが必要なターゲットをビルドする必要があります。
1.4.3: BUILDファイルを引数として受け付けるワークスペースルール
リポジトリにBUILDファイルがない場合は、そのファイルシステムのルートにBUILDファイルを入れて、外部リソースをbazelの世界観に適合させ、それらのリソースをプロジェクトで利用できるようにすることができます。
たとえば、Mark Adlerのzlibライブラリを考えてみましょう。まず、このコードに何が依存しているかを知ることから始めます。このクエリは、「Bazel、examples内のすべてのターゲットについて、すべての依存関係(推移的閉包セット)を見つけ、次に外部ワークスペースcom_github_madler_zlibのルートパッケージにあるzlibターゲットに依存しているものを教えてください」と言っています。Bazelはこの逆依存関係セットを報告します。グラフviz形式で出力を要求し、dotにパイプして図を生成します。
~/rules_protobuf$ bazel query "rdeps(deps(//examples/...), @com_github_madler_zlib//:zlib)" \
--output graph | dot -Tpng -O


したがって、gRPC関連のCコードはすべてこのライブラリに最終的に依存していることがわかります。しかし、MarkのリポジトリにはBUILDファイルがありません...それはどこから来たのでしょうか?
バリアントワークスペースルールnew_git_repositoryを使用することで、独自のBUILDファイル(cc_libraryターゲットを定義する)を次のように提供できます。
new_git_repository(
name = "com_github_madler_zlib",
remote = "https://github.com/madler/zlib",
tag: "v1.2.8",
build_file: "//bzl:build_file/com_github_madler_zlib.BUILD",
)
このnew_*ファミリーのワークスペースルールは、リポジトリをスリムに保ち、ネットワークで利用可能なほとんどすべてのリソースをベンダーインできるようにします。素晴らしいです!
また、独自のレポジトリルールを作成して、ネットワークからリソースを取得し、Bazelの世界観にバインドするカスタムロジックを持つこともできます。
1.5: Bazelの概要
コマンドとターゲットパターンが提示されると、bazelは次の3つのフェーズを実行します。
ロード:WORKSPACEおよび必要なBUILDファイルを読み取ります。依存関係グラフを生成します。
解析:グラフ内のすべてのノードについて、このビルドに実際に必要なノードはどれか?必要なリソースはすべて利用可能か?
実行:依存関係グラフ内の各必要なノードを実行し、出力を生成します。
これで、bazelの概念的な知識を十分に得て、生産的になれることを願っています。
1.6: rules_protobuf
rules_protobufはbazelの拡張機能であり、以下のことを担当します。
Protocol Bufferコンパイラ
protocをビルドします。必要なすべてのprotoc-genプラグインをダウンロードまたはビルドします。
必要なすべてのgRPC関連サポートライブラリをダウンロードまたはビルドします。
protocを(オンデマンドで)呼び出し、さまざまなprotocプラグインの奇妙な点を解消します。
これは、1つ以上のproto_language仕様をproto_compileルールに渡すことで機能します。proto_languageルールには、プラグインの呼び出し方法と予測されるファイル出力に関するメタデータが含まれており、proto_compileルールはproto_language仕様を解釈し、protocへの適切なコマンドライン引数を構築します。たとえば、複数の言語で同時に出力を生成する方法は次のとおりです。
proto_compile(
name = "pluriproto",
protos = [":protos"],
langs = [
"//cpp",
"//csharp",
"//closure",
"//ruby",
"//java",
"//java:nano",
"//python",
"//objc",
"//node",
],
verbose = 1,
with_grpc = True,
)
bazel build :pluriproto
# ************************************************************
cd $(bazel info execution_root) && bazel-out/host/bin/external/com_github_google_protobuf/protoc \
--plugin=protoc-gen-grpc-java=bazel-out/host/genfiles/third_party/protoc_gen_grpc_java/protoc_gen_grpc_java \
--plugin=protoc-gen-grpc=bazel-out/host/bin/external/com_github_grpc_grpc/grpc_cpp_plugin \
--plugin=protoc-gen-grpc-nano=bazel-out/host/genfiles/third_party/protoc_gen_grpc_java/protoc_gen_grpc_java \
--plugin=protoc-gen-grpc-csharp=bazel-out/host/genfiles/external/nuget_grpc_tools/protoc-gen-grpc-csharp \
--plugin=protoc-gen-go=bazel-out/host/bin/external/com_github_golang_protobuf/protoc_gen_go \
--descriptor_set_out=bazel-genfiles/examples/proto/pluriproto.descriptor_set \
--ruby_out=bazel-genfiles \
--python_out=bazel-genfiles \
--cpp_out=bazel-genfiles \
--grpc_out=bazel-genfiles \
--objc_out=bazel-genfiles \
--csharp_out=bazel-genfiles/examples/proto \
--java_out=bazel-genfiles/examples/proto/pluriproto_java.jar \
--javanano_out=ignore_services=true:bazel-genfiles/examples/proto/pluriproto_nano.jar \
--js_out=import_style=closure,error_on_name_conflict,binary,library=examples/proto/pluriproto:bazel-genfiles \
--js_out=import_style=commonjs,error_on_name_conflict,binary:bazel-genfiles \
--go_out=plugins=grpc,Mexamples/proto/common.proto=github.com/pubref/rules_protobuf/examples/proto/pluriproto:bazel-genfiles \
--grpc-java_out=bazel-genfiles/examples/proto/pluriproto_java.jar \
--grpc-nano_out=ignore_services=true:bazel-genfiles/examples/proto/pluriproto_nano.jar \
--grpc-csharp_out=bazel-genfiles/examples/proto \
--proto_path=. \
examples/proto/common.proto
# ************************************************************
examples/proto/common_pb.rb
examples/proto/pluriproto_java.jar
examples/proto/pluriproto_nano.jar
examples/proto/common_pb2.py
examples/proto/common.pb.h
examples/proto/common.pb.cc
examples/proto/common.grpc.pb.h
examples/proto/common.grpc.pb.cc
examples/proto/Common.pbobjc.h
examples/proto/Common.pbobjc.m
examples/proto/pluriproto.js
examples/proto/Common.cs
examples/proto/CommonGrpc.cs
examples/proto/common.pb.go
examples/proto/common_pb.js
examples/proto/pluriproto.descriptor_set
さまざまな*_proto_libraryルール(以下で使用します)は、内部的にこのproto_compileルールを呼び出し、生成された出力を消費して、必要なライブラリでコンパイルし、.class、.so、.a(またはその他のもの)オブジェクトを作成します。
それでは、実際に何かを作ってみましょう!bazelとrules_protobufを使用してgRPCアプリケーションをビルドします。
2: rules_protobufでgRPCサービスを構築する
アプリケーションは、2つの異なるgRPCサービス間の通信を伴います。
2.1: サービス
Greeterサービス:これは、
user引数を受け取るリクエストを受け付け、Hello {user}という文字列で応答する、おなじみの「Hello World」スターター例です。GreeterTimerサービス:このgRPCサービスは、Greeterサービスをバッチで繰り返し呼び出し、集計されたバッチ時間を(ミリ秒単位で)報告します。これにより、さまざまなGreeterサービス実装の平均RPC時間を比較できます。
これはデモンストレーション目的の非公式なベンチマークであり、gRPCアプリケーションの構築にのみ使用されます。より正式なパフォーマンステストについては、gRPCパフォーマンスダッシュボードを参照してください。
2.2: コンパイル済みプログラム
デモでは、4つの言語で書かれた6つの異なるコンパイル済みプログラムを使用します。
GreeterTimerクライアント(Go)。このコマンドラインインターフェースは、//proto:greetertimer.protoファイルでローカルに定義されたgreetertimer.protoサービス定義を必要とします。GreeterTimerサーバー(Java)。このNettyベースのサーバーは、//proto/greetertimer.protoファイルと、外部の@org_pubref_rules_protobuf//examples/helloworld/proto:helloworld.protoで定義されたProtobuf定義の両方を必要とします。4つの
Greeterサーバー実装(C++、Java、Go、C#)。rules_protobufはすでにこれらのサンプル実装を提供しているため、直接使用します。
2.3: Protobuf定義
GreeterTimerは、単項TimerRequestを受け取り、RPCが完了するまでBatchReponseのシーケンスをストリーミングバックします。この方法で、Greeterサービス実装の平均RPC時間を比較できます。
service GreeterTimer {
// Unary request followed by multiple streamed responses.
// Response granularity will be set by the request batch size.
rpc timeHello(TimerRequest) returns (stream BatchResponse);
}
TimerRequestには、Greeterサービスへの連絡先、総RPCコール数、およびBatchResponseのストリーミング頻度(バッチサイズで構成)に関するメタデータが含まれます。
message TimerRequest {
// the host where the grpc server is running
string host = 1;
// The port of the grpc server
int32 port = 2;
// The total number of hellos
int32 total = 3;
// The number of hellos before sending a BatchResponse.
int32 batchSize = 4;
}
BatchResponseは、バッチで行われたコール数、バッチの実行時間、および残りのコール数を示します。
message BatchResponse {
// The number of checks that are remaining, calculated relative to
// totalChecks in the request.
int32 remaining = 1;
// The number of checks actually performed in this batch.
int32 batchCount = 2;
// The number of checks that failed.
int32 errCount = 3;
// The total time spent, expressed as a number of milliseconds per
// request batch size (total time spent performing batchSize number
// of health checks).
int64 batchTimeMillis = 4;
}
非ストリーミングGreeterサービスは、単項HelloRequestを受け取り、単一のHelloReplyで応答します。
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
common.Config config = 2;
}
message HelloReply {
string message = 1;
}
common.Configメッセージタイプはここでは特に機能しませんが、インポートの使用を示すために役立ちます。rules_protobufは、複数のproto→proto依存関係を持つより複雑なセットアップを支援できます。
2.4: grpc_greetertimerのサンプルアプリケーションをビルドします。
このデモアプリケーションは、https://github.com/pubref/grpc_greetertimerでクローンできます。
2.4.1: プロジェクトレイアウトの作成
以下は、使用するディレクトリレイアウトと関連するBUILDファイルです。
mkdir grpc_greetertimer && cd grpc_greetertimer
~/grpc_greetertimer$ mkdir -p proto/ go/ java/org/pubref/grpc/greetertimer/
~/grpc_greetertimer$ touch WORKSPACE
~/grpc_greetertimer$ touch proto/BUILD
~/grpc_greetertimer$ touch proto/greetertimer.proto
~/grpc_greetertimer$ touch go/BUILD
~/grpc_greetertimer$ touch go/main.go
~/grpc_greetertimer$ touch java/org/pubref/grpc/greetertimer/BUILD
~/grpc_greetertimer$ touch java/org/pubref/grpc/greetertimer/GreeterTimerServer.java
2.4.2: WORKSPACE
まず、rules_protobufリポジトリへの参照を含むWORKSPACEファイルを作成します。//bzlパッケージにあるメインのエントリポイントskylarkファイルrules.bzlをロードし、使用したい言語(この場合はjavaとgo)を渡してそのprotobuf_repositories関数を呼び出します。Goコンパイルサポートのためにrules_goもロードします(図示せず)。
# File //:WORKSPACE
workspace(name = "org_pubref_grpc_greetertimer")
git_repository(
name = "org_pubref_rules_protobuf",
remote = "https://github.com/pubref/rules_protobuf.git",
tag = "v0.6.0",
)
# Load language-specific dependencies
load("@org_pubref_rules_protobuf//java:rules.bzl", "java_proto_repositories")
java_proto_repositories()
load("@org_pubref_rules_protobuf//go:rules.bzl", "go_proto_repositories")
go_proto_repositories()
依存関係を検査したい場合は、repositories.bzlファイルを参照してください。
Bazelは、他のルールが後で必要とするまで実際には何も取得しません。そこで、コードを記述しましょう。プロトコルバッファソースは//protoに、Javaソースは//javaに、Goソースは//goに格納します。
注:bazelワークスペース内でのGo開発は、通常のGoとは少し異なります。特に、
src/、pkg/、bin/サブディレクトリを持つ典型的なGOCODEレイアウトに従う必要はありません。
2.4.3: GreeterTimerサーバー
The Javaサーバーの主な仕事は、リクエストを受け取り、要求されたGreeterサービスにクライアントとして接続することです。実装は、残りのメッセージ数をカウントダウンし、それぞれについてブロッキングsayHello(request)を行います。batchSize制限に達した場合、observer.onNext(response)メッセージが呼び出され、クライアントに応答がストリーミングバックされます。
/* File //java/org/pubref/grpc/greetertimer:GreeterTimerServer.java */
while (remaining-- > 0) {
if (batchCount++ == batchSize) {
BatchResponse response = BatchResponse.newBuilder()
.setRemaining(remaining)
.setBatchCount(batchCount)
.setBatchTimeMillis(batchTime)
.setErrCount(errCount)
.build();
observer.onNext(response);
}
blockingStub.sayHello(HelloRequest.newBuilder()
.setName("#" + remaining)
.build());
}
}
2.4.4: GreeterTimerクライアント
The Goクライアントは、TimerRequestを準備し、client.TimeHelloメソッドからストリームインターフェースを取得します。EOFになるまでRecv()メソッドを呼び出し、その後コールは完了します。各BatchResponseの概要は、単にターミナルに出力されます。
// File: //go:main.go
func submit(client greeterTimer.GreeterTimerClient, request *greeterTimer.TimerRequest) error {
stream, err := client.TimeHello(context.Background(), request)
if err != nil {
log.Fatalf("could not submit request: %v", err)
}
for {
batchResponse, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
log.Fatalf("error during batch recv: %v", err)
return err
}
reportBatchResult(batchResponse)
}
}
2.4.5: Goのprotobuf+gRPCコードを生成します。
//proto:BUILDファイルには、rules_protobufリポジトリからロードされたgo_proto_libraryルールがあります。内部的に、このルールは、greetertimer.pb.go出力ファイルを生成する責任があることをbazelに宣言します。このルールは、どこかで依存しない限り、実際には何も*行いません*。
# File: //proto:BUILD
load("@org_pubref_rules_protobuf//go:rules.bzl", "go_proto_library")
go_proto_library(
name = "go_default_library",
protos = [
"greetertimer.proto",
],
with_grpc = True,
)
Goクライアント実装は、go_binaryルールへのソースファイルプロバイダーとしてgo_proto_libraryに依存しています。また、GRPC_COMPILE_DEPSリストに名前が付けられたコンパイル時依存関係も渡します。
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
load("@org_pubref_rules_protobuf//go:rules.bzl", "GRPC_COMPILE_DEPS")
go_binary(
name = "hello_client",
srcs = [
"main.go",
],
deps = [
"//proto:go_default_library",
] + GRPC_COMPILE_DEPS,
)
~/grpc_greetertimer$ bazel build //go:client
クライアントバイナリを実際にビルドするためにbazelを呼び出すと、次のようになります。
Bazelは、バイナリが依存する入力(ファイル)が変更されたかどうか(コンテンツハッシュとファイルタイムスタンプによって)を確認します。Bazelは、
//proto:go_default_libraryの出力ファイルがビルドされていないことを認識します。Bazelは、
go_proto_libraryに必要なすべての入力(ツールを含む)が利用可能かどうかを確認します。利用可能でない場合は、必要なすべてのツールをダウンロードしてビルドします。その後、ルールを呼び出します。google/protobufリポジトリを取得し、ソースからprotocをビルドします(cc_binaryルール経由)。protoc-gen-goプラグインをソースからビルドします(go_binaryルール経由)。適切なオプションと引数を使用して
protoc-gen-goプラグインでprotocを呼び出します。go_proto_libraryの宣言されたすべての出力が実際にビルドされたことを確認します(bazel-bin/proto/greetertimer.pb.goにあるはずです)。
生成された
greetertimer.pb.goをクライアントmain.goファイルとコンパイルし、bazel-bin/go/client実行可能ファイルを作成します。
2.4.6: JavaのProtobufライブラリを生成します。
java_proto_libraryルールは、go_proto_libraryルールと機能的に同一です。ただし、*.pb.goファイルを提供する代わりに、生成されたすべての出力を*.srcjarファイルにバンドルし(これはjava_libraryルールへの入力として使用されます)、Javaルールの実装詳細です。最終的なJavaバイナリをビルドする方法は次のとおりです。
java_binary(
name = "server",
main_class = "org.pubref.grpc.greetertimer.GreeterTimerServer",
srcs = [
"GreeterTimerServer.java",
],
deps = [
":timer_protos",
"@org_pubref_rules_protobuf//examples/helloworld/proto:java",
"@org_pubref_rules_protobuf//java:grpc_compiletime_deps",
],
runtime_deps = [
"@org_pubref_rules_protobuf//java:netty_runtime_deps",
],
)
:timer_protosは、ローカルに定義されたjava_proto_libraryルールです。@org_pubref_rules_protobuf//examples/helloworld/proto:javaは、独自のワークスペースでgreeterサービスクライアントスタブを生成する外部java_proto_libraryルールです。最後に、実行可能jarのコンパイル時および実行時依存関係を指定します。これらのjarファイルがまだMaven Centralからダウンロードされていない場合は、必要になり次第取得されます。
~/grpc_greetertimer$ bazel build java/org/pubref/grpc/greetertimer:server
~/grpc_greetertimer$ bazel build java/org/pubref/grpc/greetertimer:server_deploy.jar
この最後の形式(余分な_deploy.jarがある)は、:serverルールの暗黙的なターゲットと呼ばれます。この方法で呼び出されると、bazelは必要なすべてのクラスをパックアップし、JVMで独立して実行できるスタンドアロン実行可能jarを生成します。
2.4.7: 実行!
まず、greeterサーバーを1つずつ起動します。
~/grpc_greetertimer$ cd ~/rules_protobuf
~/rules_protobuf$ bazel run examples/helloworld/go/server
~/rules_protobuf$ bazel run examples/helloworld/cpp/server
~/rules_protobuf$ bazel run examples/helloworld/java/org/pubref/rules_protobuf/examples/helloworld/server:netty
~/rules_protobuf$ bazel run examples/helloworld/csharp/GreeterServer
INFO: Server started, listening on 50051
別のターミナルで、greetertimerサーバーを起動します。
~/grpc_greetertimer$ bazel build //java/org/pubref/grpc/greetertimer:server_deploy.jar
~/grpc_greetertimer$ java -jar bazel-bin/java/org/pubref/grpc/greetertimer/server_deploy.jar
最後に、3番目のターミナルで、greetertimerクライアントを呼び出します。
# Timings for the java server
~/rules_protobuf$ bazel run examples/helloworld/java/org/pubref/rules_protobuf/examples/helloworld/server:netty
~/grpc_greeterclient$ bazel run //go:client -- -total_size 10000 -batch_size 1000
17:31:04 1001 hellos (0 errs, 8999 remaining): 1.7 hellos/ms or ~590µs per hello
# ... plus a few runs to warm up the jvm...
17:31:13 1001 hellos (0 errs, 8999 remaining): 6.7 hellos/ms or ~149µs per hello
17:31:13 1001 hellos (0 errs, 7998 remaining): 9.0 hellos/ms or ~111µs per hello
17:31:13 1001 hellos (0 errs, 6997 remaining): 8.9 hellos/ms or ~112µs per hello
17:31:13 1001 hellos (0 errs, 5996 remaining): 9.2 hellos/ms or ~109µs per hello
17:31:13 1001 hellos (0 errs, 4995 remaining): 9.4 hellos/ms or ~106µs per hello
17:31:13 1001 hellos (0 errs, 3994 remaining): 9.0 hellos/ms or ~111µs per hello
17:31:13 1001 hellos (0 errs, 2993 remaining): 9.4 hellos/ms or ~107µs per hello
17:31:13 1001 hellos (0 errs, 1992 remaining): 9.4 hellos/ms or ~107µs per hello
17:31:13 1001 hellos (0 errs, 991 remaining): 9.1 hellos/ms or ~110µs per hello
17:31:14 991 hellos (0 errs, -1 remaining): 9.0 hellos/ms or ~111µs per hello```
```sh
# Timings for the go server
~/rules_protobuf$ bazel run examples/helloworld/go/server
~/grpc_greeterclient$ bazel run //go:client -- -total_size 10000 -batch_size 1000
17:32:33 1001 hellos (0 errs, 8999 remaining): 7.5 hellos/ms or ~134µs per hello
17:32:33 1001 hellos (0 errs, 7998 remaining): 7.9 hellos/ms or ~127µs per hello
17:32:34 1001 hellos (0 errs, 6997 remaining): 7.8 hellos/ms or ~128µs per hello
17:32:34 1001 hellos (0 errs, 5996 remaining): 7.7 hellos/ms or ~130µs per hello
17:32:34 1001 hellos (0 errs, 4995 remaining): 7.9 hellos/ms or ~126µs per hello
17:32:34 1001 hellos (0 errs, 3994 remaining): 8.0 hellos/ms or ~125µs per hello
17:32:34 1001 hellos (0 errs, 2993 remaining): 7.6 hellos/ms or ~132µs per hello
17:32:34 1001 hellos (0 errs, 1992 remaining): 7.9 hellos/ms or ~126µs per hello
17:32:34 1001 hellos (0 errs, 991 remaining): 7.9 hellos/ms or ~127µs per hello
17:32:34 991 hellos (0 errs, -1 remaining): 7.8 hellos/ms or ~128µs per hello
# Timings for the C++ server
~/rules_protobuf$ bazel run examples/helloworld/cpp:server
~/grpc_greeterclient$ bazel run //go:client -- -total_size 10000 -batch_size 1000
17:33:10 1001 hellos (0 errs, 8999 remaining): 9.1 hellos/ms or ~110µs per hello
17:33:10 1001 hellos (0 errs, 7998 remaining): 9.0 hellos/ms or ~111µs per hello
17:33:10 1001 hellos (0 errs, 6997 remaining): 9.1 hellos/ms or ~110µs per hello
17:33:10 1001 hellos (0 errs, 5996 remaining): 8.6 hellos/ms or ~116µs per hello
17:33:10 1001 hellos (0 errs, 4995 remaining): 9.0 hellos/ms or ~111µs per hello
17:33:10 1001 hellos (0 errs, 3994 remaining): 9.0 hellos/ms or ~111µs per hello
17:33:10 1001 hellos (0 errs, 2993 remaining): 9.1 hellos/ms or ~110µs per hello
17:33:10 1001 hellos (0 errs, 1992 remaining): 9.0 hellos/ms or ~111µs per hello
17:33:10 1001 hellos (0 errs, 991 remaining): 9.0 hellos/ms or ~111µs per hello
17:33:11 991 hellos (0 errs, -1 remaining): 9.0 hellos/ms or ~111µs per hello
# Timings for the C# server
~/rules_protobuf$ bazel run examples/helloworld/csharp/GreeterServer
~/grpc_greeterclient$ bazel run //go:client -- -total_size 10000 -batch_size 1000
17:34:37 1001 hellos (0 errs, 8999 remaining): 6.0 hellos/ms or ~166µs per hello
17:34:37 1001 hellos (0 errs, 7998 remaining): 6.7 hellos/ms or ~150µs per hello
17:34:37 1001 hellos (0 errs, 6997 remaining): 6.8 hellos/ms or ~148µs per hello
17:34:37 1001 hellos (0 errs, 5996 remaining): 6.8 hellos/ms or ~147µs per hello
17:34:37 1001 hellos (0 errs, 4995 remaining): 6.7 hellos/ms or ~150µs per hello
17:34:38 1001 hellos (0 errs, 3994 remaining): 6.7 hellos/ms or ~150µs per hello
17:34:38 1001 hellos (0 errs, 2993 remaining): 6.7 hellos/ms or ~149µs per hello
17:34:38 1001 hellos (0 errs, 1992 remaining): 6.7 hellos/ms or ~149µs per hello
17:34:38 1001 hellos (0 errs, 991 remaining): 6.8 hellos/ms or ~148µs per hello
17:34:38 991 hellos (0 errs, -1 remaining): 6.8 hellos/ms or ~147µs per hello
非公式な分析により、C++、Go、Javaのgreeterサービス実装で同等のタイミングが示されました。C++サーバーは全体的に最も高速で一貫したパフォーマンスを示しました。Go実装も非常に一貫していましたが、C++よりもわずかに遅かったです。JavaはJVMのウォームアップによる初期の相対的な遅延を示しましたが、すぐにC++実装と同様のタイミングに収束しました。C#は一貫したパフォーマンスを示しましたが、わずかに遅かったです。
2.5: まとめ
Bazelは、多数の言語で構築されたサービスの高機能なビルド環境を提供することにより、gRPCアプリケーションの構築を支援します。rules_protobufは、必要なすべての依存関係をパッケージ化し、protocを直接呼び出す必要性を抽象化することで、bazelを補完します。
このワークフローでは、生成されたソースコードをチェックインする必要はありません(ワークスペース内で常にオンデマンドで生成されます)。この機能が必要なプロジェクトでは、output_to_workspaceオプションを使用して、生成されたファイルをProtobuf定義の隣に配置できます。
最後に、rules_protobufはgrpc-gatewayプロジェクトを完全にサポートしており、grpc_gateway_proto_libraryおよびgrpc_gateway_binaryルールを通じて、gRPCアプリとHTTP/1.1ゲートウェイを簡単にブリッジできます。
詳細については、サポートされている言語とgRPCバージョンの完全なリストを参照してください。
そして…これで終わりです。ハッピープロシージャルコーリング!
Paul Johnstonは、科学コミュニケーションワークフローのソリューションプロバイダーであるPubRef(@pub_ref)のプリンシパルです。Bazel、gRPC、または関連技術の支援に関する組織的なニーズがある場合は、pcj@pubref.orgまでご連絡ください。ありがとうございます!