microservices-patterns

第一章 单体架构服务

🔗单体架构服务

🔗弊端

  1. 开发缓慢。编辑–建立–运行–测试的循环需要很长的时间,这严重影响了生产力。
  2. 从提交到部署是很漫长的过程。无法做到持续交付。
  3. 应用扩展困难。这是因为不同的应用模块有相互冲突的资源需求。例如,餐厅的数据被存储在一个大型的内存数据库中,最好是部署在有大量内存的服务器上。相反,图像处理模块是CPU密集型的,最好部署在有大量CPU的服务器上。由于这些模块是同一个应用程序的一部分,FTGO必须在服务器配置上做出妥协。
  4. 可靠性不能保证。一个模块的问题可能会导致应用程序的所有实例崩溃,最终导致停产。
  5. 单一的架构使其难以采用新的框架和语言。

🔗微服务

🔗好处

  1. 它使大型复杂应用的持续交付和部署成为可能。

  2. 服务规模小,易于维护。

  3. 服务可独立部署。

  4. 服务可独立扩展。

每个服务可以部署在最适合其资源要求的硬件上。这与使用单体架构的情况完全不同,在单体架构中,资源要求完全不同的组件–例如,CPU密集型与内存密集型–必须部署在一起。

  1. 微服务架构使团队能够自治。

  2. 它允许轻松实验和采用新技术。

    原则上,在开发一个新的服务时,开发者可以自由选择最适合该服务的语言和框架。

    在单体架构中,你最初的技术选择严重限制了你在未来使用不同语言和框架的能力。

  3. 它具有更好的故障隔离。

🔗缺点

  1. 寻找正确的服务组合是具有挑战性的。
  2. 分布式系统很复杂,这使得开发、测试和部署都很困难。
  3. 部署跨越多个服务的功能需要仔细协调。
  4. 决定何时采用微服务架构是困难的

🔗微服务需要解决的问题

🔗服务拆分

🔗服务间通信

使用微服务架构构建的应用程序是一个分布式系统

🔗数据一致性

使用分布式事务的传统(2PC)方法对于现代应用程序来说并不是一个可行的选择。相反,一个应用程序需要通过使用Saga模式来保持数据的一致性。

🔗数据的查询

🔗服务部署

🔗服务监控

  1. 健康检查API-暴露一个端点,返回服务的健康状况。
  2. 日志汇总-记录服务活动,并将日志写入一个集中的日志服务器,提供搜索和警报功能。log
  3. 分布式跟踪–给每个外部请求分配一个唯一的ID,请求在服务之间流动时跟踪它们。trace
  4. 异常跟踪–将异常报告给异常跟踪服务,该服务对异常进行细分,提醒开发者,并跟踪每个异常的解决情况。
  5. 应用指标-维护指标,如计数器和仪表,并将其暴露给指标服务器。metrics
  6. 审计日志-记录用户的行动。

🔗服务的自动化测试

  1. 消费者驱动的合同测试-验证一项服务是否符合其客户的期望。
  2. 消费者端合同测试-验证服务的客户端可以与服务进行通信
  3. 服务组件测试–孤立地测试一个服务

🔗处理跨领域问题

在微服务架构中,有许多关注点是每个服务必须实现的,包括可观察性模式和发现模式。它还必须实现外部化配置模式,该模式在运行时向服务提供配置参数,如数据库凭证。当开发一个新的服务时,从头开始重新实现这些关注点是非常耗时的。一个更好的方法是应用微服务底盘模式,在处理这些问题的框架之上构建服务

🔗安全

在微服务架构中,用户通常由API网关进行认证。然后,它必须将用户的信息,如身份和角色,传递给它所调用的服务。一个常见的解决方案是应用访问令牌模式。API网关将访问令牌,如JWT(JSON Web令牌),传递给服务,服务可以验证令牌并获得关于用户的信息。

第二章 服务拆分

🔗两种拆分策略

🔗》Pattern: Decompose by business capability

🔗》Pattern: Decompose by subdomain

在应用微服务架构时,DDD有两个概念非常有用:subdomains+bounded contexts(子域和有界的上下文)

🔗一些拆分准则

🔗1、单一责任原则(Single Responsibility Principle,SRP)

一个class 应该只有一个改变的理由。

一个类所拥有的每个责任都是该类变化的潜在原因。如果一个类有多个独立变化的责任,那么这个类就不会是稳定的。通过遵循SRP,你定义的类都有一个单一的责任,因此有一个单一的变化原因。

🔗2、共同封闭原则(Common Closure Principle,CCP)

一个包中的类应该被封闭在一起,以防止相同类型的变化。影响一个包的变化会影响该包中的所有类。

在创建微服务架构时,我们可以应用CCP,将因相同原因而改变的组件打包到同一个服务中。

Decomposition by business capability and by subdomain along with SRP and CCP

are good techniques for decomposing an application into services

🔗拆分会遇到的问题

  1. 网络延迟
  2. 由于同步通信,可用性降低
  3. 保持跨服务的数据一致性
  4. 获得一致的数据视图
  5. 神类会阻碍拆分

第三章 IPC in a microservice architecture

🔗前置

🔗交互方式的两个划分维度

🔗one-to-one

  1. Request/response–服务客户向服务发出请求并等待响应。客户端期望响应能够及时到达。它在等待时可能会出现事件阻塞。这是一种交互方式,通常会导致服务被紧密耦合。
  2. Asynchronous request/response–服务客户端向服务发送请求,而服务则以异步方式进行响应。客户端在等待时不会阻塞,因为服务可能在很长一段时间内不会发送响应。
  3. One-way notifications-一个服务客户向一个服务发送请求,但没有预期或发送回复。

🔗one-to-many

  1. Publish/subscribe-客户端发布一个通知消息,该消息被零个或多个感兴趣的服务所消费。
  2. Publish/async responses-客户端发布一个请求信息,然后等待一定的时间,等待来自感兴趣的服务的回应。

🔗一、Synchronous-IPC

🔗》Pattern:Remote procedure invocation

🔗1、rest

优点:

  1. 简单。
  2. 你可以在浏览器中使用例如Postman plugin来测试HTTP API,也可以在命令行中使用curl(假设使用JSON或其他文本格式)。
  3. 它直接支持请求/响应式通信。
  4. 当然,HTTP对防火墙是友好的。
  5. 它不需要中间代理,这简化了系统的架构。

缺点:

  1. 它只支持请求/响应式的通信。
  2. 降低了可用性。因为客户和服务直接通信,没有消息中间件来缓冲信息,他们必须在交换的时间内都在运行。
  3. 客户必须知道服务实例的位置(URLs)。在现代应用中这是一个不简单的问题。客户端必须使用所谓的服务发现机制来定位服务实例。
  4. 在一个请求中获取多个资源是一个挑战。
  5. 有时,将多个update操作映射到HTTP动词上是很困难的。

🔗2、grpc

优点:

  1. 设计一个具有丰富update操作的API是很容易。
  2. 它有一个高效、紧凑的IPC机制,特别是在交换大型信息时。
  3. 双向流可以实现RPI和消息传递两种通信方式。
  4. 它使得各种语言编写的客户和服务之间具有互操作性。

缺点:

  1. 与基于REST/JSON的API相比,JavaScript客户端在使用基于gRPC的API时需要更多工作。
  2. 旧的防火墙可能不支持HTTP/2。

gRPC是REST的一个引人注目的替代方案,但和REST一样,它是一个同步的通信机制,所以它也有部分失败的问题。

🔗3、RPI需要解决的两个问题

🔗一、服务部分失效
🔗1、问题描述

由于某一服务出错或者宕机,导致客户端阻塞等待响应(用户体验差、服务资源浪费等),有可能导致级联故障。

🔗2、三种解决方案
  1. 超时。(如果服务不可用,客户端还是会发出请求,等待超时时间)
  2. 限制请求失败上限,超过限制,后续请求立即失效。
  3. 记录成功和失败的请求。如果失败比例超过阈值,熔断器断开,后续请求立即失效;一段时间间隔,客户端再次请求,如果请求成功,关闭熔断器。
🔗》Pattern:Circuit breaker

熔断器状态图

🔗二、服务发现
🔗1、问题描述

微服务中的服务实例由于需要自动扩容、升级、失败等原因,会有动态分配的网络位置

🔗2、两种解决方案
  1. 服务端和客户端直接通过服务注册表交互
  2. 部署基础设施处理服务的发现
🔗3、应用级服务发现

🔗》Pattern:Client-side discovery

客户端请求服务注册中心获取服务列表,然后通过负载均衡算法(round-robin or random)将请求负载到其中一个服务实例

🔗》Pattern:Self registration

服务注册网络信息到服务注册中心、健康检测

优点:

应用级服务发现的一个好处是,它可以处理服务部署在多个部署平台上的情况。

例如服务同时存在于k8s集群和其他历史遗留环境,可以使用应用级的服务发现解决

而基于k8s的服务发现只在k8s内部工作

缺点:

  1. 额外的工作,你要负责设置和管理服务注册表
  2. 需要为你使用的每一种语言–可能还有框架–提供一个服务发现库。
🔗4、平台级服务发现

🔗》Pattern:Server-side discovery

客户端向路由器发出请求,路由器负责服务发现。

🔗》Pattern:Third party registration

服务实例是由第三方在服务注册处自动注册的。

优点:

服务发现完全由部署平台处理。无论是服务还是客户端都不包含任何服务发现代码。

服务发现机制对所有的服务和客户端都是可用的,无论它们是用哪种语言或框架编写的。

缺点:

不能跨平台使用

例如,基于Kubernetes的服务发现只适用于在Kubernetes上运行的服务

🔗二、Asynchronous-IPC

🔗》Pattern:messaging

🔗Messaging需要解决的问题

🔗一、消息的顺序

一个常见的解决方案,如Apache Kafka和AWS Kinesis等现代消息代理所使用的,是使用sharded (partitioned) channels

  1. 一个分片通道由两个或多个分片组成,每个分片的行为都类似于一个通道;
  2. 发送者在消息的头中指定一个shard-key,这通常是一个任意的字符串或字节序列。消息代理使用shard-key将消息分配到一个特定的分片/分区。例如,它可以通过计算shard-key的哈希值与分片数量的关系来选择分片;
  3. 消息代理将一个接收器的多个实例分组,并将它们视为同一个逻辑接收器。例如,Apache Kafka就使用了消费者组这个术语。消息代理将每个分片分配给一个接收器。当接收器启动和关闭时,它重新分配分片。

消息在分区内有序。每个订单事件消息都有orderId作为其分片密钥,一个特定订单的每个事件都被发布到同一个分片上,由一个消费者实例来读取。因此,这些消息被保证按顺序处理。

🔗二、消息的重复

两种方式:

  1. 编写幂等的消息处理程序

    不幸的是,消息处理程序往往不是幂等的

  2. 追踪信息并丢弃重复的信息

当消费者处理一条消息时,它在数据库表中记录消息ID,这是创建和更新业务实体的事务的一部分。在这个例子中,消费者向PROCESSED_MESSAGES表插入了一条包含消息ID的记录。如果消息是重复的,INSERT将失败,消费者可以丢弃该消息。

另一个选择是让消息处理程序在一个应用程序表中记录消息ID,而不是一个专门的表。这种方法在使用NoSQL数据库时特别有用,该数据库有一个有限的事务模型,所以它不支持把两个表作为数据库事务的一部分来更新。

🔗Transactional messaging

消息分发作为事务的一部分

🔗》Pattern:Transactional outbox

关系型数据库:

作为创建、更新和删除业务对象的数据库事务的一部分,该服务通过将信息插入 OUTBOX 表来发送信息。由于这是一个本地ACID事务,原子性得到了保证。

NoSql数据库:

在数据库中作为记录存储的每个业务实体都有一个属性,该属性是需要发布的消息的列表。当一个服务更新数据库中的一个实体时,它将一条消息附加到该列表中。这是原子性的,因为它是通过单个数据库操作完成的

🔗将消息从数据库发布到消息中间件的两种方式
🔗》Pattern:Polling publisher

缺点:

频繁地轮询数据库会很昂贵

🔗》Pattern:Transaction log tailing

一个复杂的解决方案是让MessageRelay跟踪数据库的交易日志(也称为提交日志)。应用程序所做的每一个提交的更新都被表示为数据库事务日志中的一个条目。交易日志挖掘机可以读取交易日志,并将每个变化作为消息发布给消息代理。

🔗三、Asynchronous messaging 提高系统的可用性

你应该尽可能地设计你的服务,使用异步消息传递,这样可以提高系统的可用性

🔗消除同步IPC,提高系统可用性

🔗1、副本数据

一个服务维护它在处理请求时需要的数据的副本。它通过订阅由拥有该数据的服务发布的事件来保持该副本的更新。

🔗2、返回响应后完成处理

第四章 用sages管理事务

🔗传统分布式事务(2PC)

🔗弊端

  1. 许多现代技术,包括MongoDB和Cassandra等NoSQL数据库,都不支持它们。另外,现代的消息中介,如RabbitMQ和Apache Kafka,也不支持分布式事务。因此,如果你坚持使用分布式事务,你就不能使用许多现代技术。
  2. 分布式事务的另一个问题是,它们是同步IPC的一种形式,会降低可用性。

埃里克-布鲁尔的CAP定理指出,一个系统只能有以下三个属性中的两个:

一致性、可用性和分区容错性

今天,架构师们更愿意拥有一个可用性的系统,而不是一个一致性的系统

🔗使用Saga模式来保持数据的一致性

🔗》Pattern: Saga

🔗构建Saga协调逻辑的两种方式

🔗Choreography-based sagas

优点:

  1. 简单性-服务在创建、更新或删除业务对象时发布事件。
  2. 松散耦合–参与者订阅事件,彼此并不直接了解。

缺点:

  1. 更难理解–与协调不同,代码中没有一个地方定义了saga。相反,编排将saga的实现分布在各个服务中。因此,开发人员有时很难理解一个特定的saga如何运作。
  2. 服务之间的循环依赖性,代码的坏味道。
  3. 紧密耦合的风险
🔗Orchestration-based sagas

优点:

  1. 更简单的依赖关系–协调的一个好处是它不会引入循环依赖关系。
  2. 更少的耦合–每个服务都实现了一个由协调器调用的API,所以它不需要知道saga参与者发布的事件。
  3. 简化业务逻辑。

缺点:

在协调器中集中了太多的业务逻辑的风险。这就导致了这样一种设计:智能协调器告诉哑巴服务要做什么操作。幸运的是,你可以通过设计只负责排序而不包含任何其他业务逻辑的协调器来避免这个问题。

🔗处理缺乏隔离的问题

🔗隔离性弱引起的问题
  1. 丢失的更新。一个saga覆盖而不读另一个saga所做的改变。
  2. 脏读。事务或saga读取一个尚未完成更新的saga所做的更新。
  3. 模糊/不可重复的读取。一个saga的两个不同步骤读取相同的数据,得到不同的结果,因为另一个saga进行了更新。
🔗处理方法
🔗语义锁

当使用语义锁时,saga的可补偿事务在其创建或更新的任何记录中设置一个标志。该标志表明,该记录还没有提交,并且有可能发生变化。这个标志可以是一个阻止其他事务访问该记录的锁,也可以是一个警告,表明其他事务应该怀疑地对待该记录。它可以被一个可恢复的事务清除–saga成功完成–或者被一个补偿性的事务清除:传奇正在回滚。

🔗交换性的更新?

一个直接的对策是将更新操作设计成交换性的。如果操作可以以任何顺序执行,那么它们就是交换性的。一个账户的debit()和credit()操作是互换的(如果你忽略了透支支票)。这个对策很有用,因为它消除了丢失的更新。

🔗悲观主义观点

对saga的步骤进行排序,以最大限度地减少由于脏读造成的商业风险。

🔗重读值

使用这种对策的saga在更新记录之前会重读该记录,验证其是否没有变化,然后更新该记录。

🔗版本号文件

版本号文件对策之所以被这样命名,是因为它记录了在一条记录上执行的操作,从而可以对它们进行重新排序。

🔗按价值

最后一项对策是 “按价值 “对策。这是一种基于业务风险选择并发机制的策略。使用这种对策的应用程序使用每个请求的属性来决定是否使用sagas和分布式事务。

第五章

🔗聚合规则

1、root entity是聚合体中唯一可以被聚合体之外的类所引用的部分。

2、聚合体之间的引用是通过主键而不是通过对象引用。订单聚合体包含ConsumerID和RestaurantID。

3、一个事务只能创建或更新一个聚合体。