微服务知识小结

395 阅读19分钟

本文是学习microservices.io/ 后的一个小结

什么是微服务?

微服务——也叫做微服务架构,是一种架构风格,将应用程序构建为服务的集合,它具有以下特点:

  • 高可维护性、可测试性
  • 松耦合
  • 可独立部署
  • 围绕业务能力组织
  • 小团队维护

微服务架构可以让你快速、频繁、可靠地交付大型、复杂的应用。

服务的架构(六边形架构)

image.png “六边形”的含义是:

  • 业务逻辑(核心)
  • API Client:对外提供API服务。
  • Event Publisher:对外发布事件。
  • API:调用其他服务。
  • Event Subscriber:订阅其他服务的事件。
  • Service database: 数据存储。保证服务之间松耦合,不共享数据库。只利用API通信。

什么是时候使用微服务?

  • 应用需要扩展
  • 需要对功能进行分解

如何将应用拆分为微服务?

将单体应用拆分为微服务,有多种拆分方法。如下

根据业务功能拆分

根据DDD子域拆分

根据动词或使用案例拆分

据名词或资源拆分

各个服务中数据如何存储?

一个服务一个数据库(Data per Service)

每个服务一个数据库,只通过API来访问数据。一个服务的事物只涉及其自身的数据库。

优点:

  • 服务松耦合
  • 每个服务可以选择合适的数据库

缺点:

  • 跨多个服务的事物实现困难

    • 根据CAP定义,最好避免使用分布式事物
    • 很多NoSQL数据库不支持
  • 跨多个服务的join实现困难

  • 需要管理多个数据库

共享数据库(Shared Database)

多个服务共享一个数据库。每个服务可以使用本地事物访问其他事物的数据。

优点:

  • 开发者使用熟悉
  • 保证了数据的一致性

缺点:

  • 开发时间耦合。例如,一个开发者要改表的schema,需要和其他开发者协调,降低了开发速度。
  • 运行时耦合。所有服务都需要访问同一个数据库,可能会相互干扰。
  • 单个数据库不能满足所有服务的存储和查询需求

如何实现(跨多个服务的)查询?

API Composition

定义一个API Composer,调用各个要查询的服务,然后再在内存中合并结果(可以在API网关中实现)。

优点:

  • 简单

缺点:

  • 有些查询的效率可能不高,内存中要join大数据集

Command Query Responsibility Segregation(CQRS)

定义一个用于查询的只读的数据库视图。应用通过订阅服务发布的事件(Domain event)来保持该视图的数据是最新的。

优点:

  • 支持多个可扩展且高性能的非规范化视图
  • 改善关注点分离 = 更简单的命令和查询模型
  • 在事件源架构中是必须的(CQRS通常与事件源(Event sourcing)一起使用)

缺点:

  • 增加了复杂度
  • 潜在的代码重复
  • 复制延迟/最终一致性视图

如何实现(跨多个服务的)事物?

2PC/分布式事物

对于很多应用不适合,在分布式环境中很难实现

Saga

saga是一系列的本地事物。本地事物更新后,发布一个消息或领域事件(Domain Events)触发saga中的下一个事物。如果一个本地事物(由于违背了业务规则)失败了,那么sage会执行一系列的补偿事物来撤销之前的本地事物所做的更改。

本地事物 + 消息/事件 + 补偿事物

协调saga的两种方式:

  • Choreography :每个本地事物触发其他服务中本地事物的领域事件

    image.png Choreography-based saga

    • 依赖领域事件
  • Orchestration :协调器告诉参与者要执行哪些本地事物

    image.png Orchestration-based saga

优点:

  • 不使用分布式事物就可以实现跨多个服务的数据一致性

缺点:

  • 程序架构变得复杂:必须要显示的设计事物撤销的补偿机制

问题:

  • 为了可靠性,服务必须更新自己的数据库并发送消息/事件

    • 同步方式: 服务在saga完成后响应

    • 异步方式:

      1. 服务在saga初始化后响应,客户端轮询处理结果;
      2. 或服务在saga初始化后响应,在saga完成后主动推送给客户端(例如 websocket, web hook, etc)

如何可靠地更新数据库并发布事件?

问题:如何可靠地/原子地更新数据库并发送消息/事件?

要求:

  • 不能使用2PC
  • 如果数据库事物提交,消息必须被发送。相反,如果数据库回滚了,那么消息就不能被发送。
  • 消息必须按服务发送的顺序被发送到消息代理。

事件源(Event sourcing)

将事件持久化,应用通过重放事件来重新构造实体当前的状态。

优点:

  • 可靠。无论何时状态改变,都能可靠地发布事件。
  • 避免阻抗不匹配问题。因为它持久化的是事件而不是领域对象
  • 100%可靠的审计日志。对于业务实体的更改100%被记录。
  • 可临时查询。可在任意时间点查询实体状态。
  • 业务实体之间松耦合:交换的是事件。

缺点:

  • 与熟悉的编程风格不同,有一定的学习曲线
  • 难以查询状态。查询状态通常需要先重建实体的状态。这通常很复杂,且低效。因此,应用必须使用CQRS来实现查询。这反过来意味着应用必须处理最终一致性数据。

事物发件箱(Transactional outbox)

使用一个独立的消息中继进程(Message Relay process)将插入到数据库中的事件发布到消息代理。

优点:

  • 不需要2PC
  • 只要事物提交,消息保证被发送
  • 消息发送到消息代理的顺序与被应用发送的顺序一致

缺点:

  • 存在潜在的错误。开发者忘了在更新数据库之后发布消息
  • 消息中继可能将一个消息发送多次

问题:

  • 消息中继可能将一个消息发送多次

    由于消费者通常需要进行幂等,因此这不是一个问题。

事物日志跟踪(Transaction Log Tailing)

跟踪数据库事物日志并将插入 发件箱 的每条消息/事件发布到消息代理。

与 “发件箱” 模式有关

优点:

  • 没有2PC
  • 保证准确

缺点:

  • 尽管越来越常见,但相对不透明(晦涩难懂)
  • 需要数据库特定的解决方案
  • 避免重复发送需要技巧

轮询发布(Polling publisher)

通过轮询数据库的 发件箱 表来发布消息。

与 “发件箱” 模式有关

优点:

  • 任何SQL数据库都可以工作

缺点:

  • 按序发布事件需要技巧
  • 不是所有的NoSQL数据库都支持此模式

服务之间如何通信?

服务之间通常需要合作,各个服务之间如何进行通信呢?

远程过程调用(RPI,Remote Producer Invacation)

使用RPI进行通信。客户端通过基于请求/响应的协议来向服务发起请求。例如:REST、gRPC、Apache Thrift 等。

优点:

  • 简单、熟悉
  • 请求/响应模式简单
  • 没有中间代理,是系统更简单

缺点:

  • 通常只支持请求/响应模式。不支持 请求/异步响应、发布/订阅 、发布/异步响应 模式。
  • 可用性较低。客户端和服务端在交互过程中必须同时可用。

问题:

  • 客户端需要发现服务的地址

消息(Messaging)

使用异步消息来通信。服务之间通过在消息信道上交换消息来通信。如 Apache kafka 、RabbitMQ等

优点:

  • 发送端和消费端解耦
  • 可用性较高。消息代理缓存了消息,直到消费者能够处理这些消息。
  • 支持多种通信模式。请求/响应,通知,请求/异步响应,发布/订阅,发布/异步响应等

缺点:

  • 消息代理增加了额外的复杂度,它需要是高可用的

问题:

  • 请求/响应 方式的通信更加复杂

如何实现幂等消费?

幂等的含义:对于同一个消息,处理一次和处理多次,其结果都是一样的。

如果消费者不是幂等的,多次消费可能会造成Bug。

实现方式:

  • 天生幂等
  • 唯一性约束
  • 在更新数据的时候进行检查

客户端/另一个服务 如何发现服务的地址?

微服务架构通常都是运行在虚拟或容器环境中的,其中服务实例和它们的地址会动态变化。因此,需要实现一个机制让客户端能够向一组动态变化的临时服务实例发起请求。

“客户端”可能是:客户端、API网关、其他服务 等

Client-side service discovery

每当客户端要向一个服务发起请求的时候,客户端会先查询注册中心获取该服务的地址。然后再使用负载均衡算法在所有可用的服务实例中选择一个发起请求。

注册中心(service registry) + load-balancing algorithm。

image.png

优点:

  • 网络跳数少

缺点:

  • 客户端 与 注册中心 耦合
  • 不同的编程语言/框架需要使用自己的服务发现逻辑

Server-side service discovery

当客户端要请求一个服务时,它会请求负载均衡器。负载均衡器查询注册中心,然后将请求转发到可用的服务。

router(loader balancer) + service registry

优点:

  • 客户端代码简单,不需要处理服务发现

缺点:

  • 需要配置负载均衡器组件
  • 负载均衡器必须支持必要的通讯协议(e.g. HTTP,gRPC,Thrift,etc)
  • 更多的网络跳数

注册中心

问题: 客户端(客户端服务发现)或负载均衡器(服务端服务发现)是如何知道哪些服务的实例是可用的?

要求:

  • 服务的每个实例都需要在特定的位置暴露一个远程API(如HTTP/REST)
  • 服务实例的数量和位置会动态变化

实现一个服务注册中心,它是一个服务的数据库,包含服务的实例及其地址。服务实例在启动的时候需要向注册中心进行注册并在关闭的时候取消注册。客户端或路由器通过查询注册中心来查找可用的服务。服务的注册中心需要调用服务实例的 heach check api 来验证该实例是否能够处理请求。

例子:Apache Zookeeper、Consul、Etcd 等

优点:

  • 客户端或路由器能够发现服务实例的位置

缺点:

  • 额外的复杂度。

    除非注册中心内置在基础设施中,否则就需要设置、配置和管理注册中心。此外,注册中心是重要的系统组件。尽管客户端应该缓存注册中心提供的数据,但如果注册中心挂了,数据最终也会过期。因此,注册中心应该是高可用的。

服务如何在注册中心中注册和注销?服务实例有两种注册方式:

  • 自注册:服务实例注册自己
  • 第三方注册:第三方注册者将服务实例注册到注册中心

服务注册要求:

  • 服务实例必须在启动的时候注册,退出的时候注销
  • 服务实例崩溃后必须注销
  • 在运行中但却无法处理请求的服务实例必须注销

自注册

每个服务实例负责注册自己。客户端通常必须周期性地重新注册,让注册中心知道它还活着。退出的时候必须注销。

优点:

  • 服务实例知道自己的状态,因此可以实现一个比UP/DOWN更复杂的状态模型

缺点:

  • 服务与注册中心绑定
  • 每种编程语言/框架写的服务都需要实现服务注册的逻辑
  • 正在运行但无法处理请求的服务通常没有自我注销的意识

第三方注册

第三方的注册器负责注册和注销服务实例。例如:Netflix Prana、AWS Autoscaling Groups、Registrator 等

优点:

  • 服务代码相对简单
  • 注册器可以检查服务是否健康并注册/注销服务

缺点:

  • 第三方注册器对于应用状态的了解甚少
  • 如果基础设施中没有注册器,就需要另外安装、配置并维护。同样第,它也是系统中的一个重要组件,因此需要是高可用的。

不同种类的客户端如何访问服务?

问题: 客户端要访问的信息可能涉及多个服务,如何访问各个服务?

要求:

  • 微服务提供的API的粒度通常与客户端需要的不同。微服务通常提供的是细粒度的API,这意味着客户端需要与多个服务交互。
  • 不同的客户端需要不同的数据。
  • 不同的客户端网络性能不同。
  • 服务的实例数量和地址是动态变化的
  • 服务的分区会随着时间而改变,应该对客户端隐藏
  • 服务可能使用多种不同的协议,有一些可能不是web友好的

API 网关

实现一个API网关——即所有类型的客户端都使用单个入口。AIP网关处理请求的两种方式:

  1. 将请求路由/代理到正确服务
  2. 将请求扇出到多个服务

image.png

BFF(Backends for frontends)

BFF模式是API网关模式的一种变体。它为每种类型的客户端都定一个了一个API网关。

image.png

API网关/BFF的优点:

  • 客户端不必知道微服务是如何划分的
  • 客户端不用考虑服务的位置(地址)(只用和网关交互)
  • 针对每种类型的客户端提供最优的API
  • 减少请求/rtt的数量。客户端只需要单个rtt就可以取到数据。更少地请求意味着更少地开销并且提升了用户体验。API网关对于移动应用来说很重要。
  • 将调用多个服务的逻辑从客户端移动到了API网关,简化了客户端的逻辑
  • 从“标准的”web友好型协议转换为任意内部协议

缺点:

  • 复杂度增加。API网关需要开发、部署和管理。
  • 增加了响应时间——网关增加了额外的一跳,然而对于大多数应用来说,这额外的rtt不重要。

问题:

  • 如何实现API网关?

    • Netty,Spring Reactor等
    • NodeJS

如何将请求者的身份传给处理请求的服务?

Access Token

API网关验证请求,并将标识标识请求者身份的Access Token传递给服务。一个服务可以在请求其他服务的时候带上Access Token。

优点:

  • 请求者的身份可以在系统中安全地传递
  • 服务可以验证请求者是否有权执行操作

如何观测微服务?

问题:如何才能理解微服务的行为并解决问题?

要求:

  • 所有的解决方案的运行时开销应该尽量小

Log aggregation

使用一个集中式的日志服务,聚合所有服务的日志。用户可以搜索和分析日志。可以配置告警。如 AWS Cloud Watch 。

问题:

  • 存储大量的日志需要大量的基础设施

Application metrics

让每个服务采集单个操作的信息。在集中式的metrics服务中聚合所有的metrics,由它提供报告和告警功能。聚合metrics有两种模型:

  • push—— 服务将 metrics 推送给 metrcis服务
  • pull —— metrics服务 从 服务中拉取 metrics

优点:

  • 让你对应用程序的行为有更深的理解

缺点:

  • metrics 代码和 业务逻辑交织在一起使其更加复杂

问题:

  • 聚合metrics需要大量的基础设施

Audit logging

要求:

  • 知道一个用户最近做了哪些操作有助于: 客户支持、合规性、安全等。

在数据库中记录用户的活动。

Event sourcing 是实现审计的一种可靠的方法。

优点:

  • 可以记录用户的行为

缺点:

  • 审计代码与业务逻辑代码交织在一起,使业务逻辑更加复杂

Distributed tracing

要求:

  • 外部监控只能告诉你总体的响应时间和调用次数——无法深入了解单个操作
  • 一个请求的日志记录会分散在大量日志中

在服务中添加以下功能:

  • 为每个内部请求分配一个请求id
  • 将此内部请求id传递给所有相关的服务
  • 在所有日志记录中带上此内部请求id
  • 记录请求和操作相关的信息(如 开始时间,结束时间)

优点:

  • 可以知道系统的行为,包括延迟的来源
  • 开发人员可以理解单个请求是如何被处理的

缺点:

  • 聚合和存储追踪信息需要消耗大量的基础设施

Exception tracking

要求:

  • 异常必须被去重,被记录,被开发者调查并解决异常的根本问题。

将所有异常都报告给集中式的异常跟踪服务,它聚合和追踪所有的异常并通知开发者。

优点:

  • 很容易查看异常和其栈信息

缺点:

  • 异常追踪服务需要额外的基础设施

Health check API

问题: 如何检测一个服务是否能够处理请求?

要求:

  • 服务无法处理请求时应该生成告警
  • 请求应该被路由到可以工作的服务实例

添加一个health check API ,返回服务的健康状态。它可以进行多项检查,如:

  • 与基础设施的连接的状态
  • 主机的状态。如 磁盘、CPU
  • 特定的应用逻辑

优点:

  • 可以周期性地检测服务是否正常

缺点:

  • 健康检查可能不够全面
  • 服务可能会在两次health check之间挂掉,这时请求依然会被路由到挂掉的服务

Log deployments and changes

要求:

  • 由于错误通常在系统发生更改后发生,因此查看什么时候进行了部署和更改很有用

记录对于生产环境的每次部署和每次更改。

优点:

  • 使部署和更改能够轻松地与问题相关联,可以更快地解决问题

服务如何打包和部署?

场景:一个系统由多个服务组成。为了吞吐量和可用性,每个服务需要部署多个实例。

要求:

  • 服务的语言、框架及框架版本可能不同
  • 每个服务由多个服务实例组成
  • 服务必须能够独立部署、扩展
  • 服务之间必须隔离
  • 要能够快速构建并部署服务
  • 要能够限制服务使用的资源
  • 要能够监测服务的行为
  • 服务要可靠
  • 尽可能经济高效地部署

Single Service per Host

一个服务实例一个主机。

优点:

  • 用的资源或版本不会冲突
  • 服务之间隔离
  • 服务最多只会使用单个机器上的资源
  • 便于监测、管理 和重新部署

缺点:

  • 资源利用率低

Multiple Services per Host

不同服务的多个实例运行在一个主机上

多种方式:

  • 将服务作为JVM进程部署
  • 一个JVM中部署多个服务

优点:

  • 资源利用率相对较高

缺点:

  • 资源可能会冲突
  • 依赖版本可能会冲突
  • 难以限制一个服务消耗的资源
  • 如果多个服务部署在一个进程中,则难以监控,也无法隔离每个服务。

如何测试服务?

要求:

  • 端到端测试(即 测试时启动多个服务)很困难,缓慢、脆弱且昂贵。

Service Component Test

一个测试套件,使用它所调用的任何服务的测试替身来隔离测试一个服务。

优点:

  • 单独测试一个服务很容易、快、更可靠且便宜

缺点:

  • 测试通过,但生产环境失败

问题:

  • 如何保证测试替身总是正确的模拟被调用的服务?

Service Integration Contract Test

调用者提供测试套件,验证是否符合消费服务的期望。

优点:

  • 单独测试一个服务很容易、快、更可靠和便宜

缺点:

  • 可能测试通过了,但生产环境却失败了

问题:

  • 如何保证消费者提供的测试与他实际提供相匹配?

如何防止级联故障?

场景:当一个服务调用另一个服务时,该服务可能会不可达或因延时较高而不可用。在等待其他服务响应时,线程资源就被调用者浪费了。一个服务的故障可能会潜在地影响应用中的其他服务。

问题: 如何防止网络或服务故障级联到其他服务?

Circuit Breaker

在一连串的失败数超过阈值后,陷入到断路器。在某个时间后,断路器允许有限数量的请求通过。如果这些请求成功了,那么断路器会恢复正常运行。否则,又会开启一个故障超时。

优点:

  • 服务可以处理它们调用的服务的故障

缺点:

  • 超时该设置为多少,才不会导致误报或引入过多的延迟?

如何让开发者专注于业务逻辑?

横切关注点(cross-cutting concerns)microservice chassis

场景:通常每个服务中都需要处理横切关注点逻辑。例如:

  • 安全。
  • 外部化配置。
  • 日志。
  • 健康检查。
  • Metrics。
  • Distributed tracing。

问题:一个团队如何能够快速地为一个生产就绪的服务创建和设置一个可维护的代码库,以便他们能够开始开发其业务逻辑?

要求:

  • 构建逻辑必须能够以生产环境的格式(如 Docker镜像)构建,并测试应用和包
  • 需要包含常见横切关注点 和 特定技术的横切关注点。
  • 创建一个新的微服务应该快且简单
  • 当构建逻辑和横切灌注点的逻辑发生变化时,可以很快更新现有的服务

开发一个微服务框架,作为开发微服务的基础。框架通常需要实现:

  • 可重用的构建逻辑,可以构建、测试一个服务。
  • 可以处理横切关注点的机制。

例子:

  • Java

    • Spring Boot 和 Spring Cloud
    • Dropwizard
  • Go

    • Gizmo
    • Micro
    • Go kit

参考