0%

微服务进程间通信实现

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

微服务通信的难点

在微服务架构中,不同的服务是托管在不同的代码库,运行在不同的进程甚至是机器上的,这会比单体架构中服务之间进行通信付出更大的成本

目前有很多进程间通信的技术可供开发者选择,服务可以基于同步请求/响应的通信机制,比如HTTP RESTFUL 或者 gRPC。另外也可以使用具有异步的消息队列,比如AMQP 或者 STOMP。消息的格式也不尽相同,可以选择具有可读性的JSON,也可以使用更加高效的、基于二进制的Protocol Buffers

在通信中最重要的东西其实是协议Protocol,只要双方进行通信,那就都是基于某种Protocol实现的。比如说使用MQ进行通信,以Pulsar进行举例,那么一条消息的ID通常是以下格式

分区ID:分块ID:偏移量

如果使用HTTP进行通信,那么其实URL Header Body如何编码及解析 也是一种协议 比如HTTP HEADER必带的某些属性,比如版本 以及空行等等

语义化版本控制规范(Semvers)

Semvers要求API版本号由三个部分组成

  • MAJOR 当你对API进行不兼容的更改时
  • MINOR 当你对API进行向后兼容的增强时
  • PATCH 当你进行向后兼容的错误修复时

有几个地方可以在API中使用版本号,比如实现REST API,则可以使用主要版本作为URL路径的第一个元素;或者如果要实现使用消息机制的服务,则可以在发布的消息中包含版本号。这些做法都是为了正确地为API设置版本,并以受控的方式更改它们

例如 REST 的API路径为 /v1/...为前缀 就是进行主要并且不向后兼容的改变

两类消息

消息的格式大体可以分为文本和二进制两大类

文本:JSONXML,好处是它们的可读性很高,同事也是自描述的。XMLJSON都可以允许消息的接收方只挑选它们感兴趣的值,而忽略掉其他。因此,对消息的修改可以做到很强的后向兼容性

弊端主要是消息往往会冗余过长,特别是XML。消息的每一次传递都必须反复包含除了值意外的属性名称,这样会造成额外的开销。另一个弊端是解析文本引入的额外开销,尤其是消息比较大的时候,因此在对性能和效率比较敏感的场景下,更得倾向于基于二进制格式的消息
一个典型的XML数据如下

1
2
3
4
5
6
7
8
9
10
11
12
<root>
<person>
<name>John</name>
<age>30</age>
<address>
<street>123 Street</street>
<city>New York</city>
<country>USA</country>
</address>
</person>
</root>

一个典型的JSON数据如下

1
2
3
4
5
6
7
8
9
10
11
{
"person": {
"name": "John",
"age": 30,
"address": {
"street": "123 Street",
"city": "New York",
"country": "USA"
}
}
}

二进制消息:有几种不同的二进制格式,比如Protocol buffersThrift,这两种格式都提供了一个强类型定义的IDL(接口描述文件),用于定义消息的格式。编译器会自动根据这些格式生成序列化和反序列化的代码。因此你不得不采用API优先的方法来进行服务设计

Protocol Buffers使用tagged filed(带标记的字段)来标记数据和格式

REST成熟度模型

REST成熟度模型

  • LEVEL 0 客户端只是向服务端发起POST 请求 进行服务调用 所有操作都通过POST进行实现
  • LEVEL 1 引入了资源的概念。要执行对资源的操作,客户端需要发出执行要执行的操作,和包含任何参数的POST请求 也是所有操作都通过POST实现
  • LEVEL 2 使用HTTP动词来执行操作,譬如GET 获取 POST创建 PUT更新
  • LEVEL 3 在由GET请求返回的资源中包含链接,这些链接能够执行该资源允许的操作。例如客户端通过订单资源的链接取消某一个订单,或者发送GET请求去获取该订单。这样也可以避免在客户端代码中写入硬链接的URL

在一个请求中获取多个资源的挑战–GraphQL

REST资源通常以业务为导向,因此在设计REST API时的一个常见问题就是如何使得客户端可以在单个请求中检索多个相关的对象。假设客户端想要检索Order和这个Order的Consumer,纯REST API要求客户端至少发出两个请求,一个用于Order,另一个用于Consumer。更复杂的情况需要往返并且遭受更多的延迟

一个解决方案是API允许客户端在获取资源时检索相关资源,例如客户可以受用GET /orders/order-id-1345?expand=consumer 检索Order及其consumer 。这种方法在许多场景中都很有效,但对于更加复杂的场景来说,它通常是不够的,实现它也可能很耗时。

这就导致了GraphQL的出现,它们旨在支持高效的数据获取,以下是一个典型的GraphQL请求数据 来进行参数的请求

1
2
3
4
5
6
7
{
"hero": {
"name": "Luke Skywalker",
"height": 1.72,
"mass": 77
}
}

REST优缺点

优点:
1.简单 开发者熟悉
2.可以使用浏览器拓展或者命令行进行测试 比如postman和curl
3.直接支持请求/响应方式的通信
4.HTTP对防火墙友好
5.不需要中间代理 简化结构

缺点:
1.只支持请求/响应方式的通信
2.可能导致可用性降低 由于客户端和服务直接通信而没有使用代理来缓冲 因此它们必须在REST API调用期间都保持在线
3.客户端必须知道服务实例的位置 (URL) 这是现代应用程序中的一个重要问题
4.在单个请求中获取多个资源具有挑战性
5.有时很难将多个更新操作映射到http动词

grpc:使用REST的一个挑战是由于HTTP仅提供优先数量的动词,因此设计支持多个更新操作的REST API并不容易,避免这个问题的进程间通信方式就是GRPC 这是一个用于编写跨语言客户端和服务端的框架

grpc是一种基于二进制消息的协议,可以通过Protocol Buffer的IDL来定义grpc 的api 可以使用protocol buffer编译器来生成客户端的桩 和服务端的骨架 ,分别称为stubskeleton,编译器可以为各种语言生成代码,包括java c ``nodejsgolang

客户端和服务器底层使用的是http 2协议,grpc api由一个或者多个请求/响应消息定义组成,服务定义类似于接口,是强类型方法的集合,除了支持简单的请求/响应RPC之外,grpc还支持流式rpc,分为客户端流式、服务端流式、双向流式等等,并以protocol buffer格式交换二进制的消息数据,protocol buffer是一种高效且紧凑的二进制标记格式。protocol buffers消息的每个字段都有编号,并且有一个类型代码,消息接收方可以提取所需的字段,并跳过其无法识别的字段,因此grpc使api能够在保持向后兼容的同时进行变更

grpc的好处

  • 设计具有复杂更新操作的API很简单
  • 具有高效、紧凑的进程间通信消息,效率很高,尤其是在交换大量消息的时候
  • 支持在远程过程调用和消息传递过程中使用双向流式消息方式
  • 实现了客户端和用各种语言编写的服务端之间的互操作性

弊端

  • 基于REST JSON的API机制相比,请求的客户端需要做更多的工作,比如定义对应的protocol buffer文件
  • 老的防火墙可能不支持http 2

使用服务发现

只要我们进行某些远程调用,不管是rpc还是restful api,为了发出请求,我们都需要知道服务实例的网络位置,我们把这个过程叫做服务发现。

通常RPC调用的服务发现通过服务注册与发现中心来实现,而restful api的服务发现是通过DNS + IP和端口来实现的

在传统的应用程序中,服务实例的网络位置通常是静态的。例如,代码可以从偶尔更新的配置文件中读取网络位置。但在现代的基于云微服务的应用程序中,通常不那么简单,IP地址和服务实例并不是强关联的了,某些时候服务实例会动态的销毁、创建,空出IP地址和占用IP地址

因此服务实例需要具有动态分配的网络地址,并且服务实例会因为自动扩展、故障和升级的原因进行动态的更改

服务发现在概念上非常简单:关键是一个服务注册表,这是包含服务实例网络位置信息的一个数据库,当服务实例启动和停止时,服务发现机制会更新服务注册表。当客户端调用服务时,服务发现机制会查询服务注册表以获取可用服务实例的列表,并将请求路由到其中一个服务实例

实现服务发现有一下两种主要方式

  • 服务及其客户直接与服务注册表交互,比如CONSULETCD等等 也就是自注册+客户端服务发现 服务实例掉哟个服务注册表的注册API来注册起网络位置,并且会定期进行健康检查,通过心跳机制防止过期;当客户端想要调用服务的时候,会查询服务注册表一获取服务实例的列表。为了提高性能,客户端可以选择缓存服务实例,然后通过负载均衡算法来选择服务实例之后发出请求。并且可以处理多平台部署的问题。比如,有一些服务在K8S上部署,其余服务在遗留环境中运行。在这种情况下,使用consul就能同时适用于两种环境,而基于k8s的服务发现仅能用于部署在K8S平台上的部分服务
  • 基础设施来处理服务发现 Docker\K8s,部署平台为每个服务提供DNS 虚拟IP 和解析的DNS名称等等 服务注册、发现、路由完全都通过部署平台进行处理

消息队列

消息队列和上述提到的不同的最大的点就是 不管是HTTP RPC 还是服务注册与发现中心

它们的调用都是同步的 即发出请求 立马就能得到结果

而消息队列可以是异步的 生产者生产完成消息之后 消费者并不一定需要立马去进行消息的消费和后续处理

异步 是消息队列最重要的功能之一

具体的消息队列学习内容可以看笔者之前的关于消息队列系统学习的一篇博客

简单来说 消息队列就是

通过生产者、消息中间件、消费者这几个组件实现点对点和发布订阅、PULL/PUSH几种模式

消息队列又根据有没有消息中间件这个组件分为无代理消息和有代理消息

无代理消息:

  • 允许更轻的网络流量和更低的延迟 少了两次转发
  • 避免了消息中间件可能成为性能瓶颈的可能性
  • 不需要维护消息 降低操作复杂度

弊端:

  • 服务之间需要知道彼此的位置 必须采用服务发现机制
  • 导致可用性降低 因为在交换消息时 消息的发送方和接收方都必须在线