微服务通信设计模式
Posted DeepNoMind
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务通信设计模式相关的知识,希望对你有一定的参考价值。
微服务之间的通信,需要根据业务需求和架构的实际情况选择合适的方案,基于HTTP的REST API是最常见的选择,但并不是唯一的选择,需要考虑复杂性、性能、可扩展性等方面的权衡。原文:My Favorite Interservice Communication Patterns for Microservices[1]
微服务很有意思,可以帮助我们创建可伸缩的、高效的架构,因此当前几乎所有主流平台都基于微服务架构系统。如果没有微服务,就不可能有现在的Netflix、Facebook或Instagram。
然而,将业务逻辑分解为更小的单元并以分布式方式部署它们只是第一步。我们还必须理解怎样才能让服务之间更好的通信。没错,微服务不仅仅是面向外部的——或者换句话说,为外部客户服务——很多时候它们也是同一系统中其他服务的客户。
那么,如何使两个服务相互通信呢?简单的方案是继续使用呈现给外部客户的API。例如,如果我们面向外部客户的API是REST HTTP API,那么内部服务也可以通过这些API进行交互。
这是一个很合理的设计,但让我们看看有没有其他改进方案。
注:通信是基于商定的协议,微服务之间以及服务和客户之间的通信都是如此,始终确保协议一致的一种方法是在这些解耦的代码库之间共享描述这些协议的代码,可以是类、类型、模拟数据对象等,Bit[2]就是有助于实现这一目标的工具。
Bit从源头独立的控制TS/JS模块,即使它们被部署到独立的远程主机上,也能维护它们之间的依赖关系,从而使得对某一模块的更新能够触发其所有依赖模块的持续集成。
HTTP API
HTTP API毕竟是非常有效的设计,就让我们从这儿开始。HTTP API本质上意味着让服务就像响应浏览器或者Postman[3]这样的桌面客户端那样发送信息。
HTTP API基于CS模式,意味着通信只能由客户端发起。这也是一种同步通信,意味着一旦通信由客户端发起,要一直等到服务端返回响应才会结束。
经典的CS微服务通信
因为这和我们访问互联网的方式一致,因此这种方法非常流行。HTTP是互联网的支柱,因此所有编程语言都通过某种方式支持HTTP功能,从而使其成为一种非常流行的方法。
但这种方式并不完美,我们来分析一下。
优点
容易实现。HTTP协议并不难实现,而且所有主要的编程语言都已经对它提供了原生支持,开发人员几乎不需要担心其内部是如何工作的,复杂性被类库所隐藏和抽象。
可以被标准化。如果在HTTP之上添加了REST之类的东西(正确实现的),其实就是创建了一个标准API,允许任何客户端可以快速学习如何与我们的业务逻辑进行通信。
技术中立。由于HTTP充当了客户端和服务器之间的数据传输通道,因此和两端的具体实现技术无关。可以用Node.js实现服务端,用JAVA或C#实现客户端(或其他服务),只要遵循相同的HTTP协议,就能够彼此通信。
缺点
额外的时延。作为HTTP协议的一部分,有若干个步骤确保了数据被正确发送,因此HTTP非常可靠。然而,该这样也给通信增加了延迟(额外的步骤意味着额外的时间)。因此,考虑这样一个场景:在最后一个微服务完成之前,3个或更多的微服务需要在彼此之间交换数据。换句话说,需要让A向B发送数据,这样B才可以向C发送数据,然后C才能够发送响应。除了每个服务的处理时间外,还必须考虑在它们之间建立3个HTTP通道所增加的延迟。
超时。虽然可以在大多数场景中配置超时时间,但默认情况下,如果服务器占用的时间太长,将导致客户端关闭连接。多长时间是“太长”?这取决于配置和当前的服务,但是总会有这么个时间。这为业务逻辑增加了额外的约束:需要快速执行,否则将失败。
失败难以解决。解决服务器故障并不是不可能的,但是需要有额外的基础设施。默认情况下,如果服务器关闭,将不会通知客户端。客户端只有在试图访问服务器时才会意识到这一点,但已经为时已晚。有一些方法可以缓解这种情况,例如使用负载平衡器或API网关,但需要在CS通信之上进行额外的工作,以使其更可靠。
因此,如果我们的业务逻辑快速可靠,并且需要被许多不同的客户端访问,HTTP API是一个很好的解决方案。多个团队在不同的客户端上工作时,可以基于一个标准、一致的接口通信,这会非常有用。
如果多个服务需要互相交互,或者其中一些服务中的业务逻辑需要大量时间才能完成,那么就不要使用HTTP API。
异步消息(Asynchronous Messaging)
这种模式还包括了一个在消息生产者和接收端之间的消息代理。
这绝对是我最喜欢的多服务之间通信的方式之一,尤其是当我们需要横向扩展平台的处理能力时。
微服务之间的异步通信
这种模式通常需要引入消息代理,因此会增加额外的复杂性。然而,这样做的好处远不止于抽象。
优点
容易扩展。客户端和服务器之间直接通信的一个主要问题是,为了让客户端能够发送消息,服务器需要有空闲的处理能力,但这受到单个服务可以执行的并行处理量的限制。如果客户端需要发送更多的数据,那么服务就需要扩容并拥有更多的处理能力。这有时可以通过扩展服务部署的基础设施来解决,使用更好的处理器或更多的内存,但总会有上限。相反,我们可以继续使用较低规格的基础设施,并让多个副本并行工作。消息代理可以将接收到的消息分发到多个目标服务,可以根据需求,让副本接收相同的数据或不同的消息。
易于添加新服务。创建新服务、订阅希望接收的消息类型、将新服务连接到工作流,都会很简单。生产者不需要知道新服务,只需要知道需要发送什么样的消息。
简单的重试机制。如果消息的传递由于服务器宕机而失败,只要消息代理愿意,可以自动继续尝试,不需要编写特殊的逻辑。
事件驱动。异步消息可以帮助我们创建事件驱动体系架构,这是微服务交互的最有效方式之一。与其让单个服务因为等待同步响应而被阻塞,或者更糟的是,让它不断轮询某个存储介质来等待响应,还不如编写服务代码,以便在数据准备就绪时通知它们。当需要等待响应时,服务可以处理其他事情(比如处理下一个传入的请求)。这种架构可以更快的数据处理、更有效的使用资源和提供更好的整体通信体验。
缺点
调试困难。由于没有明确的数据流,只是承诺消息会被尽快处理,因此调试数据流和数据处理路径可能会成为一场噩梦。这就是为什么通常需要在接收到消息时生成一个唯一ID,这样就可以通过日志跟踪消息在内部系统中的路径。
没有明确的直接响应。考虑到此模式的异步特性,一旦从客户端接收到请求,唯一可能的响应是“OK,收到了,一旦准备好,我会让您知道”。对于无效请求,还可以发送400错误。问题是,客户端不能直接访问服务端的执行逻辑返回的输出,而是需要单独请求。作为一种替代方法,客户端可以订阅响应消息类型。通过这种方式,一旦响应消息到达,客户端将立即得到通知。
代理成为单点故障。如果没有正确配置消息代理,它可能会成为架构的问题。虽然不必忍受自己编写的不稳定的服务,但却被迫维护一个几乎不知道如何使用的消息代理。
这绝对是一个有趣的模式,并且提供了很大的灵活性。如果生产者端需要产生大量消息,那么在生产者和消费者之间有一个类似缓冲区的结构将增加系统的稳定性。
虽然处理过程可能会很慢,但有了缓冲区后,扩展将变得容易得多。
Socket链接(Direct socket connection)
有时候我们不必依赖古老的HTTP来发送和接收消息,而是可以采用一些完全不同的路径,使用一些更快的技术,比方说socket。
为微服务通信打开socket通道
乍一看,基于socket的通信很像在HTTP中实现的客户端-服务器模式,然而,如果仔细看,还是有一些区别:
对于初学者来说,协议要简单得多,这意味着也要快得多。当然,如果希望提供可靠的通信,需要编写更多代码来实现,不过HTTP所增加的额外延迟在这里已经消失了。
通信可以由任何一方参与者启动,而不仅仅是客户端。一旦打开socket通道,它会一直保持这种状态,直到被关闭。可以把它想象成一个进行中的电话,任何人都可以开始对话,而不仅仅是打电话的人。
话虽如此,还是来看看这种方法的利弊:
缺点
没有真正的标准。与HTTP相比,基于socket的通信似乎有点混乱,没有任何结构化的标准(比如SOAP和REST)。因此,需要实现方来定义通信结构。反过来又使得创建和实现新客户端有点困难。但是,如果只是为了自己的服务可以相互交互,那么实际上是在实现自定义协议。
容易使接收端过载。如果一个服务产生太多的消息让另一个服务无法处理,那么可能会导致第二个服务无法承受并崩溃。这就是上一个模式解决的问题。在这里,发送和接收消息之间的延迟非常小,这意味着吞吐量可以更高,但也意味着接收服务必须足够快的处理所有事情。
优点
轻量级。实现基本的socket通信只需要很少的工作和设置。当然,这取决于使用的编程语言,但其中一些,例如带有Socket.io[4]的Node.js,可以通过几行代码就实现两个服务的通信。
非常优化的通信流程。由于在两个服务之间有一个长时间打开的通道,因此双方都能够在消息到达时作出反应。和拉取数据库来获取新消息的方式不一样,这是一个反射性的方法(reactive approach),没有比这个更快的方式了。
基于socket的通信是让服务彼此通信的非常有效的方式。例如,当部署为集群时,Redis使用这个方法来自动检测失败的节点,并将它们从集群中移除。由于通信速度快且成本低(意味着几乎没有额外的延迟,并且只需要很少的网络资源),才可以做到这一点。
如果能够控制服务之间交换的信息量,并且不介意定义自己的标准协议,那么就可以使用这种方法。
轻量级事件(Lightweight events)
此模式混合了前两种模式。一方面,它提供了一种让多个服务通过消息总线相互通信的方式,从而允许异步通信。另一方面,由于它只通过该通道发送非常轻量级的载荷,并要求调用相应服务的REST API将额外信息与载荷结合起来。
微服务通信中的轻量级事件和API的混合作用
当我们希望尽可能控制网络流量,或者当消息队列有包大小限制时,这种通信模式非常方便。在这种情况下,最好让事情尽可能简单,然后只在需要的时候要求额外的信息。
优点
两全其美。因为有80-90%的数据通过类似缓冲区的结构发送,因此这种方法提供了异步通信模式的优点,并且只需要通过效率较低但标准的、基于API的方法来完成一小部分网络流量。
重点优化最常见的场景。如果我们知道在大多数情况下不需要使用额外的信息来填充事件,那么将其保持在最低限度将有助于优化网络流量,并将消息代理的需求保持在非常低的水平。
基本的缓冲区。通过这种方法,每个事件的额外细节都是保密的,并且远离缓冲区。这反过来又消除了在需要为这些消息定义schema的情况下可能有的耦合。保持缓冲区的“哑(dumb)”使它更容易与其他系统交互,特别是在需要迁移或扩展的情况下(例如从RabbitMQ迁移到AWS SQS)。
缺点
可能会有太多API请求。如果不小心为不适合的用例实现此模式,那么最终将面临API请求的开销,而这会增加响应服务的额外延迟,更不用说服务之间发送的所有HTTP请求所增加的额外网络流量了。如果面临这样的场景,请考虑切换到完全基于异步的通信模型。
两倍的通信接口。服务必须提供两种不同的通信方式。一方面,需要实现消息队列所需的异步模型,但另一方面,还必须具有类似于API的接口。考虑到两种方法使用的不同,这可能会变得难以维护。
这是一种非常有趣的混合模式,考虑到需要将两种方法混合在一起,需要花费一些精力编写代码。
这可以是一种非常好的网络优化技术,确保对于对应用例的载荷混合请求只发生大约10 - 20%的比例,否则带来的好处将不值得为其编写额外的代码。
微服务之间通信的最佳方式是提供了我们想要的东西的方式,可以是性能、可靠性或者安全性,我们必须知道想要什么,然后基于这些信息来选择最佳模式。
没有通信模式的银弹,即使像我一样更喜欢其中一种模式,现实的说,还是必须找到适应当前用例的模式。
References:
[1] My Favorite Interservice Communication Patterns for Microservices: https://blog.bitsrc.io/my-favorite-interservice-communication-patterns-for-microservices-d746a6e1d7de
[2] bit: https://github.com/teambit/bit
[3] Postman: https://www.postman.com/
[4] Socket.io: https://socket.io/
你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind
《微服务架构设计模式》读书笔记 | 第3章 微服务架构中的进程间通信 #yyds干货盘点#
@[TOC](第3章 微服务架构中的进程间通信)
前言
这是一本关于微服务架构设计方面的书,这是本人阅读的学习笔记。首先对一些符号做些说明:
()为补充,一般是书本里的内容;
[]符号为笔者笔注;
微服务架构将应用程序构建为一组服务,这些服务必须经常协作才能处理各种外部请求。而服务的实例通常是在多台机器上运行的进程,所以它们必须使用进程间通信进行交互。
当前有多种进程间通信机制,比较流行的是REST(使用JSON)。选择合适的进程间通信机制是一个重要的架构决策,它影响应用程序的可用性。
1. 微服务架构中的进程间通信概述
1.1 交互方式的两个维度
- 第一个维度:
- 一对一:每个客户端请求由一个服务实例来处理;
- 一对多:每个客户端请求由多个服务实例来处理;
- 第二个维度:
- 同步模式:客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞;
- 异步模式:客户端请求不会阻塞进程,服务端的响应可以是非实时的;
1.2 交互方式的类型
- 一对一交互:
- 请求/响应:一个客户端向服务端发起请求,等待响应;客户端期望服务端很快就会发送响应。在一个基于线程的应用中,等待过程可能造成线程阻塞。这样的方式会导致服务的紧耦合;
- 异步请求/响应:客户端发送请求到服务端,服务端异步响应请求。客户端在等待时不会阻塞线程,因为服务端响应不会马上返回;
- 单向通知:客户端的请求发送到服务端,但是并不期望服务端做出任何响应;
- 一对多交互:
- 发布/订阅方式:客户端发布通知消息,被零个或多个感兴趣的服务订阅;
- 发布/异步响应方式:客户端发布请求消息,然后等待从感兴趣的服务发回的响应;
1.3 API的演化
- 语义化版本控制:用于指定如何使用版本号,并且以正确的方式递增版本。其由3部分组成:
- MAJOR:对API进行不兼容的修改时;
- MINOR:对API进行向后兼容的增强时;
- PATCH:进行向后兼容的错误修复时;
- 规范:
MAJOR.MINOR.PATCH
;
- 进行次要并且向后兼容的改变:对ADP的附加修改更换或功能增强。其包括:
- 添加可选属性;
- 向响应添加属性;
- 添加新操作;
- 进行主要并且不向后兼容的版本:需要服务在一段时间内同时支持新旧版本的API时;
1.4 消息的格式
- 基于文本的消息格式:
- 举例:JSON、XML;
- 好处:可读性高、自描述性,有良好的向后兼容性 [消息接收方只需挑选他们感兴趣的值,忽略其他];
- 弊端:信息冗长,解析文本需要额外的性能效率开销;
- 二进制消息格式:
- 举例:Tars、Protocol Buffers、Avro;
- 好处:提供强类型定义的IDL(接口描述文件),用于定义消息;编译器会根据这些格式生成序列化和反序列化代码;
- 弊端:不得不采用API优先的方法进行服务设计
2. 基于同步远程过程调用模式的通信
2.1 远程过程调用RPI
图解:客户端业务逻辑调用代理接口,这个接口由远程过程调用代理适配器类实现。远程过程调用代理向服务器发送请求,该请求由远程过程调用服务器适配器类处理,该类通过接口调用服务的业务逻辑。然后它将恢复发送回远程过程调用代理,该代理将结果返回给客户端的业务逻辑。
- 代理接口:通常是封装底层通信协议,如下面介绍的REST与gRPC。
2.2 REST通信协议的特点及优缺点
特点:
- REST使用HTTP动词来操作资源,使用URL引用这些资源;
- 资源通常使用XML文档或JSON对象的形式,也可以使用其他格式(二进制等);
- REST的成熟模型有:有4个层次(P71);
- REST API:最流行的REST IDL是Open API规范,它是从Swagger开源项目发展而来的;
- REST API的挑战:
- 在一个请求中获取多个资源的挑战:指如何在单个请求中检索多个相关对象;
- 吧操作映射为HTTP动词的挑战:指一个HTTP动词可能对应多种方法,如PUT请求更新订单可能包括取消订单、修改订单等;
好处:
- 非常简单,大家比较熟悉;
- 可以使用浏览器扩展(如Postman插件)或者curl之类的命令行测试HTTP API;
- 直接支持请求/响应方式的通信;
- HTTP对防火墙友好;
- 不需要中间代理,简化系统架构;
弊端:
- 只支持请求/响应方式的通信;
- 可能导致可用性降低。由于客户端和服务直接通信而没有代理来缓冲消息,因此它们必须在REST API调用期间保持在线;
- 客户端必须知道服务实例的位置(URL)。客户端必须使用所谓的服务发现机制来定位服务实例;
- 在单个请求中获取多个资源具有挑战性;
- 有时很难将多个更新操作映射到HTTP动词;
2.3 gRPC通信协议的特点及优缺点
特点:
- gRPC API由一个或多个服务和请求/响应消息定义组成;
- 服务定义类似Java接口,是强类型方法的集合;
- 使用Protocol Buffers作为消息格式,是一种高效且紧凑的二进制格式,是一种标记格式;
- 因此gRPC使API能够在保持向后兼容的同时进行变更;
好处:
- 设计具有复杂更新操作的API非常简单;
- 具有高效、紧凑的进程间通信机制,尤其是在交换大量消息时;
- 支持在远程过程调用和消息传递过程中使用双向流式消息方式;
- 实现了客户端和用各种语言编写的服务端之间的互操作性;
弊端:
- 与基于REST/JSON的API机制相比,JavaScript客户端使用基于gRPC的API需要做更多的工作;
- 旧式防火墙可能不支持HTTP/2;
2.4 同步通信下的局部故障风险
图解:当Order Service无响应时,OrderServiceProxy将无限期地阻塞,等待响应。会消耗时间、浪费线程等资源。最终API Gateway将资源消耗,无法处理请求,整个API不可用。
解决方法是:
- 必须让远程过程调用代理(如OrderServiceProxy)有正确处理无响应服务的能力;
- 需要决定如何从失败的远程服务中恢复;
2.5 解决局部故障的思路与方法
- 开发可靠地远程过程调用代理:使用Netflix描述的方法,可以包括以下机制的组合;
- 网络超时:在等待针对请求的响应时,不要做成无限阻塞,而是设定一个超时,用来保证不会一直在无响应的请求上浪费资源;
- 限制客户端向服务器发出请求的数量:把客户端能够向特定服务发起的请求设置一个上限,如果请求达到上限,就让请求立刻失败;
- 断路器模式:监控客户端发出请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续调用立即失败。如果大量请求都以失败告终,说明被调服务不可用。经过一定时间后,客户端继续尝试,如果调用成功,则移除断路器;
- 从服务失效故障中恢复:
- 可以只是服务向其客户端返回错误;
- 返回备用值(如默认值或缓存响应);
2.6 应用层服务发现模式
- 服务实例使用服务注册表注册其网络位置。客户端首先通过查询服务注册表获取服务实例列表来调用服务,然后它向其中一个实例发送请求;
- 这种服务发现是以下两种模式的组合:
- 自注册模式:服务实例向服务注册表注册自己;
- 可以提供运行状态检查URL(“心跳”功能,服务注册表定期调用该端点验证服务实例是否正常且可用于处理请求);
- 客户端发现模式:客户端从服务注册表检索可用服务实例的列表,并在它们之间进行负载均衡;
- 为了提高性能,客户端可能会缓存服务实例;
- 业界有Netflix开发的Eureka组件,一个高可用的服务注册表;Pivotal开发的SpringCloud
使相关组件使用非常简单;
2.7 平台层服务发现模式
- 部署平台包括一个服务注册表,用于跟踪已部署服务的IP地址;
- 部署平台为每个服务提供DNS名称、虚拟IP(VIP)地址和解析为VIP地址的DNS名称;
- 这种服务发现是以下两种模式的组合:
- 第三方注册模式:由第三方负责(称为注册服务器)处理注册,而不是服务本身先服务注册表注册自己;
- 服务端发现模式:客户端向DNS名称发出请求,对该DNS名称的请求被解析到路由器,路由器查询服务注册表并对请求进行负载均衡;
- 业界有Docker与Kubernetes,都内置有服务注册表与服务发现机制;
3. 基于异步消息模式的通信
基于消息机制的应用程序通常采用消息代理;另一种选择是使用无代理架构。
3.1 关于消息
3.2 关于消息通道
有以下两种类型的消息通道:
- 点对点通道:
- 向正在从通道读取的一个消费者传递消息;
- 如:命令式消息通常通过点对点通道发送;
- 发布 - 订阅通道:
- 将一条消息发送给所有订阅的接收方;
- 如:事件式消息通常通过发布 - 订阅通道发送;
3.3 使用消息机制实现交互方式
- 实现单向通知:
- 客户端将消息(通常是命令式消息)发送到服务所拥有的点对点通道;
- 服务订阅该通道并处理该消息,但服务不会发回回复;
- 实现发布/订阅:
- 客户端将消息发布到由多个接收方读取的发布/订阅通道;
- 发布领域事件的服务拥有自己的发布/订阅通道,通道名称往往派生自领域类;
- 如:Order Service将Order事件发布到Order通道;Delivery Service将Delivery事件发布到Delivery通道;
- 实现发布/异步响应:
- 一种更高级的交互方式,将发布/订阅与请求/响应这两种方式的元素组合实现;
- 客户端发布一条消息,在消息的头部中指定回复通道。这个通道同时也是一个发布 - 订阅通道;
- 消费者将包含相关性ID的回复消息写入回复通道;
- 客户端通过使用相关性ID来收集响应,以此将回复消息与请求进行匹配;
- 实现请求/响应和异步请求/响应:
- 客户端发送请求,服务会发回回复;
- 客户端必须告知服务发送回复消息的位置,并且必须将回复消息与请求匹配;
- 即:客户端发送具有回复通道头部的命令式消息。服务器将回复消息写入回复通道,该回复消息包含与消息标识符具有相同的相关性ID。客户端使用相关性ID将回复消息与请求匹配;
- 由于客户端和服务端使用消息机制进行通信,因此交互本质上是异步的;
- 工作原理图如下:
3.4 为基于消息机制的服务API创建API规范
- 服务的异步API包含供客户端调用的操作和由服务对外发布的事件;
- (记录异步操作)可以使用以下两种不同交互方式之一调用服务的操作:
- 请求/异步响应式API:包括服务端命令消息通道、服务接受的命令式消息的具体类型和格式,以及服务发送的回复消息的类型和格式;
- 单向通知式API:包括服务的命令消息通道,以及服务接受的命令式消息的具体类型和格式;
- (记录事件发布)服务还可以使用发布/订阅的方式对外发布事件;
- 此API风格等规范包括事件通道以及服务发布到通道的事件式消息的类型和格式;
3.5 无代理消息的利弊
好处:
- 允许更轻的网络流量和更低的延迟,因为没有中间代理过程;
- 消除了消息代理可能成为性能瓶颈或单点故障的可能性;
- 具有较低的操作复杂性,因为不需要设置和维护消息代理;
弊端:
- 服务需要了解彼此位置,因此必须使用服务发现机制;
- 降低可用性,因为在交换消息时,信息的接收方和发送方必须同时在线;
- 在实现例如确保消息能够成功投递这些复杂功能时的挑战性更大;
举例:
- ZeroMQ:一种流行的无代理消息技术;
3.6 基于代理消息的利弊
好处:
- 松耦合;
- 消息缓存:消息代理可以在消息被处理之前一直缓存消息;
- 灵活的通信:消息代理支持前面提到的所有交互方式;
- 明确的进程间通信
弊端:
- 潜在的性能瓶颈:解决方法 - 横向扩展;
- 潜在的单点故障:解决办法 - 大多数现代消息代理是高可用的;
- 额外的操作复杂性:消息系统必须是一个独立安装、配置和运维的系统组件;
举例:
- 流行的开源消息代理:Apache ActiveMQ(JMS)、RabbitMQ(AMQP)、Apache Kafka;
- 基于云的消息服务:AWS Kinesis、AWS SQS;
- 上述除了AWS SQS外都支持点对点和发布 - 订阅通道;AWS SQS只支持点对点通道;
3.7 选择消息代理需要考虑的因素
- 支持的编程语言;
- 支持的信息标准;
- 消息排序:消息代理是否能够保留消息的排序;
- 投递保证:消息代理提供怎样的消息投递保证;
- 持久性;
- 耐久性:如果接收方重新连接到消息代理,它是否会收到断开连接时发送的消息;
- 可扩展性;
- 延迟;
- 竞争性(并发)接收方:消息代理是否支持竞争性接收方;
3.8 处理并发和消息顺序
解决方法:使用分片消息通道扩展接收方;
图解:
- 分片通道由两个或多个分片组成,每个分片的行为类似于一个通道;
- 发送方在消息头部指定分片键,通常是任意字符串或字节序列。消息代理使用分片键将消息分配给特定的分片;
- 如:通过计算分片键的散列来选择分片;
- 消息代理将接收方的多个实例组合在一起,并将他们视为相同的逻辑接收方;
- 如:Apache Kafka使用术语消费者组;消息代理将每个分片分配给单个接收器;它在接收方启动和关闭时重新分配分片;
3.9 处理重复消息
有以下两种解决办法:
- 编写幂等消息处理器:
- 幂等操作特点:任意多次执行所产生的影响均与一次执行的影响相同;
- 跟踪消息并丢弃重复消息:
- 将消息处理程序注册进应用程序表(NoSQL)【第七章介绍】;
- 使用message id跟踪消息并丢弃重复消息,如下图:
3.10 事务性消息
- 使用数据库表作为消息队列:
- 事务性发件箱:通过将事件或消息保存在数据库OUTBOX表中,将其作为数据库事务是一部分发布;
- 事务性发件箱:通过将事件或消息保存在数据库OUTBOX表中,将其作为数据库事务是一部分发布;
- 通过轮询模式发布事件:
- 轮询发布数据:通过轮询数据库中的发件箱发布消息;
- 小规模下运行良好,弊端在于经常轮询数据库会造成较大开销;
- 使用事务日志拖尾模式发布事件:
- 事务日志拖尾:通过拖尾数据日志发布对数据库所做的修改;
- 一些行业案例:Debezium、Linkedln Databus、DynamoDB streams、Eventuate Tram;
- 下图解:每次应用程序提交到数据库的更新都对应着数据库事务日志中的一个条目;事务日志挖掘器可以读取事务日志,把每条跟消息有关的记录发送给消息代理;
3.11 消息相关的类库和框架
有两种方法:
- 使用消息代理的客户端库,问题有:
- 客户端库将发布消息的业务逻辑耦合到消息代理API;
- 客户端库通常只提供发送和接收消息的基本机制,不支持更高级别的交互方式;
- 消息代理的客户端库通常非常底层,需要多行代码才能发送/接收消息;
- 使用更高级别的库或框架来隐藏底层细节,并直接支持更高级别的交互方式:
- 如Eventuate Tram框架;
4. 使用异步消息提高可用性
4.1 同步消息会降低可用性
4.2 消除同步交互的方法
-
使用异步交互模式:
- 下图解:客户的通过Order Service发送一个请求消息交换消息的方式创建订单;这个服务随即采用异步交换消息的方式跟其他服务通信完成订单的创建;
- 缺点:很多情况下都要采用REST等同步通信协议API,不能替换为异步;
- 复制数据:
- 下图解:Consumer Service和Restaurant Service在它们的数据发生变化时对外发布事件;Order Service订阅这些事件,并据此更新自己的数据副本;
- 缺点:当数据量巨大时效率低下;
- 先返回响应,再完成处理:
- 下图解:Order Service创建一个未检验(Pending)状态的订单,然后通过异步交互方式直接跟其他服务通信来完成验证;
- 缺点:使客户端更复杂。
5. 本章小结
- 微服务架构是一种分布式架构,因此进程间通信起着关键作用;
- 仔细管理服务API的演化至关重要。向后兼容的更改是最容易进行的,因为它们不会影响客户端。如果对服务的API进行重大更改,通常需要同时支持旧版本和新版本,直到客户端升级为止;
- 有许多进程间通信技术,每种技术都有不同的利弊。一个关键的设计决策是选择同步远程过程调用模式或异步消息模式。基于同步远程过程调用的协议(如REST)是最容易使用的。但是,理想情况下,服务应使用异步消息进行通信,以提高可用性;
- 为了防止故障通过系统层层蔓延,使用同步协议服务的客户端必须设计成能够处理局部故障,这些故障是在被调用的服务停机或表现出高延迟时发生的。特别是,它必须在发出请求时使用超时,限制未完成请求的数量,并使用断路器模式来避免调用失败的服务;
- 使用同步协议的架构必须包含服务发现机制,以便客户端确定服务实例的网络位置。最简单的方法是使用部署平台实现的服务发现机制:服务器端发现和第三方注册模式。但另一种方法是在应用程序级别实现服务发现:客户的发现和自注册模式。它需要的工作量更大,但它确实可以处理服务在多个部署平台上运行的场景;
- 设计基于消息的架构的一种好方法是使用消息和通道模型,它抽象底层消息系统的细节。然后,你可以将该设计映射到特定的消息基础结构,该基础结构通常基于消息代理;
- 使用消息机制的一个关键挑战是以原子化的方式同时完成数据库更新和发布消息。一个好的解决方案是使用事务性发件箱模式,并首先将消息作为数据库事务的一部分写入数据库。然后,一个单独的进程使用轮询发布者模式或事务日志拖尾模式从数据库中检索信息,并将其发布给消息代理。
最后
::: hljs-center
新人制作,如有错误,欢迎指出,感激不尽!
:::
::: hljs-center
欢迎关注公众号,会分享一些更日常的东西!
:::
::: hljs-center
如需转载,请标注出处!
:::
::: hljs-center
:::
以上是关于微服务通信设计模式的主要内容,如果未能解决你的问题,请参考以下文章