引:目前微服务大行其道,当我们在谈微服务的时候,我们究竟在谈什么,我们究竟要思考哪些方面?那么这本书刚好能解答微服务的方方面面!
Java开发者社区的意见领袖Chris Richardson,涵盖44个架构设计模式,系统解决服务拆分、事务管理、查询和夸服务通信的难题。
要记住微服务不是解决所有问题的万能“银弹”
编写整洁的代码和使用自动化测试至关重要,因为这是现代软件开发的基础
关注微服务的本质,即服务的分解和定义,而不是技术,如容器和其他工具
确保你的服务松耦合、并且可以独立开发、测试和部署,不要搞成分布式单体,那将是巨大的灾难
不能只是在技术上采用微服务架构。拥抱DevOps的原则和时间在组织结构上实现跨职能的自治团队,必不可少
实现微服务架构并不是你的布标,你的目标是加速大型复杂应用程序的开发
软件的架构设计,就是选择和取舍。然后架构师就是做出取舍的人。
引言
逃离单体地域
描述“单体地域”的症状,以及如何通过微服务架构规避,并概述了微服务架构的模式语言。
微服务架构
《The Art of Scalability》 提出了一个非常可用的三维可扩展模型:扩展立方体,该模型描述了扩展一个应用程序的三种维度:X/Y/Z
x轴扩展:在多个实例之间实现请求的负载均衡,这是提高应用程序吞吐量的可用性的好方法。
Z轴扩展:根据请求的属性路由请求,他也需要运行单体应用程序的多个实例,但是每个实例仅复杂数据的一个子集,比如根据租户ID,或者用户ID,对于应用程序需要处理增加的事务和数据量时,Z轴扩展是一种很好的扩展方式。
Y轴扩展:根据功能把应用拆分为服务,Y轴扩展把一个单体应用分成了一组服务,服务实现一组相关的功能。
微服务架构的概括性定义: 把应用 程序功能性分解为一组服务的架构风格。每一个服务都是有一组专注的、内聚的功能职责组成。
微服务架构与SOA的异同:
SOA:服务间通信(智能管道,例如ESB、往往采用重量级协议,例如SOAP);数据管理(全局数据模型并共享数据库);典型服务规模(较大的单体应用)
微服务:服务间通信(哑管道,例如消息代理或者服务之间点对点通信,例如使用REST或gRPC类的轻量级协议);数据管理(每个服务都有自己的数据模型和数据库);典型服务的规模(较小的服务)
微服务架构的好处和弊端
好处:
- 使大型的复杂应用程序可以持续交付和持续部署
- 每个服务都相对较小并容易维护
- 服务可以独立部署
- 服务可以独立扩展
- 微服务架构可以实现团队的自治
- 更容易实验和采纳新的技术
- 更好的容错性
弊端:
没有一项技术可以被称为“银弹”
- 服务的拆分和定义是一项挑战(噩梦:分布式单体应用)
- 分布式系统带来的各种复杂性((挑战:跨服务的事务(saga)和查询(API组合和CQRS视图)))、使开发、测试和部署变得更困难(自动化部署工具,Paas平台,docker容器编排)
- 当部署跨越多个服务的功能时需要谨慎协调更多开发团队。(必须制定一个发布计划,把服务按照依赖关系进行排序)
- 开发者需要思考到底在应用的什么阶段使用微服务架构。
微服务架构的模式语言
模式: 是针对特定上下文中发生的问题的可重用解决方案。
模式语言: 解决特定领域内问题的相关模式的集合。
模式结构:
需求
需求部分描述了必须解决的问题和围绕这个问题的特定上下文环境。需求有时候是互相冲突的。把所有的需求明确列出是非常有帮助的,因为它可以清晰展现哪些问题需要被(优先)解决。
结果上下文
结果上下文部分描述了这个模式的结果,他包含好处(这个模式的好处和他解决了什么需求)、弊端(这个模式的弊端和他没有解决哪些需求)、问题(使用这个模式所引入的新问题),结果上下文提供了更加完整、不偏不倚的视角来描述解决方案,这有助于更好的决策。
相关模式
相关模式部分描述了这个模式和其他模式之间的关系。模式之间存在5种关系:前导(前导模式是催生这个模式的需求的模式)、后续(后续模式是指用来解决当前模式引入的新问题的模式,如果采用了微服务架构模式,你需要一系列的后续模式来解决诸如服务发现、断路器等微服务带来的新问题)、替代(当前模式的替代模式)、泛化(针对一个问题的一般性解决方案)、特化(针对特定模式的具体解决方案)
微服务架构模式分为三组:
- 基础设施相关模式组:这些模式解决通常是在开发环节和基础设施相关的问题
- 应用基础设施相关模式组:这些模式解决应用层面的基础设施相关问题
- 应用相关模式组:这些模式解决开发人员面对具体技术和架构问题
换种分组:
服务拆分的相关模式
根据业务能力分解模式、根据子域分解模式
通信的相关模式
通信风格、服务发现、可靠性、事务性消息、外部API
实现事务管理的数据一致性相关模式
2pc、saga
在微服务架构中查询数据的相关模式
API组合、CQRS
服务部署的相关模式
serverless部署、每主机单/多服务部署
可观测的相关模式
健康检查API、日志聚合、分布式追踪、异常跟踪、应用指标、审计日志(埋点)
实现服务自动化测试的相关模式
消费端驱动的契约测试、消费端锲约测试、服务组件测试
解决基础设施和边界问题的相关模式
安全相关的模式
服务之上:流程和组织
流程:DevOps
组织:小而自治的团队
康威定律:应用程序的架构往往反映了开发他的组织的结构
逆向的康威定律:设计你的企业组织,使其结构和微服务的结构意义对应
服务的拆分策略
描述可用于将应用程序分解为服务集合的模式
微服务架构到底是什么
软件架构的定义:计算机系统的软件架构是构建这个系统锁需要的一组结构,包括软件元素、他们之间的关系以及两者的属性。
应用程序的架构可以从多个视角来看:逻辑视图(由开发人员创建,元素:类和包,关系,他们之间的关系)、实现视图(由构建编译系统创建、元素:模块(jar)和组件(war),关系:他们之间的依赖关系)、进程视图(运行的组件,元素(进程),关系(进程间关系))、部署视图(运行在机器上的进程,元素(机器和进程),关系(网络)),最后通过场景将这4个视图串联起来。
应用程序的需求:一个层面是功能性需求(通常包含在用例usecase、user story)、一个层面是非功能性需求(质量属性需求,比如可扩展性和可部署性)
架构风格:根据结构组织模式定义了一系列此类系统。特定的架构风格提供了有限的元素(组件) 和关系(连接器),你可以从中定义应用程序架构的视图。应用程序通常使用多种架构风格的组合。比如分层架构、六边形架构(描述微服务架构中每个服务的架构的好方法)
微服务架构: 他的实现视图是一组可执行文件或war包,组件是服务,连接器是使这些服务能够协作的通信协议。每个服务都有自己的逻辑视图架构,通常是六边形架构。
服务: 服务具有封装实现的API。API定义了由客户端调用的操作。有两种类型的操作:命令用来更新数据,查询用来检索数据。当服务的数据发生更改时,服务会发布可供客户端订阅的时间。服务最基本的要求是具有API并且可以独立部署。
松耦合: 微服务架构最核心特性是服务之间的松耦合性。但是一般共享库就是耦合的来源。更好的方式是使用共享服务。如果一定要使用共享库,那么他一定是不太会改变的功能。
为程序定义微服务架构
根所有的软件开发过程一样,一开始我们需要拿到领域专家或有现有应用的需求文档。下面是一种常用的定义应用程序架构的三步式流程:
定义系统操作(从需求开始,通常是用户故事,然后定义出一个系统操作,代表一个外部的请求),可以是更新数据的命令,也可以是检索数据的查询,每个命令的行为都是根据抽象领域模型定义的,抽象领域模型也是从需求中派生出来的。
- 创建一个抽象领域模型 (主要来源于用户故事中提及的名词;或者使用事件风暴)
- 定义系统操作,这些操作是更具领域模型定义的(主要来源于用户故事中提及的动词)
定义服务
一种策略是源于业务架构学院的策略(定义域业务能力相对应的服务)
业务能力是指一些能够为公司产生价值的商业活动。
组织的业务能力通常是指这个组织的业务是做什么的,他们通常是稳定的,而业务能力的实现方式是随着时间不短变化的。
一个组织有哪些业务能力,是通过对组织目标、结构和商业流程的分析得来的。每个业务能力都可以被任务是一个服务。业务能力通常是集中在特定的业务对象上。
围绕能力组织服务的一个关键好处是,他们是稳定的。
另一种是围绕领域驱动设计的子域来分解和设计服务
领域驱动为每一个子域定义单独的领域模型。子域是领域的一部分,领域是DDD中用来描述应用程序问题域的一个术语。识别子域的方式和识别业务能力一样:分析业务并识别业务的不同专业领域。DDD把领域模型的边界称为界限上下文。当使用微服务架构时,每一个界限上下文对应一个或者一组服务。
拆分服务的指导原则: 单一职责原则(SRP)、闭包原则(CCP,调整需要在包内)还有上面的按业务能力和子域。
拆分单体应用为服务的难点: 网络延迟(批处理API解决,或者合并服务)、同步进程间通信导致可用性降低(使用异步消息)、在服务之间维持数据的一致性(Saga)、获取一致的数据视图(很少带来问题)、上帝类阻碍了拆分(使用DDD防腐层,每个服务都只需要保留上上帝类较少的属性)
定义服务API和协作方式(通信方式)
定义服务的API就是定义服务的操作和事件。操作分为两种:外部客户端调用和其他服务调用,服务通过对外发布事件,使其能够与其他服务协作,也可使用WebSocket将事件传递给浏览器。
- 把系统操作分配给服务,大部分情况下,将操作分配给具有处理它所需信息的服务,但是有些情况也可以分配给需要操作所提供信息的服务
- 确定支持服务协作所需要的API
微服务架构中的进程通信
介绍了微服务架构中强大的进程间通信的几种模式,解释了为什么异步和基于消息的通信通常是最佳选择。
一个理想的微服务架构应该是咋内部由松散耦合的若干服务组成,这些服务使用异步消息相互通信。REST等同步协议主要用于服务于外部其他应用程序的通信。
微服务架构中的进程通信
不同的进程间通信技术,可以是同步,也可以异步,然后通信时的数据格式也不同,可以是json,也可以pb
交互方式
交互方式的选择会影响应用程序的可用性,也可以影响你选择更合适的集成策略策略。交互的方式有以下两个维度:
- 一对一,一对多
- 同步(紧耦合)或异步
在微服务架构中定义API
服务的API是服务于其客户端之间的契约。服务的API由客户端结构可以调用的方法和服务发布的事件组成,方法具有名称、参数和返回类型、事件具有一个类型和一组字段。
无论哪种选择哪种进程间通信机制,使用某种接口定义语言(IDL)精确定义服务的API都很重要 (定义方式和事件)
API优先设计: 在开发时,先编写接口定义。
API的演化
API不可能是一层不变的,所以在演变过程中需要注意
- 语义化版本控制
- 进行次要并且向后兼容的改变(添加字段)
- 进行主要并且不向后兼容的改变(增加版本)
消息的格式
- 文本:json、xml 可读性高,兼容性高,缺点是过度冗长
- 二进制消息格式:protocol Buffers,会序列化成二进制
基于同步远程过程调用的通信
远程过程调用: 客户端使用同步的远程过程调用协议(如Rest)来调用服务
使用REST (使用HTTP协议的进程间通信机制)
REST成熟度模型
- LEVEL 0(大部分): 客户端只是向服务断点发起HTTP POST请求,进行服务调用,每个请求都指明需要执行的操作,和必要的参数
- LEVEL 1:引入资源的概念,要执行对资源的操作,客户端需要发出制定要执行的操作和包含任何参数的POST请求
- LEVEL 2: 使用HTTP动词来执行操作,GET、POST、PUT
- LEVEL 3: 基于HATEOAS原则设计,基本思想是又GET请求返回的资源信息中包含连接,这些连接能够执行该资源允许的操作。(感觉很奇怪)
定义REST API
使用Swagger
在一个请求中获取多个资源的调整
- 在获取资源的URL,加入需要的资源的信息
- 使员工GraphQL、Netflix Falcor
把操作映射为HTTP动词的挑战
我们会面临HTTP动词不够用的情况,比如使用PUT更新,但是更新可能是取消订单,也可以是更新订单,这个时候需要将动词指定为URL的参数。
这个时候发现好像REST不太行??
好处和弊端
好处:简单、可以使用curl等工具测试、支持请求和响应的方式通信、防火墙友好,不需要代理
弊端:只支持请求和响应的方式通信,可能导致可用性降低(没有代理来缓冲消息),客户端必须知道服务实例的位置(URL),在单个请求中获取多个资源(多个模型)具有挑战性,有时很难将多个更新操作映射到HTTP动词。
使用GRPC(使用HTTP/2以PB格式通信的进程间通信机制)
好处
- 设计具有复杂更新操作的API非常简单
- 他具有高效、紧凑的进程间通信机制,尤其是在交换大量消息时
- 支持在远程过程调用和消息传递过程中使用双向流式消息方式
- 他实现了客户端和用各种语言编写的服务端之间的互操作性
- 弊端
- 与基于REST/JOSN的API机制相比,JavaScript客户端使用基于grpc的API需要更多的工作
- 旧式防火墙之鞥呢不支持HTTP/2
使用断路器模式处理局部故障(A->B->C, 不能因为C的故障而导致B的完全不可用)
断路器模式: 断路器是一个远程过程调用的代理,在连续失败次数超过指定阈值后的一段时间内,这个代理会立即拒绝其他调用。
断路器模式的实现:监控客户端发出的请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续的调用立刻失效。在经过一定的时间后,客户端应该继续尝试,如果调用成功,则接触短路器。Java的Hystrix。
在考虑完如何保证自己服务不挂的同时,还需要考虑如何降级(报错或者走缓存),避免客户端完全失败 。
使用服务发现处理单机故障
实现服务发现有以下两种方式:
服务及其客户直接与服务注册表交互(服务端自注册,服务注册表定期心跳检测,客户端发现并进行负载均衡)。 Eureka+Ribbon
- 优点:服务可以多平台部署
- 缺点:服务如果夸语言,则每个语言都要实现sdk,同时开发需要自己维护服务注册表
通过部署基础设施来处理服务发现。(更棒)
部署平台包括一个服务注册表,用于跟踪已部署服务的IP地址
- 优点: 服务发现的所有方面都完全由部署平台处理。服务和客户端都不包含任何服务发现代码,而且夸语言和框架。
- 缺点:仅限于支持使用该平台部署的服务。
基于异步消息模式的通信
使用消息机制时,服务之间采用异步交换消息的方式完成。一种是使用消息代理,另一种无代理架构(反而不像消息)。基于消息机制的应用程序通常使用消息代理。
消息
消息由消息头部和消息主题组成。消息头部有描述正在发送的数据的元数据(感觉没有),以及基础设施生成的唯一消息ID,以及可选的返回地址(改地址指定发送回复的消息通道)。消息正文是以文本或二进制格式发送的数据。消息有一下几种不同的类型:
- 文档:仅包含数据的通用消息。接收者决定如何解释它。对命令式消息的恢复是文档消息的一种使用场景。(像是我们大多的使用场景)
- 命令:一条等同与RPC请求的消息。它指定要调用的操作及其参数。(定时调度的消息)
- 事件:表示发送方这一段发啥了重要的时间。事件通常是领域事件,表示领域对象的状态变更。
消息通道
- 点对点通道:向正在向通道读取的一个 消费者传递消息。服务使用点对点通道来实现前面描述的一对一的交互方式。命令式消息。
- 发布-订阅通道将一条消息发给所有订阅的接收方。服务使用发布-订阅通道来实现前面描述的一对多的交互方式。事件式消息。
使用机制实现交互方式
- 实现请求/响应和异步请求/响应:主要是客户端提供一个可回复的地址。
- 实现单向通知。
- 实现发布/订阅。服务使用发布/订阅发布领域事件,领域事件代表领域对象的更改。发布领域事件的服务拥有自己的发布/订阅通道,通道的名称往往派生自领域类。比如OrderService将Order事件发布到Order通道。对特定领域对象的事件感新校区的服务只需订阅响应的通道。
- 实现发布/异步响应
为基于消息机制的服务API创建API规范
服务的异步API包含供客户端调用的操作和由服务对外发布的事件
记录异步操作
- 请求/异步响应式API:包括服务的命令消息通道、服务接受的命令式消息的具体类型和格式,以及服务发送的恢复消息的类型和格式。
- 单向通知式API:包括服务的命令消息通道,以及服务接受的命令式消息的具体类型和格式。
基于事件发布
- 对外发布事件:包括事件通道,以及服务发布到通道的事件式消息的具体类型和格式
使用消息代理
基于消息传递对的应用程序通常使用消息代理,即服务通信的基础设施服务。基于消息代理的架构不是唯一的消息架构,还可以使用无代理的消息架构(比如ZeroMQ),无代理的消息架构服务是直接相互通信的,无代理架构更像是同步通信很相似(有client和server),有机会可以看看。下面主要看看基于代理的消息架构。
消息代理是所有消息的中介节点。发送方将消息写入消息代理,消息代理将消息发送给接收方。使用消息代理的一个重要好处是发送方不需要知道接收方的网络位置。另一个好处是消息代理缓冲消息,直到接收方能够处理他们。比如ActiveMQ、RabbitMQ、Kafka、RocketMQ
选择消息代理时,需要考虑各种因素:
- 支持的编程语言,因为需要client
- 支持的消息标准,比如AMQP和STOMP
- 消息排序
- 投递保证
- 持久性
- 耐久性
- 可扩展性
- 延迟
- 竞争性(并发)接收方
一般来说,消息顺序和可扩展性是必不可少的。
基于代理的消息的好处:
- 松耦合
- 消息缓存
- 灵活的通信
- 明确的进程间通信(有点不能体会)
基于代理的消息的弊端:
- 潜在的性能瓶颈
- 潜在的单点故障
- 额外的操作复杂性(需要支持一个消息系统)
处理并发和消息顺序
为了同时处理处理消息,提高应用系统的吞吐量,使用多个线程和服务实例来处理消息是很常见的事情。但是这样带来的挑战是如何确保每个消息只被处理一次,并且是按照他们发送的顺序来处理的。
常用的解决方案是使用分片(分区)通道:
- 分片通道由两个或者多个分片组成,每个分片的行为类型一个通道
- 发送方在消息头部指定分片键,通常是任意字符串或字节序列。消息代理使用分片键将消息分配给特定的分片。例如,它可以通过计算分片键的散列来选择分片。
- 消息代理将接收方的多个实例组合在一起,并将他们视为相同的逻辑接收方(消费组)。消息代理将每个分片分配给单个接收器。他在接收方启动和关闭时重新分配分片(感觉还是会出现不有序的情况)。
处理重复消息
使用消息消息机制时必须解决的另一个挑战是处理重复消息。理想情况下,消息情况下,消息代理应该只传递一次消息,但保证有且仅有一次的消息传递通常成本很高,相反,大多数消息代理承诺至少成功传递一次消息。
理想情况下,你应该使用消息代理,在重新传递消息时保留排序(A B C, A重投,B C也要跟着重投)
处理重复消息有以下两种不同的方法:
- 编写幂等消息处理程序(太难了)
- 跟踪消息并丢弃重复项:消息接收方使用message id(来个表,或者将message id 放入应用表中的一列)跟踪它已处理的消息并丢弃任何重复项。
事务性消息
服务通常需要在更新数据库的事务中发送消息(领域事件)。在这个时候,数据库更新和消息发送都必须在事务中运行。否则服务可能在更新数据库,然后在发送消息之前崩溃。如果服务不以原子方式执行这两个操作,则类似的故障可能使系统处于不一致状态。
传统的解决方案是在数据库和消息代理之间使用分布式事务。下面是可替代的方案:
使用数据库表作为消息队列
使用数据库表作为临时消息队列,发送消息服务有个消息数据库表(对于NoSql,可作为文档的一个属性),作为创建、更新和删除业务对象的数据库事务的一部分,服务通过将消息插入到消息数据库表中来发送消息,这样就利用了本地事务来保证了原子性。此时消息数据库表充当了临时消息队列,然后通过一个称为MessageRelay的服务读取消息并将消息发送到消息代理。
关于MessageRelay的实现有下面一种实现:
通过轮询模式发布事件
通过轮询轮询数据库的消息,把消息发送给消息dialing,然后把完成发送的消息从消息表中移除。轮询数据库是一种在小规模下运行良好的简单方法。弊端是经常轮询数据库可能造成数据库性能下降。由于这些弊端和限制,通常在某些情况下,更好的方法是使用更复杂和高性能的方法,来拖尾数据库事务日志。
使用事务日志拖尾模式发布事件
每次应用程序提交到数据库的更新都对应着数据库事务日志的一个条目。事务日志挖掘器可以读取事务日志,把每条更消息有关的记录发送给消息代理。具体实现有:Debezium(kafka)、linkedIn databus、DynamoDB streams、 Eventuate Tram(binlog kafka)
使用异步消息提高可用性
由于采用同步通信机制作为请求处理的一部分,会对系统的可用性带来影响。因此,应该竟可能选择一部通信机制来处理服务之间的调用。
同步通信会降低可用性
比如 A -> B -> C 这样的请求,同步通信要求3个服务都需要同时在线,如果每个服务的可用性是99.5,那么这个系统的可用性就是99.5 99.5 99.5 = 98.5了(系统整体的可用性是所有参与方的乘积)
消除同步交互
使用异步交互模式
想想很美好,但是很多情况下的API都要求对请求立刻做出影响
复制数据
通过维护B的数据副本,来减去对B的调用,但是弊端是有时候复制的数据量巨大,会导致效率低下
暂缓服务与其他服务的交互
立即响应请求,通过消息异步处理一些调用,然后通过前端轮询状态
使用Saga管理事务
介绍如何使用Saga模式维护服务间的数据一致性 (有时间可以自己实现一把)
微服务架构下的事务管理
由于微服务架构下的事务往往需要横跨多个服务,每个服务都有属于自己的私有数据库,所以传统的分布式事务管理(2PC,直接操纵两个数据库)不是微服务架构下选择。我们需要一些更为高级的事务管理机制来管理事务。
传统的分布式事务的挑战:不是所有数据库和消息代理都支持分布式事务,同时分布式事务是同步调用,会降低系统的可用性。
但是我们可以通过Saga模式来维护数据的一致性。一个Saga表示需要更新多个服务数据的一个系统操作。Saga由一连串的本地事务组成,每一个本地事务复杂更新它所在服务的私有数据库,这些操作仍然依赖于我们所熟悉的ACID事务框架。同时Saga使用补偿事务来回滚多个服务所做出的的改变。
Saga的协调模式
Saga的实现包含协调Saga步骤的逻辑。当通过系统命令启动Saga时,协调逻辑选择并通知第一个Saga参与方执行本地事务。一旦该事务完成,Saga协调选择并调用下一个Saga参与方。这个过程一直持续到Saga执行完所有步骤。如果任何本地事务失败,则Saga必须以相反的顺序执行补偿事务。下面是几种Saga的协调逻辑:
协同式
把Saga的决策和执行顺序逻辑分布在Saga的每个参与方中,他们通过交换事件的方式来进行沟通
好处:
- 简单:服务在创建、更新或删除业务对象时发布事件
- 松耦合
弊端:
- 更难理解:无法理解Saga是如何工作的
- 服务之间的循环依赖关系:Saga参与方订阅彼此的时间,这通常会导致循环依赖关系。
- 紧耦合的风险:每个Saga参与方都需要订阅所有影响他们的事件
编排式
把Saga的决策和执行顺序逻辑集中在一个Saga编排器类中,Saga编排器发出命令式消息给各个Saga参与方,指示这些参与方服务完成具体的操作(本地事务)
把Saga编排器视为一个状态机。状态机是建模Saga编排器的一个好方法。状态机由一组状态和一组由事件触发的状态之间的转换而成。每个转换都可以有一个动作,对Saga来说,动作就是对某个参与方的调用。状态之间的转换由Saga参与方执行的本地事务完成触发。当前状态和本地事务的特定结果决定了状态转换以及执行的动作。
好处:
- 更简单的依赖关系,Saga编排器调用Saga参与方,但参与方不会调用编排器
- 较少的耦合:每个服务实现供编排器调用的API
- 改善关注点隔离,简化业务逻辑 (领域对象更加简单)
弊端:
- 编排器中集中过多的业务逻辑的风险。
解决隔离问题
使用Saga的挑战在于他们缺乏ACID事务的隔离属性。这是因为一旦该事务提交,每个Saga的本地事务所做的更新都会立即被其他Sagas看到。
缺乏隔离导致的问题
- 丢失更新:一个Saga没有读取更新,而是直接覆盖了另一个Saga所做的改变
- 脏读:一个事务或一个Saga读取了尚未完成的Saga所做的更新。
- 不可重复读/幻读:一个Saga的两个步骤读取相同的数据却获得了不同的结果。
Saga模式下实现隔离的对策
Saga事务模型是ACD,他缺乏隔离可能导致的异常,从而导致应用程序行为错误。开发人员你有责任以一种防止异常或最小化对业务异常的方式来编写Saga。
一个有用的Saga结构模型,包含三种类型的事务:
- 可补偿性事务:可以使用补偿事务回滚的事务
- 关键性事务:Saga执行过程的关键点。如果关键性事务成功,则Saga将一直运行到完成。他可以是最后一个可补偿的事务或第一个可重复的事务
- 可重复性事务:在关键性事务之后的事务,保证成功
然后分析一下对策:
语义锁
使用语义锁对策时,Saga的可补偿性事务会在其创建或更新任何记录中设置标记,该标记表示该记录未提交且可能发生更改,该标记可以是组织其他事务访问记录的锁,也可以是指示其他事务应该谨慎地处理该记录的一个警告。这个标记会被一个可重复事务清除,这表示Saga成功完成;或通过补偿事务清除,这表示Saga发生了回滚。比如Order.state的各种*_PENDING状态。
管理语义锁只是问题的一半,还需要根据具体情况决定一个Saga应该如何处理已被锁定的记录。一个选择是让另一个saga失败并之后重试,另一个是让其阻塞,知道其他Saga释放语义锁。(如果是后一种就需要自己实现锁,并且实现死锁检测算法)
交换式更新
将更新操作设计为可交换的。如果可以按任何顺序执行,则操作是可交换的。(比如不考虑透支的情况的加减钱)
悲观视图
重新排序Saga的步骤,以最大限度得降低由于脏读而导致的业务风险。
重读值
Saga在更新之前重新读取记录,验证他是否未更改,然后更新记录,如果记录已经更改,则Saga将中止并可能重新启动。这是乐观锁的一种形式。
版本文件
记录对数据的操作(不太理解实现)
对业务风险评级
最终的决策是基于价值(业务风险)对策。对低风险请求,使用Saga,对于高风险请求,使用分布式事务。
微服务架构中的业务逻辑设计
介绍如何使用领域驱动设计(DDD)的聚合和领域事件等模式为服务设计业务逻辑
企业应用程序的核心是业务逻辑,业务逻辑实现了业务规则。开发复杂的业务逻辑总是充满挑战。
在微服务架构下,由于业务逻辑散布在多个服务上。我们需要解决两个关键问题:
- 需要避免夸服务边界的对象引用
- 必须处理分布式事务问题,比如Saga
业务逻辑组织模式
在六边形架构中,主要由业务逻辑和各种适配器组成,一般业务逻辑会有以下适配器:
- Rest API Adapter(RPC Adapter):入站适配器,实现Rest API,这些API会调用业务逻辑
- CommandHandler:入站适配器,他接受来自消息通道的命令式消息,并调用业务逻辑
- Database Adapter:由业务逻辑调用以访问数据库的出站适配器
- Domain Event Publishing Adapter:将事件发布到消息代理的出站适配器
组织业务逻辑有两种模式:
面向过程的事务脚本模式
通过实现行为的类和存储状态的类组成
面向对象的领域建模模式
有些类只有行为(Domain Service / Repository)、有些类只有状态(Value Object)、更多的类同时拥有行为和状态(Entity / Aggregate)
使用聚合模式设计领域模型
边界在业务逻辑处理中很重要,通过聚合显示将边界突出。然后在聚合上施加一些不变量。
聚合:将领域模型组织为聚合的集合,每个聚合都是可以作为一个单元进行处理的一组对象构成的图。
在领域驱动设计中,设计领域模型的关键部分是识别聚合,以及他们的边界和根。聚合内部结构的细节是次要的。聚合的机制不仅仅是帮助我们设计模块化的领域模型,更重要的是聚合必须遵守某些规则。
聚合规则
领域驱动设计要求聚合遵守一组规则。这些规则确保聚合是一个可以强制执行各种不变量约束的自包含单元。
只引用聚合根
聚合根是聚合唯一可以被外部类引用的部分,客户端只能调用聚合根上的方法(包含不变量约束)来更新聚合
聚合间的引用必须使用主键
在一个事务中,只能创建或更新一个聚合
在单个服务维护多个聚合的一致性的另一种方法是打破聚合规则,在一个事务中更新多个聚合。这是在关系型数据库才能拥有的。
聚合的颗粒度
在开发领域模型时,你必须做出关键决策是决定每个聚合的大小。
- 如果聚合小,则再序列化的还是性能好,同时降低了两个用户尝试更新一个聚合而引发冲突的可能性
- 如果聚合大,使特定的聚合更新满足事务的原子性
使用聚合设计业务逻辑
在典型的微服务中,大部分业务逻辑由聚合组成。其余的业务逻辑存在于领域服务和Saga中。Saga编排本地事务的序列(如果没有Saga,就是用UseCase层替代),以确保数据的一致性。
业务逻辑:Aggregate、DomainService、Repository和一个或者多个Saga组成。DomainService调用Repository来保存和加载Aggregate,对于能在服务内部完成护理的简单请求,服务直接更新Aggregate,如果需要请求跨越多个服务,DomainService将创建一个Saga。
发布领域事件
在领域驱动设计的上下文中,领域事件是聚合发送的事情。他由领域模型的一个类表示。事件通常表示状态的变化。
领域事件:聚合在被创建时,或发生其他重大改变时发布领域事件。下面是一些可能的使用场景
- 使用基于编排的Saga维护领域服务之间的数据一致性
- 通知维护数据副本的服务,源数据已经发生了更改,这种方法称为命令查询执行隔离(CRQS)
- 通过Webhook或消息代理通知不同的应用程序,以触发下一步业务流程
- 按顺序通知同一应用程序的不同组件
- 向用户发送短信或电子邮件通知
- 监控领域事件以验证应用功能程序是否正常运行 (event store)
- 分析领域事件,为用户行为建模(事件风暴)
什么是领域事件
在命名领域事件时,我们往往选择动词的过去分词。
事件增强
事件内容有两种选择:
- 只保留聚合根ID,当订阅方需要更多信息的时候,通过访问聚合服务获取
- 事件增强,事件包含接收方需要所有信息,该方法简化了接收方,但缺点是使领域事件的稳定性降低,每当接收方的需求发生变更时,事件类都可能需要修改,降低可维护性。
识别领域事件
事件风暴,具体方法是:把领域专家聚集在一个屋子里,准备大量便签和一个大白板。事件风暴的结果是一个以时间为中心的领域模型,它由聚合和事件组成。
事件风暴包括三个主要步骤:
- 头脑风暴:请求领域专家集体讨论领域事件。领域事件由(橙色便签)表示,这些便签子啊白板上按照时间轴顺序摆放。
- 识别事件触发器:请求领域专家确定每个事件的触发器,如用户操作(蓝色便签)、外部系统(紫色便签)、另一个领域事件、时间流逝。
- 识别聚合:请求领域专家识别哪些使用命令的聚合并发出响应的领域事件。聚合由(黄色便签)表示。
生成和发布领域事件
生成领域事件
领域事件由聚合负责发布。聚合知道其状态合适发生变化,从而知道要发布的事件。下面是几种方式:
- 聚合直接调用消息传递API,缺点:由于聚合不能使用依赖注入,所以消息传递API需要作为方法参数传递,这会把基础设施和业务逻辑交织在一起是非常不可取的。
- 在聚合方法的返回值中包含一个事件列表,然后通过调用他的服务发送
- 在聚合根内部的一个字段内部累计保持事件,然后通过调用他的服务发送
发布领域事件
服务必须使用事务性消息来发布事件,以确保领域事件是作为更新数据库中聚合的事务的一部分对外发布。
消费领域事件
定义消费Handler
使用事件溯源开发业务逻辑
解释了如何使用事件溯源模式开发业务逻辑
事件溯源(Event Sourcing)是一种以事件为中心的编写业务逻辑和持久化领域对象的方法。
使用事件溯源模式开发业务逻辑
传统持久化技术的问题
- 对象和关系的”阻抗失调” :关系型数据库的表格结构模式与领域模型及其复杂关系的图状结构之间,存在基本的概念不匹配问题。
- 缺乏聚合历史
- 实施审计功能将非常繁琐且容易出错
- 事件发布凌驾于业务之上
什么是事件溯源
事件溯源是构建业务逻辑和持久化聚合的另一种选择。它将聚合以一系列事件的方式持久化保存。每个事件代表聚合的一次状态变化。应用程序通过重放事件来重新创建聚合的当前状态。
当应用程序创建或更新聚合时,他会将聚合发出的事件插入到EVNNTS表汇总。应用程序通过事件存储中检索并重发事件来加载聚合。加载聚合包含以下三个步骤:
- 加载聚合的事件
- 使用其默认构造函数创建聚合实例
- 调用apply方法遍历事件
事件溯源对领域事件提出了新的需求。之前我们将领域事件定义为一种机制,它用来通知订阅者聚合发生了改变。事件可以包含少量的数据,也可以包含对典型事件接收方有用的数据。但是,在事件溯源的情况下,聚合主要决定事件及其结构。同时使用事件溯源时,事件不再是可有可无的。包括创建在内的每一个聚合状态变化,都必须由领域事件表示。每当聚合的状态发生变化时,它必须发出一个事件。而且事件中必须包含聚合执行状态变化所需的数据。聚合的状态由构成聚合对象的字段值组成。
创建聚合的步骤如下:(process和apply是多个重载的方法,属于聚合类)
- 使用聚合的默认构造函数实例化聚合根
- 调用process方法以生成新事件
- 遍历新生成的事件,并调用apply方法来更新聚合的状态
- 将时间保持在事件存储库中
更的聚合的步骤如下:
- 从事件存储库加载聚合事件
- 使用其默认构造函数实例化聚合根
- 遍历加载的事件,并在聚合根上调用apply方法
- 调用process方法以生成新事件
- 遍历新生成的事件,并调用apply方法来更新聚合的状态
- 将时间保持在事件存储库中
使用乐观锁处理并发更新
通过数据库的带有version列的表来作为乐观锁
事件溯源和发布事件
事件溯源将聚合作为事件进行持久化,并从这些事件中重建聚合的当前状态。同时可以将事件溯源作为可靠的事件发布机制。
使用快照提升性能
对于长生命周期的聚合可能会有大量时间,比如Account聚合,常见的解决方案是定期持久保存聚合的快照。
使用快照,将从快照创建聚合实例(SNAPSHOTS数据库表),而不是使用其默认的构造函数来创建聚合实例。如果聚合具有简单、易于串行化的结构,则快照可以使用JSON序列化。复杂结构的聚合可以使用Memento模式(备忘录模式)进行快照。
幂等方式的消息处理
- 如果应用程序使用基于关系型数据库的事件存储库,则他可以将消息ID插入Precessed_Messages表,作为Events表的事件的事务的一部分。
- 如果应用程序使员工的是NoSQL的事件存储库,则他可以吧消息的ID存在处理它时的事件中。他通过验证聚合聚合的所有事件中是否包含该消息ID来做重复检测。
事件溯源的好处
- 可靠地发布领域事件
- 保留聚合的历史
- 最大限度地避免对象与关系的”阻抗失调”的问题
- 为开发者提供一个“时光机”
事件溯源的弊端
- 这类编程模式有一定的学习曲线
- 基于消息传递的应用程序的复杂性
- 处理事件的演化(前后兼容)有一定难度
- 删除数据存在一定难度
- 查询事件存储库非常有挑战性 (根据某个属性的值来查询)
实现事件存储库
看看作者的Eventuate 框架实现
整合Saga和基于事件溯源的业务逻辑
有时间实现一把Saga
在微服务架构中实现查询
如何使用API组合模式或命令查询职责隔离(CQRS)模式,这两个模式用来查询分散在多个服务中的数据
在微服务架构中实现查询操作有两种不同的模式:
- API组合模式:这是最简单的方法,应尽可能使用。他的工作原理是让拥有数据的服务的客户端负责调用服务,并组合服务返回的查询结果
- 命令查询职责隔离(CQRS)模式:它比API组合模式更强大,但也更复杂。他维护一个或多个数据库视图,并组合服务返回的查询结果。
使用API组合模式进行查询
什么是API组合模式
API组合模式通过调用拥有数据的服务并组合结果来实现查询操作。他有两种类型的参与者:
- API组合器:他通过查询数据提供方的服务来实现查询操作
- 数据提供方服务:拥有查询返回部分数据的服务
API组合模式的设计缺陷
使用此模式,你必须解决两个设计问题:
确定架构中的哪个组件是查询操作的API组合器(可以是客户端、API Gateway、独立的服务)
API组合器要尽可能使用并行调用提供方服务,最大限度地缩短查询操作的响应时间,可以使用Java的CompletableFuture、RxJava等响应式API
如何编写有效的聚合逻辑
API组合模式的好处和弊端
好处
简单直观
弊端
- 增加了额外的开销
- 带来了可用性降低的风险(需要涉及多个服务,这个使用可以使用缓存,或者返回不完整的数据)
- 缺乏事务数据一致性(因为没有隔离性,所以可能存在返回的数据一致性的情况)
- 不能解决大规模数据查询,因为需要内存连接
使用CQRS模式实现查询
当使用API组合无法有效实现多服务查询的时候,我们就需要考虑使用CQRS了,比如订单历史列表。
不同目的的查询需要的数据库可能是不一样的,如果有地址范围查询就适合mongo,如果数据有多个副本,那么调整就转变成了原始数据发生变化使其保持最新状态。
什么是CQRS
当在微服务架构中实现查询时经常遇到下面三个问题的时候,就可以开始考虑CQRS模式:
- 使用API组合模式检索分散在多个服务中的数据会导致崩溃、低效的内存中的连接
- 拥有数据的服务将数据存储在不能有效支持所需查询的表单或数据库中
- 隔离问题的考虑意味着,拥有数据的服务不一定是会实现查询操作的服务
CQRS将持久化数据模型和使用数据的模块分为两部分:命令端和查询端。命令端模块和数据实现创建、更新和删除操作(CUD)。查询模块和数据模型实现查询(R)。查询端通过订阅命令端发布的事件,使其数据模型和命令端模式保持同步。查询端可能存在多个查询模型,与需要的查询类型一一对应。
CQRS的好处和弊端
- 好处
- 在微服务架构中高效地实现查询
- 高效地实现多种不同的查询类型
- 在基于事件溯源技术的应用程序中实现查询
- 更进一步地实现问题隔离(CQS)
- 弊端
- 更加复杂的架构
- 处理数据复制导致的延迟
- 命令端你和查询端API提供版本信息,使其能够判断查询端是否过时,客户端可以轮询查询端的视图,知道他是最新的。
- 客户端在针对聚合的命令执行成功后,更新其本地版本的领域模型,而不必发出查询来克服复制可能带来的延迟。
设计CQRS视图
CQRS视图模块包括由一个或多个查询操作组成的API。他通过订阅由一个或多个服务发布的事件来更新它的数据库视图。
在开发视图模块时,你必须做出一些重要的设计决策:
- 必须选择合适的底层数据库,并设计数据结构(文档数据库、文本搜索引擎、图数据库、关系型数据库(支持复杂的条件))
- 在设计数据访问模块时,需要确保更新是幂等的,并且能够处理并发更新(ES)
- 必须实现一种机制,可以高效的构建或重建视图(保存快照,做增量式构建)
- 决定如何设计视图的客户端,以应对复制延迟
视图模块由数据访问对象(DAO)和辅助类组成,Dao实现又时间处理程序调用的更新操作以及查询模块调用的查询操作,同时它还必须处理并发更新并确保更新是幂等的。
外部API模式
介绍了处理来自各种外部客户端请求的外部API模式
不同客户端通常需要不同的数据,同时内网访问和外网访问也不一样,这个时候会发现,拥有单一、适合所有的API通常没有意义。
外部API的设计难题
- 多次客户端请求导致用户体验不加(一般也不会用)
- 缺乏封装导前端开发做出的代码修改影响后端(后端API和前端耦合了)
- 服务可能选用对客户端不友好的进程间通信机制(grpc、消息,前端需要http、websocket)
当然会有很多问题,开发中都会遇到
API Gateway模式
模式:API Gateway:实现一个服务,该服务是外部API客户端进入基于微服务应用程序的入口点。他负责请求路由、API组合、身份验证和协议转化等各项功能。
API Gateway的架构
API Gateway具有分层的模块化架构。其架构由两层组成:API层和公共层。API层由一个或多个独立的API模块组成。每个API模块都为特定客户端实现API。公共层实现共享功能,包括边缘功能:身份验证、访问授权、速率限制、缓存、指标收集、请求日志。
API Gateway的好处和弊端
好处
封装了应用程序的内部结构。客户端不必调用特定服务,而是与API Gateway通信。
弊端
业务团队需要开发、部署和管理一个高可用的API Gateway组件。
API Gateway的设计难题
- 性能和可扩展性(Node实现的BFF)
- 使用响应式编程抽象编写可维护的代码(响应式是基于回调的,需要避免回调地狱)
- 处理局部故障(断路器)
- 成为应用程序架构中的好公民(可观测)
实现一个API Gateway
实现API Gateway有两种不同的方法:
使用现成的API Gateway产品或服务
好处是不需要代码开发,但是灵活性最低,通常不支持API组合
使用API Gateway框架或Web框架作为起点,开发属于自己的API Gateway
灵活,但是需要开发工作;现有的API Gateway框架:Kong、Tracefix、Netflix Zuul、Spring Cloud Gateway或者使用基于图形的GraphQL
微服务架构中的测试策略
测试测试 TDD
微服务架构中的测试策略
什么是测试
测试是自动化测试,测试的目的是验证被测系统的行为。一组相关的测试用例集构成一个测试套件。
自动化测试通常包括四个阶段:
- 设置环境
- 执行测试
- 验证结果
- 清理环境
使用模拟和桩进行测试
如果我们的被测系统存在依赖,就会把测试复杂化,这个时候我们需要用测试替身来消除被测系统的依赖性。
有两种类型的测试替身:桩(stub,代替依赖性向被测系统发送调用的返回值)和模拟(mock,用来验证被测系统是否正确调用了依赖项),通常可以互相使用。
测试的不同类型(基于范围)
- 单元测试:测试服务的一小部分,例如类
- 集成测试:验证服务是否可以与集成设施服务(如数据库)或其他应用程序服务进行交互
- 组件测试:单个服务的验收测试
- 端到端测试:整个应用程序的验收测试
使用测试象限进行分类
测试象限按两个维度对测试进行分类:
- 测试是面向业务还是面向技术
- 测试的目标是协助开发还是寻找产品缺陷
基于上面,有是中共不同的测试类别:
- Q1协助开发/面向技术:单元和继承测试
- Q2协助开发/面向业务:组件和端到端测试
- Q3寻找产品缺陷/面向业务:易用性和探索性测试
- Q4寻找产品缺陷/面向技术:非功能性验收测试,如性能测试
消费者驱动的锲约测试
服务消费者要为服务提供者提交锲约测试,防止服务提供者出现不兼容升级
两个流行的企业级契约测试框架是:Spring Cloud Contract、Pact系列框架
部署流水线
jenkins CI
为服务编写单元测试
有两种类型的单元测试:
- 独立型单元测试:使用针对类的依赖性模拟对象隔离测试类,控制器和服务类、消息处理器
- 协作型单元测试:测试一个类及其依赖项,领域对象(实例,值对象,saga),实体一般依赖值对象
- 为实体编写单元测试
- 为值对象编写单元测试
- 为saga编写单元测试
- 为领域服务编写单元测试
- 为控制器编写单元测试
- 为时间和消息处理程序编写单元测试
编写集成测试
集成测试必须验证服务是否可以与其客户端和依赖性进行通信
- 针对持久化层的集成测试(最好使用Docker)
- 针对基于REST的请求/响应式交互的集成测试 (契约)
- 针对发布/订阅式交互的集成测试
- 针对异步请求/响应式交互的集成契约测试
编写组件测试
组件测试(验收测试)是针对软件组件的面向业务的测试。他们从组件客户端而不是内部实现的角度描述了所需的外部行为,这些测试源自用户故事或用例。Gherkin框架
设计组件测试
- 进程内组件测试:使用常驻内存的 桩和模拟 代替器依赖性的服务,内存数据库什么的
- 进程外组件测试:使用容器
端到端测试
组件分别测试每个服务,端到端测试会测试整个应用程序。(docker compose 、k8s)
开发面向生产环境的微服务应用
介绍开发生产就绪服务的各个方面:安全性、外部化配置模式、服务可观测性模式(日志聚合、应用指标、分布式追踪)
开发安全的服务
应用程序开发人员主要负责安全性的四个不同方面:
- 身份验证:证明身份,放在API Gateway,可以使用oauth2
- 访问授权:控制权限,放在服务
- 审计:记录操作
- 安全的进程间通信:加密
对于身份验证和访问授权在微服务中要集中,比如放在API Gateway中。
设计可配置的服务
服务中有很多配置,比如消息队列broker的位置,db的用户名和密码
外部化配置机制在运行时向服务实例提供配置属性值。主要有两种方法:
推送模型:部署基础设施通过类似操作系统环境变量或配置文件,将配置属性传递给服务实例。
这种模型对于重新配置正在运行的服务很难
拉取模型:服务实例从配置服务器读取它所需要的配置属性。
配置服务器的实现有:版本控制系统如git、SQL和Nosql数据库,专用配置服务器 spring cloud config server
设计可观测的服务
可以通过以下模式来设计可观测的服务:
- 健康检查API:公开返回服务运行状况的接口。比如定期轮询healthy接口
- 日志聚合:记录服务活动并将日志写入集中式日志记录服务器,该服务器提供搜索和告警。比如graylog,elk
- 分布式跟踪:为每一个在服务之间跳转的外部请求分配唯一ID,并跟踪请求。比如jager,zipkin,可以用来看请求性能,利用aop(sleuth)
- 异常跟踪:向异常服务报告异常,该异常跟踪服务可以对异常进行重复数据删除(聚类),向开发人员发出报警并跟踪每个异常的解决方案。比如sentry
- 应用程序指标:服务运维指标,例如计数器和指标(包括基础设施的相关指标,如CPU、内存、磁盘利用率;应用程序级别的指标,如服务请求延迟和执行的请求数),并将他们公开给指标服务器。服务有两种方式向指标服务器提供数据:拉取或推动。比如prometheus(拉取),metircs
- 审计日志记录:记录用户操作,实现审计日志记录的方法:将审计日志记录代码添加到业务逻辑中,使用AOP,使用事件溯源。比如oplog。
使用微服务基底模式开发服务
微服务基底: 异常追踪、日志记录、监控检测、外部化配置和分布式追踪是微服务架构需要解决的共性问题,我们需要在能够处理那些共性问题的框架或框架集合上构建服务。
Java:Spring Boot 和Spring Cloud;Golang:Go Kit和Micro
使用微服务基底的一个弊端:开发者必须需要保证使用的编程语言/平台组合,有与之对应的服务基底框架或类库。当前,微服务基底实现的许多功能很有可能由基础设施实现,比如网络相关的功能将由所谓的服务网格处理。
服务网格: 把所有进出服务的网络流程通过一个网络层进行路由,这个网络层负责解决包括断路器、分布式追踪、服务发现、负载均衡和基于规则的流量路由等具有共性的需求。
当前的服务网格实现:Istio、Linkerd、Conduit
部署微服务应用
介绍可用于部署服务的各种部署模式:虚拟机、容器、Serverless模式、服务网格
部署包括两个相互关联的概念:部署流程和部署架构
物理机->虚拟机->容器->severless
生成环境必须包括四个关键的功能:
- 服务管理接口,是开发人员能够创建、更新和配置服务
- 运行时服务管理:确保始终运行这所需数量的服务实例,必要时可以重启
- 监控:包括日志文件和各种应用指标
- 请求路由:将用户的请求路由到服务
下面是四种重要的部署选项:
编程语言特定的发布包格式
Java:Jar包或War包
Node:源代码目录
Golang:可执行文件
好处
- 快速部署,启动时间短
- 高效的资源利用,在同一台机器或同一进程汇中运行多个实例
弊端
- 缺乏对技术栈的封装
- 无法约束服务实例消耗的资源
- 在同一计算机上运行多个服务实例时缺少隔离
- 很难自动判定放置服务实例的位置(放在哪个服务器上,根据资源需要抉择)
将服务部署为虚拟机
将服务打包为机器镜像
好处
- 虚拟机镜像封装了技术栈
- 隔离的服务实例
- 使用成熟的云计算基础设施
弊端
- 资源利用效率低(Java可能还怕资源不够,Node和Golang可能觉得有点浪费)
- 部署相对缓慢(构建虚拟机镜像通常需要几分钟,从镜像实例化虚拟机也需要时间)
- 系统管理的额外开销(给操作系统打补丁)
将服务部署为容器
容器是一种更现代、更轻量级的部署机制,是一种操作系统级的虚拟化机制。
将服务打包为容器镜像,每个服务实例都是一个容器(container)。
创建容器时,可以指定它的CPU资源和内存资源,以及依赖于容器实现的I/O资源等。
在开发和测试阶段中,可以使用Docker Compose编排工具(单机),在生成环境中,需要配合类似K8s的容器编排工具(计算机资源池)。最后利用服务网格类似istio实现将部署流程(发布到生成环境)和发布流程(将正式流程切到新服务版本)分离。
好处
- 封装技术栈,可以用容器的API实现对服务的管理
- 服务实例是隔离的
- 服务实例的资源受到限制
弊端
需要承担大量的容器镜像管理工作
Serverless 部署
终于可以不用管理系统了。(假笑)
使用公有云提供的Serverless部署机制部署服务。
开源Serverless框架 Apache Openwhisk和Fission For Kubernates
Serverless 的一种实现Fass,常用的场景:
- HTTP请求
- 事件
- 定时调用
- 直接使用API调用
不同的公有云对Fass的支持不一样,我们要具体来看。
微服务架构的重构策略
“绞杀者”
重构到微服务需要考虑的问题
为什么要重构单体应用
单例地狱而引发的业务问题:
- 交付缓慢:应用程序难以理解、维护和测试,开发人员你的工作效率低
- 充满故障的软件交付:缺乏可测试性意味着软件会经常出错
- 可扩展性差:
绞杀单体应用
绞杀者应用模式: 通过在遗留系统程序周围逐步开发新的(绞杀)应用程序来来实现应用程序的现代化。
尽早并且频繁的体现出价值(为了让产品同意)
可以先将应用程序的高价值部分迁移到微服务架构。
尽可能对单体做出修改
部署基础设施不用太早
将单体应用重构为微服务架构的若干策略
将新功能实现为服务
需要考虑新功能能否作为一个服务,如果不能则继续在单体应用中开发,然后将该功能以及其他相关功能提取到自己的服务中
隔离表现层和后端
隔离之后就可以按API拆分后端服务
通过将功能提取到服务中来分解单体
提取服务时会遇到以下这些调整:
- 拆解领域模型(使用聚合,分离聚合)
- 重构数据库(使用领域事件数据复制,小数据频率低的通过RESTAPI访问)
确定提取何种服务以及何时提取
- 提取有益的服务(为了加速开发、解决性能、可扩展或可靠性问题、允许提取一些其他服务)
- 在确定应用模块(独立的域)之后排好优先级,跟着业务需求走
完结
很多实例可以回头再看看!!