分布式系统理论与架构详解

分布式系统简介

分布式系统是一种将硬件或软件组件分布在不同网络计算机上的系统,这些组件通过消息传递进行通信和协作。

通俗地讲,就是将一个庞大业务拆分成多个子业务,分别分布在不同的服务器节点上,共同构成一个系统。在同一个分布式系统中,服务器节点在空间上的部署是灵活的,这些服务器可以放在不同的机柜,不同的机房,甚至不同的城市中。

分布式系统特性

分布式系统具有以下几个特性:

  • 扩展性:可以通过增加更多节点来提高系统的处理能力和存储容量,从而支持大规模用户和数据增长。
  • 容错性:具备较高的容错能力,即使部分节点发生故障,系统仍能继续运行,通过冗余和分片技术保证数据和服务的可靠性。
  • 并发性:能够处理大量并发请求,支持多个节点同时工作,提高整体系统的处理效率和响应速度。
  • 异地分布:节点可以分布在不同的地理位置,能够实现跨地域的数据处理和服务提供,支持全球范围内的用户访问。
  • 一致性:在确保数据的一致性和完整性方面,分布式系统采用一致性协议来保证多个节点间的数据同步和一致。
  • 可维护性:通过分层设计和模块化结构,便于系统的维护和升级,能够在不影响整体系统运行的情况下进行部分节点的维护或升级。
  • 透明性:对用户和应用程序隐藏系统的复杂性,使其感觉像在使用单一系统,透明性包括位置透明、访问透明、迁移透明等多个方面。
  • 高可用性:通过负载均衡和故障转移机制,保证系统的高可用性,提供持续的服务能力。

实现分布式系统面临的挑战

实现分布式系统需要面临以下挑战:

  • 通信异常:由于网络本身的不可靠性,每次网络通信都伴随着不可用的风险(如光纤、路由器、DNS 等硬件设备或系统的故障),这可能导致分布式系统无法顺利完成一次网络通信。此外,即使各节点之间的网络通信能够正常执行,其延迟也会大于单机操作,并且延时差异较大,这会影响消息的收发过程,因此消息丢失和延迟变得非常普遍。
  • 网络分区:网络可能会出现不连通的情况,使得各自网络内部通信正常,但整个系统被切分成若干个孤立的区域。分布式系统在这种情况下会出现局部小集群,在极端情况下,这些小集群会独立完成原本需要整个系统才能完成的功能,包括数据的事务处理,这对分布式一致性提出了极大的挑战。
  • 节点故障:所有组成分布式系统的计算节点都可能在某一时刻发生故障,节点越多,故障的概率越大。
  • 依赖全局时钟:各个节点需要通过逻辑时钟(如 Lamport 时钟算法)或其他算法来确保事件的有序性和并发控制,以消除网络传输的影响。
  • 并发冲突:多个节点可能会并发操作一些共享资源,因此需要引入同步机制。

分布式理论基础

分布式一致性原理

分布式系统中的数据通常有多个副本,如果由于网络等问题导致这些数据副本未能及时同步,就会出现所谓的“一致性问题”,即各副本之间的数据不一致了。而数据一致性并非简单的“一致”与“不一致”两种情况,就像可用性可以用 0% 到 100% 之间的任意数值来表示一样,一致性也有不同的分类。根据一致性强弱的不同,一致性模型大致可以分为强一致性模型弱一致性模型两大类:

  • 强一致性模型:系统在更新或写入一个数值后,无论何时进行读操作,都能读取到这个最新的数值。
  • 弱一致性模型:系统在更新或写入一个数值后,后续的读操作不一定能够读到这个最新的数值。

强一致性模型

强一致性模型可以进一步细分为线性一致性 (Linearizability)顺序一致性 (Sequential Consistency) 两种。

线性一致性 (Linearizability)

线性一致性又称为原子一致性,这个概念是由 Maurice P. Herlihy 与 Jeannette M.Wing 在论文 《Linearizability: A Correctness Condition for Concurrent Objects》 中提出来的。CAP 中的 C 指的就是线性一致性。

线性一致性需要满足如下约束:

  1. 任何一次读操作都可以读到某个数据的最新值。
  2. 系统中所有节点内执行的事件顺序都和系统级别时钟下看到的事件顺序一致。

第一点非常容易理解,第二点则约束了“线程”和“整个系统”这两个维度下事件的执行顺序都必须是一样的。如下图所示:

线性一致性

上图有三个线程 P1、P2、P3:

  • 从 P1 线程来看,只是执行了一次写操作。
  • 从 P2 和 P3 线程来看,事件顺序都是先执行写操作,再执行读操作。
  • 从整个系统的时钟顺序来看,先执行三次写操作,然后执行两次读操作。

所以每次读取到的值都必须为最新值 3。

顺序一致性 (Sequential Consistency)

通过线性一致性的例子可以看到五个事件在时间上没有任何重叠,但是在现实场景中,不同线程事件的执行的时间点大概率会重叠,如下图所示:

线性一致性

其中数字代表线程标识,比如 “write1” 代表 P1 线程的写操作

  • 从系统级时钟的维度来看,整个事件的执行顺序应该是 write1 -> write2 -> write3 -> read2 -> read3 -> read2

    如果按照线性一致性的约束,第一次 read2 应该读取到的值是 a=3,因为 read2 之前的写操作是 write3。然而,实际结果是 a=2,这是因为 write3 执行超时了。因此,P2 线程在执行读操作时只能读取到最近一次成功写入的值,也就是 a=2

  • 从 P2 线程的视角看,事件执行顺序应该是 write2 -> read2 -> write3 -> write1 -> read2(这里没有把 read3 排进去是因为 read 操作本身不会被其他线程所感知),P2 线程和系统级时钟的视角看到的事件执行顺序是完全不一样的。

这种时间点重叠,且不满足系统级时钟的事件执行顺序的一致性称为顺序一致性

在 Maurice P.Herlihy 与 Jeannette M.Wing 提出线性一致性之前,Lamport 在 1979 年就提出了顺序一致性的概念。比如 ZooKeeper 中的 ZAB 协议就是顺序一致性的。顺序一致性的约束如下:

  1. 任何一次读操作都可以读到某个数据的最新值,这一点和线性一致性是相同的。
  2. 所有线程看到的事件顺序是合理的,达成一致即可,并不需要所有线程的事件顺序和系统级时钟下的事件顺序一致。

它放宽了对一致性的要求,并不像线性一致性一样严格。

弱一致性模型

将弱一致性模型进一步细分,可以分为三种:分别是因果一致性单调一致性普通最终一致性

因果一致性 (Causal Consistency)

因果一致性强调在分布式系统中事件的因果关系。其约束如下:

  1. 对于具有因果关系的读/写事件,所有线程看到的事件顺序必须一致。
  2. 对于没有因果关系的读/写事件,则不作要求。
因果一致性

在上图中,P2 线程内的两次写操作具有因果关系,必须先执行赋值 a=2,再执行 a=3。所以按照因果一致性的约束,其他几个线程也必须看到这个因果关系,也就是 P3 和 P4 线程在执行读操作时,能读到值为 2 的操作一定先于能读到值为 3 的操作。而能读到 a1 的操作无关在哪个位置,因为 P1 和 P2 的写操作没有因果关系,所以其他线程可以以不同的顺序看到这些事件的执行。

下面是一个不符合因果一致性约束的例子,如下图所示:

非因果一致性

在 P3 线程的视角中先有了 a=3,然后才有 a=2,与 P2 线程看到的因果关系不一致,所以不符合因果一致性的约束。

单调一致性 (Monotonic Consistency)

单调一致性可以分为单调读一致性单调写一致性

  • 单调读一致性指的是任何时刻一旦读到某个数据项在某次更新后的值,就不会再读到比这个值更旧的值:

    单调读一致性
  • 单调写一致性指的是一个线程对某一个数据项执行的写操作必须在该线程对该数据项执行任何后续写操作之前完成:

    单调写一致性
最终一致性

最终一致性只有一个约束,就是向系统写入更新或者写入一个数值,后续一段时间内的读操作可能读取不到这个最新的值,但是在该时间段过后,一定能够读到这个最新的数值。

以上几个一致性模型的约束条件,由弱到强分别是:最终一致性 < 单调一致性 < 因果一致性 < 顺序一致性 < 线性一致性。

CAP 定理

CAP 定理又称为布鲁尔定理 (Brewer’s theorem),它指出了一个分布式系统不可能同时满足一致性 (Consistency)、可用性 (Availability)、分区容错性 (Partition Tolerance) 这三个基本要求,最多只能同时满足其中的两个,如下图所示:

CAP 理论
  • 强一致性 (Consistency): 如果对某个数据项的更新操作成功执行,那么随后对系统中任意节点的读操作都应该返回更新后的数值。简而言之,所有节点在同一时刻看到的数据应该是相同的。在分布式系统中,数据一致性至关重要。当有状态服务进行横向扩展时,首要考虑的是数据一致性问题。
  • 高可用性 (Availability):系统提供的服务应始终处于可用状态。只要客户端请求的节点不是故障节点,客户端的请求应该在有限的时间内收到返回结果,尽管这并不保证是最新的数据。如果服务本身发生故障,作为故障节点,它无法返回结果。因此,这里的服务可用性仅描述非故障节点。例如,当出现网络分区问题时,该服务无法与其他节点进行数据同步,可能导致数据不是最新的,但仍会向客户端返回响应。
  • 分区容错性 (Partition Tolerance):分布式系统在面临任何网络分区故障时,只要系统能在规定的时间内继续对外提供服务,则表明该分布式系统具备分区容错性。

对于“三选二”,并不是完全选择其中两个而放弃其中一个,这三者的制衡是有一个度的:

  • 上图中的 CA,看似舍弃了 P,实则并不能完全舍弃 P。在分布式系统中,因为各节点直接通过网络进行通信,所以网络分区很难避免,那么分区容错性就是必须要保证的。如果在设计分布式系统时选择了 CA,只能从概率上理解,假定了网络分区出现的可能性要比系统性错误低得多。但是即使认为概率很低,但还是有可能出现网络分区情况,当出现这种情况时,需要从原有的 CA 转为 AP 或者 CP。所以大多数情况下都是在一致性和可用性之间选择,寻找制衡点。
  • 上图中的 CP 更倾向于一致性,当发生网络延迟或者消息丢失时,两个节点之间的数据没有同步导致数据不一致,客户端希望得到的是最新数据。所以如果客户端请求到旧数据的节点,为了保证数据的一致性,则会对该请求响应错误信息,即使它能够正常处理请求,但是从客户端的角度来说,该服务可以理解为不可用的。
  • 上图中的 AP 更倾向于可用性,无论是否发生网络分区,客户端的请求都能获得正常的信息,只是如果两个节点的数据不一致,则返回的响应信息并不一定是最新的。

BASE 理论模型(高可用性)

除一致性外,在大型的分布式系统中,高可用 (High Availability) 也是分布式系统的重要指标之一,它指的是通过设计来减少系统不能提供服务的时间。一般会用几个 9 来作为高可用的衡量标准,比如四个 9 就是 99.99%,意味着一年 8760 个小时中,系统不能提供服务的时间在 8.76 小时以下。在一些场景中,高可用相较于强一致性更加重要。

举个例子,假设用户 A 使用某银行 App 发起一笔跨行转账给用户 B,银行系统首先扣掉用户 A 的钱,然后提示用户 A 需要几小时后到账,在一段时间后增加用户 B 账户中的余额。这就是为了改善可用性而牺牲了较强的一致性,BASE 就是基于该思想演化而来的。

BASE 是 Basically Available、Soft State、Eventually Consistent 的首字母缩写,其中涵盖了基本可用 (Basically Available)弱状态 (Soft State)最终一致性 (Eventually Consistent) 三个概念:

  • 基本可用 (Basically Available):分布式系统在出现不可预知故障时,允许损失部分可用性。
    • 响应时间上的损失:比如正常一个服务在 1 秒之内返回结果,可是由于断电等故障,返回结果的时间需要增加到 2 秒。
    • 功能上的损失:例如在“双 11”期间,由于消费者的购物行为激增,为了保障购物系统的稳定性,部分消费者可能会被引导到一个降级后的页面。
  • 弱状态 (Soft State):允许数据存在中间状态,并且该状态的存在不会影响系统的可用性,即允许系统在不同节点的数据副本之间进行数据备份的过程存在延时。
  • 最终一致性 (Eventually Consistent):系统中所有的数据副本在经过一段时间的同步后,最终能够达到一致的状态。因此最终一致性不需要实时保证一致性。

分布式一致性协议

分布式协调

分布式协调协议是用于在分布式系统中选出一个领导者 (Leader) 的协议,领导者通常负责协调和管理系统中的关键任务。常用的理论与算法有 Paxos、Raft 等。

Paxos 共识算法

Paxos 是由 Leslie Lamport 在 1990 年发表的论文 《The Part-Time Parliament》 中提出的。在 2001 年,Leslie Lamport 又以计算机领域的描述方式重新对 Paxos 算法进行了阐述,并发表了 《Paxos Made Simple》

Paxos 是一种基于消息传递且具有高度容错性的共识算法。在分布式系统中,通信是一个非常重要的环节,因为在分布式系统中,线程的崩溃、重启、通信消息丢失和延迟等情况是不可避免的。Paxos 算法的目标是确保在出现这些异常情况时,分布式系统的决议一致性不会被破坏。各个线程可以提出各种请求,但最终只有一个请求会被选中,并且一旦某个请求被选中,其他线程就能够获得该请求所带来的变化。

在 Paxos 算法中,有三类角色:

  • Acceptor (决策者):负责决策最终采用哪个提议。
  • Proposer (提议者): 该角色负责向决策者提交提议。
  • Learner (最终决策执行者): 该角色负责执行最终选定的提议。

整个过程从提议的提出到选定,再到执行,可以大致分为两个阶段。第一个阶段是决策者提出最终的提议,第二个阶段是最终决策执行者如何获取并执行该提议。

  • 第一个阶段其实是 Proposer 与 Acceptor 之间的交互,过程如下:
    1. Proposer 选择一个提议,该提议编号记为 M,然后向 Acceptor 的某个超过半数的子集成员发送编号为 M 的准备请求。
    2. 如果一个 Acceptor 收到一个编号为 M 的准备请求,并且该提议的编号 M 大于 Acceptor 已经响应的所有提议的编号,那么它会把已经响应的提议的最大编号返回给这个 Proposer, 并且承诺不会再批准任何小于编号 M 的提案。如果 Proposer 没有得到半数以上 Acceptor 的响应则将编号 +1 后继续发起请求。
      • 举个例子,一个 Acceptor a 已经响应了所有提议(编号为 1247)的提案。那么该 Acceptor 收到一个编号为 8 的提议后,会将编号为 7 的提议反馈给这个提出编号为 8 的 Proposer。
    3. 如果 Proposer 收到半数以上的 Acceptor 对于 M 编号的提议的反馈,则再次发送一个 {M,V} 的提议给 Acceptor (前面都是准备阶段,只发送编号,类似于检测),这个 V 就是反馈的那个最大的提案值,例如上述例子中的 7。如果响应中不包含任一提议,那么它就是任意值。
    4. 如果 Acceptor 收到这个 {M,V} 的提案请求,只要该 Acceptor 尚未对编号大于 M 的请求作出响应,则通过该提议。
  • 产生最终的提议后,下一阶段就是让 Learner 执行该提议,该阶段是 Acceptor 与 Learner 之间的交互。Learner 在执行提议之前先要接收最终的提议,Learner 接收最终提议也有不同的方案,大致有以下三种:
    • 方案一: Acceptor 获得一个被选定的提案的前提是该提案已经被半数以上的 Acceptor 批准。因此,最简单的做法就是一旦 Acceptor 批准了一个提议,就将该提议发送给所有的 Learner。尽管 Learner 能够在最短的时间内获得所选择的项目,但这需要每一个接受者与他们之间进行一次单独的沟通。假设有 M 个 Acceptor 和 N 个 Learner,则通信次数就是 M x N,通信次数过多。
    • 方案二:让所有的 Acceptor 将它们对提案的批准情况统一发送给一个特定的 Learner,类似于 Learner 的领导者,当这个领导者获得提案后,它会负责去通知其他 Learner。这种方案的通信次数是 M + N - 1,虽然通信次数大大减少,但主领导者可能出现单点故障。
    • 方案三: 可以将方案二中的领导者的范围扩大,Acceptor 可以将批准的提案发送给一个 Learner 领导者集合,该集合中的每一个领导者都可以在一个提案被选定后通知其他 Learner,这个领导者集合中的领导者个数越多,可靠性越好,但是通信的复杂度越高。

上述算法是最基础的 Paxos 算法,也称为 Basic Paxos,在后续又衍生出了 Multi Paxos、FastPaxos 等算法,都是基于 Basic Paxos 的变种算法。

Raft 算法

由于 Paxos 算法是一套偏向理论的算法,实现难度极大,斯坦福大学的两位教授 Diego Ongaro 和 John Ousterhout 决定设计一种更加简单、容易理解的共识算法,那就是 Raft 算法。它在论文 《In search of an Understandable Consensus Algorithm》 中最先被提出。

Raft 是一个用于管理复制状态机的算法。每个服务器存储一个包含一系列命令的日志,其状态机按顺序执行日志中的命令。每个日志中的命令都相同并且顺序也一样,因此只要处理相同的命令序列,就能得到相同的状态和相同的输出序列。这也是 Raft 实现一致性的核心思想。

Raft 算法有三种角色:

  • Leader (领袖): 该角色的职责是接收和处理一切请求,并把同步数据给 Follower。
  • Follower (群众):该角色的职责是转发请求给 Leader,接收 Leader 同步过来的数据,以及参与投票。
  • Candidate (候选人): 该角色的职责是竞选 Leader。

这三种角色分别代表服务节点的三种状态,它们之间可以互相转化。引用论文 《In Search of an Understandable Consensus Algorithm》 的身份转换图如下:

raft

从上图中可以看到集群最初的状态:所有服务器都是 Follower,当这些服务启动完成后,由于起初没有 Leader,所以 Follower 一定不会收到 Leader 的心跳消息。经过一段时间后发生选举,此时 Follower 先增加自己的当前任期号并且转换到 Candidate 身份,然后投票给自己并且并行地向集群中的其他服务器节点发送竞选请求,即图中的 “times out, starts election”。当满足以下三个条件中的一个时,Candidate 身份会发生转变:

  • 集群内超过半数的服务节点同意该 Candidate 成为 Leader,也就是超过半数的节点响应了竞选请求,此时 Candidate 会变成 Leader。即图中的 “receives votes from majority of servers”
  • 集群内其他的某个服务器节点已经成为 Leader,此时 Candidate 会变回 Follower。因为当 Leader 产生后,它会向其他的服务器节点发送心跳消息来确定自己的地位并阻止新的选举。即图中的 “discovers current leader or new term”
  • 如果有多个 Follower 同时成为 Candidate,那么选票可能会被瓜分,以至于没有 Candidate 赢得过半的投票,也就是选举超时后还是没有选出 Leader ,会通过增加当前任期号来开始一轮新的选举,但是这种情况有可能无限重复,即图中的 “times out,new election”
    • 为了防止这种情况发生,Raft 算法使用随机选举超时时间的方法来确保很少发生选票瓜分的情况。也就是每个 Candidate 在开始一次选举的时候重置一个随机的选举超时时间,然后一直等待直到选举超时。该 Candidate 会增加自己的任期号,重新发起竞选请求,此时其他 Candidate 可能还在等待中,那么其他服务节点认为该超时的 Candidate 的任期号最大,所以它会被选为 Leader。

上图中还有一种从 Leader 直接变成 Follower 的情况,这种情况多数出现在 Leader 发生了网络分区的时候。当 Leader 发生网络分区后恢复时,新的 Leader 已经产生,它会接收新 Leader 的心跳请求,发现新的 Leader 的任期号比自己的大,它会自动变成 Follower。而旧的 Leader 如果发送心跳请求给其他服务器节点时,Candidate 和 Follower 都会比对任期号,如果任期号小于自己的任期号,则直接拒绝此次心跳请求。

分布式存储

分布式存储是一种将数据分布在多个物理位置或节点上的存储方法,旨在提高数据的可用性、可靠性和性能。常用的协议有 NWR 和 Gossip 等。

NWR 协议

NWR 协议是分布式系统中常用的一种数据一致性和可用性控制机制,主要用于分布式数据库和存储系统。NWR 分别代表以下三个参数:

  • N:副本数量 (Replication Factor),表示每个数据在系统中存储的副本数量;
  • W:写入副本数量 (Write Quorum),表示一次写操作必须成功写入的副本数量;
  • R:读取副本数量 (Read Quorum),表示一次读操作必须成功读取的副本数量。

写操作:写操作要求至少 W 个副本成功写入后才认为写操作成功。这保证了即使部分副本不可用,数据仍能持久化。

读操作:读操作要求至少 R 个副本成功读取后才认为读操作成功。这确保了即使部分副本不可用,数据仍能被读取。

NWR 模型通过组合 N、W、R 这三个值控制数据的可用性、容错性和一致性:

  • W + R > N 时,例如 N=3W=3R=1,系统满足强一致性要求,但可用性下降;
  • 当 W 和 R 的值较低时,例如 N=3W=1R=1,系统满足高可用性要求,但一致性下降;
  • 当 W 和 R 的值适中,例如 N=3W=2R=2,系统获得一致性和可用性之间相对平衡;

通过调整副本数量、写入副本数量和读取副本数量,可以在一致性和可用性之间找到最佳平衡点。

Gossip 协议

Gossip 是一种用于实现数据一致性的去中心化通信协议,广泛应用于分布式数据库中的节点数据同步。顾名思义,Gossip 协议通过类似“八卦传播”的方式,随机将信息传递给其他节点,从而在一定时间内使系统中的所有节点数据达到一致。这体现了一种去中心化的思想。

Gossip 协议的核心思想是每个节点在固定时间间隔内随机选择其他节点进行信息交换,逐步将自身状态或信息传播到整个系统。具体步骤如下:

  1. 节点选择:每个节点定期随机选择一个或多个其他节点作为通信目标;
  2. 信息交换:选择的节点之间互相交换自身的状态信息或数据更新;
  3. 状态更新:节点根据接收到的信息更新自身的状态,并将新的状态信息在下一个时间间隔内继续传播给其他节点;
  4. 重复过程:上述过程持续进行,直到所有节点都获得了相同的信息或状态。

在这个过程中,消息传播方式主要有两种:

  • 反熵传播:以固定的概率传播全量数据。所有参与节点只有两种状态:病原(Suspective)和感染(Infective)。节点会将所有数据与其他节点分享,以消除节点之间的任何数据不一致。这种方法可以保证完全的一致性,但缺点是会占用大量的网络带宽,因此一般只用于新加入节点的数据初始化。
  • 谣言传播:以固定的概率传播增量数据。所有参与节点有三种状态:病原(Suspective)、感染(Infective)和痊愈(Removed)。消息只包含最新的增量数据,并且在某个时间点后就会被标记为 Removed,并终止传播。因此,这种方法的一致性略差但性能更高,通常用于正常运行期间节点数据的同步。

Gossip 协议的最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:推送模式、拉取模式和推/拉模式。

  • 推送模式 (PUSH):A 节点将数据(key, value, version)及其对应的版本号推送给 B 节点,B 节点再更新 A 节点中比自己新的数据;
  • 拉取模式 (PULL):A 节点仅将数据的版本号(key, version)推送给 B 节点,B 节点将本地比 A 节点新的数据(key, value, version)推送给 A 节点,A 节点再更新自己的数据;
  • 混合模式 (PUSH/PULL):与拉取模式类似,但 A 节点在更新完 B 节点的新数据后,还会将自己的新数据回推给 B 节点。

通过上述分析我们可知,Gossip 协议由于是去中心化的,因此没有单点故障,节点失效对整体影响更小。而且由于传播速度是指数级的,可以很快的实现最终一致性。不过随着系统规模的增大,网络通信的开销也会指数级增加。

分布式事务

事务分为本地事务分布式事务,本地事务指的是单个服务访问单一数据库资源的事务。在单个数据库内,很容易满足事务的强一致性。

强一致性事务有四个特性,简称 ACID:

  • 原子性 (Atomicity): 事务的所有操作要么全部成功执行,要么全部不执行。上述的商家发货就是一个典型的例子。
  • 一致性 (Consistency):事务的执行不能破坏数据库的完整性和一致性。
  • 隔离性 (Isolation):并发的事务需要相互隔离。
    • 有四种隔离级别,分别是 Read Uncommitted (未授权读取)Read Committed (授权读取)Repeatable Read (可重复读取)Serializable (串行化)
  • 持久性 (Durability):一个事务一旦提交,它对数据库中对应的数据状态变更就应该是永久性的。

然而,在分布式架构中,情况就复杂得多了。分布式事务是指由不同执行逻辑单元组成的一次操作,这些执行逻辑单元属于不同的应用服务,并分布在不同的服务器上。分布式事务需要确保这些执行逻辑单元要么全部成功,要么全部失败。

目前业界常用的分布式事务解决方案有 2PC、3PC、TCC 等。

2PC 协议

2PC 协议(The two-phase commit protocol)是分布式事务中协调多个资源的机制之一。它定义了事务管理器(Transaction Manager, TM)和局部资源管理器(Resource Manager, RM)之间的协作方式,将事务的提交分为两个阶段来处理:

2pc
  • prepare 阶段:
    • TM 向所有 RM 发送事务内容,询问是否可以提交事务,并等待所有 RM 答复;
    • RMs 执行事务操作,将操作信息记入事务日志中,但不提交事务;
    • 每个 RM 执行成功后,给 TM 反馈 YES;如执行失败,给 TM 反馈 NO。
  • commit 阶段:
    • 如果所有 RM 均反馈 YES,则 TM 向 RM 发出 commit 指令;
    • 任何一个 RM 反馈 NO,或者任何一个 RM 返回超时,则发出 rollback 指令;
    • RM 根据指令执行 commit 或者 rollback 操作,并释放所有资源。

2PC 的优点就是实现简单,因此目前很多关系型数据库都是采用这种协议,但它的缺点也很明显:

  1. 同步阻塞问题:执行过程中,所有 RM 的事务都是阻塞型的,当有 RM 占用公共资源时,其它访问公共资源的第三方节点将不得不处于阻塞状态,同时各个 RM 在等待 TM 的指令期间也会一直阻塞,如果 TM 宕机了(单点),RM 将会一直阻塞,无法达成一致性。
  2. 单点故障:TM 重要性极高,一旦 TM 发生故障,RMs 会一直阻塞下去。
  3. 数据不一致隐患:出现网络分区、网络故障时可能导致数据不一致。在第二个阶段中,如果 TM 向 RMs 发送 commit 请求的过程中出现了网络故障,可能会导致只有一部分 RM 收到了 commit 指令,另一部分 RM 会因为收不到任何指令而陷入阻塞,于是导致了数据不一致的现象。
  4. 太过保守:2PC 没有设计相应的容错机制,任何一个 RM 出现异常都会导致整个事务中断回滚(这一点 ZooKeeper 的 ZAB 协议就相对开放些)。
  5. 某些情况下的状态不确定问题:如果 TM 在二阶段发出 TM 指令后宕机,而唯一接收到这条消息 RM 也宕机了,这时即便选举出了新 Leader,也无法确定这条事务的状态,因为没有节点可以知道这条事务是否被提交成功。

由于 2PC 存在的这些问题,研究者们在此基础上又提出了 3PC。

3PC 协议

3PC 是 2PC 的改进版,它将二阶段提交协议的 “prepare” 阶段一分为二,形成了 CanCommit,PreCommit,DoCommit 三个阶段。相比于 2PC,3PC 有两个改进的地方:

  1. 引入了超时提交策略。当第三阶段的 RM 等待 TM 的指令超时后会自动提交事务,解决 RM 同步阻塞的问题,同时能在 TM 发生单点故障时,继续达成一致性。
  2. 新增了一个 CanCommit 阶段,缩减了同步阻塞的发生范围。

下面我们详细介绍这三个阶段:

2pc
  • CanCommit:
    • TM 向 RMs 发送 CanCommit 请求,询问是否可以执行事务提交操作,然后开始等待各 RM 的响应;
    • RMs 接到 CanCommit 请求之后,正常情况下,如果可以顺利执行事务,则返回 Yes,并进入预备状态;否则反馈 No。
  • PreCommit:
    • 正常情况:所有 RM 均反馈 YES 时,开始提交事务
      • TM 向 RM 发送 PreCommit 请求,并进入 prepared 阶段;
      • RM 接收到 PreCommit 请求后,会执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中;
      • 如果 RM 成功地执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
    • 异常情况:有任何一个 RM 向 TM 发送了 CanCommit No 响应,或者等待超时之后 TM 都没有接到任何 RM 的响应,则执行事务的中断
      • TM 向所有 RM 发送 abort 请求;
      • RM 收到来自 TM 的 abort 请求之后(或超时之后,仍未收到 PreCommit 请求),执行事务的中断。
  • DoCommit:
    • 正常情况

      • TM 接收到 RM 发送的 ACK 响应后,从 prepare 状态进入到 Commit 状态,并向所有 RM 发送 DoCommit 请求;
      • RM 接收到 DoCommit 请求之后,执行正式的事务提交,并在完成事务提交之后释放所有事务资源;
      • 事务提交完成之后,向 TM 发送 ACK 响应;
      • TM 接收到所有 RM 的 ACK 响应之后,完成事务。
    • 异常情况 1:TM 超时没有收到任何 PreCommit 成功的 ACK,或者存在 RM 返回 PreCommit 执行失败的消息时

      • TM 向所有 RM 发送 abort 请求;
      • RM 接收到 abort 请求之后,利用其在阶段二记录的 Undo 信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源;
      • RM 完成事务回滚之后,向 TM 发送 ACK 消息;
      • TM 接收到 RM 反馈的 ACK 消息之后,执行事务的中断。
    • 异常情况 2:TM 发出了最终指令后,如果某个正常进入三阶段的 RM 直到超时仍然未收到指令,则执行最终提交操作。

      异常情况 2,为什么超时要自动提交,而非回滚?
      这是由概率决定的,当 RM 成功进入了第三阶段,说明它是 CanCommit 的,所以能够 Commit 成功的概率极大。

相比于 2PC,3PC 降低了 TM 单点故障导致阻塞的可能性,但是它仍然无法杜绝数据不一致问题:假如 TM 发送的 abort 命令没有被所有 RM 收到,就会导致一部分 RM 成功回滚,另一部分 RM 超时提交成功,进而导致数据不一致。另外,3PC 的协议也比较保守:3PC 仍然只要有一个 RM 返回错误信息,就中断事务进行回滚。

TCC 协议

TCC 是 Try-Confirm-Cancel 的缩写,也是一种分布式事务解决方案。该方案基于业务逻辑的补偿机制,将分布式事务拆分为多个子事务。每个子事务都包含 try、confirm 和 cancel 三个操作,通过这些操作来实现分布式事务的提交或回滚:

tcc

详细步骤如下:

  • Try 阶段:事务参与者预留全局事务必需的资源,并执行业务逻辑的前半部分,但并不真正执行提交操作。
    • 如果所有参与者的 try 操作都成功,那么事务将进入下一个 confirm 阶段;
    • 如果任何一个参与者的 try 操作失败,事务将立即进入 cancel 阶段。
  • Confirm 阶段:事务参与者确认并执行实际的提交操作,消费之前预留的资源。
    • 如果所有的 confirm 操作都成功,那么事务最终将被提交;
    • 如果任何一个 confirm 操作失败,那么事务将进入 cancel 阶段。
  • Cancel 阶段:如果在 try 或 confirm 阶段出现了失败情况,事务将进入 cancel 阶段。在此阶段,事务参与者执行回滚操作,释放之前预留的资源。

分布式系统设计策略

设计一个分布式系统时,需要考虑以下几个问题:

  • 如何检测节点的存活状态;
  • 如果保障系统高可用;
  • 如何实现容错;
  • 如何实现负载均衡。

接下来,让我们逐一处理这些问题。

心跳检测

探测节点存活状态最常用的手段就是心跳检测。它的工作原理是每个节点定期向其他节点发送心跳信号,表示自己仍然处于活动状态。如果一个节点在一定时间内没有收到其他节点的心跳信号,就可以认为该节点已经失效,并采取相应的容错措施。

心跳检测机制可以通过多种方式实现,包括但不限于:

  • 周期性 PING/PONG:节点之间定期进行 PING/PONG 通信,确认彼此的存活状态;
  • 传输层心跳检测:利用 TCP Keep-Alive 保持心跳,检测对端节点的存活状态;

高可用

在分布式系统中,系统本身的可用性非常重要,高可用是分布式系统架构设计中必须考虑的因素之一,我们需要通过设计来减少系统不能提供服务的时间,提高系统的可用性。

分布式系统由多个服务组成,分布式系统的可用性也可以理解为该分布式系统内所包含的服务的可用性,这些服务的可用性决定了整个分布式系统的可用性。服务的可用性受以下几个比较重要的因素影响:

  • 机器硬件设备问题:比如硬件损坏造成的服务器宕机等情况。
  • 网络问题:网络问题会导致正常的服务节点无法与别的服务节点进行交互,从而失去提供服务的能力。
  • 程序 bug:比如一些代码导致的死锁,调用系统资源后没有及时释放等问题导致服务自身出现异常。
  • 大量流量涌入:一个系统所能承载的最大流量是有限的,它取决于许多原因,比如部署的机器数量、机器的性能、系统的性能等。当流量超过系统的承载能力时,系统会被击垮,比如“双 11”这样的场景,电商公司会提前预演,并且通过增加服务部署的机器实例、做扩容等操作来提升整个系统负载极限。

当某一个服务节点由于上述原因导致服务不可用时,整个分布式系统不一定受到影响,因为在分布式系统中,不会让服务出现单点问题,所以即使一个服务节点出现问题,同层级的其他节点依然能够正常提供服务,如下图所示:

高可用

S3 服务的实例 1 出现故障时,对于 S2 服务存在两种情况:

  • 第一种情况是 S2 的所有实例在进行请求路由和负载均衡选择最终请求去向时,都不应该选择 S3 的实例 1,也就是 S2 所有的实例应该感知到 S3 的实例 1 是不健康的。这种情况可以通过注册中心反向通知 Consumer,或者通过一些心跳保活机制来实现。
  • 第二种情况就是 S2 的实例中已经发往 S3 服务的实例 1 上的请求会出现异常。此时 S2 的实例需要一些策略来处理这些已经异常的请求,这种上游服务节点用于处理请求异常的策略被称为容错策略。

容错性

分布式系统中的服务稳定性会受非常多的因素影响,无法确保系统完全没有故障,所以在设计系统时就会考虑容错策略。对于 RPC 框架而言,容错策略也是需要考虑的一个能力。上述示例中的请求失败也就是一次 RPC 请求失败,RPC 框架可以提供不同的容错策略供用户选择,而不同的容错策略的应用场景也有所不同,下面列举了几种容错策略:

  • 请求失败后仅记录日志:这种级别的服务失败了一般不会影响系统的其他部分,所以下游返回调用服务失败的异常后,仅仅打印相关的异常日志后直接忽略,不会将异常抛给上游。该方式适用于写入审计日志等操作。
  • 请求失败后自动切换:当调用下游服务出现失败的时候,会自动切换到集群中其他服务节点,但重试会导致请求一直占用系统资源,并且带来更长的延迟,所以一般都会设置重试次数。该方式通常用于读操作。
  • 请求失败后抛出异常:当调用下游服务出现失败后,立即抛出异常。该方式适用于幂等操作,比如新增记录等。
  • 请求失败后自动恢复:在调用服务失败后,先返回一个空结果,并通过定时任务记录失败的调用并且发起重传操作。该方式适合执行消息通知等操作。
  • 多方一起调用:该方式会在线程池中运行多个线程来调用多个服务器节点,只要有一个节点成功调用就算调用成功。该方式通常用于实时性要求较高的读操作,但需要浪费更多服务资源。一般会设置最大并行数。

负载均衡

在分布式架构中,负载均衡策略是非常重要的内容。它不仅能在一定程度上保证整个集群的高可用,而且能够提高系统的吞吐量,降低服务响应时间。每台计算机的系统资源都是有限的,能够承载的任务及处理的请求也是有限的。在分布式系统中一般通过扩容来增加系统整体的承载能力,但是当大量并发的请求发生时,如果请求分配不均匀,就会导致部分机器收到了大量请求,而部分机器非常空闲,这种现象轻则导致吞吐率偏低、响应时间过大,不能让资源的使用达到最优,重则导致接收过载的机器宕机、请求失败,出现服务不可用的现象。所以如何让这些请求较为均匀地分摊到集群内各个服务节点上,就是负载均衡策略所需要做的事情。

负载均衡的本质就是通过合理的算法将请求均匀地分摊到各个服务节点上,它需要根据一定的算法从一大批服务节点中选择一个节点来接收和处理请求。而根据这个服务节点的集合信息是否存放在客户端,可以将负载均衡分为服务端负载均衡客户端负载均衡

服务端负载均衡

服务端负载均衡是在服务端维护了服务节点集合信息,此处的服务端并不指服务提供者所在的节点,而是一个统一维护服务节点信息的负载均衡器 (Loadbalancer),如下所示:

服务端负载均衡

在服务消费端与服务提供端的中间有一个负载均衡器,当有请求到时,负载均衡器会通过一定的负载均衡算法从服务节点集合中选择一个合适的节点,通过请求转发器将该客户端的请求转发到对应的服务节点上。从上图可以看出,服务端负载均衡可以让客户端和服务端解耦,保证对业务应用没有侵入性,无论负载均衡器如何变化,都不会影响业务代码。服务端负载均衡方案中除了负载均衡算法,最关键的就是请求的转发,根据网络请求转发发生的层级不同,可以分为二层、三层、四层和七层负载均衡。

  • 二层负载均衡: 集群中不同的服务节点采用相同的 IP 地址,但是机器的 MAC 地址不一样。当负载均衡服务器收到请求之后,通过修改请求报文的目标 MAC 地址的方式将请求转发到目标机器来实现负载均衡。
  • 三层负载均衡:集群中不同的服务节点采用不同的 IP 地址,当负载均衡服务器收到请求之后,选择一个服务节点,通过 IP 地址将请求转发至不同的真实服务器。
  • 四层负载均衡: 传输层包含源 IP 地址、目标 IP 地址,还包含源端口号及目的端口号。四层负载均衡服务器在收到客户端请求后,通过修改请求报文的地址信息(IP 地址 + 端口号)将请求转发到对应的服务节点。
  • 七层负载均衡: 网络请求的转发发生在应用层,应用层有很多不同的协议,比如 HTTP 等,这些应用层协议中包含很多具有特殊意义的字段信息,负载均衡器可以根据这些字段决定转发策略。

这四种负载均衡方案中最常见的就是四层负载均衡和七层负载均衡。四层负载均衡与七层负载均衡最大的不同就是四层负载均衡的本质是转发请求,而七层负载均衡是内容的交换和代理,因为七层负载均衡中需要根据特殊字段来选择最终的服务节点,负载均衡器就必须先反向代理最终的服务器来和客户端建立连接,接收客户端发送的真正的应用层内容的报文后才能选择最终的服务节点。

除了依据请求转发的层级进行分类,还可以根据负载均衡器的实现形态进行分类,即分为硬件负载均衡和软件负载均衡。常见的硬件负载均衡方案有 F5、NetScaler、Radware 等设备。常见的软件负载均衡方案有 NginxLVSHAProxy 等。

虽然服务端负载均衡方案能够起到解耦的作用,但这种方案有两个非常严重的缺点:

  • 负载均衡器是整个系统处理性能的瓶颈,负载均衡器的过载或者出现单点问题都会导致响应缓慢或者服务不可用。
  • 请求必须经过负载均衡器转发或者代理,传输效率有所降低。

在微服务架构中,服务之间的依赖关系较为复杂,在一个系统中存在许多服务和服务实例,如果通过服务端负载均衡方案实现负载均衡,则会导致负载均衡器变得尤为重要,系统也增加了不少复杂度,如下图所示:

服务端负载均衡

随着整个调用链路的增加,负载均衡器的影响越来越大,整个请求链路的传输效率由于负载均衡器不断增加而不断降低,所以服务端负载均衡并不适用于微服务架构的系统。

客户端负载均衡

客户端负载均衡的方案就是把服务列表维护在客户端一侧,由客户端选择某一个服务节点,并且与之通信。

客户端负载均衡无须额外部署负载均衡器,并且也不需要进行请求转发或者代理,客户端可以直接和服务端连接并通信,传输损耗相对较少。没有负载均衡器也就不存在单点问题。但是客户端负载均衡方案并不能像服务端负载均衡方案那样做到对业务应用的无侵入性,并且每一个客户端都与服务节点连接并通信,所以连接数也会相应地增加。

负载均衡算法

无论是服务端负载均衡方案还是客户端负载均衡方案,都离不开负载均衡算法。因为无论如何都需要从服务节点的集合中挑选一个合理的节点来处理请求,而算法也直接决定了是否能将流量均匀地分摊到各个服务节点上,以实现资源的最大化利用,以及保障服务节点可用性的目的。下面我们介绍几种常见的负载均衡算法。

(加权)随机算法

随机算法是最简单的一种算法,服务消费者每次请求选择的服务节点都是随机的。这种算法在使用过程中的随机性太强,非常容易出现集群中某台机器负载过高或者负载过低的情况,导致整个集群的负载不够均衡。而且每台机器的配置和性能很可能不同,所能够承载和处理的请求数量也不一样,所以现实中往往会给每台机器加上权重,将随机算法优化为加权随机算法。

加权随机算法是提供权重能力的随机算法,举个例子:一个服务集群中存在服务节点 A、B、C,它们对应的权重为 7、2、1,权重总和为 10,现在把这些权重值平铺在一维坐标系上,分别出现三个区域,A 区域为 [0,7),B 区域为 [7,9),C 区域为 [9,10),然后产生一个 [10,10) 的随机数,查找该数字落在哪个区间内,该区间所代表的机器就是此次请求选择的服务节点:

1
2
3
4
5
6
7
^
|-------+
|       |--+
|       |  |-+
|   A   | B|C|
+-------|--|-|---->
0       7  9 10

这样做可以保证权重越大的机器被击中的概率就越大,它所承受的请求数量也会越多。加权随机算法相对于普通的随机算法而言,利用权重来减小随机性,让规模和配置不同的机器可以适当调整其权重,使整个集群的负载更加平衡。

轮询算法

轮询算法可以分为三种,分别是完全轮询算法加权轮询算法平滑加权轮询算法

完全轮询算法

完全轮询算法要求消费者每次请求所选择的服务节点都是以轮询的方式决定的。比如服务提供者有三个服务节点,分别是 A、B、C,服务消费者的第一个请求分配给 A 节点,第二个请求分配给 B 节点,第三个请求分配给 C 节点,第四个请求又分配给 A 服务器。这种完全轮询的算法只适合每台机器性能相近的情况,但是所有机器性能相近是一种非常理想的情况,更多的情况是每台机器的性能都有所差异,这个时候性能差的机器被分到等额的请求,就会出现承受不了并且宕机的情况。

加权轮询算法

加权轮询算法会增加高权重节点的轮询次数。举个例子,服务节点 A、B、C 的权重比为 7:2:1,那么在 10 次请求中,服务节点 A 会收到其中的 7 次请求,服务节点 B 会收到其中的 2 次请求,服务节点 C 则收到其中的 1 次请求。也就是说,每台机器能够收到的请求归结于它的权重。加权轮询的顺序则是 {A A A A A A A B B C}

平滑加权轮询算法

加权轮询算法会导致流量先倾倒式地导向 A 服务节点,而其他服务节点却短暂处于空闲状态,降低了资源的利用率。当流量高峰到达时,也容易直接把高权重的服务节点 A 击垮。所以平滑加权轮询算法在加权轮询算法的基础上新增了平滑处理。

平滑加权轮询算法来源于 Nginx,它通过加权和降权来保证每次请求都被均匀分发。在平滑加权轮询算法中,每个服务节点都有两个权重值,分别是 originalWeightcurrentWeightoriginalWeight 为该节点的原始权重,currentWeight 的初始值为 originalWeight 的大小。下面是平滑加权轮询算法的推演过程:

  1. 请求到来,每个节点都计算一遍 currentWeight += originalWeight
  2. 比较每个节点计算过的 currentWeight 值,并从中选择最大值的节点作为最终节点。
  3. 步骤 2 中选出的最终节点的 currentWeight -= 所有节点的 originalWeight 和

依照上面的逻辑,举例推演执行过程:假设 A、B、C 三个节点的权重为 7、2、1,下表为计算过程:

执行序号currentWeight
(步骤 1 执行后的结果)
所选服务节点
(执行步骤 2 选择的结果)
选择后的 currentWeight
(执行步骤 3 后的结果)
1{14,4,2}A{4,4,2}
2{11,6,3}A{1,6,3}
3{8,8,4}A{-2,8,4}
4{5,10,5}B{5,0,5}
5{12,2,6}A{2,2,6}
6{9,4,7}A{-1,4,7}
7{6,6,8}C{6,6,-2}
8{13,8,-1}A{3,8,-1}
9{10,10,0}A{0,10,0}
10{7,12,1}B{7,2,1}

由上表可知,最后计算完成后 currentweight 又回到了原来的 7、2、1。这样做得到的 10 次请求的轮询顺序为 {A A A B A A C A A B},不再是 {A A A A A A A B B C} 的顺序。在保证按权重分配的基础上,提高了轮询的平滑性。

最少活跃数算法

最少活跃数算法是基于最小连接数算法衍生而来的,某个服务节点中活跃的调用数越小,表明该服务节点处理请求的效率越高,也就表明单位时间内能够处理的请求越多。此时服务消费端的请求应该选择该服务节点作为目标节点。

该算法实现的思路是每个服务节点都有一个活跃数 active 来记录该服务的活跃值,每收到一个请求,该 active 就会加 1,每完成一个请求,该 active 就会减 1。在服务运行一段时间后,性能相对较高的服务节点处理请求的速度更快,因此活跃数下降得也越快,此时服务消费者只需要选择 active 最小的那个服务节点作为目标节点,而这个 active 最小的服务节点就能够优先获取新的服务请求。

最少活跃数算法也可以像随机算法和轮询算法一样引入权重,演变成最少活跃数加权算法,在比较活跃数时,如果最小活跃数的服务节点存在多个,则可以利用权重法来进行选择,如果服务节点的权重也一样,则从中随机选择一个服务节点。

一致性 Hash 负载均衡算法

一致性 Hash 负载均衡算法是一致性 Hash 算法在负载均衡上的应用。它可以保证相同的请求尽可能落到同一个服务器上,下面我们分析该算法的原理。

  1. 首先利用服务节点的 IP 地址或者其他信息生成该服务节点的唯一 Hash 值,并将这个 Hash 值投射到 [0,2^32] 的圆环上,如图:

    一致性 Hash 负载均衡算法
  2. 当请求到达时,会指定某个值来计算该请求的 Hash 值,这个值可能是请求方 IP 地址、用户 ID 或者其他信息。当请求方的 Hash 值计算完成后,就会顺时针寻找最接近该请求方 Hash 值的服务节点。例如,请求 a 计算后的 Hash 值落在服务节点 A 和 B 的 Hash 值之间,请求 b 计算后的 Hash 值落在服务节点 B 和 C 的 Hash 值之间,请求 c 计算后的 Hash 值落在服务节点 C 和 A 的 Hash 值之间,此时请求 a 将选择 B 节点,请求 b 将选择 C 节点,请求 c 将选择 A 节点,如图:

    一致性 Hash 负载均衡算法
  3. 当有服务下线时,请求归属的节点也会按照顺时针方向往后移动,比如 B 节点“挂了”之后请求 a 会选择节点 C,如图:

    一致性 Hash 负载均衡算法

从上图可以看到,Hash 一致性算法并不能够保证 Hash 算法的平衡性,因为如果一直往顺时针的方向移动,就会导致后续的服务节点接收的请求越来越多,出现数据倾斜现象,最终导致整个集群都被击垮。要解决这个问题就需要引入虚拟节点。虚拟节点是实际节点在 Hash 空间的复制品,即对每一个服务节点计算多个 Hash 值,每个计算结果的位置都会放置一个此服务节点,如下图所示:

一致性 Hash 负载均衡算法

利用虚拟节点,能够在服务节点下线时,保持集群内剩余节点的负载相对均衡。举个例子,原先 [0,2^32] 的圆环被分成了三段,分别是 A~B、B~C 和 C~A,当服务节点 B 下线后,服务节点 C 将接收单位时间内 2/3 的请求,而 A 还是继续接收 C~A 范围内的请求,这部分请求仅仅占用了所有请求的 1/3。引入虚拟节点后,[0,2^32] 的圆环被分成了 3 * n 份,当服务节点 B 下线后,原先服务节点 B 所承受的压力会被服务节点 A 和 C 分担,避免了完全向服务节点 C 倾斜的现象。

分布式服务治理

服务协调

分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制,让它们有序的去访问某种临界资源。常用的实现手段有两大类:

  • 任务分区:例如通过 MapReduce 技术,任务被分配给不同的处理节点进行处理,每个节点只负责自己被分配的任务,最终由选定的节点进行汇总。然而,这种方法仅适用于定时任务等特定场景;
  • 分布式锁:允许竞争,但通过分布式锁来实现同步。常用的实现手段有两种:
    • 基于缓存的分布式锁,例如 Redis 集群等;
    • 基于文件的分布式锁,例如 Zookeper 临时顺序节点或 MySQL 表锁等。

服务熔断

在分布式系统中,当下游服务因访问压力过大而响应变慢或失败时,为了保护系统整体的可用性,上游服务可以暂时中断对下游服务的调用。这种牺牲局部、保全整体的措施称为熔断

如果流量超过系统的承受能力而不进行熔断,就很容易导致链路的连锁崩溃,即引发雪崩。

削峰填谷

当出现大批量的流量突增时,如果在一瞬间让服务处理如此多的流量,可能会导致系统负载过高,最终影响系统的稳定性。

然而,在接下来的一段时间内,可能并没有请求,或者请求量非常低。在这种情况下,若采用熔断、限流等策略将突增的请求拒绝,可能不够优雅。因此,希望能够将这部分突增的请求量均摊到后面的一段时间内处理,从而达到削峰填谷的效果。

链路追踪

分布式系统中的服务实例数量众多,微服务架构使请求链路复杂化,导致一次请求可能经过大量服务实例,增加了观测难度。链路追踪通过记录请求路径上各服务和实例的数据,实现对请求状况的全面观测。链路追踪广泛应用于 RPC 请求、SQL 执行、消息处理等领域。为了减轻存储压力,系统对正常请求采用采样机制,但对异常请求进行全量采样,确保异常链路可查询。

实现链路追踪的关键在于采集链路信息并将其上报到系统中。链路信息可以分为两部分:

  • 一部分是请求经过的节点形成的调用链信息:包括请求唯一标识、节点的链路标识、上一跳节点的链路标识等。
  • 另一部分是需要在调用链路上观测的数据,大致分为两类:
    • 与请求的整个生命周有关的数据:包括调用链路上经过的节点信息、请求总时长、请求包大小、每个节点处理请求的耗时等。
    • 不同节点的附加信息:包括异常节点的异常码或消息消费节点的 topic 信息等,这些数据有助于用户快速定位和排查问题。

在业界,链路追踪的解决方案和产品已经非常丰富,业界最早出现的产品是 eBay 的 CAI (Centralized Application Logging),它是在 2002 年 eBay 的研发团队为了解决分布式系统的复杂性,增加对分布式系统的可观测性而研发的调用链监控产品。2010 年,Google 发表了关于分布式调用链追踪基础设施 Dapper 的论文,Dapper 经过 Google 内部的大规模实践后问世,Dapper 也对业界链路追踪产品的发展产生了巨大而深远的影响。Dapper 问世后,大量的链路追踪的产品不断涌现,比如美团点评的 CAT (Centralized Application Tracking)、Twitter 的 Open Zipkin、Naver 的 Pinpoint、京东的 Hydra、阿里巴巴的 Eagleye。除此之外,开源社区也涌现了大量优秀的链路追踪的产品,比如 Uber 的 Jaeger、Google 的 OpenCensus 等。开源社区也在孵化统一的分布式链路的概念和数据标准,目的是提供统一的标准,用于适配不同的厂商和平台,比如 OpenTracing

至此,与分布式系统理论有关的核心内容就介绍完了。

0%