当前位置:   article > 正文

zookeeper全面总结_zookeeper有leader和worker组成

zookeeper有leader和worker组成

1. Zookeeper简介

1.1 Zookeeper是什么?

Zookeeper 是⼀个分布式协调服务的开源框架。 主要⽤用来解决分布式集群中应⽤用系统的一致性问题, 例例如怎样避免同时操作同一数据造成脏读的问题。分布式系统中数据存在一致性的问题!!

  • ZooKeeper 本质上是⼀个分布式的⼩文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理理。

  • ZooKeeper 提供给客户端监控存储在zk内部数据的功能,从⽽可以达到基于数据的集群管理。 诸如: 统一命名服务(dubbo)、分布式配置管理理(solr的配置集中管理)、分布式消息队列 (sub/pub)、分布式锁、分布式协调等功能。

1.2 zookeeper的架构组成

Leader:具有选举权利和被选举的权利,节点个数必须是基数个

  • Zookeeper 集群⼯作的核⼼角色

  • 集群内部各个服务器器的调度者。

  • 事务请求(写操作) 的唯一调度和处理者,保证集群事务处理的顺序性;对于 create, setData, delete 等有写操作的请求,则需要统一转发给leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务。

Follower:具有选举权利和被选举的权利,节点个数必须是基数个

  • 处理理客户端非事务(读操作) 请求,

  • 转发事务请求给 Leader;

  • 参与集群 Leader 选举投票 2n-1台可以做集群投票。 此外,针对访问量量⽐较⼤的 zookeeper 集群, 还可新增观察者角⾊。

Observer

  • 观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于⾮事务请求可以进⾏独立处理,对于事务请求,则会转发给 Leader服务器进行处理。
  • 不会参与任何形式的投票只提供非事务服务,通常用于在不不影响集群事务处理能力的前提下提升集群的非事务处理能力。增加了集群增加并发的读请求
  • 只是根leader同步数据,能够减轻zookeeper的服务压力(没有选举权和被选举权),到底有多少个没有关系,但是不能太多,如果太多,zookeeper同步数据的压力就增大

ZK也是Master/slave架构,但是与之前不同的是zk集群中的Leader不是指定而来,⽽是通过选举产生。

1.3 Zookeeper 特点

  1. Zookeeper:⼀个领导者(leader:⽼大),多个跟随者(follower:小弟)组成的集群。

  2. Leader负责进行投票的发起和决议,更新系统状态(内部原理)

  3. Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票

  4. 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。

  5. 全局数据一致:每个server保存一份相同的数据副本,Client无论连接到哪个server,数据都是一 致的。

  6. 更新请求顺序进行(内部原理)

  7. 数据更新原子性,⼀次数据更新要么成功,要么失败。

ZooKeeper作为一个集群提供数据一致的协调服务,自然,最好的方式就是在整个集群中的各服务节点进行数据的复制和同步。通俗的讲,就是 zookeeper 以一个集群的方式对外提供协调服务,集群内部的所有节点都保存了一份完整的数据。其中一个主节点用来做集群管理提供写数据服务,其他的从节点用来同步数据,提供读数据服务。这些从节点必须保持和主节点的数据状态一致。

数据复制的好处:

​ 1、容错:一个节点出错,不至于让整个集群无法提供服务

​ 2、扩展性:通过增加服务器节点能提高 ZooKeeper 系统的负载能力,把负载分布到多个节点上

​ 3、高性能:客户端可访问本地 ZooKeeper 节点或者访问就近的节点,依次提高用户的访问速度

特点:

  • 最终一致性

    client 不论连接到哪个 Server,展示给它都是同一个数据视图,这是 ZooKeeper 最重要的性能。

  • 可靠性

    具有简单、健壮、良好的性能,如果消息 message 被到一台服务器接受,那么它将被所有的服务器接受。

  • 实时性

    ZooKeeper 保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网 络延时等原因,ZooKeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据 之前调用 sync() 接口。

  • 等待无关(wait-free)

    慢的或者失效的 client 不得干预快速的 client 的请求,使得每个 client 都能有效的等待。

  • 原子性

    更新只能成功或者失败,没有中间状态。

  • 顺序性

    包括全局有序和偏序两种:

    ​ 1、全局有序:如果在一台服务器上消息 a 在消息 b 前发布,则在所有 Server 上消息 a 都将在消息 b 前被发布;

    ​ 2、偏序:指如果一个消息 b 在消息 a 后被同一个发送者发布,a 必将排在 b 前面。

2. Zookeeper数据结构与监听机制

ZooKeeper数据模型Znode

image-20210902093926426

在 Zookeeper 中,每一个数据节点都是一个 ZNode,上图根目录下有两个节点,分别是:app1 和 app2,其中 app1 下⾯又有三个子节点,所有ZNode按层次化进行组织,形成这么一颗树,ZNode的节点路径标识⽅式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示,开 发⼈员可以向这个节点写入数据,也可以在这个节点下⾯创建⼦节点。

2.1 ZNode 的类型

Zookeeper 节点类型可以分为三大类:

  • 持久性节点(Persistent)

  • 临时性节点(Ephemeral)

  • 顺序性节点(Sequential)

在开发中在创建节点的时候通过组合可以生成以下四种节点类型:持久节点、持久顺序节点、临时节点、临时顺序节点。不不同类型的节点则会有不同的⽣命周期。

  • **持久节点:**是Zookeeper中最常⻅见的一种节点类型,所谓持久节点,就是指节点被创建后会一直存在服务器,直到删除操作主动清除

  • **持久顺序节点:**就是有顺序的持久节点,节点特性和持久节点是⼀样的,只是额外特性表现在顺序上。 顺序特性实质是在创建节点的时候,会在节点名后⾯加上⼀个数字后缀,来表示其顺序。

  • **临时节点:**就是会被⾃动清理掉的节点,它的生命周期和客户端会话绑在⼀一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建子节点。

  • **临时顺序节点:**就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。

事务ID

⾸先,先了解,事务是对物理和抽象的应用状态上的操作集合。往在现在的概念中,狭义上的事务通常指的是数据库事务,⼀般包含了一系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特 性,即原子性(Atomic)、⼀致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

而在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新等操作。对于每⼀个事务请求,ZooKeeper都 会为其分配⼀个全局唯⼀的事务ID,⽤用 ZXID 来表示,通常是⼀个 64 位的数字。每一个 ZXID 对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序

zk中的事务指的是对zk服务器状态改变的操作(create,update data,更新字节点);zk对这些事务操作都会编号,这个编号是⾃增长的被称为ZXID。

2.2 ZNode 的状态信息

 
#使⽤用bin/zkCli.sh 连接到zk集群
[zk: localhost:2181(CONNECTED) 2] get /zookeeper
cZxid = 0x0															# cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime = Wed Dec 31 19:00:00 EST 1969		# ctime 就是 Create Time,表示节点创建时间。
mZxid = 0x0															#	mZxid 就是 Modified ZXID,表示节点最后⼀一次被修改时的事务ID。
mtime = Wed Dec 31 19:00:00 EST 1969		#	就是 Modified Time,表示节点最后⼀一次被修改的时间。
pZxid = 0x0		# pZxid 表示该节点的⼦节点列表最后一次被修改时的事务ID。只有子节点列表变更才会更新pZxid,⼦节点内容变更不会更新。
cversion = -1					# cversion 表示⼦节点的版本号。
dataVersion = 0				# dataVersion 表示内容版本号。
aclVersion = 0				# aclVersion 标识acl版本
ephemeralOwner = 0x0	# ephemeralOwner 表示创建该临时节点时的会话sessionID,如果是持久性节点那么值为0
dataLength = 0				# dataLength 表示数据⻓度。
numChildren = 1				# numChildren 表示直系子节点数。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2.3 znode文件系统

不同于Linux,分为文件夹和文件,znode文件系统中只有znode节点(既可以存数据,也可以当作为文件夹)。

但是每个节点都必须要有一个唯一的绝对路径,每个节点都可以存储数据(根节点除外,可以存但是不建议存数据),每隔节点下都可以挂载子节点

虽然说zookeeper每个节点都存储了一份完整数据,但是不能存储大量的数据。所以znode只适合存储非常小量数据,不能超过1M,最好小于1K。

znode的分类:

  • 按照生命周期可以分为:

    • 短暂(ephemeral)(断开连接自己删除)
    • 持久(persistent)(断开连接不删除,默认情况)
  • 按照是否自带序列编号可以分为:

    • SEQUENTIAL(带自增序列编号,由父节点维护)
    • 非SEQUENTIAL(不带自增序列编号,默认情况)
节点类型详解
PERSISTENT持久化 znode 节点,一旦创建这个 znode 节点,存储的数据不会 主动消失,除非是客户端主动 delete
PERSISTENT_SEQUENTIAL自动增加自增顺序编号的 znode 节点,比如 ClientA 去 zookeeper service 上建立一个 znode 名字叫做 /zk/conf,指定 了这种类型的节点后zk会创建 /zk/conf0000000000,ClientB 再 去创建就是创建 /zk/conf0000000001,ClientC 是创建 /zk/conf0000000002,以后任意 Client 来创建这个 znode 都会 得到一个比当前 zookeeper 命名空间最大 znod e编号 +1 的 znode,也就说任意一个 Client 去创建 znode 都是保证得到的 znode 编号是递增的,而且是唯一的 znode 节点
EPHEMERAL临时 znode 节点,Client 连接到 zk service 的时候会建立一个 session,之后用这个 zk 连接实例在该 session 期间创建该类型的 znode,一旦 Client 关闭了 zookeeper 的连接,服务器就会清除 session,然后这个 session 建立的 znode 节点都会从命名空间消 失。总结就是,这个类型的 znode 的生命周期是和 Client 建立的 连接一样的。比如 ClientA 创建了一个 EPHEMERAL 的 /zk/conf 的 znode 节点,一旦 ClientA 的 zookeeper 连接关闭,这个 znode 节点就会消失。整个zookeeper service命名空间里就会删 除这个znode节点
EPHEMERAL_SEQUENTIA临时自动编号节点 znode 节点编号会自动增加 但是会随session 消失而消失

注意点:

1、创建 znode 时设置顺序标识,znode 名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护

2、在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序

3、EPHEMERAL 类型的节点不能有子节点,所以只能是叶子结点

4、客户端可以在 znode 上设置监听器

2.4 ZNode的理解

1、znode 的约束(znode 的节点存储的最大数据是 1M,最好不要超过 1kb)为什么?

​ 每个节点都有相同的数据视图:每个节点都存储了这个 zookeeper 中的所有数据,每个节点的数据状态都和 leader 保持一致

​ a、同步的压力:写入过程至少要超过半数节点写成功才能认为该数据写成功,节点数越多,成功的难度越大

​ b、存储的压力:所有数据的规模超出单台服务器的存储能力

​ 2、znode 的分类

​ a、按照生命周期:临时节点EPHEMERAL 和 永久节点PERSISTENT

​ 持久类型(显示创建,显示删除,只有当使用显示命令来删除节点,否则这个节点知道被创建成功,则会一直存在)

​ 临时类型/短暂类型(跟会话绑定,那个会话创建的这个节点,如果这个会话断开,则这个会话创建的所有临时节点被系统删除)。

​ 每个znode节点必然都是由某一个session创建的。如果当前这个session断开,那么该znode节点会被自动删除

​ 比如 HDFS ,Kafka,HBase, 等各种组件使用 ZK 的时候,都是先建立跟 zk 的会话链接,通过这个会话链接去创建一个 临时znode 节点。如果这个链接断开,则 zk 系统会自动删掉这个链接创建的所有临时节点

​ b、按照是否带序列编号分,每个节点都各自维护了一个序列编号,当前节点的序列编号是由它的父节点维护的,编号是自增序列编号,和 mysql 的自增主键是一 样的道理

​ 1、 带

​ 2、 不带

​ create /a “data”

​ c、总结一下,总共分成四种:

​ 1、CreateMode.PERSISTENT 持久

​ 2、CreateMode.PERSISTENT_SEQUENTIAL 持久带序列编号

​ 3、CreateMode.EPHEMERAL 临时

​ 4、CreateMode.EPHEMERAL_SEQUENTIAL 临时带序列编号

​ 3、znode 的小知识

​ 临时节点的下面不能挂载子节点,临时节点,只能作为叶子节点

2.5 Watcher 机制

Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能

⼀个典型的发布/订阅模型系统定义了一种 ⼀对多的订阅关系,能够让多个订阅者同时监听某⼀个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。

在 ZooKeeper 中,引⼊了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的⼀些指定事件触发了这个 Watcher,那么Zk就会向指定客户端发送一个事件通知来实现分布式的通知功能。

image-20210902111639164

image-20210902111654487

注意:注册的监听在事件响应之后就失效了。那么怎么做到连续监听?请思考回答。

​ zookeeper默认监听触发一次就结束,所以需要重新实现WatchedEvent中的process方法,核心就是对watcher的循环调用。

WatchedEvent包含两方面重要信息:

  • 与zk服务器连接的状态信息

    可以调用watchedEvent.getState()方法获取与zk服务器连接的状态信息,状态信息取值主要包括SyncConnected、Disconnected、ConnectedReadOnly和AuthFailed等等。

  • 发生的具体事件类型信息

    watchedEvent.getState()方法只是获取与zk服务器连接的状态信息,但在同一个连接状态下,还会发生很多事件的类型。例如在zk中,我们可以watch一个节点的数据内容,当这个节点的数据被改变时,我们可以获取到这个事件。类似的还有子节点列表变化事件等等。这就需要我们在SyncConnected同一种连接状态下区分多个事件类型。可以通过watchedEvent.getType()方法获取具体的事件类型。事件类型的取值包括None、NodeCreated、NodeDeleted、NodeDataChanged和NodeChildrenChanged

监听工作原理:ZooKeeper 的 Watcher 机制主要包括客户端线程、客户端 WatcherManager、 Zookeeper 服务器三部分。客户端在向 ZooKeeper 服务器注册的同时,会将 Watcher 对象存储在客户端的 WatcherManager 当中。当 ZooKeeper 服务器触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatcherManager 中取出对应的 Watcher 对象来执行回调逻辑。

整个Watcher注册与通知过程如图所示。

image-20210902095335323

Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager**、Zookeeper服务器器**三部 分。

具体⼯作流程为:

  • 客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中

  • 当Zookeeper服务器触发Watcher事件后,会向客户端发送通知

  • 客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑

3. Zookeeper内部原理

3.1 Leader选举

选举机制

半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器器。 Zookeeper虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper⼯作时,是有一个节 点为Leader,其它为Follower,Leader是通过内部的选举机制产⽣的。

集群首次启动

假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这⼀点上,都是⼀样的。假设这些服务器依序启动,来看会发⽣什么

image-20210902095637687

全新集群选主

(1)服务器器1启动,此时只有它⼀台服务器启动了,它发出去的报文没有任何响应,所以它的选举状态一直是LOOKING状态。

(2)服务器2启动,它与最开始启动的服务器1进行通信,互相交换⾃己的选举结果,由于两者都没有历史数据,所以id值较大的服务器2胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是3),所以服务器1、2还是继续保持LOOKING状态。

3)服务器3启动,根据前⾯的理论分析,服务器3成为服务器1、2、3中的⽼大,⽽与上面不同的是,此时有三台服务器选举了它,所以它成为了了这次选举的Leader。

(4)服务器4启动,根据前面的分析,理论上服务器4应该是服务器1、2、3、4中最大的,但是由于前⾯已经有半数以上的服务器选举了服务器3,所以它只能接收当小弟的命了。

(5)服务器5启动,同4一样称为follower。

总结:zookeeper server的三种工作状态

  • LOOKING:当前Server不知道leader是谁,正在搜寻,正在选举

  • LEADING:当前Server即为选举出来的leader,负责协调事务

  • FOLLOWING:leader已经选举出来,当前Server与之同步,服从leader的命令

非全新集群选主

每个节点在选举时都会参考自身节点的zxid值(事务ID);优先选择zxid值大的节点称为Leader!!

那么,初始化的时候,是按照上述的说明进行选举的,但是当zookeeper运行了一段时间之后,有机器down 掉,重新选举时,选举过程就相对复杂了。

需要加入数据 version、serverid 和 逻辑时钟。

​ 逻辑时钟:这个值从0开始递增,每次选举对应一个值,也就是说:如果在同一次选举中,那么这个值应该是一致的;逻辑时钟值越大,说明这一次选举 leader 的进程更新,也就是每次选举拥有一个 zxid,投票结果只取 zxid 最新的

​ 数据 version:数据新的 version 就大,数据每次更新都会更新 version

​ server id:就是我们配置的 myid 中的值,每个机器一个

选举的标准就变成:

1、逻辑时钟小的选举结果被忽略,重新投票

2、统一逻辑时钟后,数据version大的胜出

3、数据version相同的情况下,server id大的胜出

根据这个规则选出 leader。

数据同步

选完 leader 以后,zk就进入状态同步过程。

详细步骤:

1、leader等待server连接;

2、follower连接leader,将最大的zxid发送给leader;

3、leader根据follower的zxid确定同步点;

4、完成同步后通知follower 已经成为uptodate状态;

5、follower收到uptodate消息后,又可以重新接受client的请求进行服务了。

image-20210902110550450

3.2 ZAB一致性协议

1. 分布式数据⼀致性问题

为什么会出现分布式数据一致性问题?

  • 将数据复制到分布式部署的多台机器中,可以消除单点故障,防止系统由于某台(些)机器器宕机导致的不可用。

  • 通过负载均衡技术,能够让分布在不同地⽅的数据副本全都对外提供服务。有效提高系统性能。

在分布式系统中引入数据复制机制后,多台数据节点之间由于网络等原因很容易产生数据不一致的情 况。 举例

当客户端Client1将系统中的一个值K1由V1更新为V2,但是客户端Client2读取的是一个还没有同步更新的副本,K1的值依然是V1,这就导致了数据的不一致性。其中,常见的就是主从数据库之间的复制延时问题。

image-20210902100147125

2. ZAB协议

ZK就是分布式一致性问题的⼯业解决方案,paxos是其底层理论算法(晦涩难懂著名),其中zab,raft和众多开源算法是对paxos的⼯业级实现。ZK没有完全采用paxos算法,⽽是使⽤了⼀种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原⼦子消息广播协议)的协议作为其数据⼀致性的核心算法。

ZAB协议

ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种⽀持崩溃恢复和原⼦广播协议。

主备模式保证⼀致性

image-20210902100413372

ZK怎么处理集群中的数据?所有客户端写入数据都是写入Leader中,然后,由 Leader 复制到Follower 中。ZAB会将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上,ZAB协议能够保证了事务操作的一个全局的变更序号(ZXID)。

⼴播消息

ZAB 协议的消息⼴广播过程类似于 ⼆二阶段提交过程。对于客户端发送的写请求,全部由 Leader 接收, Leader 将请求封装成⼀一个事务 Proposal(提议),将其发送给所有 Follwer ,如果收到超过半数反馈 ACK,则执⾏行行 Commit 操作(先提交⾃自⼰己,再发送 Commit 给所有 Follwer)。

  1. 发送Proposal到Follower

image-20210902100532387

  1. Leader接收Follower的ACK

    image-20210902100601515

  2. 超过半数ACK则Commit

    image-20210902100632041

不能正常反馈Follower恢复正常后会进入数据同步阶段最终与Leader保持⼀致!!

细节

  • Leader接收到Client请求之后,会将这个请求封装成⼀个事务,并给这个事务分配一个全局递增的唯⼀ ID,称为事务ID(ZXID),ZAB 协议要求保证事务的顺序,因此必须将每一个事务按照 ZXID 进行先后排序然后处理。

ZK集群为了保证任何事务操作能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是 Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理。

zk提供的应该是最终一致性的标准。zk所有节点接收写请求之后可以在一定时间内保证所有节点都能看到该条数据!!

Leader 崩溃问题

Leader宕机后,ZK集群⽆法正常工作,ZAB协议提供了了一个⾼效且可靠的leader选举算法。

Leader宕机后,被选举的新Leader需要解决的问题

  • ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
  • ZAB 协议确保丢弃那些只在 Leader 提出/复制,但没有提交的事务。

基于上⾯的⽬的,ZAB协议设计了⼀个选举算法:能够确保已经被Leader提交的事务被集群接受,丢弃还有提交的事务。

这个选举算法的关键点:保证选举出的新Leader拥有集群中所有节点最大编号(ZXID)的事务!!

3.3 Paxos算法和ZAB协议

Paxos算法是莱斯利·兰伯特(英语:Leslie Lamport)于1990年提出的一种基于消息传递且具有高度容错特性的一致性算法。

分布式系统中的节点通信存在两种模型:

  • 共享内存(Shared Memory)

  • 消息传递(Messages Passing)

基于消息传递通信模型的分布式系统,不可避免的会发生以下错误:进程可能会慢、被杀死或者重启, 消息可能会延迟、丢失、重复,在基础Paxos场景中,先不考虑可能出现消息篡改即拜占庭错误 (Byzantine failure,即虽然有可能一个消息被传递了两次,但是绝对不会出现错误的消息)的情况。

Paxos算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发 生以上任何异常,都不会破坏决议一致性。

Paxos算法使用一个希腊故事来描述,在Paxos中,存在三种角色,分别为:

  • Proposer(提议者,用来发出提案proposal),

  • Acceptor(接受者,可以接受或拒绝提案),

  • Learner(学习者,学习被选定的提案,当提案被超过半数的Acceptor接受后为被批准)

下面更精确的定义Paxos要解决的问题:

  • 决议(value)只有在被proposer提出后才能被批准

  • 在一次Paxos算法的执行实例中,只批准(chose)一个value

  • learner只能获得被批准(chosen)的value

ZooKeeper的选举算法有两种:一种是基于 Basic Paxos(Google Chubby采用)实现的,一种是基于 Fast Paxos(ZooKeeper采用)算法实现的。ZooKeeper 默认的选举算法为 Fast Paxos,并且 ZooKeeper在3.4.0 版本后只保留了 FastLeaderElection 算法。

ZooKeeper 的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做 ZAB协议(Zookeeper Atomic BroadCast)。

ZAB协议有两种模式,它们分别是:

  • 崩溃恢复模式(选主)

​ 当服务启动或者在领导者崩溃后,ZAB就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和follower之间具有相同的系统状 态。

  • 原子广播模式(同步)

​ 当ZooKeeper集群选举出leader同步完状态退出恢复模式之后,便进入了原子广播模式。所有的写请求都被 转发给leader,再由leader将更新proposal广播给follower

为了保证事务的顺序一致性,zookeeper 采用了递增的事务 id 号(zxid)来标识事务。所有的提议 (proposal)都在被提出的时候加上了 zxid。实现中 zxid 是一个 64 位的数字,它高32位是 epoch 用 来标识 leader 关系是否改变,每次一个 leader 被选出来,它都会有一个新的 epoch,标识当前属于那 个 leader 的统治时期。低 32 位用于递增计数。

Basic Paxos流程:

1、选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server

2、选举线程首先向所有Server发起一次询问(包括自己)

3、选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的 serverid(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息 (serverid,zxid),并将这些信息存储到当次选举的投票记录表中

4、收到所有Server回复以后,就计算出id最大的那个Server,并将这个Server相关信息设置成下一次要 投票的Server

5、线程将当前id最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数, 设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状 态,否则,继续这个过程,直到leader被选举出来。

通过流程分析我们可以得出:

1、要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1

2、且存活的Server的数目不得少于n+1

每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,zookeeper 会记录事务日志并定期进行快照,方便在 恢复时进行状态恢复。

Fast Paxos 流程是在选举过程中,某Server首先向所有 Server 提议自己要成为 leader,当其它 Server 收到提议以后,解决 epoch 和 zxid 的冲突,并接受对方的提议,然后向对方发送接受提议完成的消 息,重复这个流程,最后一定能选举出 Leader

4. Zookeeper应⽤

ZooKeeper是⼀个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使用它来进行分布式数据的发布与订阅。另⼀⽅面,通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher 事件通知机制,可以非常⽅便地构建一系列分布式应用中都会涉及的核心功能,如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应用场景来做下介绍

Zookeeper的两大特性:

  1. 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。

  2. 对在Zookeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除

利⽤其两⼤特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册⼀个 Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节 点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。

4.1 服务器动态上下线监听

分布式系统中,主节点会有多台,主节点可能因为任何原因出现宕机或者下线,⽽任意一台客户端都要能实时感知到主节点服务器的上下线。

思路分析

image-20210902101338911

具体实现

服务端

package com.erainm.zk.test;
import org.I0Itec.zkclient.ZkClient;

public class ServerMain {
	private ZkClient zkClient = null;
		
  //获取到zk对象
	private void connectZK(){
			zkClient = new ZkClient("lg01:2181,lg02:2181,lg03:2181");
      if(!zkClient.exists("/servers")){
      		zkClient.createPersistent("/servers");
      }
	}

  //注册服务端信息到zk节点
	private void registerServerInfo(String ip,String port){
  		//创建临时顺序节点
  		final String path = zkClient.createEphemeralSequential("/servers/server", ip +":"+port);
			System.out.println("---->>> 服务器器注册成功,ip="+ip+";port ="+port+";节点 路路径信息="+path);
	}
  public static void main(String[] args) {
      final ServerMain server = new ServerMain();
      server.connectZK();
       
			server.registerServerInfo(args[0],args[1] );
			//启动⼀一个服务线程提供时间查询
      new TimeServer(Integer.parseInt(args[1])).start();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

服务端提供时间查询的线程类

package com.erainm.zk.test;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class TimeServer extends Thread {
    private int port=0;
    
  	public TimeServer(int port) {
    	this.port = port;
		}

  @Override
	public void run() { 
  //启动serversocket监听⼀一个端⼝

		try {
    	final ServerSocket serverSocket = new ServerSocket(port);
   		while(true){
      		final Socket socket = serverSocket.accept();
       		final OutputStream out = socket.getOutputStream();
       		out.write(new Date().toString().getBytes());
   		}
		} catch (IOException e) {
    		e.printStackTrace();
  	}
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

client端

package com.erainm.zk.onoffline;

 
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

//注册监听zk指定⽬目录
//维护⾃己本地⼀个servers信息,收到通知要进行更新
//发送时间查询请求并接受服务端返回的数据

public class Client {
		//获取zkclient
		ZkClient zkClient = null;
		//维护⼀一个serversi 信息集合
		ArrayList<String> infos = new ArrayList<String>();

  	private void connectZk() {
      	// 创建zkclient
				zkClient = new ZkClient("lg01:2181,lg02:2181");
      //第⼀次获取服务器信息,所有的子节点
			final List<String> childs = zkClient.getChildren("/servers");
      for (String child : childs) {
					//存储着ip+port
					final Object o = zkClient.readData("/servers/" + child);
        	infos.add(String.valueOf(o));
			}

      //对servers⽬目录进⾏行行监听
			zkClient.subscribeChildChanges("/servers", new IZkChildListener() {
        	public void handleChildChange(String s, List<String> children) throws Exception {
            	//接收到通知,说明节点发⽣了变化,client需要更新infos集合中的数据
            	ArrayList<String> list = new ArrayList<String>();
            	//遍历更更新过后的所有节点信息
            	for (String path : children) {
                	final Object o = zkClient.readData("/servers/" + path);
                	list.add(String.valueOf(o));
               }
							
            	//最新数据覆盖⽼老老数据
            	infos = list;
            	System.out.println("--》接收到通知,最新服务器信息为:" + infos);
          }
      });
    }
  
  	//发送时间查询的请求
  	public void sendRequest() throws IOException {
      	//⽬标服务器地址
				final Random random = new Random();
				final int i = random.nextInt(infos.size()); final String ipPort = infos.get(i);
				final String[] arr = ipPort.split(":");

      	//建立socket连接
				final Socket socket = new Socket(arr[0], Integer.parseInt(arr[1]));
      	final OutputStream out = socket.getOutputStream();
				final InputStream in = socket.getInputStream();
				//发送数据
				out.write("query time".getBytes());
				out.flush();
				//接收返回结果
				final byte[] b = new byte[1024];
				in.read(b);//读取服务端返回数据
      	System.out.println("client端接收到server:+" + ipPort + "+返回结果:" + new String(b));
				
      	//释放资源
      	in.close();
      	out.close();
      	socket.close();
    }

  	public static void main(String[] args) throws InterruptedException {
      	final Client client = new Client();
      	client.connectZk(); //监听器逻辑
      	while (true) {
          	try {
              	client.sendRequest(); //发送请求
            } catch (IOException e) {
                e.printStackTrace();
                try {
                    client.sendRequest();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
    
          	//每隔⼏秒中发送一次请求到服务端
          	Thread.sleep(2000);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

4.2 分布式锁

1. 什么是锁

  • 在单机程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,为了保证线程安全 (数据不能出现脏数据)就需要对变量或代码块做同步,使其在修改这种变量时能够串行执行消除并发修改变量。
  • 对变量或者堆代码块做同步本质上就是加锁。目的就是实现多个线程在⼀个时刻同一个代码块只能有一个线程可执行

2. 分布式锁

分布式的环境中会不会出现脏数据的情况呢?类似单机程序中线程安全的问题。观察下面的例子

image-20210902103155956

上⾯的设计是存在线程安全问题

问题

假设Redis ⾥面的某个商品库存为 1;此时两个用户同时下单,其中一个下单请求执行到第 3 步,更新数据库的库存为 0,但是第 4 步还没有执⾏行。⽽另外一个用户下单执行到了第 2 步,发现库存还是 1,就继续执行第 3 步。但是商品库存已经为0, 所以如果数据库没有限制就会出现超卖的问题。

解决方法

⽤用锁把 2、3、4 步锁住,让他们执行完之后,另⼀个线程才能进来执行。

image-20210902103342196

公司业务发展迅速,系统应对并发不断提高,解决方案是要增加一台机器,结果会出现更大的问题

image-20210902103417236

假设有两个下单请求同时到来,分别由两个机器执行,那么这两个请求是可以同时执行了,依然存在超卖的问题。

因为如图所示系统是运行在两个不同的 JVM ⾥⾯,不同的机器上,增加的锁只对⾃己当前 JVM ⾥面的线程有效,对于其他 JVM 的线程是无效的。所以现在已经不是线程安全问题。需要保证两台机器加的锁是同一个锁,此时分布式锁就能解决该问题。

分布式锁的作用:在整个系统提供一个全局、唯一的锁,在分布式系统中每个系统在进行相关操作的时候需要获取到该锁,才能执行相应操作。

3. zk实现分布式锁

利用Zookeeper可以创建临时带序号节点的特性来实现一个分布式锁实现思路

锁就是zk指定⽬录下序号最小的临时序列节点,多个系统的多个线程都要在此目录下创建临时的顺 序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。

每个线程都是先创建临时顺序节点,然后获取当前⽬录下最小的节点(序号),判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。

获取锁失败的线程获取当前节点上⼀个临时顺序节点,并对此节点进行监听,当该节点删除的时候(上⼀个线程执行结束删除或者是掉线zk删除临时节点)这个线程会获取到通知,代表获取到了锁。

流程图

zk实现分布式锁流程图

main方法

package com.erainm.zk.dislock;

//zk实现分布式锁
public class DisLockTest {
  	public static void main(String[] args) {
      	//使⽤用10个线程模拟分布式环境
      	for (int i = 0; i < 10; i++) {
          	new Thread(new DisLockRunnable()).start();//启动线程
        }
    }
    
  	static class DisLockRunnable implements Runnable {
      	public void run() {
          	//每个线程具体的任务,每个线程就是抢锁,
          	final DisClient client = new DisClient(); client.getDisLock();
          	//模拟获取锁之后的其它动作
          	try {
        				Thread.sleep(2000);
            } catch (InterruptedException e) {
              	e.printStackTrace();
            }
          	//释放锁
          	client.deleteLock();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

核⼼实现

 
package com.lagou.zk.dislock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
//抢锁
//1. 去zk创建临时序列节点,并获取到序号
//2. 判断⾃己创建节点序号是否是当前节点最小序号,如果是则获取锁
//执行相关操作,最后要释放锁
//3. 不是最小节点,当前线程需要等待,等待你的前一个序号的节点
//被删除,然后再次判断⾃己是否是最⼩节点。。。

public class DisClient {
  	public DisClient() {
      	//初始化zk的/distrilocl节点,会出现线程安全问题
      	synchronized (DisClient.class){
          	if (!zkClient.exists("/distrilock")) {
              	zkClient.createPersistent("/distrilock");
            }
        }
    }
  	
  	//前⼀个节点
  	String beforNodePath;
  	String currentNoePath;
		//获取到zkClient
		private ZkClient zkClient = new ZkClient("lg01:2181,lg02:2181");
  	//把抢锁过程为量部分,一部分是创建节点,⽐较序号,另⼀部分是等待锁
		//完整获取锁⽅方法
		public void getDisLock() {
      	//获取到当前线程名称
				final String threadName = Thread.currentThread().getName();
      	//⾸先调用tryGetLock
      	if (tryGetLock()) {
          	//说明获取到锁
          	System.out.println(threadName + ":获取到了锁");
        } else {
          	// 没有获取到锁,
          	System.out.println(threadName + ":获取锁失败,进⼊入等待状态");
          	waitForLock();
          	//递归获取锁
          	getDisLock();
        }
    }
  	CountDownLatch countDownLatch = null;
		
  	//尝试获取锁
		public boolean tryGetLock() {
      	//创建临时顺序节点,/distrilock/序号
      	if (null == currentNoePath || "".equals(currentNoePath)) {
          	currentNoePath = zkClient.createEphemeralSequential("/distrilock/", "lock");
        }
				//获取到/distrilock下所有的⼦节点
				final List<String> childs = zkClient.getChildren("/distrilock");
      	//对节点信息进行排序
				Collections.sort(childs); //默认是升序
				final String minNode = childs.get(0);
      	//判断⾃自⼰己创建节点是否与最⼩序号⼀一致
				if (currentNoePath.equals("/distrilock/" + minNode)) {
          	//说明当前线程创建的就是序号最小节点
            return true;
        } else {
          	//说明最小节点不是⾃己创建,要监控⾃己当前节点序号前一个的节点
            final int i = Collections.binarySearch(childs,currentNoePath.substring("/distrilock/".length()));
						//前⼀一个(lastNodeChild是不包括父节点)
						String lastNodeChild = childs.get(i - 1); beforNodePath = "/distrilock/" + lastNodeChild;
        }
      	return false;
    }

  	//等待之前节点释放锁,如何判断锁被释放,需要唤醒线程继续尝试tryGetLock
  	public void waitForLock() {
      	//准备⼀一个监听器
      	final IZkDataListener iZkDataListener = new IZkDataListener() {
          	public void handleDataChange(String s, Object o) throws Exception{
            }
          	//删除
          	public void handleDataDeleted(String s) throws Exception {
              	//提醒当前线程再次获取锁
              	countDownLatch.countDown();//把值减1变为0,唤醒之前await线程
            }
        };
  			//监控前⼀一个节点
				zkClient.subscribeDataChanges(beforNodePath, iZkDataListener);
				//在监听的通知没来之前,该线程应该是等待状态,先判断⼀次上一个节点是否还存在
      	if (zkClient.exists(beforNodePath)) {
          	//开始等待,CountDownLatch:线程同步计数器器
          	countDownLatch = new CountDownLatch(1);
          	try {
              	countDownLatch.await();//阻塞,countDownLatch值变为0
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
      	
      	//解除监听
        zkClient.unsubscribeDataChanges(beforNodePath, iZkDataListener);
    }
  	
  	//释放锁
  	public void deleteLock() {
      	if (zkClient != null) {
          	zkClient.delete(currentNoePath);
            zkClient.close();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109

分布式锁的实现可以是 Redis、Zookeeper,相对来说⽣产环境如果使用分布式锁可以考虑使用Redis实现⽽非Zk。

4.3 Zookeeper典型应用场景

1. 命名服务

命名服务是分布式系统中较为常见的一类场景,分布式系统中,被命名的实体通常可以是集群中的 机器、提供的服务地址或远程对象等,通过命名服务,客户端可以根据指定名字来获取资源的实 体、服务地址和提供者的信息。ZooKeeper也可帮助应用系统通过资源引用的方式来实现对资源 的定位和使用,广义上的命名服务的资源定位都不是真正意义上的实体资源,在分布式环境中,上 层应用仅仅需要一个全局唯一的名字。ZooKeeper可以实现一套分布式全局唯一ID的分配机制

2. 配置管理

程序总是需要配置的,如果程序分散部署在多台机器上,要逐个改变配置就变得困难。现在把这些 配置全部放到ZooKeeper上去,保存在 ZooKeeper 的某个目录节点中,然后所有相关应用程序对 这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 ZooKeeper 的通知,然 后从 ZooKeeper 获取新的配置信息应用到系统中就好。

3. 集群管理

所谓集群管理无在乎两点:是否有机器退出和加入(上下线)、选举master

对于第一点,所有机器约定在父目录 GroupMembers 下创建临时目录节点,然后监听父目录节点 的子节点变化消息。一旦有机器挂掉,该机器与 ZooKeeper 的连接断开,其所创建的代表该节点 存活状态的临时目录节点被删除,所有其他机器都将收到通知:某个兄弟目录被删除,于是,所有 人都知道:有兄弟节点挂掉了。新机器加入也是类似,所有机器收到通知:新兄弟目录加入,又多 了个新兄弟节点。

对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器 作为 master 就好。当然,这只是其中的一种策略而已,选举策略完全可以由管理员自己制定。

4. 分布式锁

有了 ZooKeeper 的一致性文件系统,锁的问题变得容易。

锁服务可以分为两三类

​ 一个是写锁,对写加锁,保持独占,或者叫做排它锁,独占锁

​ 一个是读锁,对读加锁,可共享访问,释放锁之后才可进行事务操作,也叫共享锁

​ 一个是控制时序,叫时序锁

对于第一类,我们将 ZooKeeper 上的一个znode看作是一把锁,通过 createznode() 的方式来实 现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。 用完删除掉自己创建的 /distribute_lock 节点就释放出锁。

对于第二类,/distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点, 和选 Master 一样,编号最小的获得锁,用完删除,依次有序

5. 队列管理

两种类型的队列:

​ 1、同步队列:当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。

​ 2、先进先出队列:队列按照 FIFO 方式进行入队和出队操作。

第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。

第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。

5. 扩展

思考:

1、在分布式场景中,怎么确保一定拿到最新的准确值

2、在分布式场景中存储一个值,为了保证安全,存储了多份

NWR理论: — 抽屉原理

​ N:总节点数

​ W:写入副本数

​ R:读取的副本数

保证拿到最新的数据:R+W>N

至少读取N-W+1个副本才行

Zookeeper每个节点都存储了所有数据的副本,但是zk要求写入成功的节点数达到一半,就认为写入数据成功。

如果W大,这次写入成功的概率就越小,读取数据的压力就越小

如果W小,R读取的额节点就要多,读取的性能就差

所以Zookeeper查询效率高,写入效率低

读取时,读取超过一半节点的数据,就一定会拿到最新的数据,然后根据版本,就可以拿到最新的准确值。

Quorum机制:超过一半的数据写入成功就认为写成功,剩下的暂时剔除(投票机制)

CAP理论:

​ C:强一致性,数据副本写的越多,一致性越好

​ A:高可用性,

​ P:分区容错性,在分布式环境中,一定要保证,当出现网络分区的时候,也能使客户端拿到最新的数据

​ 任何分布式系统,都不可能同时满足以上三个要求;满足两个即可。

BASE理论:针对CAP理论的妥协

​ 一定是会满足P的,C和A进行权衡,就是BASE理论

​ 可以满足,C:最终一致性

​ A:基本可用

Zookeeper解决问题:

​ 分布式同步、分布式配置管理、分布式集群管理(主节点状态管理、从节点上下线管理)、分布式命名管理、分布式队列管理、分布式锁

Zookeeper底层主要依赖两个组件:

  • znode文件系统(存储数据的能力)

  • watch监听系统(监听数据变化的能力)

6. 分布式系统理论

6.1. 分布式一致性解决方案产生背景

为解决从集中式服务扩展到分布式服务时使用多副本手段,为了保证数据和服务高可用而产生的副本间数据状态不一致的问题

1. 集中式服务

集中式系统就是指一台或者多台主计算机组成中心节点,数据集中存储于这个中心节点中,并且整个系统的所有业务单元都集中部署在这个中心节点上,系统所有的功能均由其集中处理。也就是说,集中式系统中,每个终端或客户端及其仅仅负责数据的录入和输出,而数据的存储与控制处理完全由主机来完成。

优点:

​ 1. 组成机构简单

​ 2. 应用部署简单

​ 3. 项目架构简单

缺点:

​ 1、大型主机的研发人才和维护人才培养成本非常高

​ 2、大型主机非常昂贵

​ 3、单点故障问题,主机一挂,所有服务终止

​ 4、大型主机的性能扩展受限于摩尔定律

摩尔定律是由英特尔(Intel)创始人之一戈登·摩尔(Gordon Moore)提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。

摩尔定律告诉我们:纵向扩展理论上是受限,所以只能考虑横向扩展,而且理论上来说,横向扩展理论上不受限!

纵向扩展:提升服务器性能,上限的

横向扩展:提升服务器数量(分布式)

“去IOE” 运动(IOE 指的是 IBM 小型机、Oracle 数据库、EMC 的高端存储)。

为什么要去IOE?

​ 1、企业成本越来越高,升级单机处理能力的性价比越来越低

​ 2、单机处理能力存在瓶颈

​ 3、稳定性和可用性这两个指标很难达到

2. 分布式服务

分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。

​ 分布式意味着可以采用更多的普通计算机(相对于昂贵的大型机)组成分布式集群对外提供服务。计算机越多,CPU、内存、存储资源等也就越多,能够处理的并发访问量也就越大。

​ 一个由分布式系统实现的电子商城,在功能上可能被拆分成多个应用,分别提供不同的功能,组成一个分布式系统对外提供服务。

​ 而系统内的各个子系统之间通过网络进行通信和协调,如异步消息或者 RPC/HTTP 请求调用等。

​ 所以,分布式系统中的计算机在空间上几乎没有任何限制,这些计算机可能被放在不同的机柜上,也可能被部署在不同的机房中,还可能在不同的城市中,对于大型的网站甚至可能分布在不同的国家和地区。

分布式系统的特点:

  • 分布性:分布式系统中的多台计算机都会在空间上随意分布,同时,它们的分布情况也会随时变动

  • 对等性:集群中的每个工作节点的角色是一样的。注意副本这个概念

  • 并发性:多个机器可能同时操作一个数据库或者存储系统,可能引发数据不一致的问题(串行,并行,并发)

  • 缺乏全局时钟:分布式系统中的多个主机上的事件的先后顺序很难界定(分布式场景中最复杂的一个问题之一)

  • 故障总发生(服务器宕机,网络拥堵和延迟):组成分布式系统的所有计算机,都有可能发生任何形式的故障。

3. 分布式系统常见异常问题

1、通信异常:网络不可用(消息延迟或者丢失),会导致分布式系统内部无法顺利进行一次网络通信,所以可能造成多节点数据丢失和状态不一致,还有可能造成数据乱序。解决方案:重试机制

2、网络分区:网络不连通,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域,分布式系统就会出现局部小集群造成数据不一致。解决方案:把数据状态不是最新的给下线掉

3、节点故障/机器宕机:服务器节点出现的宕机或"僵死"现象,这是常态,而不是异常。解决方案:数据副本,异步复制

4、分布式三态:即成功、失败和超时,分布式环境中的网络通信请求发送和结果响应都有可能丢失,所以请求发起方无法确定消息是否处理成功。分布式系统的可用性:在用户能忍受的时间范围内,一定给出响应!解决方案:超时处理

5、存储数据丢失:对于有状态节点来说,数据丢失意味着状态丢失,通常只能从其他节点读取、恢复存储的状态。解决方案:副本协议

异常处理原则:被大量工程实践所检验过的异常处理黄金原则是:任何在设计阶段考虑到的异常情况一定会在系统实际运行中发生,但在系统实际运行遇到的异常却很有可能在设计时未能考虑,所以,除非需求指标允许,在系统设计时不能放过任何异常情况。

4. 衡量分布式系统的性能指标

1、性能:下面三个性能指标往往会相互制约,追求高吞吐的系统,往往很难做到低延迟;系统平均响应时间较长时,也很难提高QPS。(并行,串行,并发)

​ 系统的吞吐能力,指系统在某一时间可以处理的数据/请求总量,通常可以用系统每秒处理的总数据/总请求量来衡量;

​ 系统的响应延迟,指系统完成某一功能需要使用的时间;

​ 系统的并发能力,指系统可以同时完成某一功能的能力,通常也用QPS(query per second)来衡量。

2、可用性:系统的可用性(availability)指系统在面对各种异常时可以正确提供服务的能力。系统的可用性可以用系统停服务的时间与正常服务的时间的比例来衡量,也可以用某功能的失败次数与成功次数的比例来衡量。可用性是分布式的重要指标,衡量了系统的鲁棒性,是系统容错能力的体现。(5个 9的可靠性:一年只有5分钟的宕机时间!6个9的可靠性,也就是31秒)99.999%

3、可扩展性:系统的可扩展性(scalability)指分布式系统通过扩展集群机器规模 提高系统性能(吞吐、延迟、并发)、存储容量、计算能力的特性。好的分布式系统总在追求“线性扩展性”,也就是使得系统的某一指标可以随着集群中的机器数量线性增长。最期望的情况:动态热部署

4、一致性:分布式系统为了提高可用性,总是不可避免的使用副本的机制,从而引发副本一致性的问题。越是强的一致性的模型,对于用户使用来说使用起来越简单。

​ 串行:只有一个线程,所有的请求,都由着一个线程排队执行

​ 并发:现在有多个线程,但是使用到了临界资源,同时只能有一个线程拿到这个资源在执行

​ 并行:分布式系统,总结来说,就是并行系统!分布式并发!

5. 一致性理解

1、强一致性 :写操作完成之后,读操作一定能读到最新数据。在分布式场景中,很难实现,后续的 Paxos 算法,Quorum 机制,ZAB 协议等能实现!

2、弱一致性 :不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。

3、读写一致性 :用户读取自己写入结果的一致性,保证用户永远能够第一时间看到自己更新的内容。比如我们发一条朋友圈,朋友圈的内容是不是第一时间被朋友看见不重要,但是一定要显示在自己的列表上。

解决方案:

​ a、一种方案是对于一些特定的内容我们每次都去主库读取。(问题主库压力大)

​ b、我们设置一个更新时间窗口,在刚更新的一段时间内,我们默认都从主库读取,过了这个窗口之后,我们会挑选最近更新的从库进行读取

​ c、我们直接记录用户更新的时间戳,在请求的时候把这个时间戳带上,凡是最后更新时间小于这个时间戳的从库都不予以响应。

4、单调读一致性 : 本次读到的数据不能比上次读到的旧。多次刷新返回旧数据出现灵异事件。解决方案:通过 hash 映射到同一台机器。

5、因果一致性 :如果节点 A 在更新完某个数据后通知了节点 B,那么节点 B 之后对该数据的访问和修改都是基于 A 更新后的值。于此同时,和节点 A无因果关系的节点 C 的数据访问则没有这样的限制。

6、最终一致性 :是所有分布式一致性模型当中最弱的。不考虑中间的任何状态,只保证经过一段时间之后,最终系统内数据正确。它最大程度上保证了系统的并发能力,也因此,在高并发的场景下,它也是使用最广的一致性模型。

6. 分布式一致性的作用

分布式一致性的作用:

​ · 为了提高系统的可用性,以防止单点故障引起的系统不可用

​ · 提高系统的整体性能,通过负载均衡技术,能够让分布在不同地方的数据副本,都能够为用户提供服务

HDFS为了保证数据的安全性,提供了两个解决方案:

​ · 副本机制

​ · 纠删码(3.0新特性):如果一个数据要保证安全,默认3个副本,所以占用了三倍磁盘资源。纠删码,消除了副本机制,每个数据块都只有一个副本,怎么保证安全?往原始数据文件中,插入一些辅助数据,进行编码,然后拆开成多个数据块进行存储。如果丢失一个数据块,我们从通过这个文件的剩下的数据块的内容,通过逆编码来恢复。通过专业机构测试:加入的辅助数据大概是原始数据的40%就能保证和3个副本一样的安全级别。目前常用于:不常使用的冷数据,因为分布式计算框架spark、mapreduce等不太喜欢就删码。

其实上面这么多的内容组中只有一个用处:引出一个问题:分布式系统的数据一致性的问题!

解决方案:

​ 1、事务 + 分布式事务

​ 2、分布式一致性算法 + 分布式共识算法

​ 3、Quorum机制 + NWR机制

分布式系统的核心设计指导思想:CAP 和 BASE 理论

6.2 分布式事务2PC和3PC详解

事务:单机存储系统中。用来保证存储系统的数据状态的一致性的

​ 广义上的概念:一个事务中的所有操作,要么都成功,要么都不成功,没有中间状态

​ 狭义上的事务: 数据库的事务

特征:

​ ACID : 原子性,一致性,持久性, 隔离性

分布式事务之前需清楚:

​ 分布式系统中,每个节点都能知道自己的事务操作是否成功,但是没法知道系统中的其他节点的事务是否成功。这就有可能会造成分布式系统中的各节点的状态出现不一致。因此当一个事务需要跨越服务器节点,并且要保证事务的ACID特性时,就必须引入一个"协调者"的角色。那么其他的各个进行事务操作的节点就都叫做“参与者"。

典型的两种分布式事务的提交模式:2PC 和 3PC

​ 2PC: 两阶段提交 操作简单,事务的一致性保证就没那么好,事务的操作效率就会低一些

​ 3PC: 三阶段提交 数据一致性或者事务的执行效率必然会好一些

1. 2PC两阶段提交

2PC(two-phase commit protocol,两阶段提交协议),2PC是一个非常经典的强一致、中心化的原子提交协议。这里说的中心化是指协议中有两类节点:一个是中心化协调节点(coodinator)N个参与者节点(partcipant)

1.1执行过程解析

第一阶段:请求/表决阶段

​ 1、在分布式事务发起者向分布式事务协调者发送请求的时候,事务协调者向所有参与者发送事务预处理请求(vote request)

​ 2、这个时候参与者会开启本地事务并开始执行本地事务,执行完成后不会commit,而是向事务协调者报告是否可以处理本次事务

第二阶段:提交/执行/回滚阶段

​ 分布式事务协调者收到所有参与者反馈后,所有参与者节点均响应可以提交,则通知参与者和发起者执行commit,否则rollback

image-20210902112222431

1.2 2PC的问题

第一点:性能问题(参与者阻塞)

​ 从流程上面可以看出,最大的缺点就是在执行过程中节点都处于阻塞状态。各个操作数据库的节点都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局commit/rollback,参与者进行本地事务commit/rollback之后才会释放资源,对性能影响较大。

第二点:单点故障问题(协调者可能宕机)

​ 事务协调者是整个分布式事务的核心,一旦事务协调者出现故障,会导致参与者收不到commit/rollback的通知,从而导致参与者节点一直处于事务无法完成的中间状态。

第三点:数据不一致(消息丢失问题)

​ 在第二阶段的时候,如果发生局部网络问题,一部分事务参与者收不到 commit/rollback 消息,那么就会导致节点间数据不一致。

第四点:太过保守(没有容错机制),比较悲观

​ 必须收到所有参与者的正反馈才提交事务:如果有任意一个事务参与者的响应没有收到,则整个事务失败回滚。

1.3 2PC优点

2PC原理简单,实现方便,很多其它分布式技术都是基于2PC进行了改进,或者提供了补偿方案来设计实现的。

2. 3PC三阶段提交

3PC(three-phase commit)即三阶段提交,是2阶段提交的改进版,其将二阶段提交协议的"提交事务请求"一分为二,形成了cancommit,precommit,docommit 三个阶段。

除了在 2PC 的基础上增加了CanCommit阶段,还引入了超时机制。一旦事务参与者指定时间没有收到协调者的 commit/rollback 指令,就会自动本地commit,这样可以解决协调者单点故障的问题。

2.1 执行过程解析

image-20210902112425734

第一阶段:CanCommit阶段(提交询问):先问问事务参与者能不能执行

​ 分布式事务协调者询问所有参与者是否可以进行事务操作,参与者根据自身健康情况,是否可以执行事务操作响应Y/N。

第二阶段:PreCommit阶段(预提交):发送命令让事务参与者执行事务

​ 1、如果参与者返回的都是同意,协调者则向所有参与者发送预提交请求,并进入prepared阶段。

​ 2、参与者收到预提交请求后,执行事务操作,并保存Undo和Redo信息到事务日志中。

​ 3、参与者执行完本地事务之后(uncommitted),会向协调者发出Ack表示已准备好提交,并等待协调者下一步指令。

​ 4、如果协调者收到预提交响应为拒绝或者超时,则执行中断事务操作,通知各参与者中断事务(abort)。

​ 5、参与者收到中断事务(abort)或者等待超时,都会主动中断事务/直接提交。

第三阶段:doCommit阶段(最终提交):提交事务

​ 1、协调者收到所有参与者的Ack,则从预提交进入提交阶段,并向各参与者发送提交请求。

2、参与者收到提交请求,正式提交事务(commit),并向协调者反馈提交结果Y/N。

​ 3、协调者收到所有反馈消息,完成分布式事务。

​ 4、如果协调者超时没有收到反馈,则发送中断事务指令(abort)。

​ 5、参与者收到中断事务指令后,利用事务日志进行rollback。

​ 6、参与者反馈回滚结果,协调者接收反馈结果或者超时,完成中断事务。

2.2 3PC的问题

第一点:降低阻塞范围

​ 相对于二级段提交协议,三阶段提交协议的最大的优点就是降低了事务参与者的阻塞的范围,并且能够在出现单点故障后继续达成一致。对于协调者和参与者都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯 (协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了 整个事务的阻塞时间和范围。

第二点:最后提交以前状态一致

​ 通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。

第三点:依然可能数据不一致

​ 三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是参与者接收到 precommit 消息后,如果出现网络分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。

2.3 3PC 和 2PC 最大的区别

1、执行事务之前,先进行询问,查看所有事务参与者是否具备执行事务的条件,如果协调者收到所有事务参与者的正反馈,则执行第二阶段,否则发送rollback 命令回滚事务

2、第二阶段如果执行 precommit 命令,意味着,所有的事务参与者都具备执行事务的条件,所以在第二阶段中,所有事务参与者都会执行事务

3、当第二阶段中的所有事务参与者都发回正反馈,则协调者发送 docommit 命令来提交事务。如果此时协调者宕机,则不能发送命令,则第二阶段执行事务成功的参与者会在超时时间到达的时候,自动提交事务

6.3 分布式一致性算法

在分布式系统中,网络通信的异常情况是一定存在的(通信的延迟和中断,消息的丢失和延迟)但是上述2PC和3PC依然出现数据不一致的情况,意味着我们并不能在生产中直接使用2PC或3PC,所以我们需要能确保数据一致性的算法或协议。

分布式一致性算法:就算发生了网络分区(消网络分区一定存在:消息的延迟和丢失的可能性一定是有的),也能保证分布式数据一致!

1. Paxos算法

**Paxos 算法是莱斯利·兰伯特(Lesile Lamport)提出的一种基于消息传递且具有高度容错特性的一致性算法,获得2013年图灵奖。**是目前公认的解决分布式一致性问题最有效的算法之一。Paxos 算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。如何证明的具有高度容错,可以详解”拜占庭将军问题”。

分布式系统中的节点通信存在两种模型: 共享内存和消息传递。基于消息传递通信模型的分布式系统,不可避免会发生进程变慢被杀死,消息延迟、丢失、重复等问题,Paxos算法就是在存在以上异常的情况下仍能保持一致性的协议。

Paxos之于分布式一致性算法类似于Hadoop至于大数据。自问世以来一直保持垄断地位,甚至可以号称其它分布式一致性算法都是Paxos的不完美版本。

Google的很多大型分布式系统都采用了Paxos算法来解决分布式一致性问题,如Chubby、Megastore以及Spanner等。开源的Zookeeper,以及MySQL5.7推出的用来取代传统的主从复制的MySQL Group Replication等纷纷采用Paxos算法解决分布式一致性问题。

Paxos以及Raft都假设不存在拜占庭将军问题,只考虑节点宕机、网络分区、消息不可靠等问题。属于CFT(Crash Fault Tolerance)算法(简单理解为崩溃容错算法)。

Paxos 算法使用一个希腊故事来描述,在 Paxos中,存在三种角色,分别为

​ 1. Proposer(提议者,用来发出提案 Proposal), Proposal信息包括提案编号(Proposal ID)和提议的值(Value)。

​ 2. Acceptor(接受者,可以接受或拒绝提案), 参与决策,回应Proposer的提案。收到Proposal后可以接受提案,若Proposal获得多数Acceptors的接受,则称该Proposal被批准。

​ 3. Learner(学习者,学习被选定的提案,当提案被超过半数的 Acceptor 接受后为被批准)。不参与决策,从Proposer/Acceptors学习最新达成一致的提案(Value)。

举例:

​ Paxos 人大代表大会

​ Proposer 人大代表大会推举的主持

​ Acceptor 人大代表

​ Learner 普通民众

映射到 zookeeper 集群:

leader:发起提案 主席,单点故障(解决方案:leader 选举机制)

follower:参与投票 人大代表

observer:被动接受 全国所有人

Proposer具备顺序发起提议的能力,Acceptor对提议进行投票,如果投票通过(通过规则:少数服从多数),则Learner被动接受学习。

议会制:当部分节点宕机之后,最终仍然能够投票通过。在一定时间之后,在重启之后就会找 proposal 来同步数据

​ 弱一致性:少数服从多数,保证超过半数达成一致即可的协议

Paxos算法通过一个决议分为两个阶段(Learn阶段之前决议已经形成):

  • 第一阶段:Prepare阶段

    • Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。

    • 如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编号,那么它就会将已经接受过的编号最大的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于N的提案。

  • 第二阶段:Accept阶段

    • 如果Proposer收到半数以上Acceptor对其发出编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor。注意:V就是阶段一收到的响应编号最大的提案的value,如果响应中不包含任何提案,那么V就由proposer自己决定(任意值)。
    • 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Accept没有对编号大于N的Prepare请求作出过响应,它就接受该提案。
  • 第三阶段:Learn阶段

    • Proposer在收到多数Acceptors的Accept之后,标志着本次Accept成功,决议形成,将形成的决议发送给所有的Learners。

image-20210902112741677

Proposer可以随时丢弃提案,并且提出新的提案;Acceptor也可以随时响应,接受编号更大的提案。Zookeeper为了解决出现系统中多个Proposer互相提出编号更大的提案而陷入“活锁”死循环状态的问题,提供了选举算法从众多Acceptor推举一个唯一的Proposer来主持分布式事务,从而解决分布式系统缺乏全局时钟的问题。

更精确的定义 Paxos 要解决的问题:

​ 1、决议(value)只有在被 proposer 提出后才能被批准

​ 2、在一次 Paxos 算法的执行实例中,只批准 (choose) 一个value, multi-paxos

​ 3、learner 只能获得被批准 (chosen) 的 value

在 ZooKeeper 的内部,所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为 Leader 服务器,而余下的其他服务器则成为 Follower 服务器。Leader 服务器负责将一个客户端事务请求转换成一个事务 proposal,并将该 proposal 分发给集群中所有的 Follower 服务器。之后 Leader 服务器需要等待所有 Follower 服务器的反馈,一旦超过半数的 Follower 服务器进行了正确的反馈后,那么 leader 就会再次向所有的 Follower 服务器分发 commit 消息,要求其将前一个 proposal 进行提交。

Paxos的一大特点就是晦涩难懂,甚至连作者Lesile Lamport发表的第一篇Paxos论文都没人看懂,工程实践落地极其困难。所以Paxos算法衍生了多种变种实现,比如Base-Paxos、Cheap Paxos、Multi-Paxos等。

  • 所有各种Paxos都是Basic Paxos的变种。(更详尽的划分有四个阶段:prepare、proposer、accept、learner)

  • Cheap Paxos是引入了辅助节点暂时替代宕机的工作节点,保证系统的可用。即节省资源又保证节点正常共识。

  • Fast-Paxos是在Basic Paxos的基础上直接让Acceptor Accept而不是经历Proposer节点,从而提高效率。

  • Multi-Paxos是为了解决Basic Paxos一次只能就一个值达城共识的问题,高并发情况下Basic Paxos就不够用了,所以衍生出Multi-Paxos可以一次执行多个Basic Paxos实例,就一系列值达城共识。大名鼎鼎的Raft算法就是一种基于Multi-Paxos思想的共识算法。

这些变种paxos算法实现的应用场景:

  • Google Chubby使用了Basic Paxos

  • Apache Zookeeper使用了ZAB协议,内部封装了Fast-Paxos实现

  • Alibaba Etcd使用Raft共识算法,Raft算法思路和Multi-Paxos算法的思路基本一致

2. Raft算法

Etcd 的底层实现: Raft 算法。想要了解 Raft 算法,推荐参考:

http://thesecretlivesofdata.com/raft/,大约10分钟的时间就可以搞定这个算法了!

follower: 跟随者 没有线条

candidate:候选人 虚线

leader: 领导者 实线

Leader Election 选主!

Log Replicatoin

附:Kafka-2.8.X 开始,脱离了zookeeper,使用了raft算法来进行分布式一致性的控制。

3. ZAB协议

ZAB全称Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议),是一种专门为Zookeeper设计的支持崩溃恢复原子广播协议,是Zookeeper保证数据一致性的核心算法,它的设计实现借鉴了Paxos算法原理,但是并不是一种通用一致性算法,是专门为Zookeeper设计实现的。

ZAB协议的核心是 定义了对于那些会改变zookeeper服务器数据状态的事物请求的处理方式,所有事物必须由一个 全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器。

  • Leader 服务器负责将一个客户端事务请求转化为一个事务 Proposal(提案),并将该 Proposal 分发给集群中所有的 Follower 服务器

  • Leader 服务器等待所有 Follower 服务器的反馈,一旦超过半数的 Follower 服务器进行了正确的反馈后,Leader 就会向所有的 Follower 服务器发 送 Commit 消息,要求将前一个 Proposal 进行提交。

image-20210902112815601

黑色的线:客户端发送请求给服务端

蓝色的线:Follower转发客户端提交的写数据请求给Leader

红色的线:Leader广播Proposal给所有Follower服务器

黄色的线:所有Follower服务器返回ACK消息给Leader

绿色的线:Server发送提交事物的Commit命令给所有执行Proposal的Follower节点

ZooKeeper 的底层工作机制,就是依靠 ZAB 实现的。实现 崩溃恢复消息广播 两个主要功能。

  • 崩溃恢复:在整个服务框架启动过程中,如果 Leader 服务器出现网络中断、崩溃退出或重启等异常情况,ZAB 协议就会进入崩溃恢复模式。同时 选举出新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步(数据同步)之 后,ZAB 协议会退出恢复模式。选举算法通过 FastLeaderElection 实现。

  • 消息广播:当 ZAB 退出崩溃恢复模式(LOOKING 状态),就进入到了消息广播模式(非 LOOKING 状态:LEADING 或者 FOLLOWING 状态)在 消息广播模式中,ZooKeeper 集群可以接受客户端的读写请求,进行业务处理。消息广播通过原子广播实现。见上图。

ZAB协议需要确保那些已经在 leader服务器上提交的事务最终被所有服务器都提交。

ZAB协议需要确保丢弃那些只在 leader服务器上被提出的事务。

如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群中所有机器最高事务编号(ZXID)的事务 proposal,那么就可以保证这个新选 举出来的 leader 一定具有所有已经提交的提案。

ZAB两种基本的模式:崩溃恢复和消息广播。

64 位长度的 long 类型的数值:前面 32 epoch 后 32 位是 txid

​ 1、epoch: leader的任期代号 康熙 雍正 乾隆

​ 2、txid: 当前 leader 在任期间执行的事务给定的一个全局唯一编号

ZooKeeper 底层分布式一致性算法实现: ZAB 分布式一致性协议

Etcd 底层分布式一致性算法实现:raft 分布式共识算法

4. 抽屉原理/鸽巢原理

鸽巢原理,又名狄利克雷抽屉原理、鸽笼原理。

其中一种简单的表述法为:

  • 若有 n 个笼子和 n+1 只鸽子,所有的鸽子都被关在鸽笼里,那么至少有一个笼子有至少 2 只鸽子。

另一种为:

  • 若有 n 个笼子和 kn+1 只鸽子,所有的鸽子都被关在鸽笼里,那么至少有一个笼子有至少 k+1 只鸽子。

image-20210902112831959

为什么从抽屉原理说起?一来大家对这个比较熟悉,也容易理解,二来它与 Quorum 机制有异曲同工的地方。

回顾抽屉原理,2 个抽屉每个抽屉最多容纳 2 个苹果,现在有 3 个苹果无论怎么放,其中一个抽屉里面肯定会至少有 2 个苹果。那么我们把抽屉原理变变 型,2 个抽屉一个放了 2 个红苹果,另一个放了 2 个青苹果,我们取出 3 个苹果,无论怎么取至少有 1 个是红苹果,这个理解起来也很简单。我们把红 苹果看成更新了的有效数据,青苹果看成未更新的无效数据。便可以看出来,不需要获取全部数据(并非全部是红苹果)我们就可以得到有效数据,当然 我们需要读取多个数据副本完成(取出多个苹果)。

5. Quorum NWR 机制

Quorum NWR:Quorum 机制是分布式场景中常用的,用来保证数据安全,并且在分布式环境中实现最终一致性的投票算法。这种算法的主要原理来源于鸽巢原理。它最大的优势,既能实现强一致性,而且还能自定义一致性级别!

  • Write to all copies with latest version N, wait synchronously for W success Read from all copies, wait for first R responses, pick the highest version number

**N:**复制的节点数,即一份数据被保存的副本数。

**W:**写操作成功的节点数,即每次数据写入写成功的副本数。W 肯定是小于等于 N 的。

**R:**读操作获取最新版本数据所需的最小节点数,即每次读取成功至少需要读取的副本数。

重要结论:这三个因素决定了可用性,一致性 和 分区容错性。只要保证(W + R > N)就一定能读取到最新的数据,数据一致性级别完全可以根据读写副 本数的约束来达到强一致性!

分以下三种情况讨论:前提,当 N 已经固定了。

  • **W = 1, R = N,Write Once Read All:**WORA(Write One Read All) 是一种简单的副本控制协议,当 Client 请求向某副本写数据时(更新数据),只要有一个副本更新成功之后,这次写操作就可以算成功,否则视为失败。

在分布式环境中,数据写一份副本,那么写操作高效,但是如果要读取到最新数据,就必须要读取所有节点,然后取最新版本的值,这样做读操作效率低。数据副本少,所以分区容错性很差,副本间的一致性高,写可用性高,读可用性低

  • **R = 1, W = N, Read Only Write All:**WARO(Write All Read one) 是一种简单的副本控制协议,当 Client 请求向某副本写数据时(更新数据),只有 当所有的副本都更新成功之后,这次写操作才算成功,否则视为失败。

​ 在分布式环境中,数据写入多个副本,所有节点都同步完毕,才能读取,所以只要读取任意一个节点就可以读取到最新数据。读操作高效,但是写操作效率低。因为副本个数多,所以分区容错性好,但是一致性差,实现难度更高,读可用性高,写可用性低。

  • W = Q, R = Q where Q = N/2 + 1

​ 可以简单理解为写超过一半节点,那么读也要超过一半节点,才能读取到最新的数据副本,取得读写性能平衡。适用于读写平衡的应用场景。如 N=3, W=2, R=2,分区容错性和可用性都可接受,一致性都取得一个平衡。

举个栗子:

image-20210902112844572

​ 对于要读取到 data1 的最新数据: W=2, n=5 R = n-w+1 =4 一定能读取到最新数据

​ 对于要读取到 data3 的最新数据: W=4, n=5, R = n-w+1 =2 一定能读取到最新数据

​ 变形之后: R=n-w+1 ==> r+w>n

在分布式场景中,为了读取到一致性的数据,在读和写的个数中,我可以追求一个平衡

极端情况:

​ 1、如果 N=W,读取效率高,读取任意一个节点即可读取到最新数据,所以:读取效率高,写入效率低

​ 2、如果 N=R,写入效率高,因为数据只要写入任意一个节点即可,所以:写入效率高,读取效率低

​ 3、如果 R = W = N/2 + 1, 写入成功超过一半,读取成功也需要超过一半。达到读写平衡。

思考一个问题:在分布式系统中,肯定会通过数据副本协议来保证数据安全,提高容错性,那么系统的服务高可用性和数据的副本强一致性是否存在冲突呢?

  • 分布式系统为了提高容错,必然写入多个数据副本

  • 为了数据强一致性,必须保证所有数据副本都写入成功才能返回,必然消耗的资源和时间都增大

  • 写入的复杂度增加了,系统为了保证数据的多个副本的强一致性,所以系统的可用性没有那么好

6. CAP 定理和BASE理论

1. CAP 理论

CAP 理论:2000 年 7 月份被首次提出,CAP 理论告诉我们,一个分布式系统不可能同时满足 C,A,P 三个需求

  • C:Consistency,强一致性:分布式环境中多个数据副本保持一致

  • A:Availability,高可用性:系统提供的服务必须一直处于可用,对于用户的每一个操作请求总是能在有限时间内返回结果

  • P:Partition Tolerance 分区容错性:分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务

依据上述思考的分析可以得知,分布式系统必然要满足 P,但是不能同时满足 C,A,P,所以分布式系统就需要在 C 和 A 上做权衡。

既然一个分布式系统不能同时满足 C,A,P 三个需求,那么如何抉择呢?

  • 放弃 P:最简单的极端做法,就是放置在一个节点上,也就只有一个数据副本,所有读写操作就集中在一台服务器上,有单点故障问题。放弃 P 也 就意味着放弃了系统的可扩展性,所以分布式系统一般来说,都会保证 P

  • 放弃 A:一旦系统遇到网络分区或者其他故障时,服务需要等待一段时间,在等待时间内就无法正常对外提供服务,即服务不可用

  • 放弃 C:事实上,放弃一致性是指放弃数据的强一致性,而保留最终一致性,具体多久达到数据同步取决于系统的设计

image-20210902112907759

CAP 只能 3 选 2,因为在分布式系统中,容错性P肯定是必须有的,所以这时候无非就两种情况,网络问题导致要么错误返回,要么阻塞等待,前者牺牲 了一致性,后者牺牲了可用性。

经验总结:

  • 架构师不要花费精力浪费在设计同时满足 CAP 的分布式系统

  • 分区容错性往往是分布式系统必然要面对和解决的问题。所以架构师应该把精力放在如何根据业务特点在 A 和 C 之间寻求平衡。

  • 对于单机软件,因为不用考虑 P,所以肯定是 CA 型,比如 MySQL

  • 对于分布式软件,因为一定会考虑 P,所以又不能兼顾 A 和 C 的情况下,只能在 A 和 C 做权衡,比如 HBase, Redis 等。做到服务基本可用,并且数据最终一致即可。

所以,就产生了 BASE 理论。

2. BASE 理论

儒家思想的精髓:中庸

多数情况下,其实我们也并非一定要求强一致性,部分业务可以容忍一定程度的延迟一致,所以为了兼顾效率,发展出来了最终一致性理论 BASE,来自 ebay 的架构师提出。BASE理论全称:全称:Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。**核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。**一句话概括,做事别走极端,BASE 是对 CAP 理论中的 C 和 A 进行权衡得到的结果。

​ 不是强一致,而是最终一致

​ 不是高可用,而是基本可用。

  • Basically Available(基本可用):基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用响应时间的损失:出现故障或者高峰,查询结果可适当延长,以用户体验上限为主。功能上的损失:例如淘宝双11,为保护系统稳定性,正常下单,其他边缘服务可暂时不可用。

  • Soft State(软状态):软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。通俗的讲:允许存在不同节点同步数据时出现延迟,且出现数据同步延迟时存在的中间状 态也不会影响系统的整体性能

  • Eventually Consistent(最终一致):最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况,要求最终达到一致,而不是实时强一致

以后,开发分布式系统,根据业务和需求场景来决定到底追求高可用还是追求强一致性,或是做到平衡!举例:

image-20210902112924638

7. ZooKeeper 核心工作机制

ZooKeeper 是一个分布式协调服务,劝架者,仲裁机构。 多个节点如果出现了意见的不一致,需要一个中间机构来调停!

ZooKeeper 就是一个小型的议会!当分布式系统中的多个节点,如果出现不一致,则把这个不一致的情况往 ZooKeeper 中写。ZooKeeper 会给你返回写成功的响应。但凡你接收到成功的响应,意味着 ZooKeeper 帮你达成了一致!

ZooKeeper 以一个集群的方式对外提供协调服务,集群内部的所有节点都保存了一份完整的数据。其中一个主节点用来做集群管理提供写数据服务,其他的从节点用来同步数据,提供读数据服务。这些从节点必须保持和主节点的数据状态一致。

  • ZooKeeper 是对等架构,工作的时候,会举行选举,变成 Leader + Follower 主从架构,但是人人都有当 Leader 的机会。

  • **在 ZooKeeper 中,没有沿用 Master/Slave(主备)概念,而是引入了 Leader、Follower、Observer 三种角色概念。**通过 Leader 选举算法来选定一 台服务器充当 Leader 节点,Leader 机器为客户端提供读写服务,其他角色,也就是 Follower 提供读服务,在提供读服务的 Follower 和 Observer 中,唯一区别就是 Observer 不参与 Leader 选举过程,没有选举权和被选举权,因此 Observer 可以在不影响写性能情况下提高集群读性能。

  • ZooKeeper 系统还有一种角色叫做 Observer,Observer 和 Follower 最大的区别就是 Observer 除了没有选举权 和 被选举权以外,其他的和 Follower 完全一样

  • Observer 的作用是分担整个集群的读数据压力,同时又不增加分布式事务的执行压力,因为分布式事务的执行操作,只会在 Leader 和 Follower 中执行。Observer 只是保持跟 Leader 的同步,然后帮忙对外提供读数据服务,从而减轻 ZooKeeper 的数据读取服务压力。

  • ZooKeeper 系统中的 Leader 角色可以进行读,写操作,Follower 角色可以进行读操作执行,但是接收到写操作,会转发给 Leader去执行。 ZooKeeper 系统的 Leader 就相当于是一个全局唯一的分布式事务发起者,其他所有的 Follower 是事务参与者,拥有投票权

  • ZooKeeper 中的所有数据,都在所有节点保存了一份完整的。所以只要所有节点保持状态一致的话,肯定是所有节点都可以单独对外提供读服务的。

    ZooKeeper 集群中的所有节点的数据状态通过 ZAB 协议保持一致。ZAB 有两种工作模式:

    • 崩溃恢复:集群没有 Leader 角色,内部在执行选举,选举结束之后,Leader 和 Follower 执行状态同步,然后退出恢复模式
    • 原子广播:集群有 Leader 角色,Leader 通过 ZAB 原子广播协议主导分布式事务的执行,向所有的 Follower 节点,严格按照顺序广播事务
  • ZooKeeper 的所有事务操作在 Zookeeper 系统内部都是严格有序串行执行的。

  • ZooKeeper 系统虽然提供了存储系统,但是这个存储,只是为自己实现某些功能做准备的,而不是提供出来,给用户存储大量数据的,所以,切忌往 ZooKeeper 中存储大量数据。

  • ZooKeeper 提供了znode 节点的常规的增删改查操作,使用这些操作,可以模拟对应的业务操作,使用监听机制,可以让客户端立即感知这种变化。

  • **ZooKeeper 集群和其他分布式集群最大的不同,在于 ZooKeeper 是不能进行线性扩展的。**因为像 HDFS 的集群服务能力是和集群的节点个数成正 比,但是 ZooKeeper 系统的节点个数到一定程度之后,节点数越多,反而性能越差。

  • ZooKeeper 集群的最佳配置:比如 5,7,9,11,13 个这样的总结点数,Observer 若干,Observer 最好是在需要提升 ZooKeeper 集群服务能力的再进行扩展,而不是一开始就设置 Observer 角色!Follower 切记不宜太多!

  • ZooKeeper 系统如果产生了这么一种情况:某个 znode 的数据变化非常的快,每次变化触发一次 Watcher 的 process 方法回调!由于 ZooKeeper 执行事务的时候,是串行单节点严格有序执行的。Leader 负责这个事务的顺序执行。多个事件来不及执行,上一个事件还没有执行,下个动作触发,ZooKeeper 会忽略! 影响不大!

  • ZooKeeper 并不保证读取的是最新数据。如果客户端刚好链接到一个刚好没执行事务成功,也就是说没和 Leader 保持一致的 Follower 节点的话, 是有可能读取到非最新数据的。如果要保证读取到最新数据,请使用 sync 回调处理。这个机制的原理:是先让 Follower 保持和 Leader 一致,然后再返回结果给客户端。ZooKeeper 实现了 A 可用性、P 分区容错性、C 中的写入强一致性,丧失的是 C 中的读取一致性。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/555213
推荐阅读
相关标签
  

闽ICP备14008679号