编辑– 本系列文章共七部分,现已完成:
您还可以下载完整的文章集,以及有关使用 NGINX Plus 实现微服务的信息,作为电子书 -微服务: 从设计到部署。 另外,请查看新的微服务解决方案页面。
这是我们关于使用微服务架构构建应用的系列文章的第三篇。 第一篇文章介绍了微服务架构模式,并将其与单片架构模式进行了比较,并讨论了使用微服务的优点和缺点。 第二篇文章描述了应用的客户端如何通过称为API 网关的中介与微服务进行通信。 在本文中,我们将了解系统内的服务如何相互通信。 第四篇文章探讨了密切相关的服务发现问题。
在单片应用中,组件通过语言级方法或函数调用相互调用。 相比之下,基于微服务的应用是在多台机器上运行的分布式系统。 每个服务实例通常都是一个进程。 因此,如下图所示,服务必须使用进程间通信 (IPC) 机制进行交互。
稍后我们将研究具体的 IPC 技术,但首先让我们探讨各种设计问题。
当为服务选择 IPC 机制时,首先考虑服务如何交互是很有用的。 客户⇔服务互动风格多种多样。 它们可以按照两个维度进行分类。 第一个维度是交互是一对一还是一对多:
第二个维度是交互是同步的还是异步的:
下表显示了各种交互风格。
一对一 | 一对多 | |
---|---|---|
同步 | 请求/响应 | — |
异步 | 通知 | 发布/订阅 |
请求/异步响应 | 发布/异步响应 |
一对一交互有以下几种类型:
一对多交互有以下几种类型:
每项服务通常都会结合使用这些交互方式。 对于某些服务来说,单一的IPC机制就足够了。 其他服务可能需要使用多种 IPC 机制。 下图展示了当用户请求行程时出租车叫车应用中的服务如何交互。
这些服务使用通知、请求/响应和发布/订阅的组合。 例如,乘客的智能手机向行程管理服务发送通知以请求接载。 行程管理服务使用请求/响应调用乘客服务来验证乘客的帐户是否活跃。 然后,行程管理服务创建行程并使用发布/订阅功能通知其他服务(包括调度员,用于定位可用的司机)。
现在我们已经了解了交互风格,让我们看看如何定义 API。
服务的 API 是服务与其客户端之间的契约。 无论您选择哪种 IPC 机制,使用某种接口定义语言 (IDL) 精确定义服务的 API 都非常重要。 甚至有很好的论据支持使用API 优先方法来定义服务。 您可以通过编写接口定义并与客户端开发人员一起审查来开始服务的开发。 只有在迭代 API 定义之后,您才能实现该服务。 提前进行这种设计可以增加您构建满足客户需求的服务的机会。
正如您将在本文后面看到的,API 定义的性质取决于您所使用的 IPC 机制。 如果您正在使用消息传递,则 API 由消息通道和消息类型组成。 如果您使用 HTTP,API 由 URL 和请求和响应格式组成。 稍后我们将更详细地描述一些 IDL。
服务的 API 总是会随着时间而改变。 在单片应用中,更改 API 并更新所有调用者通常很简单。 在基于微服务的应用中,这会变得更加困难,即使 API 的所有使用者都是同一应用中的其他服务。 通常,您无法强制所有客户端与服务同步升级。 此外,您可能会逐步部署服务的新版本,以便服务的新旧版本可以同时运行。 制定解决这些问题的策略非常重要。
如何处理 API 更改取决于更改的大小。 一些变化很小并且与以前的版本向后兼容。 例如,您可以向请求或响应添加属性。 设计客户端和服务以使其遵守稳健性原则是有意义的。 使用旧版 API 的客户端应继续使用新版本的服务。 服务为缺失的请求属性提供默认值,客户端忽略任何额外的响应属性。 使用 IPC 机制和消息传递格式非常重要,它们使您能够轻松改进 API。
但有时,您必须对 API 进行重大的、不兼容的更改。由于您无法强迫客户端立即升级,因此服务必须在一段时间内支持旧版本的 API。 如果您使用基于 HTTP 的机制(例如 REST),一种方法是将版本号嵌入 URL 中。 每个服务实例可能同时处理多个版本。 或者,您可以部署不同的实例,每个实例处理特定的版本。
例如,考虑该文章中的产品详细信息场景。 假设推荐服务没有响应。 客户端的简单实现可能会无限期地阻塞以等待响应。 这不仅会导致糟糕的用户体验,而且在许多应用中还会消耗线程等宝贵的资源。 最终,运行时将耗尽线程并变得无响应,如下图所示。
为了防止出现此问题,必须设计您的服务来处理部分故障。
一个值得遵循的好方法是Netflix 描述的方法。 处理部分故障的策略包括:
Netflix Hystrix是一个实现这些和其他模式的开源库。 如果您正在使用 JVM,那么您绝对应该考虑使用 Hystrix。 并且,如果您在非 JVM 环境中运行,则应该使用等效库。
有很多不同的 IPC 技术可供选择。 服务可以使用同步基于请求/响应的通信机制,例如基于 HTTP 的 REST 或 Thrift。 或者,他们可以使用异步的基于消息的通信机制,例如 AMQP 或 STOMP。 还有多种不同的消息格式。 服务可以使用人类可读的基于文本的格式,例如 JSON 或 XML。 或者,他们可以使用二进制格式(更高效),例如 Avro 或 Protocol Buffers。 稍后我们将讨论同步 IPC 机制,但首先让我们讨论一下异步 IPC 机制。
使用消息传递时,进程通过异步交换消息进行通信。 客户端通过发送消息向服务发出请求。 如果希望服务回复,它会向客户端发送单独的消息。 由于通信是异步的,因此客户端不会阻塞等待答复。 相反,客户端假设不会立即收到答复。
消息由标头(元数据,例如发件人)和消息正文组成。 信息通过渠道进行交换。 任意数量的生产者都可以向一个频道发送消息。 类似地,任意数量的消费者都可以从一个频道接收消息。 有两种类型的渠道:点对点和发布订阅。 点对点通道将消息准确地传递给正在从该通道读取消息的其中一个消费者。 服务使用点对点通道来实现前面描述的一对一交互风格。 发布订阅渠道将每条消息传递给所有连接的消费者。 服务使用发布订阅渠道来实现上面描述的一对多交互方式。
下图展示了出租车叫车应用如何使用发布订阅渠道。
行程管理服务通过向发布订阅频道写入“行程创建”消息来通知调度程序等感兴趣的服务有关新的行程的信息。 调度程序找到可用的驱动程序,并通过向发布订阅频道写入驱动程序建议消息来通知其他服务。
有很多消息传递系统可供选择。 您应该选择一个支持多种编程语言的。 一些消息系统支持标准协议,例如 AMQP 和 STOMP。 其他消息传递系统具有专有但有记录的协议。 有大量开源消息系统可供选择,包括RabbitMQ 、 Apache Kafka 、 Apache ActiveMQ和NSQ 。 从高层次上讲,它们都支持某种形式的消息和渠道。 他们都力求做到可靠、高性能和可扩展。 然而,每个代理的消息传递模型的细节存在显著差异。
使用消息传递有很多优点:
然而,使用消息传递也存在一些缺点:
现在我们已经了解了使用基于消息的 IPC,让我们来研究一下基于请求/响应的 IPC。
当使用同步的、基于请求/响应的 IPC 机制时,客户端会向服务发送请求。 该服务处理请求并返回响应。 在许多客户端中,发出请求的线程在等待响应时被阻塞。 其他客户端可能使用异步的、事件驱动的客户端代码,这些代码或许由 Futures 或 Rx Observables 封装。 然而,与使用消息传递不同,客户端假定响应将及时到达。 有许多协议可供选择。 两种流行的协议是 REST 和 Thrift。 我们首先来看一下 REST。
如今,以RESTful风格开发 API 十分流行。 REST 是一种 (几乎总是) 使用 HTTP 的 IPC 机制。 REST 中的一个关键概念是资源,它通常表示业务对象(例如客户或产品)或业务对象的集合。 REST 使用 HTTP 动词来操作资源,这些资源通过 URL 来引用。 例如, GET
请求返回资源的表示,该表示可能是 XML 文档或 JSON 对象的形式。 POST
请求创建新资源,而PUT
请求更新资源。 引用 REST 的创建者 Roy Fielding 的话:
下图展示了出租车叫车应用可能使用 REST 的方式之一。
乘客的智能手机通过向行程管理服务的/trips
资源发出POST
请求来请求行程。 该服务通过向乘客管理服务发送有关乘客信息的GET
请求来处理该请求。 在验证乘客有权创建行程后,行程管理服务将创建行程并返回201
响应智能手机。
许多开发人员声称他们基于 HTTP 的 API 是 RESTful。 然而,正如菲尔丁在这篇博文中所描述的,事实上并非所有人都是如此。 Leonard Richardson(无关)为 REST 定义了一个非常有用的成熟度模型,它由以下级别组成。
POST
请求来调用服务。 每个请求都指定要执行的操作、操作的目标(例如业务对象)以及任何参数。POST
请求,指定要执行的操作和任何参数。GET
表示检索、 POST
表示创建、 PUT
表示更新。 请求查询参数和正文(如果有)指定操作的参数。 这使得服务能够利用 Web 基础设施,例如GET
请求的缓存。GET
请求返回的资源表示包含对该资源执行允许操作的链接。 例如,客户端可以使用响应于发送以检索订单的GET
请求而返回的订单表示中的链接取消订单。 HATEOAS 的好处包括不再需要将 URL 硬连线到客户端代码中。 另一个好处是,由于资源的表示包含允许操作的链接,因此客户端不必猜测在当前状态下可以对资源执行哪些操作。使用基于 HTTP 的协议有许多好处:
curl
在命令行中测试(假设使用 JSON 或其他文本格式)。使用 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 支持多种消息格式: JSON、二进制和紧凑二进制。 二进制比 JSON 更高效,因为它解码速度更快。 而且,顾名思义,紧凑二进制是一种节省空间的格式。 当然,JSON 对人类和浏览器都是友好的。 Thrift 还为您提供了传输协议的选择,包括原始 TCP 和 HTTP。 原始 TCP 可能比 HTTP 更高效。 但是,HTTP 对防火墙、浏览器和人类都是友好的。
现在我们已经了解了 HTTP 和 Thrift,让我们来研究一下消息格式的问题。 如果您使用消息系统或 REST,您可以选择消息格式。 其他IPC机制(例如Thrift)可能仅支持少数消息格式,也许只支持一种。 无论哪种情况,使用跨语言消息格式都很重要。 即使您今天只用一种语言编写微服务,将来也可能会使用其他语言。
消息格式主要有两种:文本和二进制。 基于文本的格式的示例包括 JSON 和 XML。 这些格式的优点在于它们不仅是人类可读的,而且是自我描述的。 在 JSON 中,对象的属性由名称值对的集合表示。 类似地,在 XML 中,属性由命名元素和值表示。 这使得消息的消费者能够挑选出它感兴趣的值并忽略其余的值。 因此,消息格式的微小变化可以轻松向后兼容。
XML 文档的结构由XML 模式指定。 随着时间的推移,开发者社区逐渐意识到 JSON 也需要类似的机制。 一种选择是使用JSON Schema ,可以是独立的,也可以是 IDL 的一部分,比如 Swagger。
使用基于文本的消息格式的缺点是消息往往很冗长,尤其是 XML。 因为消息是自描述的,所以每条消息除了包含属性值之外还包含属性的名称。 另一个缺点是解析文本的开销。 因此,您可能需要考虑使用二进制格式。
有多种二进制格式可供选择。 如果您使用 Thrift RPC,则可以使用二进制 Thrift。 如果您选择消息格式,流行的选项包括协议缓冲区和Apache Avro 。 这两种格式都提供了一种类型化的 IDL 来定义消息的结构。 然而,一个区别是,协议缓冲区使用标记字段,而 Avro 消费者需要知道模式才能解释消息。 因此,使用协议缓冲区比使用 Avro 更容易进行 API 演变。 这篇博文对 Thrift、Protocol Buffers 和 Avro 进行了出色的比较。
微服务必须使用进程间通信机制进行通信。 在设计服务的通信方式时,您需要考虑各种问题:服务如何交互、如何为每个服务指定 API、如何发展 API 以及如何处理部分故障。 微服务可以使用两种IPC机制:异步消息传递和同步请求/响应。 在本系列的下一篇文章中,我们将研究微服务架构中的服务发现问题。
编辑– 本系列文章共七部分,现已完成:
您还可以下载完整的文章集,以及有关使用 NGINX Plus 实现微服务的信息,作为电子书 -微服务: 从设计到部署。
客座博主 Chris Richardson 是原CloudFoundry.com的创始人,该网站是针对 Amazon EC2 的早期 Java PaaS(平台即服务)。 他现在为各组织提供咨询服务,以改进他们开发和部署应用的方式。 他还定期在http://microservices.io上撰写有关微服务的博客。
“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”