引言
本系列文章记录阅读微服务架构设计模式中遇到的一些有意思的概念和思想,并记录我对作者观念的思考
本书的目标是让架构师和程序员学会使用微服务架构成功开发应用程序。本书讨论了微服务架构的好处和弊端,教会读者如何在单体和微服务架构之间进行权衡。
本书的重点是架构和开发,适合负责开发和交付软件的任何人来阅读。
本书中讨论的架构,可以查阅并参考www.microservices.io/
本书内容摘要
本书共13章,主题是围绕"模式语言"展开的
- 第1章描述"单体地狱",描述了微服务架构"模式语言"
- 第2章描述可以用来把应用程序分解为服务的模式,以及在此过程中遇到的各种障碍
- 第3章介绍了微服务架构中的IPC(进程间通信)的模式
- 第4章介绍如何使用Saga模式维护服务间数据一致性
- 第5章介绍如何使用DDD(领域驱动设计)
- 第6章介绍如何使用事件溯源模式
- 第7章介绍API组合模式和CQRS(命令查询职责分离)模式
- 第8章介绍外部API模式
- 第9-10章介绍微服务自动化测试
- 第11章介绍生产环境中微服务需要满足的质量属性
- 第12章介绍各种部署模式
- 第13章介绍如何把单体架构重构为微服务架构
单体架构的好处
- 开发简单
- 易于更改
- 方便测试
- 部署简单
- 方便横向扩展
单体地狱
随着公司成功,研发团队规模壮大,应用程序在单体架构下会面临如下许多问题,开发将变得痛苦,部署将不再可能。
- 系统过度复杂,开发者无法维护
- 开发速度缓慢
- 从代码提交到实际部署周期很长,容易出问题
- 难以横向扩展
- 难以进行可靠的测试,难以交付应用
- 需要长期依赖某些过时的技术栈
微服务架构
定义:把应用程序功能性分解为一组服务的架构风格。每个服务由一组专注的、哪女的功能职责组成。 特性:每个服务都是松耦合的,都有自己的数据库。
扩展立方体
扩展立方体描述了扩展应用程序的三个维度
- X轴扩展:在多个实例之间实现请求的负载均衡
- Z轴扩展:根据请求的属性路由请求
- Y轴扩展:根据功能把应用拆分为服务
微服务与SOA
SOA | 微服务 | |
---|---|---|
服务间通信 | ESB,例如SOAP或者WS* | REST或者gRPC |
数据管理 | 全局数据模型 | 每个服务有自己的数据模型和数据库 |
典型服务的规模 | 较大的单体 | 较小的服务 |
微服务的好处和弊端
好处
- 大型应用程序可以持续交付和持续部署
- 服务相对较小维护容易
- 服务可以独立部署
- 服务可以独立扩展
- 微服务架构可以实现团队自治
- 更容易采纳新技术
- 更好的容错性
弊端
- 服务的拆分和定义难
- 分布式系统带来额外的复杂性
- 部署跨越多服务的功能需要协调多个团队
- 需要考虑引入微服务架构的阶段
服务拆分策略
什么是软件架构
计算机系统的软件架构是构建这个系统所需要的一组结构,包括软件元素、他们之间的关系以及两者的属性
软件架构风格
分层架构
- 表现层
- 业务逻辑层
- 数据持久化层
六边形架构
六边形架构由业务逻辑和多个适配器组成。业务逻辑由几部分构成:服务和领域对象、入站适配器、出站适配器
定义微服务架构
- 识别系统操作
- 拆分服务
- 定义服务API和协作方式
识别系统操作
这一步实际上就是根据需求(包括用户故事和用户场景),进行面向对象设计的抽象领域模型设计。系统操作就是来自需求中提及的动词。
拆分服务
有两种拆分服务的方式
- 根据业务能力进行服务拆分
- 根据子域进行服务拆分(DDD)
拆分单体应用到服务的障碍
- 网络延迟
- 同步IPC导致可用性降低
- 服务间的数据一致性
- 获取一致的数据视图
- 上帝类
微服务架构中的IPC(进程间通信)
服务交互方式
API优先设计
首先编写接口定义,只有在反复几轮API定义之后,才开始实现具体的服务实现,有助于减少开发中的冲突
API演化
随着应用程序变化,API也会随着变化,下面有一些对API变化制定的应对措施
- 语义化版本控制(Semvers)
- 进行次要并且向后兼容的改变
- 进行主要并且不向后兼容的改变:在URL中或者在MIME中嵌入主要版本号
同步的远程过程调用(RPC)
- REST(HTTP)
- gRPC
处理局部故障
分布式系统中,当服务向另一个服务发送同步请求,面临局部故障的风险,服务端可能因为等待响应而被阻塞,会导致服务中断
断路器模式
是一个RPC的代理,在连续失败次数超过阈值的一段时间内,代理会拒绝其他调用
降级
从服务失效的故障中恢复。一种选择是服务向客户端返回错误,另一种是返回一个备用值(fallback value)
服务发现模式
基于云的微服务应用程序中,服务实例具备动态分配的网络位置,会动态更改
有两种方式来实现服务发现,分为客户端实现和服务端实现两种方式
- 应用层服务发现模式:自注册、客户端发现
- 平台层服务发现模式:第三方注册、服务端发现
基于异步消息通信
服务之间采用异步交换消息通信。消息通过消息通道进行交换。有两种类型的消息通道
- 点对点通道
- 发布订阅通道
(同步)请求/响应和异步请求/响应
这两种模式下,客户端会发送请求,服务端会发送回复。对于同步方式,客户端阻塞等待服务端回,是强实时的。对于异步方式,客户端发送后不阻塞等待响应,而是通过另一个通道返回结果。
单向通知
服务不会发送回复通知
发布/订阅
发布/异步响应
消息代理
消息传递使用消息代理,即服务通信的基础设施服务。
有两种消息通信的软件架构,无代理架构服务可以直接交换消息(ZeroMQ)。基于消息代理(RabbitMQ、Kafka)的消息,发送方把消息写入消息代理,代理再把消息发送给接收方。消息代理的好处是发送方不需要知道接收方的位置,并且能够缓冲消息。
- 无代理的消息
- 基于消息代理的消息
选择消息代理需要考虑的因素有:
- 支持的编程语言
- 消息标准
- 消息有序性
- 投递保证
- 消息持久性
- 消息耐久性
- 消息代理的可扩展性
- 消息延迟
- 并发性
基于代理的消息的好处和弊端
好处 | 弊端 |
---|---|
松耦合 | 性能瓶颈 |
消息缓存 | 单点故障 |
明确的进程间通信 | 额外的复杂性 |
基于消息的架构可能遇到的设计问题
以下是在基于消息的架构中,可能会遇到的设计难题
处理并发和消息顺序
现代消息代理(如Kafka)的常见的解决方案是使用分片通道。
- 分片通道由多个分片组成,每个分片的行为类似于一个通道
- 发送方在消息头部指定分片键,消息代理使用分片键把消息分配给特定分片(例如计算分片键的散列来选择分片)
- 消息代理把接收方的多个实例组合在一起,并把他们视作相同的逻辑接收方(Kafka中的消费者组),消息代理会把每个分片分配给单个接收器,并在接收方启动和关闭时重新进行分片
处理重复消息
大部分消息代理承诺至少成功传递一次消息,而不是有且仅有一次。只有在理想情况下才是仅一次,如果出现客户端、网络或者消息代理故障就可能多次传递。
有几种处理重复消息的方法
- **幂等(idempotent)**的消息处理程序
- 跟踪消息并且丢弃重复消息的消息处理程序
幂等性:相同的输入重复调用,不会产生额外的效果
这里由于很多时候,操作并不是幂等的,必须通过跟踪消息并且丢弃重复消息来实现。
这里要根据数据库事务模型来进行选择具体的实现方法。对于ACID事物模型的数据库(SQL数据库),可以用专用表(如PROCESSED_MESSAGES)记录。而对于受限事务模型的数据库(NoSQL数据库),把消息ID存储在某个应用程序的数据库表内来实现。
事务性消息
有时候需要保证更新数据库和发送消息两个操作是原子方式执行的,必须在一个事务中执行。传统做法是数据库和消息代理之间使用分布式事务(这里说的应该是全局事务,也就是DTP、X/Open XA定义的事务),现代的消息代理(Kafka)不支持分布式事务,这里有几个处理方案
全局事务(Global Transaction):由DTP模型定义的强一致性事务处理方案
使用数据库作为消息队列
通过将消息保存在数据库的OUTBOX表(作为临时消息队列)中,作为数据库事务的一部分。然后轮询数据库的发件箱(OUTBOX)来向消息队列发送消息。这种方法仅适用于SQL数据库。
事务日志拖尾
每次应用程序提交到数据库的更新都对应着一条事务日志。事务日志挖掘器可以读取日志,把和消息有关的记录发给消息代理。这种方式适用于SQL数据库和NoSQL数据库。
异步消息提高可用性
同步消息会降低应用程序的可用性,应该最大限度的降低同步通信,就能提高可用性
减少同步的措施
- 使用异步消息进行通信
- 服务复制数据,减少通信
- 暂缓与其它服务的交互,先响应再异步处理