原文链接:https://www.nginx.com/blog/building-microservices-inter-process-communication/
这是我们系列中关于使用微服务架构构建应用程序的第三篇文章。
在本文中,我们将了解系统中的服务如何相互通信。 第四篇文章将探讨服务发现密切相关问题。
在单体应用中,组件通过语言级方法或函数调用相互通信。相比之下,基于微服务的应用程序是在多台机器上运行的分布式系统,每个服务实例通常都是一个进程。因此,如下图所示,服务必须使用进程间通信(IPC)机制进行交互。
稍后我们将介绍具体的IPC技术,但首先让我们探讨各种设计问题。
在为服务选择IPC机制时,首先应考虑服务如何交互。有各种client⇔service
交互模式。它们可以按两个维度进行分类:
下表显示了各种交互方式:
一对一 | 一对多 | |
---|---|---|
同步 | 请求/响应 | - |
异步 | 通知 | 发布/订阅 |
异步 | 请求/异步响应 | 发布/异步响应 |
存在以下类型的一对一交互:
存在以下类型的一对多交互:
每项服务通常使用这些交互方式的组合。对于某些服务,单个IPC机制就足够了。其他服务可能需要使用IPC机制的组合。下图显示了当乘客请求行程时,出租车应用程序中的服务可能如何交互。
这些服务使用:
现在我们已经了解了交互风格,让我们来看看如何定义API。
服务的API是服务与其客户端之间的契约。无论选择哪种IPC机制,使用某种接口定义语言(IDL)精确定义服务的API都很重要。使用API优先方法定义服务有很好的理论依据。可以通过编写接口定义并与客户端开发人员一起查看来开始开发服务。只有在对API定义进行迭代后才能开始实现该服务。预先进行此设计可增加构建满足客户端需求的服务的机会。
正如将在本文后面看到的,API定义的性质取决于使用的IPC机制:
稍后我们将更详细地描述一些IDL。
服务的API总是随着时间的推移而变化。
通常无法强制所有客户端与服务保持同步升级。此外,可能会逐步部署新版本的服务,以便同时运行旧版本和新版本的服务。制定处理这些问题的战略非常重要。
如何处理API的更改取决于这次API变更的大小。
某些更改是次要的,并且这些变更能够向后兼容。
例如,向请求或响应添加属性。此时,应该遵守稳健性原则来设计客户端和服务的API。保证使用旧版API的客户端能够继续访问新版本的服务。该服务为缺少的请求属性提供默认值,客户端忽略任何额外的响应属性。
使用IPC机制和消息传递格式,使得我们可以轻松的升级API。
有时必须对API进行主要的,不兼容的更改。
由于无法强制客户端立即升级,因此服务必须在一段时间内支持旧版本的API。如果使用的是基于HTTP的机制(如REST),则一种方法是将版本号嵌入URL中。每个服务实例可能同时处理多个版本。或者,可以部署各自处理特定版本的不同实例。
正如前一篇关于API网关的文章所述,在分布式系统中,存在部分失败的风险。由于客户端和服务是单独的进程,因此服务可能无法及时响应客户端的请求:
例如,前一篇文章中的商品详细信息方案。假设推荐服务没有响应,客户端的简单实现可能会无限期地阻止响应。这不仅会导致糟糕的用户体验,而且在许多应用程序中它会消耗诸如线程之类的宝贵资源。最终运行时将耗尽线程并变得无响应,如下图所示。
要防止出现此问题,必须设计服务以处理部分故障。
Netflix描述了一个很好的方法。处理部分失败的策略包括:
Netflix Hystrix是一个开源库,可以实现上述和其他模式。如果使用的是JVM,那么一定要考虑使用Hystrix。而且,如果在非JVM环境中运行,则应使用等效库。
有许多不同的IPC技术可供选择。
稍后我们将研究同步IPC机制,但首先让我们讨论异步IPC机制。
进程通过异步交换消息进行通信。客户端通过向服务发送消息来向服务发出请求。
消息由消息头(元数据,如发件人)和消息体组成,并且,消息是通过通道交换。任何数量的生产者都可以向通道发送消息,同样,任何数量的消费者都可以从通道接收消息。有两种渠道:
下图显示出租车应用程序如何使用发布/订阅通道。
行程管理服务通过将生产的消息写入发布/订阅通道来向感兴趣的服务(如分派系统Dispatcher)通知新的行程。分派系统通过向发布/订阅通道写入驱动响应消息来查找可用的驱动程序并通知其他服务。
有许多消息系统可供选择,应该选择一个支持各种编程语言的:
在较高的层面上,它们都支持某种形式的消息和通道,它们都力求稳定,高性能和可扩展。但是,每个消息代理的消息传递模型的细节存在显着差异。
现在我们已经考虑使用基于消息传递的IPC,让我们查看基于请求/响应的IPC。
当使用基于请求/响应的同步IPC机制时,客户端向服务发送请求。该服务处理请求并发回响应。在许多客户端中,使请求在等待响应时阻塞的线程。其他客户端可能使用由Futures
或Rx Observables
封装的异步,事件驱动的客户端代码。但是,与使用消息传递时不同,客户端假定响应将及时到达。有许多协议可供选择。两种流行的协议是REST和Thrift。我们先来看看REST。
今天,以RESTful风格开发API是时髦的。REST是一种(几乎总是)使用HTTP的IPC机制。
引用REST的创建者Roy Fielding:"REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.”---《Architectural Styles and the Design of Network-based Software Architectures》
REST提供了一组架构约束,当作为一个整体应用时,强调组件交互的可扩展性,接口的通用性,组件的独立部署以及中间组件,以减少交互延迟,实施安全性并封装遗留系统。
下图显示了出租车应用程序可能使用REST的方法之一。
乘客的智能手机通过向行程管理服务的/trip
资源发出POST
请求来发起行程。该服务通过向乘客管理服务发送有关乘客信息的GET请求来处理请求。在验证乘客被授权创建行程后,行程管理服务创建行程并向智能手机返回201响应。
许多开发人员声称他们的基于HTTP的API是RESTful的。然而,正如菲尔丁在这篇博文中所描述的那样,并非所有这些都是。 Leonard Richardson(无关系)定义了一个非常有用的REST成熟度模型,它包含以下级别。
请求查询参数和请求体(如果有)来指定操作的参数。这使服务能够利用Web基础结构,例如缓存GET请求。
级别3:API的设计基于HATEOAS(Hypertext As The Engine Of Application State,超文本作为应用程序状态引擎)原则。基本思想是GET请求返回的资源表示包含用于对该资源执行允许操作的链接。例如,客户端可以使用为响应发送的GET请求而返回的Order结果中的链接来取消订单在一个检索订单的请求中。
HATEOAS的好处包括:
使用基于HTTP的协议有很多好处:
使用HTTP有一些缺点:
开发人员社区最近重新发现了RESTful API的接口定义语言的价值。有几个选项,包括RAML和Swagger。某些IDL(如Swagger)允许定义请求和响应消息的格式。其他如RAML要求使用单独的规范,如JSON Schema。除了描述API之外,IDL通常还具有从接口定义生成客户端存根和服务器骨架的工具。
Apache Thrift是REST的有趣替代品。它是用于编写跨语言RPC客户端和服务器的框架。Thrift提供了一个C风格的IDL来定义的API。可以使用Thrift编译器生成客户端存根和服务器端骨架。编译器为各种语言生成代码,包括C ++,Java,Python,PHP,Ruby,Erlang和Node.js。
Thrift接口由一个或多个服务组成。服务定义类似于Java接口。它是强类型方法的集合。Thrift方法可以返回(可能是void)值,也可以定义为单向请求。
Thrift支持各种消息格式:
Thrift还为提供了包括原始TCP和HTTP在内的传输协议选择。原始TCP可能比HTTP更有效。但是,HTTP是对防火墙,浏览器和人类友好的。
现在我们已经查看了HTTP和Thrift,让我们来看看消息格式的问题。如果使用的是消息系统或REST,则可以选择消息格式。其他IPC机制(如Thrift)可能只支持少量消息格式,可能只支持一种。在任何一种情况下,使用跨语言消息格式都很重要。即使今天用单一语言编写微服务,未来很可能会使用其他语言。
消息格式主要有两种:
XML文档的结构由XML模式指定。随着时间的推移,开发人员社区已经意识到JSON也需要类似的机制。一种选择是使用JSON Schema,可以是独立的,也可以作为IDL的一部分,如Swagger。
使用基于文本的消息格式的缺点是消息往往是冗长的,尤其是XML。由于消息是自描述的,因此除了值之外,每条消息都包含属性的名称。另一个缺点是解析文本的开销。因此,可能需要考虑使用二进制格式。
二进制:有几种二进制格式可供选择。
因此,试用Protocol Buffers的API演变比使用Avro更容易
这篇博文是Thrift,Protocol Buffers和Avro的绝佳比较。
微服务必须使用进程间通信机制进行通信。在设计服务的通信方式时,需要考虑各种问题:服务如何交互,如何为每个服务指定API,如何更新API以及如何处理部分失败故障。微服务可以使用两种IPC机制,异步消息传递和同步请求/响应。在本系列的下一篇文章中,我们将研究微服务架构中的服务发现问题。