微服务架构将应用程序构建为一组服务。这些服务必须经常协作才能处理各种外部请求。
当前有多种进程间通信机制供开发者选择。比较流行的是REST(使用JSON)。但需要牢记“没有银弹”这个大原则。
一个理想的微服务架构应该是在内部由松散耦合的若干服务组成,这些服务使用异步消息相互通信。
微服务架构中的进程间通信概述
交互方式
考虑交互方式将有助于你专注于需求,并避免陷入特定进程间通信技术的细节。
交互方式可以分为两个维度。第一个维度关注的是一对一和一对多。
- 一对一:每个客户端请求由一个服务实例来处理。
- 一对多:每个客户端请求由多个服务实例来处理。
交互方式的第二个维度关注的是同步和异步。
- 同步模式:客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞。
- 异步模式:客户端请求不会阻塞进程,服务端的响应可以是非实时的。
在微服务架构中定义API
服务的API是服务与其客户端之间的契约(contract)。设计良好的接口会在暴露有用功能同时隐藏实现的细节。
服务的API由客户端结构可以调用的方法和服务发布的事件组成。方法具备名称、参数和返回类型。事件具有一个类型和一组字段,发布到消息通道。
API优先设计。服务和它的客户端并不会一起编译。首先编写接口定义,然后与客户端开发人员一起查看这些接口定义。只有在反复迭代几轮API定义之后,才开始具体的服务实现编程。这种预先设计有助于你构建满足客户端需求的服务。
API的演化
语义化版本控制规范(http://semver.org )为API版本控制提供了有用的指导。它是一组规则,用于指定如何使用版本号,并且以正确的方式递增版本号。
语义化版本控制规范(Semvers)要求版本号由三部分组成:MAJOR.MINOR.PATCH。必须按如下方式递增版本号:
- MAJOR:当你对API进行不兼容的更改时。
- MINOR:当你对API进行向后兼容的增强时。
- PATCH:当你进行向后兼容的错误修复时。
如果你正在实现REST API,则可以使用主要版本作为URL路径的第一个元素。
如果你要实现使用消息机制的服务,则可以在其发布的消息中包含版本号。
- 进行次要并且向后兼容的改变
添加可选属性。
向响应添加属性。
添加新操作。 - 进行主要并且不向后兼容的改变
因此服务必须在一段时间内同时支持新旧版本的API。
如果你使用的是基于HTTP的进程间通信机制,- 一种方法是在URL中嵌入主要版本号。
- 另一种选择是使用HTTP的内容协商机制,并在MIME类型中包含版本号。
消息的格式
进程间通信的本质是交换消息。消息通常包括数据。使用跨语言的消息格式尤为重要。
消息的格式可以分为两大类:文本和二进制。我们来逐一分析。
基于文本的消息格式
第一类是JSON和XML这样的基于文本的格式。
好处是:可读性很高,同时也是自描述的。这样的格式允许消息的接收方只挑选他们感兴趣的值,而忽略掉其他。因此,对消息结构的修改可以做到很好的后向兼容性。
弊端是:消息往往过度冗长(特别是XML),且解析文本引入的额外开销(尤其是在消息较大的时候,或者在对效率和性能敏感的场景下需要特别关注)。
二进制消息格式
有几种不同的二进制格式可供选择。常用的包括Protocol Buffers(https://developers.google.com/Protocol-buffers/docs/overview )和Avro(https://avro.apache.org )。
这两种格式都提供了一个强类型定义的IDL(接口描述文件),用于定义消息的格式,对于格式要求严格。编译器会自动根据这些格式生成序列化和反序列化的代码。因此你不得不采用API优先的方法来进行服务设计。此外,如果使用静态类型语言编写客户端,编译器会强制检查它是否使用了正确的API格式。
基于同步远程过程调用模式的通信
image.png代理接口通常封装底层通信协议。有许多协议可供选择以REST和gRPC为例说明。
使用REST
REST是一种(总是)使用HTTP协议的进程间通信机制。
REST中的一个关键概念是资源,它通常表示单个业务对象,例如客户或产品,或业务对象的集合。
EST使用HTTP动词来操作资源,使用URL引用这些资源。
可能遇到的挑战
在一个请求中获取多个资源
REST资源通常以业务对象为导向,例如Consumer和Order。因此,设计REST API时的一个常见问题是如何使客户端能够在单个请求中检索多个相关对象。
此问题的一个解决方案是API允许客户端在获取资源时检索相关资源。例如,客户可以使用GET/orders/order-id-1345?expand=consumer检索Order及其Consumer。请求中的查询参数用来指定要与Order一起返回的相关资源。这种方法在许多场景中都很有效,但对于更复杂的场景来说,它通常是不够的。实现它也可能很耗时。
这导致了替代技术的日益普及,例如GraphQL(http://graphql.org)和Netflix Falcor(http://netflix.github.io/falcor),它们旨在支持高效的数据获取。
把操作映射为HTTP动词的挑战
另一个常见的REST API设计问题是如何将要在业务对象上执行的操作映射到HTTP动词。 REST API应该使用PUT进行更新,但可能有多种方法来更新订单,包括取消订单、修改订单等。此外,更新可能不是幂等的,但这却是使用PUT的要求。
一种解决方案是定义用于更新资源的特定方面的子资源。例如,Order Service具有用于取消订单的POST/orders/{orderId}/cancel端点,以及用于修订订单的POST/orders/{orderId}/revise端点。
另一种解决方案是将动词指定为URL的查询参数。可惜的是,这两种解决方案都不是特别符合RESTful的要求。
映射操作到HTTP动词的这个问题导致了REST替代方案的日益普及,例如gPRC,
REST的好处和弊端
好处:
- 简单,易于接受。
- 方便使用浏览器扩展(Postman插件)或者curl之类的命令行来测试。
- 直接支持请求/响应方式的通信。
- HTTP对防火墙友好。
- 不需要中间代理,简化了系统架构。
弊端:
- 只支持请求/响应方式的通信。
- 可能导致可用性降低。
- 客户端必须知道服务实例的位置(URL)。
- 在单个请求中获取多个资源具有挑战性。
- 有时很难将多个更新操作映射到HTTP动词。
使用gRPC
gRPC是一种基于二进制消息的协议,这意味着需要采用API优先的方法来进行服务设计。
好处:
- 设计具有复杂更新操作的API非常简单。
- 它具有高效、紧凑的进程间通信机制,尤其是在交换大量消息时。
- 支持在远程过程调用和消息传递过程中使用双向流式消息方式。
- 它实现了客户端和用各种语言编写的服务端之间的互操作性。
弊端:
- 与基于REST/JSON的API机制相比,JavaScript客户端使用基于gRPC的API需要做更多的工作。
- 旧式防火墙可能不支持HTTP/2。
使用断路器模式处理局部故障
当服务试图向另一个服务发送同步请求时,永远都面临着局部故障的风险。
image.png要通过合理地设计服务来防止在整个应用程序中故障的传导和扩散。解决这个问题分为两部分:
- 必须让远程过程调用代理(例如OrderServiceProxy)有正确处理无响应服务的能力。
- 需要决定如何从失败的远程服务中恢复。
开发可靠的远程过程调用代理,应对一下问题:
- 网络超时:在等待针对请求的响应时,一定不要做成无限阻塞,而是要设定一个超时。
- 限制客户端向服务器发出请求的数量:把客户端能够向特定服务发起的请求设置一个上限,如果请求达到了这样的上限,很有可能发起更多的请求也无济于事,这时就应该让请求立刻失败。
- 断路器模式:监控客户端发出请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续的调用立刻失效。在经过一定的时间后,客户端应该继续尝试,如果调用成功,则解除断路器。
使用JVM:Netflix Hystrix
使用非JVM:Polly库(.NET)
从服务失效故障中恢复
- 一种选择是服务只是向其客户端返回错误。
- 返回备用值(fallback value,例如默认值或缓存响应)可能会有意义。
每个服务的数据对客户来说重要性可能不同。Order Service的数据至关重要。如果此服务不可用,API Gateway应返回其数据的缓存版本或错误。来自其他服务的数据不太重要。例如,即使送餐状态不可用,客户也可以向用户显示有用的信息。如果Delivery Service不可用,API Gateway应返回其数据的缓存版本或从响应中省略它。
使用服务发现
在物理硬件上运行的传统应用程序中,服务实例的网络位置通常是静态的。
但在现代的基于云的微服务应用程序中,服务实例具有动态分配的网络位置。此外,由于自动扩展、故障和升级,服务实例集会动态更改。
服务发现,其关键组件是服务注册表,它是包含服务实例网络位置信息的一个数据库。
实现服务发现有以下两种主要方式:
- 服务及其客户直接与服务注册表交互。
- 通过部署基础设施来处理服务发现。
服务及其客户直接与服务注册表交互
应用程序的服务及其客户端与服务注册表进行交互。这种服务发现方法是两种模式的组合。第一种模式是自注册模式(注册)。第二种模式是客户端发现模式(调用)。
Netflix开发并开源了几个组件,包括:
Eureka,这是一个高可用的服务注册表;Eureka Java客户端;
Ribbon,这是一个支持Eureka客户端的复杂HTTP客户端。
平台层服务发现模式
许多现代部署平台(如Docker和Kubernetes)都具有内置的服务注册表和服务发现机制。但是有限制,基于Kubernetes的发现仅适用于在Kubernetes上运行的服务。
基于异步消息模式的通信
使用消息机制时,服务之间的通信采用异步交换消息的方式完成。
消息由消息头部和消息主体组成。消息通过消息通道进行交换。
image.png
两种类型的消息通道:点对点和发布-订阅。
- 点对点通道:向正在从通道读取的一个消费者传递消息。一对一交互方式
- 发布-订阅通道:将一条消息发给所有订阅的接收方。一对多交互方式。
服务的异步API规范必须指定消息通道的名称、通过每个通道交换的消息类型及其格式。
使用消息代理
- 消息代理,即服务通信的基础设施服务。
- 基于无代理的消息传递架构,服务直接相互通信。
无代理消息
ZeroMQ(http://zeromq.org )是一种流行的无代理消息技术。它既是规范,也是一组适用于不同编程语言的库。它支持各种传输协议,包括TCP、UNIX风格的套接字和多播。
好处:
- 允许更轻的网络流量和更低的延迟,因为消息直接从发送方发送到接收方,而不必从发送方到消息代理,再从代理转发到接收方。
- 消除了消息代理可能成为性能瓶颈或单点故障的可能性。
- 具有较低的操作复杂性,因为不需要设置和维护消息代理。
弊端:
- 服务需要了解彼此的位置,必须使用服务发现机制。
- 会导致可用性降低,因为在交换消息时,消息的发送方和接收方都必须同时在线。
- 在实现例如确保消息能够成功投递这些复杂功能时的挑战性更大。
基于代理的消息
消息代理是所有消息的中介节点。发送方将消息写入消息代理,消息代理将消息发送给接收方。
好处:
有许多消息代理可供选择。流行的开源消息代理包括:
- Apache ActiveMQ(http://activemq.apache.org )。
- RabbitMQ(https://www.rabbitmq.com )。
- Apache Kafka(http://kafka.apache.org )。
- 基于云的消息服务,例如AWS Kinesis(https://aws.amazon.com/kinesis )和AWS SQS(https://aws.amazon.com/sqs/ )。
好处
- 松耦合:发送方不需要知道接收方的网络位置。
- 消息缓存:消息代理可以在消息被处理之前一直缓存消息。
- 灵活的通信:消息机制支持前面提到的所有交互方式。
- 明确的进程间通信:消息机制让远程服务调用跟本地调用的差异变得很明确,这样程序员不会陷入一种“太平盛世”的错觉。
弊端
- 潜在的性能瓶颈:消息代理可能存在性能瓶颈。消息代理的横向扩展。
- 潜在的单点故障:消息代理的高可用性至关重要,否则系统整体的可靠性将受到影响。大多数现代消息代理都是高可用的。
- 额外的操作复杂性:消息系统是一个必须独立安装、配置和运维的系统组件。
基于消息的架构的挑战
- 处理并发和消息顺序,挑战之一是如何在保留消息顺序的同时,横向扩展多个接收方的实例。现代消息代理(如Apache Kafka和AWS Kinesis)使用的常见解决方案是使用分片(分区)通道
- 处理重复消息
- 编写幂等消息处理程序。程序的幂等性,是指即使这个应用被相同输入参数多次重复调用时,也不会产生额外的效果。
- 跟踪消息并丢弃重复项。接收方使用message id跟踪它已处理的消息并丢弃任何重复项。
- 事务性消息。如果服务不以原子方式执行这两个操作,则类似的故障可能使系统处于不一致状态。
- 使用数据库表作为消息队列,使用应用事务性发件箱模式。对于从发件箱到消息代理,有两种方式。
- 通过轮询模式发布事件。
- 使用事务日志拖尾模式发布事件。
- 使用数据库表作为消息队列,使用应用事务性发件箱模式。对于从发件箱到消息代理,有两种方式。
使用异步消息提高可用性
应该尽可能选择异步通信机制来处理服务之间的调用。同步消息会降低可用性。
消除同步交互
在必须处理同步请求的情况下,尽最大限度地降低同步通信的数量。
- 尽量使用异步交互模式
- 复制数据。服务维护一个数据副本,这些数据是服务在处理请求时需要使用的。其实是利用缓存。
- 先返回响应,再完成处理。例如,客户端请求后,先设置中间状态,返回PENDING。客户端轮询请求状态。服务端异步处理。
网友评论