当前有多种进程间通信机制供开发者选择。比较流行的是REST(使用JSON)。但是,需要牢记“没有银弹”这个大原则。你必须仔细考虑这些选择。本章将探讨各种进程间通信机制,包括REST和消息传递,并讨论如何进行权衡。
选择合适的进程间通信机制是一个重要的架构决策。它会影响应用程序可用性。更重要的是,一个理想的微服务架构应该是在内部由松散耦合的若干服务组成,这些服务使用异步消息相互通信。REST等同步协议主要用于服务与外部其他应用程序的通信。
有多种客户端与服务的交互方式。可以分为两个维度。
第一个维度关注的是一对一和一对多。
交互方式的第二个维度关注的是同步和异步。
API或接口是软件开发的中心。应用是由模块构成的,每个模块都有接口,这些接口定义了模块的客户端可以调用若干操作。一个设计良好的接口会在暴露有用功能同时隐藏实现的细节。因此,这些实现的细节可以被修改,而接口保持不变,这样就不会对客户端产生影响。
相比单体架构,我们面临的挑战在于:并没有一个简单的编程语言结构可以用来构造和定义服务的API。根据定义,服务和它的客户端并不会一起编译。如果使用不兼容的API部署新版本的服务,虽然在编译阶段不会出现错误,但是会出现运行时故障。
API优先设计
即使在那些最简单的项目中,组件和API之间也常常发生冲突。例如,负责后端的Java程序员和负责前端的AngularJS程序员都声称他们完成了开发,然而这个应用程序却无法工作。前端的REST和 Web Socket API无法与后端的API一起工作。最终导致这 个应用程序的前端和后端无法正常通信!
服务的API很少一成不变,它可能会随着时间的推移而发展。让我们来看看如何做到这点,并讨论你将面临的问题。
语义化版本控制规范为API版本控制提供了有用的指导。它是一组规则,用于指定如何使用版本号,并且以正确的方式递增版本号。语义化版本控制最初的目的是用软件包的版本控制,但你可以将其用在分布式系统中对API进行版本控制。
语义化版本控制规范要求版本号由三部分组成: MAJOR. MINOR. PATCH。必须按如下方式递增版本号:
当使用基于远程过程调用的进程间通信机制时,客户端向服务发送请求,服务处理该请求并发回响应。有些客户端可能会处在堵塞状态并等待响应,而其他客户端可能会有一个响应式的非阻塞架构。但与使用消息机制时不同,客户端假定响应将及时到达。
好处:
弊端:
虽然存在这些缺点,但REST似乎是API的事实标准,尽管有几个有趣的替代方案。例如,通过 GraphQL实现灵活、高效的数据提取。
好处:
gRPC是REST的一个引人注目的替代品,但与REST一样,它是一种同步通信机制,因此它也存在局部故障的问题。
分布式系统中,当服务试图向另一个服务发送同步请求时,永远都面临着局部故障的风险。因为客户端和服务端是独立的进程,服务端很有可能无法在有限的时间内对客户端的请求做出响应。服务端可能因为故障或维护的原因而暂停。或者,服务端也可能因为过载而对请求的响应变得极其缓慢。
客户端等待响应被阻塞,这可能带来的麻烦就是在其他客户端甚至使用服务的第三方应用之间传导,并导致服务中断。
要通过合理地设计服务来防止在整个应用程序中故障的传导和扩散,这是至关重要的。
解决这个问题分为两部分:
假设你正在编写一些调用具有REST API的服务的代码。为了发出请求,你的代码需要知道服务实例的网络位置(IP地址和端口)。在物理硬件上运行的传统应用程序中,服务实例的网络位置通常是静态的。例如,你的代码可以从偶尔更新的配置文件中读取网络位置。但在现代的基于云的微服务应用程序中,通常不那么简单。现代应用程序更具动态性。
服务实例具有动态分配的网络位置。此外,由于自动扩展、故障和升级,服务实例集会动态更改。因此,你的客户端代码必须使用服务发现。
这种服务发现方法是两种模式的组合:
应用层服务发现的一个好处是它可以处理多平台部署的问题(服务发现机制与具体的部署平台无关)其余服务在遗留环境中运行。在这种情况下,使用 Eureka的应用层服务发现同时适用于两种环境,而基于Kubernetes的服务发现仅能用于部署在 Kubernetes平台之上的部分服务。
应用层服务发现的一个弊端是:你需要为你使用的每种编程语言(可能还有框架)提供服务发现库。Node. js或GoLang则必须找到其他一些服务发现框架。应用层服务发现的另一个弊端是开发者负责设置和管理服务注册表,这会分散一定的精力。因此,最好使用部署基础设施提供的服务发现机制。
许多现代部署平台(如Docker和Kubernetes)都具有内置的服务注册表和服务发现机制。部署平台为每个服务提供DNS名称、虚拟IP(VIP)地址和解析为VIP地址的DNS名称。客户端向DNS名称和VIP发出请求,部署平台自动将请求路由到其中一个可用服务实例。因此,服务注册、服务发现和请求路由完全由部署平台处理。
这种方法是以下两种模式的组合:
由平台提供服务发现机制的主要好处是服务发现的所有方面都完全由部署平台处理。服务和客户端都不包含任何服务发现代码。因此,无论使用哪种语言或框架,服务发现机制都可供所有服务和客户使用。
平台提供服务发现机制的一个弊端是它仅限于支持使用该平台部署的服务。例如,如前所述,在描述应用程序级别发现时,基于 Kubernetes的发现仅适用于在 Kubernetes上运行的服务。尽管存在此限制,我建议尽可能使用平台提供的服务发现。
使用消息机制时,服务之间的通信采用异步交换消息的方式完成。基于消息机制的应用程序通常使用消息代理,它充当服务之间的中介。另一种选择是使用无代理架构,通过直接向服务发送消息来执行服务请求。服务客户端通过向服务发送消息来发出请求。如果希望服务实例回复,服务将通过向客户端发送单独的消息的方式来实现。由于通信是异步的,因此客户端不会堵塞和等待回复。相反,客户端都假定回复不会马上就收到。
两种类型的消息通道:
如图所示,服务的异步API规范必须指定消息通道的名称、通过每个通道交换的消息类型及其格式。你还必须使用诸如JSON、XML或 Protobuf之类的标准来描述消息的格式。但与REST和 Open API不同,并没有广泛采用的标准来记录通道和消息类型,你需要自己编写这样的文档。
服务还可以使用发布/订阅的方式对外发布事件。此API风格的规范包括事件通道以及服务发布到通道的事件式消息的类型和格式。
基于消息传递的应用程序通常使用消息代理,即服务通信的基础设施服务。但基于消息代理的架构并不是唯一的消息架构。你还可以使用基于无代理的消息传递架构,其中服务直接相互通信。这两种方法具有不同的利弊,但通常基于代理的架构是一种更好的方法。
无代理的架构好处:
弊端
实际上,这些弊端中的一些(例如可用性降低和需要使用服务发现),与使用同步请求/响应交互方式所导致的弊端相同。
有许多消息代理可供选择。流行的开源消息代理包括:
基于代理的消息的好处和弊端
好处:
弊端:
挑战之一是如何在保留消息顺序的同时,横向扩展多个接收方的实例。为了同时处理消息,拥有多个实例是一个常见的要求。而且,即使单个服务实例也可能使用线程来同时处理多个消息。使用多个线程和服务实例来并发处理消息可以提高应用程序的吞吐量。但同时处理消息的挑战是确保每个消息只被处理一次,并且是按照它们发送的顺序来处理的。
现代消息代理(Apache Kafka)使用的常见解决方案是使用分片(分区)通道。
使用消息机制时必须解决的另一个挑战是处理重复消息。理想情况下,消息代理应该只传递一次消息,但保证有且仅有一次的消息传递通常成本很高。相反,大多数消息代理承诺至少成功传递一次消息。
处理重复消息有以下两种不同的方法。
如果应用程序处理消息的逻辑是满足幂等的,那么重复的消息就是无害的。不幸的是,应用程序逻辑通常不是幂等的。或者你可能正在使用消息代理,该消息代理在重新传递消息时不会保留排序。重复或无序消息可能会导致错误。
例如,考虑一个授权消费者信用卡的消息处理程序。它必须为每个订单仅执行一次信用卡授权操作。这段应用程序逻辑在每次调用时都会产生不同的效果。如果重复消息导致消息处理程序多次执行该逻辑,则应用程序的行为将不正确。执行此类应用程序逻辑的消息处理程序必须通过检测和丢弃重复消息而成为幂等的。
一个简单的解决方案是消息接收方使用 message id跟踪它已处理的消息并丢弃任何重复项。例如,它可以存储它在数据库表中使用的每条消息的 message id。
当接收方处理消息时,它将消息的 message id作为创建和更新业务实体的事务的部分记录在数据库表中。在此示例中,接收方将包含 message id的行插入 PROCESSED MESSAGES表。如果消息是重复的,则 INSER将失败,接收方可以选择丢弃该消息。
另一个选项是消息处理程序在应用程序表,而不是专用表中记录 message id。当使用具有受限事务模型的 NoSQL数据库时,此方法特别有用,因为 NOSQL数据库通常不支持将针对两个表的更新作为数据库的事务。
服务通常需要在更新数据库的事务中发布消息。如果服务不以原子方式执行这两个操作则类似的故障可能使系统处于不一致状态。
传统的解决办法是在数据库和消息代理之间使用分布式事务。然而,分布式事务对现今的应用程序而言并不是一个很好的选择。而且,很多新的消息代理不支持分布式事务。
解决方案:
REST是一种非常流行的进程间通信机制。你可能很想将它用于服务间通信。但是,REST的问题在于它是一个同步协议:HTTP客户端必须等待服务端返回响应。只要服务使用同步协议进行通信,就可能降低应用程序的可用性。
从统计意义上讲,一个系统操作的可用性,由其所涉及的所有服务共同决定。如果Order service服务和它所调用的两个服务的可用性都是99.5%3,那么这个系统操作的整体可用性就是99.5%3,大约是98.5%3,这其实是个非常低的数值了。每一个额外增加的服务参与到其中,都会更进一步降低整体系统操作的可用性。