使用 bazel 和 rules_protobuf 构建 gRPC 服务
gRPC 通过提供各种不同语言生成的服务入口点,使构建高性能微服务变得更加容易。Bazel 则通过其强大且快速的多语言构建环境来补充这些工作。
rules_protobuf 扩展了 Bazel,使 gRPC 服务的开发更加便捷。
它通过以下方式实现:
- 构建 `protoc`(协议缓冲区编译器)以及所有必要的 `protoc-gen-*` 插件。
- 构建 gRPC 相关代码编译所需的 Protobuf 和 gRPC 库。
- 抽象化 `protoc` 插件调用(你无需学习或记住如何调用 `protoc`)。
- 当 Protobuf 源文件更改时,重新生成并重新编译输出。
在这篇博文中,我将介绍 Bazel 的工作原理(第一部分)以及如何开始使用 rules_protobuf 构建 gRPC 服务(第二部分)。如果你已经是 Bazel 爱好者,可以直接跳到第二部分。
为了更好地跟着操作,请安装 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` 文件包含定义*目标*的*规则*,可以使用*目标模式语法*来选择这些目标。规则是用一种类似 Python 的语言编写的,称为 Skylark。Skylark 比 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 文件到没有它们的仓库中。
检查上面的列表,我们看到一个名为 `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` 访问。
例如,我们可以通过以下方式查看 `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` 文件,你可以将其放置在其文件系统根目录中,以将外部资源适应 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_compile` 规则传递一个或多个 `proto_language` 规范。`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` 中定义的 Protobuf 定义。
四个 `Greeter` 服务器实现(C++、Java、Go 和 C#)。rules_protobuf 已提供这些示例实现,因此我们将直接使用它们。
2.3:Protobuf 定义
GreeterTimer 接受一个一元 `TimerRequest`,并流式传输回一系列 `BatchResponse`,直到所有消息都已处理完毕,此时远程过程调用完成。
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 仓库。我们在 `//bzl` 包中加载主入口 Skylark 文件 `rules.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 开发略有不同。特别是,无需遵循典型的 `GOCODE` 布局,即包含 `src/`、`pkg/`、`bin/` 子目录。
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` 仓库并从源代码构建 `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` 是一个外部 `java_proto_library` 规则,它在我们自己的工作区中生成 Greeter 服务客户端存根。
最后,我们命名可执行 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 服务器(一次一个)。
~/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 项目通过 `grpc_gateway_proto_library` 和 `grpc_gateway_binary` 规则完全支持 grpc-gateway 项目,因此你可以轻松地将 gRPC 应用程序与 HTTP/1.1 网关桥接。
有关更多信息,请参阅支持的语言和 gRPC 版本的完整列表。
就这样……大功告成。祝你远程过程调用愉快!
Paul Johnston 是 PubRef (@pub_ref) 的负责人,这是一家为科学通信工作流程提供解决方案的供应商。如果您有组织需要 Bazel、gRPC 或相关技术方面的帮助,请联系 pcj@pubref.org。谢谢!