Post

分布式理论学习笔记

  • 部署在多节点上的应用就是分布式应用

    部署在多个节点(物理机、虚拟机或者容器)上的应用就是分布式应用或者分布式系统,DNS 系统就是典型的分布式系统。

    微服务应用几乎总是分布式应用,因为每个微服务部署在不同的节点中。

    例外的情况是,虽然应用被拆分成了多个微服务,但是所有的微服务都部署在了同一个节点上。

  • 分布式解决了单点故障和单机性能瓶颈问题

    单体式应用中,一个模块的故障可能导致整个系统变得不可用。

    单体式应用的性能也受到它所部署的机器的绝对限制。

  • 包括分布式在内的任何技术在解决问题的同时,必然会引入其他问题
    • 怎么在对外提供统一的入口的前提下,在多个节点之间做负载均衡
    • 节点之间怎么互相调用对方的接口
      • 对于 “有状态” 的节点,这些节点之间的状态同步怎么做

        节点的本质是一个接受请求、处理事务、给出响应的状态机,它一定是有自己的状态的,包括当前的 CPU 堆栈、内存信息、磁盘信息等。

        但是,当我们提及一个节点是否 “有状态” 时,我们指的是这个节点是否保存了 “外部状态”,就是来自其他节点的 “状态”,比如其他节点的传递过来的用户信息、订单的状态信息等。

        比如,数据库就是一个典型的 “有状态” 的节点。

    • 多个节点产生的海量日志该怎么分析

    ……

  • 已有的经验证明,分布式系统不可能同时满足 CAP
    • 一致性(Consistency):所有有状态节点的状态保持一致。
    • 可用性(Availability):整个分布式系统可读可写。
    • 分区容错性(Partition Tolerance):整个分布式系统可以在部分节点宕机的情况下运行。

    分布式节点之间总是通过网络通信的,而网络在物理上是不可靠的,我们不能假设网络永远是连通的,所以在设计上,我们一定要保证分区容错性。

    所以现实世界的分布式系统的设计问题,总是围绕 AP 和 CP 展开。

    对于 AP 系统,在所有节点的状态完成一致化之前,整个分布式系统是不可用的。

    对于 CP 系统,整个分布式系统总是可用的,即使分布式系统中的不同节点的状态尚未一致。

    实践中,我们追求的是在保证状态最终一致的前提下,尽可能达到更高的可用性。比如,社交平台的点赞和评论就是一个对强一致性要求不高的场景,点赞或者评论之后可能并不是所有人都能马上看到,但只要最后所有人能看到就可以,即使延迟几分钟。

  • BASE 理论补充和延申了 CAP 理论
    • 基本可用性(Basically Available):只保证核心节点的可用性,允许非核心节点故障。
    • 软状态(Soft State):不追求所有节点的状态强一致,允许系统出现中间状态。
    • 最终一致性(Eventual Consistency):允许系统中的所有副本在一段时间后才最终一致。

    这个理论其实更符合工程实践。实时的数据一致和永远的系统可用在物理上是无法实现的。

  • 同类节点依靠共识算法实现 CP

    为了让多个节点的状态达成一致,需要借助分布式共识算法

    • Paxos
      • Basic Paxos

        定义四个角色,两个阶段。Proposer 代替 Client 向 Acceptor 发起提案(准备),得到 Quorum(法定数量,实践中几乎总是指多数派)规定数量 Acceptor 的成功响应后,带着提案值要求 Acceptor 接受提案(接受),得到 Quorum(法定数量,实践中几乎总是指多数派)规定数量 Acceptor 接受后,Learner 会认为当前提案可以学习,记录提案后向 Client 返回成功响应。

        这种方案存在活锁问题:即多个 Proposer 不断地争抢着提出议案,导致每个议案都走不到接受阶段就被新的议案打断了。

      • Multi Paxos

        Basic Paxos 模型相对比较复杂。Multi Paxos 在此基础上做了简化。这也是工程实践中最常用的 Paxos 变体。

        为了解决活锁问题,在正式开始提案之前,先使用 Basic Paxos 选举出一个 Leader Proposer,后续所有的提案都由这个 Leader Proposer 发起,避免出现争抢。

        另外,有了 Leader Proposer 后,后续的 准备 阶段就可以跳过,直接进入接受阶段了。Acceptor 不用再在多个 提案 之间抉择。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        
          Client   Proposer      Acceptor     Learner
             |         |          |  |  |       |  |  --- First Request ---
             X-------->|          |  |  |       |  |  Request
             |         X--------->|->|->|       |  |  Prepare(N)
             |         |<---------X--X--X       |  |  Promise(N,I,{Va,Vb,Vc})
             |         X--------->|->|->|       |  |  Accept!(N,I,V)
             |         |<---------X--X--X------>|->|  Accepted(N,I,V)
             |<---------------------------------X--X  Response
             |         |          |  |  |       |  |
             |         |          |  |  |       |  |  --- Following Requests ---
             X-------->|          |  |  |       |  |  Request
             |         X--------->|->|->|       |  |  Accept!(N,I+1,W)
             |         |<---------X--X--X------>|->|  Accepted(N,I+1,W)
             |<---------------------------------X--X  Response
             |         |          |  |  |       |  |
        

        另外的优化是,将 Proposer、Acceptor 和 Learner 直接合并。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        
          Client      Servers
             |         |  |  |  --- First Request ---
             X-------->|  |  |  Request
             |         X->|->|  Prepare(N)
             |         |<-X--X  Promise(N, I, {Va, Vb})
             |         X->|->|  Accept!(N, I, Vn)
             |         X<>X<>X  Accepted(N, I)
             |<--------X  |  |  Response
             |         |  |  |
             |         |  |  |  --- Following Requests ---
             X-------->|  |  |  Request
             |         X->|->|  Accept!(N,I+1,W)
             |         X<>X<>X  Accepted(N,I+1)
             |<--------X  |  |  Response
             |         |  |  |
        
      • Cheap Paxos、Fast Paxos、Generalized Paxos、Byzantine Paxos 等其他变体

    • Raft

      这个算法将共识问题分成了三个子问题(选主问题、状态机复制问题、安全问题),每个节点维护三个状态(Flower、Candidate、Leader)。官网为这个算法提供了非常详尽且生动的案例演示和讲解:https://raft.github.io/

    • ZAB
  • 不同类节点依靠分布式事务的达成 CP

    分布式事务与单体应用的本地事务都要求具有 ACID 的特性。

    • 原子性(Atomicity):事务中定义的所有操作要么全部成功,要么全部失败,不会结束在中间某个环节
    • 一致性(Consistency):事务开始之前和事务结束以后,数据库的完整性没有被破坏
    • 隔离性(Isolation):允许多个并发事务同时对其数据进行读写和修改的能力
    • 持久性(Durability):事务结束后,对数据的修改就是永久的,即便系统故障也不会遗失

    单体应用的本地事务是在多个线程对应的不同事务之间做协调,而分布式事务是在多个节点对应的不同本地事务之间做协调。

  • 分布式事务模型
    • 强一致性模型
      • 基于 XA 规范实现的 2PC(Two-Phase-Commit) 和 3PC(Three-Phase-Commit)

        XA 代表 eXtended Architecture,是 X/Open 组织(一个开放系统国际标准组织,现已并入 The Open Group)在 20 世纪 90 年代初提出的一套分布式事务处理标准规范。

        XA 规范定义了全局事务管理器 (Transaction Manager, TM) 与局部资源管理器 (Resource Manager, RM) 之间的接口。这个规范的目的是允许多个资源(如不同的数据库实例、消息队列等)能够参与到同一个全局事务中,从而保证 ACID 属性可以跨越多个系统而保持有效。

        实践中,主要用的还是 2PC,极少使用 3PC。2PC 的流程与 Basic Paxos 有点相似。

        第一阶段(准备)

        1. 全局事务管理器向所有本地资源管理器发送 Prepare 消息,询问它们是否可以执行并提交任务
        2. 本地资源管理器收到 Prepare 消息后,开启本地事务,执行所有事务操作,Redo Log 刷盘,将事务状态修改为 prepare。根据执行结果,回复全局事务管理器一个 Vote-YES 或者 Vote-No 消息。

        只要有一个本地资源管理器回复了 Vote-No,全局事务管理器就会向所有本地资源管理器发送 Abort/Rollback 消息回滚事务。

        第二阶段(提交)

        1. 全局事务管理器向所有本地资源管理器发送 Commit 消息,要求它们正式提交事务并回复 ACK 消息。
        2. 本地资源管理器收到 Commit 消息后,将 Bin Log 和 Buffer Pool 刷盘,将事务状态修改为 commit。回复全局事务管理器一个 ACK 消息。

        全局事务管理器会对那些回复超时的节点进行重试。

        在实际开发中,可以引入 JTA 和 Atomikos 依赖,简单配置之后,在对应方法上打 @Transactional 注解即可享受分布式 XA 事务。也可以直接引入 Seata 作为分布式事务框架,在配置文件中指定事务模式为 XA 即可。

    • 基于事务补偿的最终一致性模型
      • TCC(Try-Confirm-Cancel)

        将分布式事务的提交分成了两个阶段,第一个阶段预留资源,第二个阶段事务补偿。

        1. Try:尝试预留资源,比如冻结库存或者余额之类

          这里的预留资源是指业务层面的控制,比如给库表增加一个冻结库存字段,代表等待 Confirm 或者 Cancel 的库存数量,通过维护冻结库存和实际库存字段实现 TCC。不是指数据库的行锁和表锁这些。

        2. Confirm:开始实际执行并提交事务
        3. Cancel:异常发生,取消并回滚事务

        这种方式需要开发者自己手动在业务代码中实现对应的接口逻辑,侵入性比较大。

      • Saga

        在文学和历史学中,Saga 指的是一种长篇的、关于英雄事迹的史诗或传说。这个名字带有 “长流程” 和 “连贯性” 的隐喻。Saga 事务是一个由多个章节(本地事务)组成的连贯流程,如果在某个章节出错,需要有一个回溯机制(补偿事务)来修正错误,确保故事有一个合理的结局。

        将分布式事务分成了串行化的多个本地事务流程,通过补偿机制保证最终一致性。

        一个 Saga 事务序列可以分为 $T_1, T_2, \dots, T_k, \dots, T_n$

        • 正常执行序列:$T_1$ 成功 → $T_2$ 成功 → …… → $T_n$ 成功。整个 Saga 提交。
        • 失败回滚路径: 假设 $T_{k+1}$ 执行失败(其中 $k < n$)。Saga 必须启动回滚流程,执行已成功事务的补偿事务序列 $T_{k+1}$ 失败 → $C_k$, $C_{k-1}$, ……, $C_1$。整个 Saga 被中止并回滚。

        Saga 事务可以有两种实现方式。

        • 基于事件驱动的协调器模式

          本地事务之间通过消息队列进行通信。一个事务执行完毕后,通过消息队列告诉下一个事务开始执行。

        • 集中中央协调器的编排器模式

          中央协调器负责定义事务流,并在合适的时机通知每个本地事务开始执行。

        这两种方式各有利弊。事件驱动方式松耦合,但是难以追踪和监控整个事务流。中央协调器方式方便追踪和监控,但是存在单点故障和强耦合的问题。

      • AT(Automatic Transaction)

        分布式事务框架 Seata 独创柔性事务模型。基于 Spring AOP 和代理数据源,在事务方法前后自动执行分布式事务逻辑,实现了无侵入式的分布式事务。

        将分布式事务的提交分成了两个阶段,第一个阶段执行事务,第二个阶段事务补偿。

        • 第一阶段

          Seata 通过 Spring AOP 拦截业务 SQL,解析 SQL 语义,找到业务 SQL 要更新的业务数据,在业务数据被更新前,将其保存成 before image,然后执行业务 SQL 更新业务数据,在业务数据更新之后,再将其保存成 after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

        • 第二阶段

          二阶段如果是提交的话,因为业务 SQL 在一阶段已经提交至数据库,所以 Seata 只需将一阶段保存的 image 和行锁删掉,完成数据清理即可。

          二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的业务 SQL,还原业务数据。回滚方式便是用 before image 还原业务数据;但在还原前要首先要校验脏写,对比数据库当前业务数据和 after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,否则就需要转人工处理。

        上述过程都由 Seata 框架自动生成,用户只需编写 “业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

    • 对于补偿机制,都必须要保证三个原则性的技术要求。
      • 补偿失败重试机制
      • 补偿幂等性
      • 补偿允许空操作性
  • 服务的高可用主要通过限流、熔断和降级来实现
    • 限流限制进入系统的流量的手段,防止系统被过高的负载压垮。

      经典的限流算法包括:

      • 固定窗口计数:将时间轴划分为固定大小的窗口(比如每秒、每分钟),每个窗口维护一个请求计数器,如果窗口期请求数超出阈值,则拒绝后续请求,进入下一个时间窗口时,计数器重置。

        这种算法的优点是简单易实现,缺点是无法解决临界突刺问题。

        临界突刺问题是指两个窗口交界处出现峰值流量的叠加导致实际的负载达到两倍阈值。例如,窗口 $W_1$ 允许 100 个请求。在 $W_1$ 的最后 1 毫秒内来了 100 个请求,在 $W_2$ 的最初 1 毫秒内又来了 100 个请求。在极短的 2 毫秒内,系统实际上处理了 200 个请求,是阈值的两倍,这可能瞬间压垮后端服务。

      • 滑动窗口计数:维护一个固定大小的时间窗口(比如每秒、每分钟),这个窗口会随着时间推移以固定大小的步长(比如每毫秒)向前移动,给这个窗口维护一个请求计数器,如果窗口期请求数超出阈值,则拒绝后续请求。

        这种算法的优点是可以解决临界突刺问题,缺点是实现相对复杂。

      • 令牌桶:维护一个固定大小的桶,用于存放令牌。系统以 $R$ 个每秒的生成速率不断往桶中放入令牌,如果桶满了,多余的令牌会被丢弃。每个请求到来时,必须先从桶中拿到一个令牌,如果桶中没有令牌了,那么则拒绝该请求。

        这种算法的优点是,允许突发流浪,系统空闲时,桶是满的,可以应对突发的高峰流量,平均速率稳定,也比较简单,缺点是生成速率和桶大小不好确定。

      • 漏桶:维护一个固定大小和漏出速率的桶。每个请求到来时,如果桶没有满,则入桶排队,如果桶满了,则拒绝后续的请求。桶以预设的漏出速率不断执行请求。

        这种算法的优点是,强制匀速(漏出速率)处理请求,特别适合需要平滑下游服务的场景,缺点是无法应对突发流量,即使系统长时间空闲,突发流量到来时,仍然会以固定速率处理请求。

    • 熔断隔离下游异常服务的手段,防止故障扩散到上游服务。

      熔断器的核心设计思路是,根据窗口期内请求的一组指标,在关闭、开启和半开启之间维护状态的流转,保证服务的错误率始终低于阈值。

      • 熔断器默认是关闭状态,允许所有请求通过,并监控窗口期内请求一组指标(请求数、错误数、请求错误率、响应时间等)
      • 当窗口期内的请求指标达到熔断阈值时,熔断器会进入开启状态,在一定时间的休眠期内拒绝后续的请求,也叫短路后续的所有请求
      • 休眠期结束后,进入半开启状态,尝试放行一个或一组请求,用于测试服务的可用性,
        • 如果测试请求成功,则转回关闭状态,继续正常提供服务
        • 如果测试请求失败或达到失败次数,则转回开启状态,进入下一个休眠期。
    • 降级给异常服务提供有损服务的手段,保障核心功能的可用性。

      本质就是在特定的异常或降级条件触发时,执行备用逻辑,而不是原有的业务逻辑。

      触发降级的条件通常有

      • 服务被限流:系统负载达到限流阈值,触发限流逻辑。
      • 服务被熔断:服务触发熔断逻辑,熔断器开启。
      • 手动配置:运营人员手动根据需求,关闭部分非核心功能

    当触发限流和熔断时,可以简单地拒绝后续请求,也可以执行降级逻辑,这三者是相互补充的关系,共同用于实现整个系统的高可用。

This post is licensed under CC BY 4.0 by the author.