在monolithic单体应用程序中,不同的组件之间通过编程语言级别的方法或函数调用相互调用。 相反,基于微服务的应用程序是在多台计算机上运行的分布式系统。 每个服务实例通常是一个进程。 因此,如下图所示,服务必须使用进程间通信(IPC)机制进行交互。
为服务选择IPC机制时,首先考虑服务如何交互是必要的。 客户端⇔服务交互方式多种多样。 它们可以沿两个维度进行分类。 第一个维度是互动是一对一还是一对多:
第二个维度是交互是同步还是异步:
下表显示了各种交互样式。
One-to-One | One-to-Many | |
---|---|---|
Synchronous | Request/response | — |
Asynchronous | Notification | Publish/subscribe |
Request/async response | Publish/async responses |
存在以下类型的一对一交互:
一对多互动有以下几种:
每个服务通常使用这些交互样式的组合。对于某些服务,单个IPC机制就足够了。其他服务可能需要结合使用IPC机制。下图显示了当用户请求一个旅行时,出租车服务应用程序中的服务如何交互。
服务使用notifications, request/response, 和publish/subscribe的组合。 例如,乘客的智能手机向行程管理服务发送通知以请求接机。 行程管理服务通过使用请求/响应调用旅客服务来验证旅客的帐户是否处于活动状态。 然后,行程管理服务创建行程,并使用发布/订阅来通知其他服务,包括分派器,后者查找可用的驱动程序。
现在我们已经研究了交互样式,让我们看一下如何定义API。
服务的API是服务与其客户之间的contract。 无论选择哪种IPC机制,使用某种接口定义语言(IDL)精确定义服务的API都是很重要的。 使用API优先方法定义服务甚至有很好的论据。 您可以通过编写接口定义并与客户开发人员一起审查来开始开发服务。 只有在对API定义进行迭代之后,您才能实现服务。 预先进行此设计会增加您建立满足其客户需求的服务的机会。
正如您将在本文后面看到的那样,API定义的性质取决于您所使用的IPC机制。 如果使用messaging消息服务,则API包含消息通道和消息类型。 如果使用的是HTTP,则API由URL以及请求和响应格式组成。
服务的API始终会随着时间而变化。在整体应用程序中,更改API和更新所有调用程序通常很简单。在基于微服务的应用程序中,即使API的所有使用者都是同一应用程序中的其他服务,也要困难得多。通常,您无法强制所有客户端与服务同步升级。另外,您可能会逐步部署服务的新版本,以便服务的旧版本和新版本将同时运行。制定处理这些问题的策略很重要。
处理API更改的方式取决于更改的大小。某些更改是次要的,并且与以前的版本向后兼容。例如,您可以将属性添加到请求或响应。设计客户和服务以使其遵循健壮性原则是有意义的。使用较旧API的客户端应继续使用新版本的服务。该服务为缺少的请求属性提供默认值,客户端忽略任何额外的响应属性。重要的是要使用IPC机制和消息传递格式,使您能够轻松地开发API。
但是,有时您必须对API进行重大的,不兼容的更改。由于您无法强制客户端立即升级,因此服务必须在一段时间内支持旧版API。如果使用的是基于HTTP的机制(例如REST),则一种方法是将版本号嵌入URL中。每个服务实例可能会同时处理多个版本。或者,您可以部署不同的实例,每个实例都处理特定的版本。
如有关API gateway的文章中我们提到了,在分布式系统中,存在着永远存在的部分故障风险。 由于客户和服务是独立的流程,因此服务可能无法及时响应客户的请求。 服务可能由于故障或维护而关闭。 否则服务可能过载,并且对请求的响应非常缓慢。
例如,考虑该文章中的“产品详细信息”方案。 假设推荐服务没有响应。 客户端的实现可能会无限期地阻塞等待响应。 这不仅会导致不良的用户体验,而且在许多应用程序中会消耗宝贵的资源,例如线程。 最终,运行时将耗尽线程并变得无响应,如下图所示。
为避免此问题,必须设计服务以处理部分故障。
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。
REST
如今,以RESTful风格开发API已成为一种时尚。 REST是(几乎始终)使用HTTP的IPC机制。 REST中的一个关键概念是一种资源,通常代表一个业务对象,例如客户或产品,或业务对象的集合。 REST使用HTTP谓词来操纵资源,这些谓词是使用URL引用的。例如,GET请求返回资源的表示形式,该形式可以是XML文档或JSON对象的形式。 POST请求创建一个新资源,而PUT请求更新一个资源。
下图显示了出租车服务应用程序可能使用REST的方式之一。
乘客的智能手机通过向旅行管理服务的/ trips资源发出POST请求来请求旅行。该服务通过向乘客管理服务发送有关乘客信息的GET请求来处理该请求。在确认乘客被授权创建行程后,行程管理服务将创建行程并将201响应返回至智能手机。
许多开发人员声称他们基于HTTP的API是RESTful的。但是,正如Fielding在此博客文章中所描述的那样,实际上并非全部。 Leonard Richardson(无关系)为REST定义了一个非常有用的成熟度模型,该模型包含以下级别。
使用基于HTTP的协议有很多好处:
使用HTTP有一些缺点:
开发人员社区最近重新发现了RESTful API的接口定义语言的价值。有一些选项,包括RAML和Swagger。一些IDL(例如Swagger)允许您定义请求和响应消息的格式。其他(例如RAML)要求您使用单独的规范(例如JSON Schema)。除了描述API之外,IDL通常还具有从接口定义生成客户端存根和服务器框架的工具
现在,我们已经研究了HTTP和Thrift,现在让我们检查消息格式的问题。如果您使用的是消息传递系统或REST,则可以选择消息格式。其他IPC机制(例如Thrift)可能仅支持少量消息格式,也许仅支持一种。无论哪种情况,都必须使用跨语言消息格式。即使您今天用一种语言编写微服务,将来也可能会使用其他语言。
消息格式主要有两种:文本和二进制。基于文本的格式的示例包括JSON和XML。这些格式的优点是它们不仅易于阅读,而且是自描述的。在JSON中,对象的属性由名称/值对的集合表示。同样,在XML中,属性由命名的元素和值表示。这使消息的使用者可以选择其感兴趣的值,而忽略其余值。因此,对消息格式的微小更改可以轻松地向后兼容。
XML文档的结构由XML模式指定。随着时间的流逝,开发人员社区逐渐意识到JSON也需要类似的机制。一种选择是使用JSON Schema,它既可以独立使用,也可以作为IDL(例如Swagger)的一部分使用。
使用基于文本的消息格式的缺点是消息往往很冗长,尤其是XML。因为消息是自描述的,所以每条消息除其值外还包含属性的名称。另一个缺点是解析文本的开销。因此,您可能要考虑使用二进制格式。
总结
微服务必须使用进程间通信机制进行通信。 在设计服务如何通信时,您需要考虑各种问题:服务如何交互,如何为每个服务指定API,如何扩展API以及如何处理部分故障。 微服务可以使用两种IPC机制:异步消息传递和同步请求/响应。