在WePay上将API从REST迁移到gRPC

 

服务网格系列前几篇文章中,我们讨论了如何设置服务网格基础结构以使微服务和负载平衡架构现代化,以及如何确保服务网格基础结构高度可用,以便我们可以使用其所有功能。随时随地都有出色的功能。

在本文中,我们将把注意力转移到我们的微服务如何使用服务网格相互通信上。具体来说,什么有效负载用于在它们之间发送的请求,以及我们如何从一个迁移到另一个。我们将比较当前基于代表性状态转移(REST)的有效负载和高性能的远程过程调用(gRPC)框架,并克服采用,使用和迁移到gRPC的挑战。

从RESTful到gRPC

如我们之前的文章所述,我们目前在Google Kubernetes Engine(GKE)中运行许多微服务,其中一些微服务与同一数据中心中的其他微服务进行通信,形成微服务的组或图,以实现某些业务逻辑。整个:

 

service_graphs

图1:更大的服务图需要更快的通信和更轻松的API管理

 

由一组这些微服务组成的每个服务图可以在微服务之间进行传递通信,即微服务Service X可能正在向该组中的N个服务发送请求,而N个服务可能正在向M个服务发送请求,并且以此类推。这些微服务之间进行的请求(如图1所示)包括RESTful有效负载,这些有效负载使用“ HTTP / 1.1”发送。

随着基础架构中微服务数量的增加,为微服务提供通信能力的平台的可扩展性和维护性也随之提高。具体来说,当我们开始考虑将任务关键型微服务迁移到gRPC时,我们希望获得多语言和平台支持,可伸缩性,客户端和服务器之间的持久和可重用连接(使用'HTTP / 2'和Linkerd),以及bi转向gRPC进行双向流式传输

 

services_group

图2:图中的一组服务以多种数据序列化格式进行通信

 

具有仅REST的API的旧版服务正在缓慢地采用和采用gRPC,以提高我们整体产品的性能。图2显示了一组服务,它们在REST和gRPC中相互通信,因为我们的服务正在迁移到gRPC。在文章的后面,我们将介绍如何更新服务以接受两种序列化格式,直到将所有API都迁移到gRPC。

随着我们在基础架构中添加了新的微服务和语言,gRPC产品对我们变得越来越重要,特别是在通过基础架构的可伸缩性使开发更容易并且使开发人员更容易进行微服务方面。gRPC文档详细介绍了在每种情况下使用gRPC的重要性,而我们最近召开的gRPC重点会议则更多地讨论了一些用例以及在类似基础架构中可以使用的更重要的功能。

为什么要使用gRPC?

在WePay的微服务世界中,每个微服务都使用带有JSON有效负载的REST作为彼此通信的标准方式。REST在服务到服务的通信中具有其优势:

  • JSON负载很容易理解。
  • 作为成熟的序列化格式,有许多可用的框架可用于使用REST构建服务。
  • REST是非常流行的标准,并且与语言和平台无关。

随着基础架构中微服务数量的增加,服务图变得更加复杂,结果,使用REST进行通信存在痛点和局限性:

  • 例如,在图1中,当客户端依次调用服务X时,服务X可以调用其他服务以向客户端提供响应。
  • 对于服务之间的每个REST调用,都会建立一个新的连接,并且存在SSL握手的开销。这将导致总体延迟增加。
  • 必须使用每种要求的语言为每种服务实现客户端,并且只要API定义发生更改,就必须更新客户端。
  • JSON有效负载是简单的消息,其序列化和反序列化性能相对较慢。

考虑到这些痛苦点,gRPC似乎是我们改善基础架构中微服务通信的不错选择。

在gRPC中,客户端应用程序可以直接在不同机器上的服务器应用程序上调用方法,就好像它是本地对象一样,从而使创建分布式服务变得更加容易。

 

grpc_server_client

图3:gRPC客户端将原始请求发送到gRPC服务并接收原始响应(

 

gRPC有其自身的优点:

  • 与RPC协议类似,gRPC基于定义服务及其方法的思想,可以使用其参数和返回类型远程调用该服务及其方法。
  • 构建低延迟和高度可扩展的微服务调用图
  • gRPC默认情况下使用协议缓冲区,与JSON相比,它们提供了更好的性能。
  • 能够自动生成多种语言的客户端存根,从而减少了构建和维护客户端库的责任。

使用Protobufs的请求

Protobuf是文本文件,用于定义消息的结构并具有“ .proto”扩展名。它们紧凑,强类型并支持向后兼容。它们用于定义服务以及请求和响应数据的结构和类型。

 

sample_protobuf

代码1:具有单个RPC方法“ SayHello”的HelloWorld gRPC服务

 

作为有效载荷的Protobuf具有更好的序列化/反序列化为二进制格式。这使其比JSON更快。而且,客户端可以接收类型化的对象,而不是自由格式的JSON数据。

 

proto_vs_json

图4:JSON与协议缓冲区

 

每种语言都有其自己的protobuf编译器,可将这些文件转换为代码。例如,如果我们试图获取Java中生成的代码以用于代码1中的服务,则编译器将生成:

  1. 可以扩展为实现RPC方法的基本实现。

 

grpc_server_base_class

代码2:用Java自动生成的HelloWorld实现基类

 

  1. 客户端可以实例化以调用服务器的存根

 

grpc_client_stub

代码3:Java客户端的HelloWorld存根

 

  1. 在服务器和客户端之间交换的请求/响应消息的对象。

 

generate_code

代码4:在Java中为protobuf消息生成代码

 

连接重用

gRPC通信通过HTTP / 2进行HTTP / 2通过使用标头字段压缩并允许在同一连接中进行并发交换,可以更有效地利用网络资源并减少延迟。在gRPC中,连接和交换称为通道和调用。通过这种方式,gRPC有助于减少为每个请求创建新连接的等待时间,这是将REST用于更复杂的服务图的限制之一。

如何从REST迁移到gRPC?

基于上述好处,我们决定将REST服务迁移到gRPC。我们考虑了许多迁移策略。其中一种方法是构建gRPC应用程序,并使用grpc-gateway生成反向代理服务器,该服务器将RESTful JSON API转换为gRPC。这要求在protobufs中的gRPC定义中添加自定义选项,并添加一个运行此反向代理服务器的附加容器。

将服务迁移到gRPC的一些重要要求包括:

  1. 需要最少的返工。
  2. 不更改当前的构建和部署管道。
  3. 第一步是启用REST和gRPC,然后逐步将所有通信迁移到gRPC。这样,现有客户端可以继续通过REST与服务器通信,直到迁移它们为止。
  4. 符合我们的gRPC与REST微服务相同的基础平台。例如,REST微服务继承了一些常见的默认端点,这些端点可以帮助我们监视这些微服务。gRPC服务也应支持相同的功能。

基于这些标准,我们决定将gRPC服务器作为使用REST框架的应用程序中的线程运行。

 

grpc_structure_wepay

图5:WePay上的gRPC服务和客户端的结构

 

结果,我们构建了一个可以插入到我们的微服务中的共享库。该共享库包括一个监视服务,该服务实现RPC方法以检查gRPC服务的运行状况。共享库提供了一个“ GrpcServerBuilder”,其中包含:

  1. 能够默认添加拦截器。通用拦截器将包​​括可以执行异常处理,请求/响应跟踪,身份验证等。
  2. 默认情况下添加通用监视服务,该服务提供rpc方法来检查服务的运行状况及其依赖性。

开发人员使用共享库提供的GrpcServerBuilder,为微服务添加gRPC服务实现并构建gRPC服务器,这将确保监控我们所有微服务运行状况的标准模式。

使用服务网格进行通信

正如我们在早期博客中提到的在WePay上使用Linkerd作为服务网格代理一样,Linkerd开箱即用地支持HTTP / 2和gRPC。但是将gRPC调用路由到适当的Kubernetes服务的设置很棘手。在服务网格中,通过在路径中提供服务名称,将来自客户端的REST调用重定向到Kubernetes中运行的服务。例如,如果我们有一个foo运行REST服务的名为“ 的Kubernetes服务,则使用Linkerd,可以使用路径“ foo/将客户端调用路由到该服务

gRPC使用HTTP / 2作为传输。gRPC服务与客户端之间的请求/响应遵循规范请求的主要字段之一是“路径”。请求的路径设置为:

<package_name>.<grpc_service_name>/<method_name>

Linkerd可以使用路径或标头作为标识符来将请求从客户端路由到服务。对于gRPC,选择标识符时存在服务器方面的挑战:

  1. 通常,Kubernetes服务名称不必与gRPC服务名称相同。例如,可以有一个名为“ greeter”的Kubernetes服务,该服务运行名为“ GreeterService”的gRPC服务。
  2. 运行gRPC服务器的应用程序可以运行一个或多个gRPC服务实现。例如,默认gRPC监视服务与主要gRPC服务实现一起添加到了我们的gRPC微服务中,以在WePay上构成一个完整的gRPC微服务。

这些情况使得很难将路径用作标识符。我们通过强制客户端将自定义标头“服务”设置为Kubernetes服务名称来解决该问题。并且Linkerd配置为将标头“ service”用作gRPC调用的标识符。

 

grpc_call_routing

图6:演示使用请求标头作为标识符的gRPC调用路由。


在为客户端构建的公共库中,我们添加了一个选项,将标头设置为目标Kubernetes服务名称。Linkerd使用此标头和标识符将请求从客户端路由到服务。

CI / CD gRPC服务的生命周期

gRPC服务具有其自己的生命周期:

  1. 以protobuf的形式编写服务以及请求和响应的定义。
  2. 在使用protobuf文件生成用于实现客户端的代码之前,请对其进行验证。
  3. 实施服务器和客户端。

在确定协议缓冲区的生命周期时,需要考虑一些标准。

  • 在哪里存储protobuf文件?
  • 如何生成代码并将其用于实现服务器和客户端?
  • 如果原型文件发生更改,如何识别客户端已更新为使用最新的protobuf定义?

本节介绍如何满足以下条件并设置gRPC服务的生命周期。

 

wepay_grpc_lifecycle

图7:WePay上gRPC的生命周期。

 

由于我们在WePay使用面向服务的体系结构,因此我们需要为许多服务编写和维护protobuf文件。我们决定维护一个中央git存储库,以存储包含WePay上所有gRPC服务器定义的protobuf文件。

protos/
  |-<service-x>/
     |- **/*.proto
  |-<service-y>/
     |- **/*.proto
  |-commons/
     |- **/*.proto

然后,使用我们的持续集成(CI)服务器中的prototool验证这些原型文件,然后在git存储库中的主版本中创建发布标签以进行更改。

要实现gRPC服务器/客户端,我们需要从protobufs生成代码。为此,我们使用release标记将protobuf提取到应用程序存储库中,并使用protoc编译器以所需的语言生成代码,并使用它们来实现服务器/客户端。

当前,我们使用RAML,Swagger等工具生成微服务的REST API文档。我们常见的微服务构建过程生成API文档。同样,我们为微服务建立了构建过程,该过程也配置为使用protoc -gen-doc插件生成gRPC文档

迁移到gRPC的挑战

gRPC具有许多优点,但是并非REST中的所有微服务都可以迁移到gRPC。例如,迁移外部服务可能需要使用REST API的客户端进行返工。

在将现有的REST微服务迁移到gRPC时,对我们来说gRPC面临一些开发挑战,而以下是最重要的挑战。

浏览器支持

直到最近,gRPC还不支持浏览器。当前,让浏览器客户端访问gRPC服务的一种流行方法是使用grpc-web浏览器客户端使用特殊的网关代理连接到gRPC服务。我们没有将grpc-web用于外部服务,因为当我们做出此决定时,GA版本不可用。我们的前端也有许多JSON模式,将所有这些转换为protos并非易事。此更改还需要更新我们所有的SDK。

Protobuf限制

我们的某些API需要识别请求/响应数据之间的区别:

  • 字段不存在
  • 存在空值的字段
  • 存在非空值的字段。

使用proto3,我们无法轻松识别这些差异。虽然,我们可以使用Google包装器来识别是否设置了该字段。我们要标识空值的潜在用例是请求的更新类型,服务器在其中接收将某些字段设置为空的请求。即,如果后端中存在现有消息,并且我们需要将其中一个字段设置为null(在protobuf世界中也意味着清除该字段)。我们可以在这些类型的场景中使用场掩码

gRPC和HTTP框架

当前,使用REST框架编写的微服务假定请求处理程序是HTTP堆栈的一部分,并具有执行中间件的能力,例如身份验证检查,请求/响应日志记录,度量跟踪等。

没有任何现有的gRPC框架与我们使用的HTTP框架具有同等的功能。但是,gRPC提供了“拦截器”来启用这些中间件。我们为gRPC服务器所需的每个功能构建拦截器,并将它们打包为所有这些微服务都将使用的通用库。

Protobuf版本

协议缓冲区语言具有两种语法版本:Proto2和Proto3。gRPC支持两个版本。Proto2和Proto3相似,但差别不大。

特征原型2原型3
字段可以设置为NULL 没有 没有
必填消息字段 没有
能够为字段设置自定义默认值 删除默认值。设置为默认值的原始字段不会序列化
JSON编码 使用二进制protobuf编码 JSON编码的添加
UTF-8检查 不严格 严格执行
能够识别是否不包含缺少的字段,或者是否为其分配了默认值。 不可以。在Proto3中,当我们使用基本字段时,我们无法区分设置的默认值和未设置的字段。我们可以通过使用包装器(众所周知的原型)而不是基元来解决此问题。

表1:proto2和proto3之间的比较

 

很难决定要使用哪个版本的协议缓冲区。如上面比较表(表1)所述,从proto2到proto3有一些向后不兼容的更改。因此,最好不要更新是否已使用proto2构建gRPC服务。如果我们想使用诸如设置自定义默认值,识别缺失字段,必填消息字段之类的功能,那么我们可以使用proto2。如果我们想利用新功能,并为仅在proto2中可用的功能使用替代功能,则可以选择proto3。

结论

总之,根据微服务的数量,性能和可维护性要求,我们决定使用gRPC构建新的微服务,并在适用的情况下将所有现有微服务迁移到gRPC。

这给我们:

  1. 一次编写的强类型服务和请求/响应定义,并以多种语言生成代码。
  2. 使用HTTP / 2的性能更好,其中可重新使用连接。
  3. 在构建客户端库方面要少维护一件事。
  4. 支持流式调用以增加数据负载。

现在,我们能够改善整个基础架构中的服务到服务的通信,我们需要保持这些服务正在运行的基础架构和平台为最新并不断改进。

在下一篇文章中,我们将研究如何管理基础结构的生命周期,以实现持续的基础结构改进,而停机时间为零,而又不影响生产服务的整体运行状况和性能。

posted @ 2020-11-08 12:43  DaisyLinux  阅读(321)  评论(0编辑  收藏  举报