使用 bazel 和 rules_protobuf 构建 gRPC 服务
gRPC 通过提供多种不同语言的生成服务入口点,使构建高性能微服务变得更加容易。Bazel 通过功能强大且快速的多语言构建环境来补充这些努力。
rules_protobuf 扩展了 bazel,使其更容易开发 gRPC 服务。
它通过以下方式实现:
- 构建
protoc
(协议缓冲区编译器)和所有必要的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 经历的三个阶段(加载、分析和执行)。
WORKSPACE 文件:定义项目根目录的必需文件。它主要用于声明外部依赖项(外部工作区)。
BUILD 文件:目录中存在
BUILD
文件将其定义为包。BUILD
文件包含规则,这些规则定义了可以使用目标模式语法选择的目标。规则是用一种名为 skylark 的类似 python 的语言编写的。Syklark 比 python 具有更强的确定性保证,但有意最小化,不包括诸如递归、类和 lambda 之类的语言特性。
1.1:包结构
为了说明这些概念,让我们看看 rules_protobuf 示例子目录 的包结构。我们来看一下文件树,只显示包含 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.
...
正如我们稍后将看到的,我们使用特定的提交 ID 来命名 protobuf 外部依赖项,因此我们使用的 protoc 版本没有任何歧义。这样,您就可以将工具与您的项目一起可靠、可重复、安全地精确地供应商化,而无需通过签入二进制文件、求助于 git 子模块或类似的黑客手段来膨胀您的仓库。 非常干净!
注意:gRPC 仓库也有一个 BUILD 文件:
$ bazel query @com_github_grpc_grpc//... --output label_kind
1.3:目标模式语法
有了这些示例,让我们进一步检查一下目标语法。 当我第一次开始使用 bazel 时,我发现目标模式语法有些令人生畏。 实际上,它还不错。 这是一个更仔细的观察


@
(at 符号)选择一个外部工作区。 这些由 工作区规则 建立,该规则将名称绑定到通过网络(或您的文件系统)获取的内容。//
(双斜杠)选择工作区根目录。:
(冒号)选择包内的目标(规则或文件)。 回想一下,包是通过在工作区的子文件夹中存在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:需要预先存在 WORKSPACE 的工作区规则
这些规则假定远程资源或 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 文件的工作区规则
这些仓库规则的实现包含自动生成 WORKSPACE 文件和 BUILD 文件以使资源可用的逻辑。 与往常一样,建议提供已知的 sha265 以确保安全性,以防止恶意代理通过受损的网络偷偷注入受污染的代码。
http_jar:来自 URL 的外部 jar。 该 jar 文件作为
java_library
依赖项可用,为@WORKSPACE_NAME//jar
。maven_jar:来自 URL 的外部 jar。 该 jar 文件作为
java_library
依赖项可用,为@WORKSPACE_NAME//jar
。http_file:来自 URL 的外部文件。 该资源作为
filegroup
通过@WORKSPACE_NAME//file
可用。
例如,我们可以通过以下方式查看 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 文件,您可以将其放入其文件系统根目录,以将外部资源适配到 bazel 的世界观中,并使这些资源可用于您的项目。
例如,考虑 Mark Adler 的 zlib 库。 首先,让我们了解一下哪些代码依赖于此代码。 此查询表示“好的 bazel,对于示例中的所有目标,找到所有依赖项(传递闭包集),然后告诉我哪些依赖于外部工作区 com_github_madler_zlib 的根包中的 zlib 目标。” Bazel 报告此反向依赖项集。 我们请求以 graphviz 格式输出,并将此管道传递给 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 会经历以下三个阶段
加载:读取 WORKSPACE 和所需的 BUILD 文件。 生成依赖关系图。
分析:对于图中的所有节点,此构建实际需要哪些节点? 我们是否有所有必要的资源?
执行:执行依赖关系图中的每个所需节点并生成输出。
希望您现在对 bazel 有足够的概念性了解,可以高效地工作了。
1.6:rules_protobuf
rules_protobuf 是 bazel 的一个扩展,它负责:
构建协议缓冲区编译器
protoc
,下载和/或构建所有必要的 protoc-gen 插件。
下载和/或构建所有必要的 gRPC 相关支持库。
为您调用 protoc(按需),消除不同 protoc 插件的特性差异。
它的工作方式是将一个或多个 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 服务
该应用程序将涉及两个不同 gRPC 服务之间的通信
2.1:服务
Greeter 服务:这是一个熟悉的 “Hello World” 入门示例,它接受一个带有
user
参数的请求,并回复字符串Hello {user}
。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
中定义的 proto 定义。四个
Greeter
服务器实现(C++、java、go 和 C#)。rules_protobuf 已经提供了这些示例实现,所以我们将直接使用它们。
2.3:Protobuf 定义
GreeterTimer 接受一个一元 TimerRequest
并流回一系列 BatchReponse
,直到处理完所有消息,此时远程过程调用完成。
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
我们将首先创建一个 WORKSPACE 文件,其中包含对 rules_protobuf 存储库的引用。我们加载主入口 skylark 文件 rules.bzl 在 //bzl
包中,并调用其 protobuf_repositories
函数,其中包含我们要使用的语言(在本例中为 java
和 go
)。我们还加载 rules_go 以支持 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 服务器
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 客户端
Go 客户端准备一个 TimerRequest
并从 client.TimeHello
方法获取一个流接口。我们调用它的 Recv()
方法直到 EOF,此时调用完成。每个 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
规则。在内部,该规则向 bazel 声明它负责创建 greetertimer.pb.go
输出文件。除非我们在其他地方依赖它,否则此规则实际上不会做任何事情。
# 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_proto_library
作为 go_binary
规则的源文件提供程序。我们还在 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
存储库并从源代码(通过 cc_binary 规则)构建protoc
。从源代码(通过 go_binary 规则)构建
protoc-gen-go
插件。使用
protoc-gen-go
插件以及适当的选项和参数调用protoc
。确认
go_proto_library
的所有声明的输出都已实际构建(应该在bazel-bin/proto/greetertimer.pb.go
中)。
使用客户端
main.go
文件编译生成的greetertimer.pb.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
是一个外部java_proto_library
规则,它在我们自己的工作区中生成 greeter 服务客户端存根。最后,我们为可执行 jar 命名编译时和运行时依赖项。如果这些 jar 文件尚未从 maven 中心下载,它们将在我们需要时立即获取。
~/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 服务器(一次一个)。
~/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
最后,在第三个终端中,调用 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_proto_library 和 grpc_gateway_binary 规则完全支持 grpc-gateway 项目,因此您可以轻松地将您的 gRPC 应用程序与 HTTP/1.1 网关桥接起来。
有关更多信息,请参阅 支持的语言和 gRPC 版本的完整列表。
就这样了。祝您过程调用愉快!
Paul Johnston 是 PubRef(@pub_ref)的负责人,这是一家为科学通信工作流程提供解决方案的公司。如果您在 Bazel、gRPC 或相关技术方面需要组织上的帮助,请联系 pcj@pubref.org。谢谢!