选择合适的进程间通信机制是一个重要的架构决策,它会影响应用的可用性,甚至与事务管理相互影响。
首先考虑交互方式有助于你专注于需求,避免陷入细节。
一对一 | 一对多 | |
---|---|---|
同步模式 | 请求/响应 | 无 |
异步模式 | 异步请求/响应 单向通知 | 发布/订阅 发布/异步响应 |
一对一: 每个客户端请求由一个服务实例处理
一对多: 每个客户端请求由多个服务实例处理
单向通知: 客户端的请求发送到服务端,并不期望服务端做出任何响应
发布/订阅方式: 客户端发布通知消息,被零个或多个感兴趣的服务订阅
发布/异步响应方式: 客户端发布请求消息,等待从感兴趣的服务发回的响应
服务的API是服务与其客户端之间的契约,它由客户端结构可以调用的方法、服务发布的事件组成。
挑战:
没有一个简单的编程语言结构来构造和定义服务的API。若使用不兼容的API部署新版本的服务,虽然在编译阶段不会出现错误,但会出现运行时故障。
首先编写接口定义,然后与客户端开发人员一起查看这些接口的定义。只有在反复迭代几轮API定义后,才可以具体服务实现编程。这种预先设计有助于你构建满足客户端需求的服务。
挑战:
1、不能够强行要求客户端和服务端API版本保持一致
2、你一般采用滚动升级的方式更新服务,因此一个服务的旧版本和新版本肯定会共存
语义化版本控制
它是一组规则,用于指定如何使用版本号,并且以正确的方式递增版本号,版本号由三部分组成,必须按如下方式递增版本号:
你可以在实现REST API或消息机制的服务时,包含版本号
进行次要且向后兼容的改变
理性情况下应该只进行向后兼容的更改:
服务应该为缺少的请求属性提供默认值,客户端应忽略任何额外的响应属性,这样老版本的客户端能直接只用更新的服务
进行主要且不向后兼容的改变
此时必须在一段时间内同时支持新旧版本的API
假如使用REST,可以在URL中嵌入主要版本号,或者使用HTTP的内容协商机制,在MIME类型中包含版本号。
实现API的服务适配器将包含在旧版本与新版本之间进行转换的逻辑,如API Gateway几乎会使用版本化的API
考虑到以后会扩展到其他语言,我们不应该使用类似java序列化这样跟语言强相关的消息格式
基于文本的消息格式
如JSON和XML,可读性高,自描述的。但往往过度冗长,解析开销过大。
二进制消息格式
对效率和性能敏感的场景下,比较适用。常见的如Protocol Buffers和Avro,它们提供了强类型定义的IDL,编译器会自动根据其格式生成序列化和反序列化的代码,因此你不得不采用API优先的方法来进行服务设计。
客户端的业务逻辑调用由RPI代理适配器类实现的接口,RPI代理类向服务发出请求,RPI服务器适配器类通过调用服务的业务逻辑来处理请求
REST是使用HTTP协议的进程间通信机制
其关键概念是资源,它通常表示单个业务对象。REST使用HTTP动词操作资源,使用URL引用这些资源。
REST成熟度模型
最流行的REST IDL是Open API的规范,他是从Swagger开源项目发展而来的。
一个请求中获取多个资源的挑战
REST资源通常以业务对象为导向,设计REST API时常见问题是如何使客户端能够在单个请求中检索多个相关对象。纯REST API要求客户端发出多个请求,更复杂的情况时需要更多往返并遭受过多延迟,其中一个解决方案是API允许客户端在获取资源时检索相关其他资源,如果情况更复杂耗时,则使用GraphQL和Falcor等替代技术。
把操作映射为HTTP动词的挑战
如何将在业务对象上执行的操作映射到HTTP动词。但很难将多个更新操作映射到HTTP动词,且更新可能不是幂等的,但这却是使用PUT的要求。
一种解决方案是定义用于更新资源的特定方面的子资源,还有就是将动词指定为URL的查询参数。但这不是很符合RESTful的要求。
REST的好处和弊端
好处:
弊端:
由于HTTP仅提供有限数量的动词,设计支持多个更新操作的REST API不总是很容易,gRPC可以避免此问题。它是一种跨语言客户端和服务端的框架,基于二进制消息,你可以基于Protocol Buffer的IDL定义gRPC API,能够保持在向后兼容的同时进行变更。
gRPC除简单的请求/响应RPC外,还支持流式RPC。
好处:
弊端:
服务端可能因为故障等无法在有限时间内对客户端请求做出响应,客户端等待响应被阻塞,这可能会在其他客户端甚至使用服务的第三方应用之间传导,导致服务中断。
解决方案:
1、开发可靠的远程过程调用代理,包括:
2、从服务失效故障中恢复
服务实例具有动态分配的网络位置,由于自动扩展、故障和升级,服务实例会动态更改,因此客户端代码必须使用服务发现
什么是服务发现
服务发现的关键组件是服务注册表
两种方式实现服务发现:
应用层服务发现模式
它是两种模式的组合
例子:如Euraka,高可用的服务注册表;Euraka java客户端;Ribbon,支持Eureka客户端的复杂Http客户端
好处:可以处理多平台部署的问题
弊端:需要为使用的每种编程语言提供服务发现库。开发者需要负责设置、管理服务注册表,分散一定精力。
平台层服务发现模式
它是两种模式的组合:
例子:Docker和Kubernetes
好处:服务发现的所有方面完全由部署平台处理
弊端:仅限于支持使用该平台部署的服务
客户端使用异步消息调用服务
消息由消息头部和消息主体组成 类型:
关于消息通道
发送方中的业务逻辑调用发送端接口,该接口由消息发送方适配器实现。消息发送方通过消息通道向接收方发送消息。消息通道是消息传递基础设施的抽象。调用接收方的消息处理程序适配器来处理消息。它调用接收方业务逻辑实现的接收端端口。
类型:
足够灵活,支持上面描述的所有交互方式
实现请求/响应和异步请求/响应
消息机制本质上是异步的,因此只提供异步请求/响应,但客户端可能会阻塞,直到收到回复。
通过在请求消息中包含回复通道和消息标识符来实现异步请求/响应。接收方处理消息将回复发送到指定的回复通道,回复消息包含与消息标志符具有相同值的相关性ID,用以匹配验证。
实现单向通知
实现发布/订阅
客户端将消息发布到由多个接收方读取的发布/订阅通道,对特定领域对象的事件感兴趣的服务只需订阅相应的通道。
实现发布/异步响应
它把发布/订阅和请求/响应两种方式的元素组合在一起
客户端发布一条消息,在头部指定回复通道,该通道也是发布-订阅通道。消费者将包含相关性ID的回复消息写入回复通道,客户端通过相关性ID来收集响应
不像REST,没有广泛采用的标准来记录通道和类型,需要自己定义。服务的异步API一般由消息通道和命令、回复和事件消息类型组成
记录异步操作
记录事件发布
服务可使用发布/订阅的方式对外发布事件
无代理消息
无代理架构中,服务可以直接交换消息,如ZeroMQ
好处:
弊端:
基于代理的消息
如ActiveMQ,Kafka 好处:
选择消息代理考虑因素:
使用消息代理实现消息通道:
每个消息代理都用自己与众不同的概念来实现消息通道,如Kafka使用主题实现点对点通道和发布-订阅通道,RabbitMQ使用交换+队列实现点对点通道,使用组播式交换和每客户端队列实现发布-订阅通道
好处:
弊端:
如何在保留消息顺序的同时,横向扩展多个接收方的实例
采用分片通道方案,如将orderId作为分片键,特定订单的每个事件都发布到同一个分片,该消息也由同一个接收方实例读取
1、分片通道由两个或多个分片组成,分片的行为类似于通道
2、发送方在消息头部指定分片键,消息代理使用分片键将消息分配给分片
3、消息代理将接收方的多个实例组合在一起。并将它们视为相同的逻辑接收方,如kafka中的消费者组。消息代理将每个分片分配给单个接收器。
正常情况下,保证传递的消息代理只会传递一次消息。但故障可能导致消息被多次传递。
两种方法处理重复消息:
编写幂等消息处理器:
幂等指这个应用被相同输入参数多次重复调用时,也不会产生额外的效果,但要保证消息代理在重新传递消息时保持相同顺序。
跟踪消息并丢弃重复消息:
简单的解决方案是消息接收方使用message id跟踪它已处理的消息并丢弃任何重复项
数据库更新和消息发送都必须在事务中进行,否则系统可能处于不一致状态。
使用数据库表作为消息队列
通过事务性发件箱模式,即将事件或消息保存在数据库的OUTBOX表中,将其作为数据库事务的一部分发布。
将消息从数据库移动到消息代理的两种方法:
通过轮询模式发布事件 轮询数据库中的发件箱,将消息发送给消息代理,它在小规模下运行良好,但经常轮询数据库可能会导致数据库性能下降
使用事务日志拖尾模式发布事件 应用提交到数据库的更新对应着数据库事务日志中的一个条目。事务日志挖掘器可以读取事务日志,将跟消息有关的记录发送给消息代理。
其挑战在于需要一些开发努力,现有框架有Debezium,Eventuate Tram等。
直接使用消息代理客户端库的弊端:
更好的方法是使用更高级别的库或框架,如Eventuate Tram
如REST,当服务必须从另一个服务获取信息后才能返回它客户端的调用,就会导致可用性问题。每增加一个额外的服务,会更进一步降低可用性。
要最大化一个系统的可用性,就应该最小化系统的同步操作量
方法:
使用异步交互模式:
客户端和服务端使用消息通道发送消息来实现异步通信。这种架构很有弹性,消息代理会一直缓存消息,直到服务端接收并处理消息。但服务很多情况采用同步通信协议的外部API,需要对请求立即作出响应。
复制数据:
服务维护一个数据副本,这些数据是服务在处理请求时需要使用的,数据的源头会在数据发生变化时发出消息,服务订阅这些消息来确保数据副本的实时更新。
弊端:
先响应,后处理
如Order Service,它在不调用任何其他服务的情况下创建订单,然后通过与其他服务交换信息来异步验证新创建的Order
优点:即使其他服务中断, Order Service仍然会创建订单响应客户
弊端:为了使客户端知道订单是否已成功创建,需要定期轮询或者向客户端发送通知。