分布式架构原理解析

1. 分布式术语

1.1. 异常

服务器宕机

内存错误、服务器停电等都会致使服务器宕机,此时节点没法正常工做,称为不可用。程序员

服务器宕机会致使节点失去全部内存信息,所以须要将内存信息保存到持久化介质上。算法

网络异常

有一种特殊的网络异常称为——网络分区 ,即集群的全部节点被划分为多个区域,每一个区域内部能够通讯,可是区域之间没法通讯。sql

磁盘故障

磁盘故障是一种发生几率很高的异常。数据库

使用冗余机制,将数据存储到多台服务器。缓存

1.2. 超时

在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态。服务器

能够将服务器的操做设计为具备 幂等性 ,即执行屡次的结果与执行一次的结果相同。若是使用这种方式,当出现超时的时候,能够不断地从新请求直到成功。网络

1.3. 衡量指标

性能

常见的性能指标有:吞吐量、响应时间。架构

其中,吞吐量指系统在某一段时间能够处理的请求总数,一般为每秒的读操做数或者写操做数;响应时间指从某个请求发出到接收到返回结果消耗的时间。并发

这两个指标每每是矛盾的,追求高吞吐的系统,每每很难作到低响应时间,解释以下:负载均衡

  • 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms,那么吞吐量为 100 req/s,所以高吞吐也就意味着低响应时间。
  • 可是在并发的系统中,因为一个请求在调用 I/O 资源的时候,须要进行等待。服务器端通常使用的是异步等待方式,即等待的请求被阻塞以后不须要一直占用 CPU 资源。这种方式能大大提升 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms,若是在并发的系统中,那么吞吐量将大于 100 req/s。所以为了追求高吞吐量,一般会提升并发程度。可是并发程度的增长,会致使请求的平均响应时间也增长,由于请求不能立刻被处理,须要和其它请求一块儿进行并发处理,响应时间天然就会增高。

可用性

可用性指系统在面对各类异常时能够提供正常服务的能力。能够用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。

一致性

能够从两个角度理解一致性:从客户端的角度,读写操做是否知足某种特性;从服务器的角度,多个数据副本之间是否一致。

可扩展性

指系统经过扩展集群服务器规模来提升性能的能力。理想的分布式系统须要实现“线性可扩展”,即随着集群规模的增长,系统的总体性能也会线性增长。

2. 数据分布

分布式存储系统的数据分布在多个节点中,经常使用的数据分布方式有哈希分布和顺序分布。

数据库的水平切分(Sharding)也是一种分布式存储方法,下面的数据分布方法一样适用于 Sharding。

2.1. 哈希分布

哈希分布就是将数据计算哈希值以后,按照哈希值分配到不一样的节点上。例若有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。

传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎全部的数据都须要从新分布,将致使大量的数据迁移。

一致性哈希

Distributed Hash Table(DHT):对于哈希空间 [0, 2n-1],将该哈希空间当作一个哈希环,将每一个节点都配置到哈希环上。每一个数据对象经过哈希取模获得哈希值以后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。

一致性哈希的优势是在增长或者删除节点时只会影响到哈希环中相邻的节点,例以下图中新增节点 X,只须要将数据对象 C 从新存放到节点 X 上便可,对于节点 A、B、D 都没有影响。

2.2. 顺序分布

哈希分布式破坏了数据的有序性,顺序分布则不会。

顺序分布的数据划分为多个连续的部分,按数据的 ID 或者时间分布到不一样节点上。例以下图中,User 表的 ID 范围为 1 ~ 7000,使用顺序分布能够将其划分红多个子表,对应的主键范围为 1 ~ 1000,1001 ~ 2000,...,6001 ~ 7000。

顺序分布的优势是能够充分利用每一个节点的空间,而哈希分布很难控制一个节点存储多少数据。

可是顺序分布须要使用一个映射表来存储数据到节点的映射,这个映射表一般使用单独的节点来存储。当数据量很是大时,映射表也随着变大,那么一个节点就可能没法存放下整个映射表。而且单个节点维护着整个映射表的开销很大,查找速度也会变慢。为了解决以上问题,引入了一个中间层,也就是 Meta 表,从而分担映射表的维护工做。

2.3. 负载均衡

衡量负载的因素不少,如 CPU、内存、磁盘等资源使用状况、读写请求数等。

分布式系统存储应当可以自动负载均衡,当某个节点的负载较高,将它的部分数据迁移到其它节点。

每一个集群都有一个总控节点,其它节点为工做节点,由总控节点根据全局负载信息进行总体调度,工做节点定时发送心跳包(Heartbeat)将节点负载相关的信息发送给总控节点。

一个新上线的工做节点,因为其负载较低,若是不加控制,总控节点会将大量数据同时迁移到该节点上,形成该节点一段时间内没法工做。所以负载均衡操做须要平滑进行,新加入的节点须要较长的一段时间来达到比较均衡的状态。

3. 分布式理论

3.1. CAP

分布式系统不可能同时知足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时知足其中两项。

一致性

一致性指的是多个数据副本是否能保持一致的特性。

在一致性的条件下,系统在执行数据更新操做以后可以从一致性状态转移到另外一个一致性状态。

对系统的一个数据更新成功以后,若是全部用户都可以读取到最新的值,该系统就被认为具备强一致性。

可用性

可用性指分布式系统在面对各类异常时能够提供正常服务的能力,能够用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。

在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每个操做请求老是可以在有限的时间内返回结果。

分区容忍性

网络分区指分布式系统中的节点被划分为多个区域,每一个区域内部能够通讯,可是区域之间没法通讯。

在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然须要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。

权衡

在分布式系统中,分区容忍性必不可少,由于须要老是假设网络是不可靠的。所以,CAP 理论实际在是要在可用性和一致性之间作权衡。

可用性和一致性每每是冲突的,很难都使它们同时知足。在多个节点之间进行数据同步时,

  • 为了保证一致性(CP),就须要让全部节点下线成为不可用的状态,等待同步完成;
  • 为了保证可用性(AP),在同步过程当中容许读取全部节点的数据,可是数据可能不一致。

3.2. BASE

BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。

BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的核心思想是:即便没法作到强一致性,但每一个应用均可以根据自身业务特色,采用适当的方式来使系统达到最终一致性。

基本可用

指分布式系统在出现故障的时候,保证核心可用,容许损失部分可用性。

例如,电商在作促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。

软状态

指容许系统中的数据存在中间状态,并认为该中间状态不会影响系统总体可用性,即容许系统不一样节点的数据副本之间进行同步的过程存在延时。

最终一致性

最终一致性强调的是系统中全部的数据副本,在通过一段时间的同步后,最终能达到一致的状态。

ACID 要求强一致性,一般运用在传统的数据库系统上。而 BASE 要求最终一致性,经过牺牲强一致性来达到可用性,一般运用在大型分布式系统中。

在实际的分布式场景中,不一样业务单元和组件对一致性的要求是不一样的,所以 ACID 和 BASE 每每会结合在一块儿使用。

4. 分布式事务问题

4.1. 两阶段提交(2PC)

两阶段提交(Two-phase Commit,2PC)

主要用于实现分布式事务,分布式事务指的是事务操做跨越多个节点,而且要求知足事务的 ACID 特性。

经过引入协调者(Coordinator)来调度参与者的行为,并最终决定这些参与者是否要真正执行事务。

运行过程

准备阶段

协调者询问参与者事务是否执行成功,参与者发回事务执行结果。

提交阶段

若是事务在每一个参与者上都执行成功,事务协调者发送通知让参与者提交事务;不然,协调者发送通知让参与者回滚事务。

须要注意的是,在准备阶段,参与者执行了事务,可是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

问题

同步阻塞

全部事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,没法进行其它操做。

单点问题

协调者在 2PC 中起到很是大的做用,发生故障将会形成很大影响,特别是在阶段二发生故障,全部参与者会一直等待状态,没法完成其它操做。

数据不一致

在阶段二,若是协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。

太过保守

任意一个节点失败就会致使整个事务失败,没有完善的容错机制。

2PC 优缺点

优势:尽可能保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能 100%保证强一致) 缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

4.2. 补偿事务(TCC)

补偿事务(TCC)其核心思想是:针对每一个操做,都要注册一个与其对应的确认和补偿(撤销)操做。它分为三个阶段:

  1. Try 阶段主要是对业务系统作检测及资源预留。
  2. Confirm 阶段主要是对业务系统作确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 必定成功。
  3. Cancel 阶段主要是在业务执行错误,须要回滚的状态下执行的业务取消,预留资源释放。

举个例子,假设 Bob 要向 Smith 转帐,思路大概是:

  1. 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
  2. 在 Confirm 阶段,执行远程调用的转帐的操做,转帐成功进行解冻。
  3. 若是第 2 步执行成功,那么转帐成功,若是第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

TCC 优缺点

  • 优势:跟 2PC 比起来,实现以及流程相对简单了一些,但数据的一致性比 2PC 也要差一些。
  • 缺点:缺点仍是比较明显的,在 2,3 步中都有可能失败。TCC 属于应用层的一种补偿方式,因此须要程序员在实现的时候多写不少补偿的代码,在一些场景中,一些业务流程可能用 TCC 不太好定义及处理。

4.3. 本地消息表(异步确保)

本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操做知足事务特性。

  1. 在分布式事务操做的一方完成写业务数据的操做以后向本地消息表发送一个消息,本地事务能保证这个消息必定会被写入本地消息表中。
  2. 以后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,若是转发成功则将消息从本地消息表中删除,不然继续从新转发。
  3. 在分布式事务操做的另外一方从消息队列中读取一个消息,并执行消息中的操做。

这种方案遵循 BASE 理论,采用的是最终一致性。

本地消息表利用了本地事务来实现分布式事务,而且使用了消息队列来保证最终一致性。

本地消息表优缺点

  • 优势:一种很是经典的实现,避免了分布式事务,实现了最终一致性。
  • 缺点:消息表会耦合到业务系统中,若是没有封装好的解决方案,会有不少杂活须要处理。

4.4. MQ 事务消息

有一些第三方的 MQ 是支持事务消息的,好比 RocketMQ,他们支持事务消息的方式也是相似于采用的二阶段提交。可是市面上一些主流的 MQ 都是不支持事务消息的,好比 RabbitMQ 和 Kafka 都不支持。

以阿里的 RocketMQ 中间件为例,其思路大体为:

  1. Prepared 消息,会拿到消息的地址。
  2. 执行本地事务。
  3. 经过第一阶段拿到的地址去访问消息,并修改状态。

也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。若是确认消息发送失败了 RocketMQ 会按期扫描消息集群中的事务消息,这时候发现了 Prepared 消息,它会向消息发送者确认,因此生产方须要实现一个 check 接口,RocketMQ 会根据发送端设置的策略来决定是回滚仍是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

MQ 事务消息优缺点

  • 优势:实现了最终一致性,不须要依赖本地数据库事务。
  • 缺点:实现难度大,主流 MQ 不支持。

5. 共识性问题

5.1. Paxos

用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出惟一一个值。

主要有三类节点:

  • 提议者(Proposer):提议一个值;
  • 接受者(Acceptor):对每一个提议进行投票;
  • 告知者(Learner):被告知投票的结果,不参与投票过程。

算法须要知足 safety 和 liveness 两方面的约束要求(实际上这两个基础属性是大部分分布式算法都该考虑的):

  • safety:保证决议结果是对的,无歧义的,不会出现错误状况。

    • 决议(value)只有在被 proposers 提出的 proposal 才能被最终批准;
    • 在一次执行实例中,只批准(chosen)一个最终决议,意味着多数接受(accept)的结果能成为决议;
  • liveness:保证决议过程能在有限时间内完成。

    • 决议总会产生,而且 learners 能得到被批准(chosen)的决议。

基本过程包括 proposer 提出提案,先争取大多数 acceptor 的支持,超过一半支持时,则发送结案结果给全部人进行确认。一个潜在的问题是 proposer 在此过程当中出现故障,能够经过超时机制来解决。极为凑巧的状况下,每次新的一轮提案的 proposer 都刚好故障,系统则永远没法达成一致(几率很小)。

Paxos 能保证在超过 $1/2$ 的正常节点存在时,系统能达成共识。

单个提案者+多接收者

若是系统中限定只有某个特定节点是提案者,那么一致性确定能达成(只有一个方案,要么达成,要么失败)。提案者只要收到了来自多数接收者的投票,便可认为经过,由于系统中不存在其余的提案。

但一旦提案者故障,则系统没法工做。

多个提案者+单个接收者

限定某个节点做为接收者。这种状况下,共识也很容易达成,接收者收到多个提案,选第一个提案做为决议,拒绝掉后续的提案便可。

缺陷也是容易发生单点故障,包括接收者故障或首个提案者节点故障。

以上两种情形其实相似主从模式,虽然不那么可靠,但由于原理简单而被普遍采用。

当提案者和接收者都推广到多个的情形,会出现一些挑战。

多个提案者+多个接收者

既然限定单提案者或单接收者都会出现故障,那么就得容许出现多个提案者和多个接收者。问题一会儿变得复杂了。

一种状况是同一时间片断(如一个提案周期)内只有一个提案者,这时能够退化到单提案者的情形。须要设计一种机制来保障提案者的正确产生,例如按照时间、序列、或者你们猜拳(出一个数字来比较)之类。考虑到分布式系统要处理的工做量很大,这个过程要尽可能高效,知足这一条件的机制很是难设计。

另外一种状况是容许同一时间片断内能够出现多个提案者。那同一个节点可能收到多份提案,怎么对他们进行区分呢?这个时候采用只接受第一个提案而拒绝后续提案的方法也不适用。很天然的,提案须要带上不一样的序号。节点须要根据提案序号来判断接受哪一个。好比接受其中序号较大(每每意味着是接受新提出的,由于旧提案者故障几率更大)的提案。

如何为提案分配序号呢?一种可能方案是每一个节点的提案数字区间彼此隔离开,互相不冲突。为了知足递增的需求能够配合用时间戳做为前缀字段。

此外,提案者即使收到了多数接收者的投票,也不敢说就必定经过。由于在此过程当中系统可能还有其它的提案。

5.2. Raft

Raft 算法是 Paxos 算法的一种简化实现。

包括三种角色:leader、candidate 和 follower,其基本过程为:

  • Leader 选举 - 每一个 candidate 随机通过必定时间都会提出选举方案,最近阶段中得票最多者被选为 leader;
  • 同步 log - leader 会找到系统中 log 最新的记录,并强制全部的 follower 来刷新到这个记录;

注:此处 log 并不是是指日志消息,而是各类事件的发生记录。

单个 Candidate 的竞选

有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每一个 Follower 都设置了一个随机的竞选超时时间,通常为 150ms~300ms,若是在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。

  • 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间以后,没收到 Leader 发来的心跳包,所以进入竞选阶段。

  • 此时 A 发送投票请求给其它全部节点。

  • 其它节点会对请求进行回复,若是超过一半的节点回复了,那么该 Candidate 就会变成 Leader。

  • 以后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会从新开始计时。

多个 Candidate 竞选

  • 若是有多个 Follower 成为 Candidate,而且所得到票数相同,那么就须要从新开始投票,例以下图中 Candidate B 和 Candidate D 都得到两票,所以须要从新开始投票。

  • 当从新开始投票时,因为每一个节点设置的随机竞选超时时间不一样,所以能下一次再次出现多个 Candidate 并得到一样票数的几率很低。

同步日志

  • 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。

  • Leader 会把修改复制到全部 Follower。

  • Leader 会等待大多数的 Follower 也进行了修改,而后才将修改提交。

  • 此时 Leader 会通知的全部 Follower 让它们也提交修改,此时全部节点的值达成一致。

6. 分布式缓存问题

6.1. 缓存雪崩

缓存雪崩是指:在高并发场景下,因为原有缓存失效,新缓存未到期间(例如:咱们设置缓存时采用了相同的过时时间,在同一时刻出现大面积的缓存过时),全部本来应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存形成巨大压力,严重的会形成数据库宕机。从而造成一系列连锁反应,形成整个系统崩溃。

解决方案:

  • 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
  • 还有一个简单的方案,就是将缓存失效时间分散开,不要全部缓存时间长度都设置成 5 分钟或者 10 分钟;好比咱们能够在原有的失效时间基础上增长一个随机值,好比 1-5 分钟随机,这样每个缓存的过时时间的重复率就会下降,就很难引起集体失效的事件。

缓存失效时产生的雪崩效应,将全部请求所有放在数据库上,这样很容易就达到数据库的瓶颈,致使服务没法正常提供。尽可能避免这种场景的发生。

6.2. 缓存穿透

缓存穿透是指:用户查询的数据,在数据库没有,天然在缓存中也不会有。这样就致使用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,而后返回空(至关于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是常常提的缓存命中率问题。

当在流量较大时,出现这样的状况,一直请求 DB,很容易致使服务挂掉。

解决方案:

  1. 在封装的缓存 SET 和 GET 部分增长个步骤,若是查询一个 KEY 不存在,就以这个 KEY 为前缀设定一个标识 KEY;之后再查询该 KEY 的时候,先查询标识 KEY,若是标识 KEY 存在,就返回一个协定好的非 false 或者 NULL 值,而后 APP 作相应的处理,这样缓存层就不会被穿透。固然这个验证 KEY 的失效时间不能太长。
  2. 若是一个查询返回的数据为空(无论是数据不存在,仍是系统故障),咱们仍然把这个空结果进行缓存,但它的过时时间会很短,通常只有几分钟。
  3. 采用布隆过滤器,将全部可能存在的数据哈希到一个足够大的 bitmap 中,一个必定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

6.3. 缓存预热

缓存预热这个应该是一个比较常见的概念,相信不少小伙伴都应该能够很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就能够避免在用户请求的时候,先查询数据库,而后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案:

  1. 直接写个缓存刷新页面,上线时手工操做下;
  2. 数据量不大,能够在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

6.4. 缓存更新

除了缓存服务器自带的缓存失效策略以外(Redis 默认的有 6 中策略可供选择),咱们还能够根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  1. 定时去清理过时的缓存;
  2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过时,过时的话就去底层系统获得新数据并更新缓存。

二者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪一种方案,你们能够根据本身的应用场景来权衡。

6.5. 缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然须要保证服务仍是可用的,即便是有损服务。系统能够根据一些关键数据进行自动降级,也能够配置开关实现人工降级。

降级的最终目的是保证核心服务可用,即便是有损的。并且有些服务是没法降级的(如加入购物车、结算)。


本文的重点是你有没有收获与成长,其他的都不重要,但愿读者们能谨记这一点。同时我通过多年的收藏目前也算收集到了一套完整的学习资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、Jvm性能调优、Spring,MyBatis,Nginx源码分析,Redis,ActiveMQ、、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多个知识点高级进阶干货,但愿对想成为架构师的朋友有必定的参考和帮助

须要更详细架构师技能思惟导图和如下资料的能够加一下技术交流分享群:“708 701 457”免费获取




相关文章
相关标签/搜索