简介
《微服务架构设计模式/Microservices Patterns: With Examples in Java》是由 Chris Richardson 撰写的关于微服务架构的书籍,详细介绍了微服务设计模式,旨在帮助开发人员、架构师和技术领导者了解微服务架构的优点和挑战,以及如何在实际项目中应用这些设计模式。书中示例是一小部分简单易懂Java代码,对非Java开发者也无障碍。
本书内容大概有四个部分:
- 架构基础:微服务架构的基本概念、优势和挑战,以及与传统单体架构的对比。
- 设计模式:各种微服务设计模式,如服务分解、API网关、服务注册与发现等。
- 跨服务通信:微服务之间的通信方式,包括同步通信(如REST和gRPC)和异步通信(如消息队列)。此外,还有如何处理跨服务通信中的各种问题,如数据一致性、服务容错和服务链路追踪等。
- 部署与运维:微服务的部署、监控、日志管理和安全等方面的实践。介绍了如何使用容器化技术(如Docker和Kubernetes)部署和管理微服务等。
有感
这本书内容丰富、实用性强,从理论到实践、从设计到部署,全面覆盖了微服务架构的各个方面。同时,出版时间是2019年,书中所提的具体框架、组件,依旧是当下热门技术。
随着当下系统功能越来越复杂、技术语言越来越多,单体架构已经无法满足需要,又因Docker、Kubernetes等新技术的出现,使得利用微服务架构设计系统服务成为大势所趋。
微服务架构设计的核心分为拆分服务和管理服务。拆分服务是基础,根据业务需求将功能模块合理的拆分为服务,再通过各种手段管理好各个服务及它们之间的联系。
微服务的目标是加速大型复杂应用程序的开发,关注业务功能的开发实现更为重要。不要脱离实际构建庞大、眼花缭乱的微服务世界,而需要根据实际情况、循序渐进地构建适合自己、够用的微服务。
摘录与归纳
作者的忠告
- 要记住微服务不是解决所有问题的万能“银弹”。
- 编写整洁的代码和使用自动化测试至关重要,因为这是现代软件开发的基础。
- 关注微服务的本质,即服务的分解和定义,而不是技术,如容器和其他工具。
- 确保你的服务松耦合,并且可以独立开发、测试和部署,不要搞成分布式单体(Distributed Monolith),那将会是巨大的灾难。
- 也是最重要的,不能只是在技术上采用微服务架构。拥抱DevOps的原则和实践,在组织结构上实现跨职能的自治团队,这必不可少。
- 还必须记住:实现微服务架构并不是你的目标。你的目标是加速大型复杂应用程序的开发。
定义微服务
微服务架构模式是将应用程序功能性分解为一组可独立部署的服务。每个服务由一组专注的、内聚的功能职责组成,并拥有自己的私有数据库。
微服务特点
- 模块化:模块化是开发大型、复杂应用程序的基础,微服务架构使用服务作为模块化的单元,构筑一个不可逾越的边界。
- 私有数据库:每个服务都拥有自己的私有数据库,让服务之间都是松耦合的。无论是开发、运行期间,服务之间相互独立,互不干扰。
- 架构风格:架构决定软件的非功能性因素,微服务架构给应用程序带来了更高的可维护性、可测试性、可部署性和可扩展性。
- 扩展立方体:利用扩展立方体,能更清晰的展示微服务的框架特点。
微服务好处与弊端
- 好处
- 使大型的复杂应用程序可以持续交付和持续部署。
- 每个服务都相对较小并容易维护。
- 服务可以独立部署。
- 服务可以独立扩展。
- 微服务架构可以实现团队的自治。
- 更容易实验和采纳新的技术。
- 更好的容错性。
- 单体架构适合简单应用,微服务架构适合大型复杂应用。
- 弊端
- 服务的拆分和定义是一项挑战。服务拆分和定义像是一门艺术,糟糕的拆分会构建出分布式的单体应用。
- 分布式系统带来的各种复杂性,使开发、测试和部署变得更困难。
- 当部署跨越多个服务的功能时需要谨慎地协调更多开发团队。
- 开发者需要思考到底应该在应用的什么阶段使用微服务架构。
服务拆分
微服务中的服务是根据业务需求进行组织的,按照业务能力或者子域进行拆分,而不是技术上的考量。
服务拆分模式
- 按业务能力分解,其起源于业务架构。
- 基于领域驱动设计(DDD)的概念,通过子域进行分解。
拆分指导原则
- 单一职责原则(SRP):设计小的、内聚的、仅仅含有单一职责的服务。
- 共同闭合原则(CCP):两个类修改时耦合发生,就应该放在一个包(服务)里,CCP是解决分布式单体的法宝。
单体拆分微服务的难点
- 网络延迟:服务之间需要大量调用,如果使用进程间通信代价高昂,可以利用RPC框架实现函数调用(内存中数据交换)。
- 同步进程间通信导致可用性降低:可以使用消息队列,实现异步调用。
- 在服务之间维持数据一致性:使用分布式事务管理机制 Saga。
- 获取一致的数据视图:一般不会造成问题。
- 上帝类阻碍了拆分:利用DDD模型拆分上帝类。
定义服务API
- 对外API:由外部客户端调用,也可能由其他服务调用。如果某个API需多个服务提供,要根据业务流程确定哪个服务作为对外入口。
- 对内API:仅由其他服务调用
服务进程间通信
微服务是分布式架构,服务间的通信机制至关重要,通常根据需要选择不同的通信机制。
- 通信机制:分为同步通信(如 HTTP REST 或 gRPC)和异步通信(如 AMQP 或 STOMP)
- 消息格式:有具备可读性的 JSON 或 XML 格式,也有高效的、基于二进制的 Avro 或 Protocol Buffers 格式
服务API
- 定义API:先定义接口,然后与前端人员评审确定接口,再着手服务代码开发。
- API版本控制:API迭代时,尽量向后兼容迭代。如果不兼容,需使用版本号控制,如在REST请求URL中嵌入主要版本号等
消息格式
- 文本:基于文本的消息格式,可读性很高,如 JSON、XML,缺点是消息冗长、通信开销大
- 二进制:序列化的二进程消息,如 Avro 或 Protocol Buffers
同步通信
- REST 通信机制:使用HTTP协议,通常使用 JSON 消息格式。定义了资源、动词等概念,如 GET 获取资源、POST 创建资源等。定义 REST API 可以参考 Open API 规范(www.openapis.org),结合 Swagger 项目提供了从接口定义、开发调试等整套工具。
- gRPC 通信机制:基于二进制消息的协议,使用 Protocol Buffers 作为消息格式。支持简单的请求/响应RPC、流式RPC。
- 断路器:同步通信需要利用断路器实现处理服务无响应、恢复失败服务,断路器有设置超时防止堵塞、限制服务请求并发数、监控请求失败率等功能。断路器能避免服务过载、雪崩等严重故障。
- 服务发现:由于分布式微服务内服务实例的网络位置是动态分配的,客户端必须使用服务发现正确找到服务位置。
- 应用层服务发现模式:服务和客户端通过服务注册表进行交互,服务自注册API、客户端查询API。此法缺点是每个服务都要实现服务发现功能。
- 平台层服务发现模式:使用 Kubernetes 部署平台实现服务注册、发现和请求路由,服务和客户端不需要处理任何服务发现逻辑。此法缺点是仅限于支持使用 Kubernetes 部署的服务。
异步通信
- 服务间通信采用异步交换消息的方式完成。
- 消息通道有两种:
- 点对点:向正在从通道读取的一个消费者传递消息,实现一对一交互。通常命令式消息使用此通道。
- 发布-订阅:将一条消息发给所有订阅的接收方,实现一对多交互。通常事件式消息使用此通道。
- 消息代理:所有消息的中介节点,发送方写入消息并由消息代理自动发送给接收方。好处是发送方不需要知道接收方位置、缓存消息。开源消息代理如 RabbitMQ、Kafka等
- 处理并发和消息顺序:消息代理使用分片(分区)通道解决
- 处理重复消息:消费者提供幂等性接口(无法完全做到),或消费者将message_id记录到数据表里,每次检查到重复则丢弃。
- 事务性消息:数据库更新和消息发送必须事务内进行,保证原子性执行。解决方法是创建临时消息队列的数据表,将事务中的消息先保存到这个消息表(事务性发件箱)里,然后轮询此表发送消息,完成后删除消息。轮询发布适合小规模消息发送,更高性能的方式可以使用事务日志拖尾模式,如将利用Mysql的binlog跟踪数据表更改。
- 消息代理客户端:服务发送和接收消息,可以直接使用消息代理的客户端库,也可以封装自己的客户端,好处是减少重复代码、适合切换消息代理、扩展消息的交互功能。
- 提高可用性:同步消息会降低可用性,服务间尽可能使用异步通信。
分布式事务 Saga
微服务架构中处理ACID事务,需要使用 Saga 分布式事务机制。Saga 是通过使用异步消息来协调一系列本地事务,从而维护多个服务之间的数据一致性。
Saga 机制
- 使用补偿事务来回滚,无法自动回滚。
- Saga 协调的多个事务,可以分为可补偿性事务(定义补偿操作)、关键性事务(无法补偿回滚)、可重复性事务(不会失败、总是成功)。这几项事务总是按照该顺序编排的。
Saga 协调模式
- 协同式:把Saga决策和执行顺序逻辑分布在Saga各参与方中,它们通过交换事件进行沟通。此方式简单,但代码不易维护、循环依赖。
- 编排式:由一个Saga编排器集中处理Saga决策和执行顺序,Saga编排器通知各个参与方具体执行操作。此方法依赖简单、松耦合、减化业务逻辑,但编排器需避免包含业务逻辑、只负责事务排序。
Saga 解决隔离问题
- Saga 事务模型是 ACD,会缺乏隔离导致一些问题:
- 丢失更新:一个Saga没有读取更新,而是直接覆盖了另一个Saga所做的更改。
- 脏读:一个事务或一个Saga读取了尚未完成的Saga所做的更新。
- 模糊或不可重复读:一个Saga的两个不同步骤读取相同的数据却获得了不同的结果,因为另一个Saga已经进行了更新。
- Saga 模式实现隔离的对策:
- 语义锁:应用程序级的锁。如定义 *_PENGDING 状态。
- 交换式更新:把更新操作设计成可以按任何顺序执行。
- 悲观视图:重新排序Saga的步骤,以最大限度地降低业务风险。如将存在脏读风险的事务排在Saga靠后的步骤。
- 重读值:通过重写数据来防止脏写,以在覆盖数据之前验证它是否保持不变。类似乐观锁。
- 版本文件:将更新记录下来,以便可以对它们重新排序。
- 业务风险评级(by value):使用每个请求的业务风险来动态选择并发机制。
业务逻辑设计
业务逻辑通常是开发中最复杂的部分,如何让代码更优雅、更易维护的实现业务逻辑,这里使用到领域模型中聚合(Aggregate)概念。
组织业务逻辑的两种模式
- 面向过程:面向过程的事务脚本模式,此模式特征是实现行为的类与存储状态的类是分开的。适合简单的业务逻辑。
- 面向对象:面向对象的领域建模模式,此模式是由具有状态和行为的类构成的对象模型。适合复杂的业务逻辑、建议使用的方式。
聚合/Aggregate
- 在一个服务内,将多个类组成一个大类(聚合,如Order聚合包含Order根类、OrderItem类等)。
- 只引用聚合根:客户端只能通过调用聚合根上的方法更新(新增、删除等)聚合。
- 一个本地事务仅更新一个聚合:一个本地事务只能创建或更新一个聚合,多个事务使用Saga管理。但在单个服务中,如果关系型数据库(如MySQL),可以一个本地事务更新多个聚合。
- 聚合粒度:一方面粒度越小、并发性越高,另一方面聚合是事务的范围、定义更大的聚合以处理更大的事务。因此,聚合粒度要适中。
业务逻辑设计
- 典型的微服务中,每个服务的大部分业务逻辑由聚合组成。
- 小部分业务逻辑由领域服务(如下图的 OrderService 业务逻辑的入口)、Saga等完成。
发布领域事件
- 当聚合被创建时,或发生重大更改时发布领域事件。如订单创建、取消、支付等聚合。
- 发布领域事件的用途:让其他组件或服务收到此消息做出相应调整,监控和分析程序运行。
- 领域事件内包含事件ID、时间戳、其他属性等,也含事件详细内容(如订单详情,但会增加耦合性)
- 创建单独的事件类方便聚合调用,使用事务性消息发布事件更可靠。
- 消费领域事件:可以订阅消息队列,如Kafka等。
多服务数据查询
由于每个服务的数据都是私有的,从多个服务检索数据的查询具有挑战性。
实现查询的方法
- API 组合查询:最简单方法,应尽可能使用。
- API组合器:通过查询数据提供方的服务来实现查询操作。可以由某个服务、API网关或单独的API组合器服务来充当此角色。
- 数据提供方服务:拥有查询返回的部分数据的服务。
- 缺点:增加额外的开销、降低服务可用性、缺乏事务数据一致性。
- 局限性:某些复杂查询需要大型数据集的低效内存链接。
- 命令查询职责隔离(CQRS):功能更强大,但实现更复杂。聚合多个服务的数据在一处。
- 将持久化数据和使用数据的模块分开:命令端(增、改及删)和查询端(查)。
- 查询端通过订阅命令端发布的事件,使数据模型与命令端保持同步。
- 查询服务:独立的查询服务,聚合多服务的数据,提供统一的查询API。
- 数据库:NoSQL 是CQRS视图更好的选择,如MongoDB
- CQRS 优点:查询高效、支持多种查询类型、实现问题隔离。
- CQRS 缺点:更加复杂的架构(需要编写查询服务)、处理数据复制导致的延迟。
- CQRS 在复杂、特殊的查询需求下可以使用,其他情况尽量用 API组合查询。
网关/API Gateway
外部 API 客户端进入微服务应用程序的入口,负责请求路由、API组合和身份验证等功能。
网关的功能
- 请求路由:将请求路由到相应的服务,类似Nginx的反向代理。
- API组合:实现API组合查询功能。
- 协议转换:外部客户端与内部服务的通信协议可能不同,需要网关转换。如外部API用的RESTful和内部服务的API用的 gRPC。
- 客户端专用API:不同客户端所需接口数据不同,如PC端和移动端,相比提供单一的万能API,为不同客户端提供专用API,性能更好。
- 实现边缘功能:身份验证、访问授权、速率限制、缓存、指标收集、请求日志等。
网关的优缺点
- 优点:封装了应用程序的内部结构,客户端仅与网关通信、不需要调用特定服务。
- 缺点:可能存在瓶颈,需要尽量开发高可用的网关。
面向生产的微服务应用
在生产环境中,微服务需要满足三个关键的质量属性:安全性、可配置性和可观测性。
安全性
- 鉴权职责:使用API Gateway 统一处理验证用户身份和访问授权,让内部服务实现更简单。
- token格式:JWT,流行的JSON Web 透明令牌,安全传递用户信息,自我验证无需再请求验证、高可用高并发。但是无法主动过期,JWT有效期要较短。
- OAuth2.0:流行的访问授权协议,利用刷新令牌(长效、可被撤销)获取新的JWT,弥补JWT短时效的问题。
可配置性
- 外部化配置:在运行时向服务提供配置属性值,如数据库信息等。
- 有两种提供外部化配置的方式:
- 推送模式:部署基础设施(如Kubernets)通过类似操作系统环境变量(如Docker环境变量)或配置文件,将配置属性传递给服务实例。此模式是有效且广泛使用的配置服务机制。但更改配置需重新启动服务、定义配置属性分散在众多服务内。
- 拉取模式:服务实例从配置服务器读取它所需要的配置属性。此模式的好处是集中配置、易于管理、允许定义全局属性,敏感数据的透明解密,服务轮询检测动态重新配置。
- 配置服务器:拉取模式中的配置服务器,可以使用自己实现,也可以使用一些开源框架,让运行配置服务器更加容易。如Spring Cloud Config等。
可观测性
- 健康检查 API:服务公开健康检查 API 接口,如 GET/health,它返回服务的健康状况。此接口可能会检查服务所必需的数据库、适配器等健康状况。用于监测服务健康状况。实际中需考虑与基础设施服务配合。
- 日志聚合:记录服务获得并将日志写入集中式日志记录服务器,提供日志搜索和告警。用于排查故障。服务选择合适的日志库,如 Java流行的Logback、log4j等,日志基础设置的流行方案是 ELK套件。
- Elasticsearch:面向文本搜索的NoSQL数据库,用作日志记录服务器。
- Logstash:聚合服务日志并将其写入Elasticsearch的日志流水线。
- Kibana:Elasticsearch的可视化工具。
- 分布式跟踪:为每个在服务之间跳转的外部请求分配唯一ID,并跟踪请求。用于分析优化服务性能。分布式追踪分为:供每个服务使用的追踪工具类库和分布式追踪服务器。追踪工具类库管理追踪和跨度、并向分布式追踪服务器报告追踪,如Java的Spring Cloud Sleuth,分布式追踪服务器存储追踪信息、提供可视化界面,如 Open Zipkin。
- 异常跟踪:服务把产生的异常报告给中央服务,该服务对异常进行重复数据删除、生成警报并管理异常的解决方案。解决传统依靠日志服务处理异常的一些缺陷。
- 应用程序指标:服务将指标数据(如CPU、内存等)发送给负责聚合、可视化和告警的中央服务器。开源监控和警报系统 Prometheus,配合数据可视化工具 Grafana。
- 审核日志记录:记录数据库中的用户操作,以帮助客户支持、确保合规性,并检测可疑行为。
微服务基座
- 微服务基座是用来处理服务共性问题(如日志记录、健康检查、外部化配置等),让服务在此基座上更敏捷专注地开发业务逻辑。
- 开源微服务基座框架,如Java的 Spring Boot和Spring Cloud,GoLang的 Go Kit 和 Micro 等。
- 微服务基座缺点是编程语言的局限,不是每种语言都能找到对应的基座框架。
- 服务网格:把所有进出服务的网络流量通过一个网络层进行路由,它负责解决包括断路器、分布式追踪等共性问题。是微服务基座的新兴替代方案,且不受语言限制。
- 开源服务网格:Istio、Linkerd、Conduit等。
自动化部署
基于 DevOps 思想,自动化部署是当下流行的部署应用的方法。
生产环境必须实现的四个关键功能
- 服务管理接口:使开发人员能够创建、更新和配置服务。
- 运行时服务管理:确保始终运行所需数量的服务实例。
- 监控:让开发人员深入了解服务正在做什么,如日志、应用指标。可观测性
- 请求路由:将用户的请求路由到服务。
部署模式:传统的编程语言特定的发布包格式部署服务有诸多弊端,当下流行的两种部署模式分别是 将服务部署为Docker容器和Serverless部署。
将服务部署为Docker容器
- 将服务打包为容器镜像部署到生产环境,每个服务实例都是一个容器。
- 构建镜像:部署流水线使用容器镜像构建工具,将服务代码创建为容器镜像并存储到镜像仓库。
- 运行容器:从镜像仓库中拉去容器镜像,并创建容器。
- Docker Compose 近适合单机环境下定义一组容器,一般用作开发和测试环境。Kubernetes 适合集群化部署,用在生产环境。
- Docker容器好处:封装技术栈、服务隔离、服务资源控制。
- Kubernetes:Docker 编排框架,三个主要功能是资源管理(将一组计算机视为资源池)、调度(选择要运行容器的机器)和服务管理(实现命名和版本化服务)。
- 边车(sidecar):在与服务实例一起运行的容器中实现服务运行时所需的一些公共功能。如 Istio Envoy 作为 Istio 的边车,代理进出服务的所有流量。
Serverless 部署
- 使用公共云提供的 Serverless 部署服务,如 AWS Lambda。轻量级部署方式。
- 好处:有许多 AWS 云服务可供集成,无需负责底层系统管理、专注应用开发,弹性伸缩、无需配置服务器,基于使用情况付费。
- 弊端:长尾延迟(AWS需要时间启动应用,不适合对延迟敏感的服务),基于有限事件与请求的编程模型、不适合部署长时间运行的服务,稳定性稍差、AWS Lambda 发生过数次重大故障。
微服务架构的重构策略
在从单体过渡到微服务架构时,需要采用循序渐进的方法逐步完成,不要做“一步到位,推倒重来”式的改造。
逐步重构微服务架构的好处
- 立即获得投资回报。
- 有助于获得业务团队对重构工作的支持。
- 先将高价值部分迁移到微服务架构。
重构微服务的策略
- 将新功能实现为服务。
- 隔离表现层和后端。
- 通过将功能提取到服务中来分解单体。
- 复制数据:拆分单体时,原数据表设为只读,微服务数据表更改后同步单体,避免单体广泛修改。