gRPC是用于远程过程调用的开源框架。 在Yandex.Market中,gRPC用作REST的更方便替代方法。 为市场合作伙伴运行工具开发服务的Sergey Fedoseenkov分享了他使用gRPC作为在Java和C ++服务之间建立集成的协议的经验。 从该报告中,您将了解到在REST之后开始使用gRPC时如何避免常见问题,如何返回错误,实现跟踪,调试查询以及测试客户端调用。
最后 ,是该报告的非正式记录。
-首先,我想向您介绍有关Yandex.Market的一些事实,它们将作为报告的一部分。 第一个事实:我们用不同的语言编写服务。 这对客户提出了服务要求。
而且,如果我们有Java服务,那么如果该服务的客户端也是一个加号或一个加号,那就太好了。

我们拥有的所有服务都是独立的,没有计划整个市场的大型发行版。 微服务将独立发布,这里的向后兼容性对我们来说很重要,因此协议可以支持它。
第三个事实:我们同时拥有同步和异步集成。 在报告中,我将主要讨论同步。
我们使用了什么? 当然,现在,我们集成的基础是REST或类似REST的服务,它们通过HTTP 1.1交换XML / JSON。 还有XML-RPC-我们主要在与Python代码集成时使用它,即Python具有内置的XML-RPC服务器。 将其部署到那里足够方便,我们对此提供了支持。
我们曾经有CORBA。 幸运的是,我们放弃了它。 现在主要是REST和HTTP上的XML / JSON。

同步集成存在现有协议的问题。 我们遇到此类问题,并尝试使用gRPC对其进行处理。 这些问题是什么? 如我所说,我希望有不同语言的客户。 建议仍然不必自己编写它们。 而且,总的来说,如果客户端可以是同步的又可以是异步的,这将很酷-这取决于服务用户的目标。
我还希望我们使用的协议足够好地支持向后兼容性:这对于并行独立发行版非常重要。 我们所有的版本都是向后兼容的,我们不会破坏反馈。 如果您破坏了它,这是一个错误,您只需要尽快修复它。
还需要一种一致的错误处理方法:提供REST服务的每个人都知道您不能仅使用HTTP状态。 他们通常不允许对问题进行详细描述,您必须输入其某些状态和详细信息。 在REST服务中,每次您必须对此进行不同的处理时,每个人都会介绍自己对这些错误的实现。 这并不总是很方便。
我还想在客户端进行超时管理。 再次,使用HTTP的人会理解,如果我们在客户端设置超时并且超时,则客户端将停止等待请求完成,但是服务器将不知道任何有关该请求的信息并将继续执行该请求。 此外,中间有许多设置全局超时的代理。 客户端可能根本不了解它们,并且对其进行配置并不总是那么琐碎。
最后是文档问题。 并不总是很清楚从何处获得REST资源或某些方法的文档,它们接受哪些参数,可以传递哪个主体以及如何与服务使用者交流此文档。 很明显,这里有Swagger,但是有了它,并不是所有的事情都是微不足道的。
gRPC 理论
我想谈谈gRPC的理论部分-它是什么,有什么想法。 然后我们将继续练习。

通常,gRPC是抽象规范。 它描述了抽象的RPC(远程过程调用),即具有某些属性的远程过程调用。 现在我们将列出它们。 第一个属性是支持单次呼叫和流式传输。 也就是说,实现此规范的所有服务都支持这两种选择。 下一项是元数据的可用性,也就是说,与有效负载一起,您可以传递某种元数据-有条件地发送标头。 并且-支持取消请求和超时。
它还假定消息和服务本身的描述是通过某种接口定义语言或IDL进行的。 该规范还描述了基于HTTP / 2的有线协议,即gRPC假定它仅适用于HTTP / 2。

大多数情况下都有一个典型的gRPC实现。 我们也使用它,现在我们将看到它。 原始格式用作IDL。 用于proto编译器的gRPC插件使您可以从proto描述中获取生成的服务的源。 并且有使用不同语言的运行时库-Java,C ++,Python。 通常,几乎所有流行的语言都受支持,存在针对它们的运行时库。 并且,作为服务之间交换的消息,将使用原型消息,即根据原型方案的风格化消息。

我想深入探讨一些特定功能。 他们在这里。 强类型(即原始消息)是强类型的消息。 那些曾经使用protobuf的人知道,您可以在其中用类型描述消息中的字段。 类型既存在基本类型,也存在字符串,字节数组。 它们可以是标量,可以是矢量。 而且,实际上,消息可以作为字段包含其他消息,这非常方便,通常可以表示任何模型。

关于向后兼容性。 我想指出的是,proto IDL是一种开箱即用的格式,也就是说,它被构想为具有向后兼容性的积压,而Google发布了proto3版本,与proto2相比,proto3进一步提高了向后兼容性。 那里还有各种各样的规范,可以更改的方式和内容,以便在某些不平凡的情况下保持向后兼容性。
实际上,可能存在默认值,您可以添加新字段,并且使用者不需要更改任何内容。 proto3中的所有字段都是可选的,例如可以删除,访问远程字段不会在客户端上引起错误。

gRPC的另一个功能是使用原型编译器和基于原型说明的gRPC插件生成客户端和服务器。 在编写代码时,有一种选择使用哪个客户端的可能性。 也就是说,根据您编写的代码类型,选择异步或同步客户端。 例如,异步客户端非常适合于反应式代码。 这个机会适用于任何语言。 也就是说,一旦编写了原型说明,之后就可以为任何语言生成一个客户端,而无需以某种方式单独开发它们。 您可以仅作为原型描述来分发服务接口。 任何消费者都可以为自己创造一个客户。

关于取消请求和截止日期,我想指出可以在服务器和客户端上取消请求。 如果我们了解所有内容,则无需进一步满足要求,那么我们可以取消它。 可以根据要求设置超时。 在gRPC中,大多数运行时库都使用期限作为超时的概念。 但实际上是一样的。 也就是说,这是请求应完成的时间。
最有趣的是,服务器既可以找到有关取消请求和超时的信息,又可以停止执行请求。 这很酷,在我看来,其他地方都没有。
关于文档,我想指出的是,由于IDL中的gRPC使用了原始格式,因此这是常规代码。 您可以在此处写评论,包括非常详细的评论。 而且,您需要了解,为了与您的服务集成,用户需要在家中拥有此原型格式,并且该格式会与他们一起获得评论,他们不会躺在其他地方。 非常方便。 您可以扩展此描述,也就是说,它是一个非常方便的功能,使文档位于代码旁边,就像它可以以javadoc或任何其他注释的形式位于方法旁边一样。
gRPC一元调用。 练习
让我们继续前进,看看一些练习。 使用gRPC的最基本示例是所谓的一元调用或单次调用。 这是一种经典的方案-我们向服务器发送请求,并从服务器获得一个响应。 看起来这在HTTP中有效。

考虑我们做的回声服务的例子。 服务器将以加号编写,客户端将以Java编写。 这里使用了经典的平衡电路。 也就是说,客户端寻址平衡器,然后平衡器已经选择了特定的后端来处理请求。
我想引起注意-由于gRPC在HTTP / 2上运行,因此使用了一个TCP连接。 而且,各种流通过它。 在这里,您可以看到客户端和平衡器之间的连接建立一次并保持持久,然后平衡器为每个呼叫平衡不同后端上的负载。 如果您看的话,它是这样发生的,并且如果消息已分发,也是如此。

这是我们的原始文件的示例代码。 您会注意到,我们首先描述了消息,即我们有EchoRequest和EchoResponse。 它只有一个字符串字段来存储消息。
第二步,我们描述我们的程序。 输入过程接受EchoRequest,结果返回EchoResponse,一切都很琐碎。 这是gRPC服务和将要跟踪的消息的描述。

例如,让我们看一下加号情况。 它分为三个阶段。 在第一阶段,我们的任务是生成消息源。 在这里,我们正在与这个团队合作。 我们调用原始编译器,将原始文件传递到输入,指示将输出文件放置在何处。
第二队。 我们还以相同的方式生成服务。 与前一个命令的唯一区别是我们传递了插件,并根据原型格式的描述生成了服务。
第三步-我们将所有这些收集在一个binar中,以便可以启动我们的服务器。
另一个标志被传递给链接器,称为grpc ++ _ Reflection。 我要指出-gRPC服务器具有这样的功能,即服务器反射。 它使您能够探索该服务具有什么样的服务,RPC调用和消息。 默认情况下,它是关闭的,只有手头有原型格式,您才能访问该服务。 但是,例如,对于调试而言,这非常方便,无需手头的原型格式,只需打开具有反射功能的服务器即可立即接收信息。

现在让我们看一下实现。 该实现也是简约的。 也就是说,我们的主要任务是实现生成的echo服务。 它具有一种getEcho方法。 它只是生成消息并将其发送回去。 状态正常-成功状态。
接下来,我们创建ServerBuilder,在其中注册我们的服务,我们将其构建得更高一些。

现在,我们开始并等待传入的请求。

现在,让我们看看Java中的客户端。 我们收集gradle。 我们的任务是首先连接protobuf插件。
我们需要为服务拖动一些基本的依赖项,在编译阶段就需要它们。
我还想指出,有一个运行时库。 对于Java,它使用netty作为服务器和客户端,它支持HTTP / 2,非常方便且高性能。
接下来,我们配置原型编译器。 不需要在Java本地安装编译器本身;可以从构件中获取编译器。
插件也一样。 对于Java,在本地不需要。 您可以拖动工件。 简单配置它很重要,这样对于所有混洗也都称为它,以便生成存根。

让我们继续Java代码。 在这里,我们是第一个创建服务存根的人。 这是Java提供Channel的任务。 运行时库中有一个ChannelBuilder,我们可以使用它来构建此通道。 在这里,为简单起见,我们手动打开了纯文本,但是HTTP2和gRPC默认情况下会加密所有内容并使用TLS。
我们有一个客户端存根,此处生成了一个同步客户端。 同样,您可以生成一个异步客户端,还有其他选项。
接下来,我们创建原型请求,即构造一个原型消息。

就是这样,发送它,在我们的客户端上,我们调用getEcho并打印结果。 一切都很简单。 如您所见,需要大量代码,并且已集成。
gRPC流。 练习
现在让我们看一个更高级的东西,这就是流。 我将告诉您它的工作原理,然后再告诉您如何使用它。

流客户端服务器在架构上看起来差不多。 也就是说,我们在客户端和平衡器之间建立了持久的连接。 然后差异开始。 流式传输的本质是将客户端连接到某个最终后端,并通过该连接保存连接。 也就是说,它像这样继续进行。 等等。 在这里,我要单独指出,平衡器对于流式传输并不常见,也就是说,您需要了解流式传输请求的寿命很长。 也就是说,您可以打开它们并长时间交换消息。 这些消息将通过平衡器,但是实际上,它们总是到达相同的后端。 尚不清楚为什么根本需要它。
一种常见的做法是,例如,当服务是纯流式传输或主要是流式传输时,则使用服务发现。 GRPC有一个扩展点,可以在其中嵌入服务发现。

为了实现流媒体服务,我们需要什么? 我们具有相同的原始格式。 我们正在添加另一个RPC,在这里您可以注意到我们在请求之前和响应之前添加了两个关键字。 因此,我们声明了EchoRequest和EchoResponse流。

更有趣的开始。 为了使流服务能够正常工作,我们的编译不会发生任何变化。 我们的下一个任务是在Echo服务中覆盖将使用流的新方法。 对于服务器,这一切都比较容易。 也就是说,我们可以不断地从信息流中读取信息并回答某些问题。 我们可以异步响应。 也就是说,它们是独立的,可以写入流,可以读取流,在这里,对于简单的场景,一切都很简单。

这是现在的读数,这是录音。

在Java客户端中,事情要复杂一些。 在那里您无法使用任何同步API,也就是说,它仅适用于流。 在那里使用了异步API。 也就是说,我们的任务是实现观察者模板。 那里有一个StreamObserver接口。 它包含三种方法:onNext,onCompleted和onError。 在这里,为简单起见,我仅实现了onNext。 只有当答案从服务器传给我们时,它才会抽搐。

在这里,我只是将线程之间的消息传递放入队列中。

有什么区别? 而不是blockingStub,我们只是制作newStub。 这是一个异步实现,只能与Observer一起使用。 实际上,您可以在Observer上进行一元调用,只是不太方便。 至少我们没有那么积极地使用它。
接下来,我们构造观察器。
然后我们进行RPC调用。 我们将ResponseObserver传递给输入,并在输出处向我们发出RequestObserver。 此外,我们可以在RequestObserver上进行调用,从而将消息传输到服务器。 并且我们的ResponseObserver将抽搐和处理消息。
这是一个例子。 我们只是打个电话。 呼叫onNext,在那儿传递请求。
离队列更远,我们等待服务器响应并打印。

我想提请注意一个事实,我们负责执行流传输的人员在这里的任务是正确处理此RequestObserver的关闭。 也就是说,如果发生错误,则必须对其调用onError方法;如果成功完成,则当我们认为流可以关闭时,必须调用onCompleted方法。

我们继续前进。 什么是流媒体应用程序? 这是更高级的事情,不是事实对每个人都直接有用,但有时会使用。 也就是说,第一个是下载和上传大量数据。 服务器或客户端可以在某些部分中产生数据。 这些部分可能已经以某种方式在客户端或服务器上分组了。 也就是说,您已经可以在此处进行其他优化。
而且,流传输方案非常适合服务器推送。 您需要了解,当我们进行双向流传输时,我认为这是最极端的选择。 也许在一个方向上流式传输。 例如,从客户端到服务器或从服务器到客户端。 对于从服务器到客户端的服务器,我们可以连接到某个服务器,服务器将向我们发送小便,为此,我们不需要定期轮询。
流式传输的下一个优势是绑定到一台计算机。 正如我已经说过的,将为流中的所有消息建立一个端到端连接,并且该连接将绑定到一台计算机上,并且绝对不会切换到任何位置。 因此,首先可以简化某些事情,简化服务器间的同步,此外,您还可以执行事务性的事情。
正如我展示的一个示例,双向流传输是构建自己的某些协议的能力。 足够有趣的事情。 Yandex中有内部队列,它们仅使用双向流。 如果突然有人执行了这些任务,那么就有足够的机会使用它。
, . . . , - , , . . gRPC .
, gRPC.

, . - . gRPC . , , , , , . , runtime- . , . , OK, runtime- .
, Java . . google.rpc.Status 3 : , . , . , . — , , .
error details, , . : , , , stack traces, . , .
— , HTTP , ? . BadRequest . , , error details, .
. , , BadRequest - ( ), - error detail. , , , - . , .

. . , , , . - - , - - , , . . , , Zipkin. , HTTP , — metadata. .
, . , - , , , .
runtime-, - , . Java ClientInterceptor ServerInterceptor. , , . , , , , , - . , - API - . , , , , - . , gRPC, , - . , , - , , , .

- . -. Java . , , - . - , .

. gRPC — . HTTP/2 . - , ? : , . . , gRPC grpc_cli, curl. , . , -, . , gRPC , .
, evans. , CLI: , , , . , . - , , , , , .
- UI — , Postman, — BloomRPC. Postman . Postman, , , . , BloomRPC , .
- , . , , grpc_cli. . , . , , . , . , - - . — .

, , gRPC. . - , - , . Swagger. , HTTP/1 . OpenAPI , . . , HTTP/2, Swagger — .
WSDL — , . . Swagger, , . . -.
, , , , JAX-RS, Java . .
Twirp. ? Go, . . , , Go , gRPC Twirp. ? , gRPC — , , , IDL . proto- , gRPC-. protoc, , .
Twirp . proto- , HTTP/1.1 , JSON. , Twirp Go. , , Java Jetty. , .

? gRPC — REST . , , , , HTTP/2 balancer. service discovery, . gRPC , . .
gRPC — , . CLI, UI. , .
, gRPC. inter-process-. , sidecar pattern. , . , . , -. - , , -. . , , , , - .
, . gRPC . , . , unary-. , .
:
—
C gRPC — , . , , , .
—
Awesome gRPC — GitHub . , , , . . — , .
您可以在几张幻灯片上找到Internet上的许多其他资源。但是我最喜欢这些。演示文稿中经过稍微修改的代码在这里。谢谢你