RSS

使用 bazel 和 rules_protobuf 构建 gRPC 服务

gRPC 通过提供多种不同语言生成的服务入口,使构建高性能微服务变得更加容易。Bazel 以其强大而快速的多语言构建环境对这些工作提供了补充。

rules_protobuf 扩展了 bazel,使开发 gRPC 服务更加容易。

它通过以下方式实现:

  1. 构建 protoc(协议缓冲区编译器)以及所有必需的 protoc-gen-* 插件。
  2. 构建 gRPC 相关代码编译所需的 protobuf 和 gRPC 库。
  3. 抽象化 protoc 插件调用(您不必一定学习或记住如何调用 protoc)。
  4. 在 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 启发了其他强大而快速的构建工具,包括 PantsBuck。Bazel 概念上很简单,但有一些核心概念和术语需要理解:

  1. Bazel 命令:从命令行调用时执行某种工作的函数。常见的命令包括 bazel build(编译库)、bazel run(运行二进制可执行文件)、bazel test(执行测试)和 bazel query(告诉我构建依赖图的信息)。使用 bazel help 查看所有命令。

  2. 构建阶段:Bazel 在调用 Bazel 命令时经过的三个阶段(加载、分析和执行)。

  3. WORKSPACE 文件:一个必需的文件,定义了项目根目录。它主要用于声明外部依赖项(外部工作区)。

  4. 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 文件“注入”到尚未拥有 BUILD 文件的仓库中。

检查上面的列表,我们看到一个名为 protoccc_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 执行根目录交互,但可以在 $(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,对于 examples 中的所有目标,找到所有依赖项(一个传递闭包集),然后告诉我哪些依赖项依赖于外部工作区 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 会经历以下三个阶段

  1. 加载 (Loading):读取 WORKSPACE 和所需的 BUILD 文件。生成依赖图。

  2. 分析 (Analysis):对于图中的所有节点,哪些节点是本次构建实际需要的?我们是否拥有所有必要的可用资源?

  3. 执行 (Execution):执行依赖图中的每个必需节点并生成输出。

希望你现在已经掌握了足够的 Bazel 概念知识,可以进行高效工作了。

1.6:rules_protobuf

rules_protobuf 是 Bazel 的一个扩展,它负责处理

  1. 构建协议缓冲区编译器 protoc

  2. 下载和/或构建所有必需的 protoc-gen 插件。

  3. 下载和/或构建所有必需的 gRPC 相关支持库。

  4. (按需)为你调用 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:服务

  1. Greeter 服务:这是熟悉的“Hello World”入门示例,它接受一个带有 user 参数的请求,并回复字符串 Hello {user}

  2. 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 接受一个一元 (unary) 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 服务接受一个一元 (unary) 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 函数,指定我们想要使用的语言(在此例中是 javago)。我们还加载了 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 来实际构建客户端二进制文件时,会发生以下情况

  1. Bazel 检查二进制文件依赖的输入(文件)是否已更改(通过内容哈希和文件时间戳)。Bazel 识别出 //proto:go_default_library 的输出文件尚未构建。

  2. Bazel 检查 go_proto_library 的所有必要输入(包括工具)是否可用。如果不可用,则下载并构建所有必需的工具。然后,调用该规则。

    1. 获取 google/protobuf 仓库,并通过 cc_binary 规则从源代码构建 protoc

    2. 通过 go_binary 规则从源代码构建 protoc-gen-go 插件。

    3. 使用 protoc-gen-go 插件调用 protoc,并传入适当的选项和参数。

    4. 确认 go_proto_library 声明的所有输出文件均已实际构建(应位于 bazel-bin/proto/greetertimer.pb.go 中)。

  3. 将生成的 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",
    ],
)
  1. :timer_protos 是一个本地定义的 java_proto_library 规则。

  2. @org_pubref_rules_protobuf//examples/helloworld/proto:java 是一个外部 java_proto_library 规则,它在我们的工作区中生成 greeter 服务的客户端 stub。

  3. 最后,我们指定可执行 jar 的编译时和运行时依赖项。如果这些 jar 文件尚未从 maven central 下载,一旦需要它们,Bazel 将立即获取。

~/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_librarygrpc_gateway_binary 规则,完全支持 grpc-gateway 项目,因此你可以轻松地将 gRPC 应用程序与 HTTP/1.1 网关桥接。

有关更多信息,请参阅 支持的语言和 gRPC 版本完整列表

就这样……结束了。祝你过程调用愉快!

Paul Johnston 是 PubRef (@pub_ref) 的负责人,PubRef 是一家为科学传播工作流程提供解决方案的公司。如果您在 Bazel、gRPC 或相关技术方面有组织性的协助需求,请联系 pcj@pubref.org。谢谢!