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 如何工作的背景信息(第 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 是其他功能强大且快速的构建工具的灵感来源,包括 PantsBuck。Bazel 在概念上很简单,但有一些核心概念和术语需要理解

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

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

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

  4. 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 文件的仓库中。

查看上面的列表,我们看到一个名为 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 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 会经历以下三个阶段

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

  2. 分析:对于图中的所有节点,此构建实际需要哪些节点? 我们是否有所有必要的资源?

  3. 执行:执行依赖关系图中的每个所需节点并生成输出。

希望您现在对 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 接受一个一元 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 函数,其中包含我们要使用的语言(在本例中为 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 有点不同。特别是,不必遵守具有 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 来实际构建客户端二进制文件时发生的事情

  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. 使用客户端 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",
    ],
)
  1. :timer_protos 是本地定义的 java_proto_library 规则。

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

  3. 最后,我们为可执行 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_librarygrpc_gateway_binary 规则完全支持 grpc-gateway 项目,因此您可以轻松地将您的 gRPC 应用程序与 HTTP/1.1 网关桥接起来。

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

就这样了。祝您过程调用愉快!

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