概念:
zookeeper是一个开源的基于Google chubby的分布式协调框架,主要用于分布式数据一致性解决方案,zookeeper可以提供配置中心服务、数据的发布和订阅、集群管理保证集群的数据强一致性、负载均衡、选举、分布式锁、分布式队列等功能。
zookeeper的特性:
- 顺序性:客户端发起的请求会按照严格的顺序应用到zookeer中
- 原子性:所有的事务请求在集群的所有的机器处理的结果是一致的,使整个集群的所有的机器要么都成功的应用了某一事务,要么全部都没有应用某一事务
- 可靠性:集群的机器器应用了某一数据并成功的响应了客户端后,数据会在整个集群中同步的保存下来,保证数据不丢失
- 数据一致性:一旦集群的服务器应用了某一事务后,客户端就能够立即从服务器端读取事务变更后的最新数据状态,zookeeper的实时性重点的之处在于只是在一定时间进行,允许分布式数据经过一个时间窗口达到最终一致,这个时间窗口也是差不多接近实时的。
zookeeper集群为何是奇数?
1、容错:在容错能力相同的情况下,奇数台更节省资源。
由于在增删改操作中需要半数以上服务器通过,来分析以下情况。
2台服务器,至少2台正常运行才行(2的半数为1,半数以上最少为2),正常运行1台服务器都不允许挂掉
3台服务器,至少2台正常运行才行(3的半数为1.5,半数以上最少为2),正常运行可以允许1台服务器挂掉
4台服务器,至少3台正常运行才行(4的半数为2,半数以上最少为3),正常运行可以允许1台服务器挂掉
5台服务器,至少3台正常运行才行(5的半数为2.5,半数以上最少为3),正常运行可以允许2台服务器挂掉
6台服务器,至少3台正常运行才行(6的半数为3,半数以上最少为4),正常运行可以允许2台服务器挂掉
通过以上可以发现,3台服务器和4台服务器都最多允许1台服务器挂掉,5台服务器和6台服务器都最多允许2台服务器挂掉
但是明显4台服务器成本高于3台服务器成本,6台服务器成本高于5服务器成本。这是由于半数以上投票通过决定的。
2、防止由脑裂造成的集群不可用。
首先,什么是脑裂?集群的脑裂通常是发生在节点之间通信不可达的情况下,集群会分裂成不同的小集群,小集群各自选出自己的master节点,导致原有的集群出现多个master节点的情况,这就是脑裂。
下面举例说一下为什么采用奇数台节点,就可以防止由于脑裂造成的服务不可用:
(1) 假如zookeeper集群有 5 个节点,发生了脑裂,脑裂成了A、B两个小集群:
(a) A : 1个节点 ,B :4个节点
(b) A : 2个节点, B :3个节点
可以看出,上面这两种情况下,A、B中总会有一个小集群满足 可用节点数量 > 总节点数量/2 。所以zookeeper集群仍然能够选举出leader , 仍然能对外提供服务,只不过是有一部分节点失效了而已。
(2) 假如zookeeper集群有4个节点,同样发生脑裂,脑裂成了A、B两个小集群:
(a) A:1个节点 , B:3个节点
(b) A:2个节点 , B:2个节点
可以看出,情况(a) 是满足选举条件的,与(1)中的例子相同。 但是情况(b) 就不同了,因为A和B都是2个节点,都不满足 可用节点数量 > 总节点数量/2 的选举条件, 所以此时zookeeper就彻底不能提供服务了。
综合上面两个例子可以看出: 在节点数量是奇数个的情况下, zookeeper集群总能对外提供服务;如果节点数量是偶数个,会存在zookeeper集群不能用的可能性,脑裂成两个均等的子集群的时候。
在生产环境中,如果zookeeper集群不能提供服务,那将是致命的 , 所以zookeeper集群的节点数一般采用奇数个。
最典型集群模式:Master/Slave 模式(主备模式)
-
Leader:负责进行投票的发起和决议,分布式读写,更新请求转发;
-
Follower:负责接收客户端请求并向客户端返回结果,在选举Leader过程中参与投票;
在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三种角色。
image.png
ZooKeeper 集群中的所有机器选举过程来选定 Leader
Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。
observer的行为在大多数情况下与follower完全一致,Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,他只接受读请求,将写请求转发给leader,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。
数据一致和分区容错
ZAB 协议
ZAB(ZooKeeper Atomic Broadcast 原子广播)协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。
ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
这里所说的主备系统架构模型是指,在zookeeper集群中,只有一台leader负责处理外部客户端的事物请求(或写操作),然后leader服务器将客户端的写操作数据同步到所有的follower节点中。
ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播:
ZAB的协议核心是在整个zookeeper集群中只有一个节点即Leader将客户端的写操作转化为事物(或提议proposal)。Leader节点再数据写完之后,将向所有的follower节点发送数据广播请求(或数据复制),等待所有的follower节点反馈。在ZAB协议中,只要超过半数follower节点反馈OK,Leader节点就会向所有的follower服务器发送commit消息。即将leader节点上的数据同步到follower节点之上。
当整个服务框架在启动过程中,或是当Leader服务器出现网络中断崩溃退出与重启等异常情况时,ZAB就会进入崩溃恢复模式并选举产生新的Leader服务器。
当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出崩溃恢复模式,进入消息广播模式。
当新的机器加入到集群中的时候,如果已经存在leader服务器,那么新加入的服务器就会自觉进入崩溃恢复模式,找到leader进行数据同步。
以上其实大致经历了三个步骤:
- 1.崩溃恢复:主要就是Leader选举过程
- 2.数据同步:Leader服务器与其他服务器进行数据同步
- 3.消息广播:Leader服务器将数据发送给其他服务器
消息广播模式
ZAB协议的消息广播过程使用的是一个原子广播协议,类似二阶段提交2PC。
一阶段:发送Proposal,客户端执行事务
客户端的请求时,而在第一阶段,Leader机器会将事物消息生成对应的Proposal进行广播,并且会生成一个单调递增的ID,作为事物的ID(ZXID),由于ZAB协议需要保证事物严格的上下顺序性,所以会严格按照ZXID的先后来处理对应的消息。而在消息广播发起后,Leader会为每一个Follower服务器分配一个单独的队列,然后将需要广播出去的事物Proposal依次存放进这个FIFO的队列中,每个Follower机器收到事物消息后,会按照事物日志的方式写入,成功后反馈给Leader机器Ack响应。
二阶段:半数提交事务
当收到半数以上的Ack响应后,Leader就会发起第二阶段的Commit消息给所有的Follower机器,并且这个时候Leader也会和Follower一样进行本地事物的提交操作,完成整个消息的传递和提交。
整个步骤如下:
- 客户端发起一个写操作请求
- Leader服务器将客户端的request请求转化为事物proposql提案,同时为每个proposal分配一个全局唯一的ID,即ZXID。
- leader服务器与每个follower之间都有一个队列,leader将消息发送到该队列
- follower机器从队列中取出消息处理完(写入本地事物日志中)毕后,向leader服务器发送ACK确认。
- leader服务器收到半数以上的follower的ACK后,即认为可以发送commit
- leader向所有的follower服务器发送commit消息。
崩溃恢复:
崩溃恢复包括两部分:Leader选举和 数据恢复
ZAB协议崩溃恢复要求满足如下2个要求:
- 确保已经被leader提交的proposal必须最终被所有的follower服务器提交。
- 确保丢弃已经被leader发出但是没有被提交的proposal。
根据上述要求,新选举出来的leader不能包含未提交的proposal,即新选举的leader必须都是已经提交了的proposal的follower服务器节点。同时,新选举的leader节点中含有最高的ZXID。这样做的好处就是可以避免了leader服务器检查proposal的提交和丢弃工作
ZXID
zxid,也就是事务id,为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid,实际中zxid是一个64位的数字,高32(左)位是epoch,低32位(右)用于递增计数。
- epoch:
ZAB协议通过epoch编号来区分Leader周期变化的策略,用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch=(原来的epoch+1),标识当前属于那个leader的统治时期。可以理解为当前集群所处的年代或者周期,每个leader就像皇帝,都有自己的年号,所以每次改朝换代,leader变更之后,都会在前一个年代的基础上加1。这样就算旧的leader崩溃恢复之后,也没有人听他的了,因为follower只听从当前年代的leader的命令。 - 计数器:
低 32 位是一个简单的单调递增的计数器,针对客户端每个事务请求,计数器加 1
leader服务器发生崩溃时分为如下场景:
- 1、leader在提出proposal时未提交之前崩溃。
当leader接收到消息请求生成proposal后就挂了,其他follower并没有收到此proposal,因此经过恢复模式重新选了leader后,这条消息是被跳过的。 此时,之前挂了的leader重新启动并注册成了follower,他保留了被跳过消息的proposal状态,与整个系统的状态是不一致的,需要将其删除。leader都换代了,所以以前leader的proposal失效了。 -
2、leader在发送commit消息之后,崩溃。
当leader收到合法数量follower的ack后,就向各个follower广播commit命令,同时也会在本地执行commit并向连接的客户端返回「成功」。但是如果各个follower在收到commit命令前leader就挂了,导致剩下的服务器并没有执行到这条消息。leader对事务消息发起commit操作,该消息在follower1上执行了,但是follower2还没有收到commit,leader就已经挂了,而实际上客户端已经收到该事务消息处理成功的回执了。所以在zab协议下需要保证所有机器都要执行这个事务消息,必须满足已经被处理的消息不能丢失。 还没有所有的follower提交这个事务,leader就挂了
Zab的解决机制:
1、新选举出来的 Leader 不能包含未提交的 Proposal :
新选举的 Leader 必须都是已经提交了 Proposal 的 Follower 服务器节点。老的leader就算是及时恢复也不会被选为leader因为它有为提交的Proposal
2、新选举的 Leader 节点中含有最大的 zxid :
如果leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高编号(ZXID 最大)的事务Proposal,那么就可以保证这个新选举出来的leader一定具有已经提交的提案。因为所有提案被commit之前必须有超过半数的follower ack,即必须有超过半数节点的服务器的事务日志上有该提案的proposal,因此只要有合法数量的节点正常工作,就肯定有一个节点保存了所有被commit消息的proposal状态。
3、新Leader 选出后:epoch +1、低32为清零、旧的proposal回滚
每当选举产生一个新的 Leader ,就会从这个 Leader 服务器上取出本地事务日志充最大编号 Proposal 的 zxid,并从 zxid 中解析得到对应的 epoch 编号,然后再对其加1,之后该编号就作为新的 epoch 值,并将低32位数字归零,由0开始重新生成zxid。
当一个包含了上一个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,当这台机器加入集群中,以 Follower 角色连上 Leader 服务器后,Leader 服务器会根据自己服务器上最后提交的 Proposal 来和 Follower 服务器的 Proposal 进行比对,比对的结果肯定是 Leader 要求 Follower 进行一个回退操作,回退到一个确实已经被集群中过半机器 Commit 的最新 Proposal。
Zab 数据同步
-
1、完成 Leader 选举后(新的 Leader 具有最高的zxid),在正式开始工作之前(接收事务请求,然后提出新的 Proposal),Leader 服务器会首先确认事务日志中的所有的 Proposal 是否已经被集群中过半的服务器 Commit。
-
2、Leader 服务器需要确保所有的 Follower 服务器能够接收到每一条事务的 Proposal ,并且能将所有已经提交的事务 Proposal 应用到内存数据中。等到 Follower 将所有尚未同步的事务 Proposal 都从 Leader 服务器上同步过啦并且应用到内存数据中以后,Leader 才会把该 Follower 加入到真正可用的 Follower 列表中。
leader选举
服务器启动时期的Leader选举
假设我们拥有五台zookeeper机器,分别为1,2,3,4,5,启动顺序依次排列,他们的myid分别为1,2,3,4,5,由于是服务器初始化启动,原始数据皆为空,所以ZXID都为0,投票的格式为[myid,zxid];
此时,先启动机器1,机器1首先投票给自己,选举自己为Leader,记为[1,0],由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking(选举状态);
启动机器2,首先投票给自己,选举自己为Leader,记为[2,0],此时接收到其他机器(机器1)的投票[1,0],与自己的投票进行对比,发现ZXID都一样,都是0,则去对比myid大小,发现机器2的myid大,则机器2的投票不变,通知机器1更改投票结果,此时机器1,2投票都为[2,0],但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING;
启动机器3,首先投票给自己,选举自己为Leader,记为[3,0],此时接收到其他机器(机器1,2)的投票[(2,0),(2,0)],与自己的投票进行对比,发现ZXID都一样,都是0,则去对比myid大小,发现机器3的myid大,则机器3的投票不变,通知机器1,机器2更改投票结果,此时机器1,2投票更改为[3,0],此时超过了半数机器支持机器3作为Leader,此时选举结束,机器3成为Leader,机器1,2作为Follower,此时机器3是LEADING状态,机器1,2是FOLLOWING状态;
启动机器4,首先投票给自己,选举自己为Leader,记为[4,0],此时接收到其他机器(机器1,2,3)的投票[(3,0),(3,0),(3,0)],与自己的投票进行对比,发现ZXID都一样,都是0,则去对比myid大小,尽管发现机器4的myid大,但是因为第三部时已经选举出来机器3作为Leader,所以机器4只能作为Follower,所以机器4是FOLLOWING状态;
启动机器5,结果同机器4;
其他的observer机器是OBSERVING状态,不参与投票
服务器运行时期的Leader选举
在集群运行期间,有可能会发生新的节点加入或者Leader宕机的情况,如果是新节点加入,不会影响到Leader的更替(参见上面启动机器4,5的情况),但如果是Leader机器宕机,则需要重新选举Leader;
当发现Leader宕机,所有的非observer的机器状态都会变成LOOKING,同时首先投票给自己选举自己作为Leader,但此时有可能会出现不同机器的数据新旧不一致,大体步奏和服务器启动时期的Leader选举一致,唯一不同的是ZXID不同,需要选取ZXID最大的并且不包含未提交的Proposal的Follower作为Leader
zookeeper的读写流程
以3台服务器的Zookeeper集群为例,一个Leader,两个Follower即server1和server2
image.png写流程
(1)Client向Zookeeper的server1发送一个写请求,客户端写数据到服务器1上;
(2)如果server1不是Leader,那么server1会把接收到的写请求转发给Leader;然后Leader会将写请求转发给每个server;server1和server2负责写数据,并且每个server的写入数据是一致的,保存相同的数据副本;server1和server2写数据成功后,通知Leader,此时server并没有提交事务;
(3)Leader收到集群半数以上的节点的写操作成功的ACK,这时,客户端就判定整个集群写操作成功了,就通知各个server提交事务写操作成功。否则回滚事务,写操作失败。
读流程
相比写数据流程,读数据流程就简单得多;因为每台server中数据一致性都一样,所以随便访问哪台server读数据就行;没有写数据流程中请求转发、数据同步、成功通知这些步骤。
zookeeper的数据模型
1、zookeeper的数据模型是一个树形结构,Zookeeper将数据存储于内存中,Znode是存储数据的最小单元。而Znode以层次化的结构进行组织,形成一棵树。其对外提供的视图类似于Unix文件系统。树的根Znode节点相当于Unix文件系统的根路径。正如Unix中目录下可以有子目录一样,znode结点下也可以挂载子结点,最终形成如下所示结构。
2、每一个znode节点都有各自的版本号, 可以通过命令行来显示节点信息,每当节点数据发生变化, 那么该节点的版本号会累加(乐观锁),删除/修改过时节点, 版本号不匹配会报错,每个znode存储的数据不宜过大, 几k即可节点可以设置权限acl, 可以通过权限来限制用户的访问
znode的类型
-
持久节点:最常见的Znode类型,一旦创建将在一直存在于服务端,除非客户端通过删除操作进行删除。持久结点下可以创建子结点。
- 持久有序节点:在具有持久结点基本特性的基础上,会通过在结点路径后缀一串序号来区分多个子结点创建的先后顺序。这工作由Zookeeper服务端自动给我们做,只要在创建Znode时指定结点类型为该类型。 image.png
-
临时节点:临时结点的生命周期和客户端会话保持一致。客户端段会话存在的话临时结点也存在,客户端会话断开则临时结点会自动被服务端删除。临时结点下不能创建子结点。
-
临时有序节点:具有临时结点的基本特性,又有顺序性。
session会话的基本原理
sessions是服务端与客户端连接的会话,存在着固定的超时时间,超时则session被清除,客户端可以向服务端发送ping包去保持有效的session,也就是心跳机制,另外客户端的心跳结束也会造成session被清除,session过期的同时会删除相应的临时节点。
wathcer机制
zk可以通过wathcer来实现事件的监听机制,客户端可以向服务端注册一个wathcer来监听某一事件,客户端操作zk的节点完成之后就会触发wathcer事件,当监控的znode对象发生了变化就会去触发wathcer,wathcer类似数据库的触发器,但是这个触发器是一次性的,在进行触发后就会被销毁,但是也可以通过一些Zookeeper的开源客户端设置其为永久就不会被销毁,Watcher是Zookeeper原生API中提供的事件监听接口,用户要实现事件监听必须实现该接口并重写process(WatchedEvent event)方法,该方法定义了客户端在接收到服务端事件通知后的回调逻辑。究竟服务端的什么事件可以被监听?按通知状态划分有SyncConnected,Disconnected,Expired,AuthFailed等好多种,这里主要讲下SyncConnected状态下的几种事件类型:
-
Node(-1)
客户端与服务端成功建立会话 -
NodeCreated(1)
Watcher监听的对应Znode被创建 -
NodeDeleted(2)
Watcher监听的Znode被删除 -
NodeDataChanged(3)
Watcher监听的Znode的数据内容被改变,注意即使变更前后的数据内容完全一样也会触发该事件,或者理解成该事件的触发条件是Znode的版本号变更也没问题 -
NodeChildrenChanged(4)
Watcher监听的对应Znode的子结点发生变更
zookeeper的权限控制
直接看博客吧,不想写了
https://www.jianshu.com/p/4aa232ebf1cf
zookeeper的常用命令
先启动zk,配置好zk的环境变量,启动zk服务,和进入zk客户端
image.png image.png
help看看zk有什么命令
image.png
-
ls ls2 stat get 查看zk的目录
ls /
ls2 / 、stat / 、 get / 查看目录及状态消息
image.png image.png image.png
其状态的含义:
- cZxid :创建节点的id
- ctime : 节点的创建时间
- mZxid :修改节点的id
- mtime :修改节点的时间
- pZxid :子节点的id
- cversion : 子节点的版本
- dataVersion : 当前节点数据的版本
- aclVersion :权限的版本
- ephemeralOwner :判断是否是临时节点
- dataLength : 数据的长度
- numChildren :子节点的数量
增删改命令
create:创建、set:设置、delete:删除
create /xxxx xxxx 创建一个默认的临时节点并赋值,此时节点的cversion 还是0
create -e /xxxx xxxx 创建一个临时节点并赋值,这个临时节点在session会话过期或者是固定时间内客户端没有发送心跳包就会自动清除,创建临时节点后根节点的cversion加一了
image.pngcreate -a /xxxxx xxxxx 创建一个顺序节点
image.pngset /xxxx xxxx 修改设置一个节点的值 在没有set值之前dataVersion还是为0的,使用了set命令之后 dataVersion进行了加一操作
image.png
set /xxxx xxxx x 带版本号的set操作,版本号必须与节点的版本一致,版本号不一致则报错
delete /xxxx 删除某节点 delete /xxxx x带着版本号进行删除,版本号不一致则报错
watcher事件命令
通过 get /xxxx watch 、ls /xxxx watch 、stat /xxxx wacth设置事件
ls /xxxx watch 为节点设置事件,创建子节点时触发了NodeChildrenChanged事件,但是,在做set命令时没有触发,原因在于watch触发一次就会被销毁
image.png我们用 get /xxxx watch再次设置watch事件,在做set命令时触发了NodeDataChanged事件
image.png要注意的是如果修改子节点时,是不会触发NodeChildrenChanged,要触发NodeChildrenChanged必须使用 get 命令对子节点设置watch
我们看到此时并没有产生NodeChildrenChanged事件
image.png当我们用get命令对子节点设置了事件后,就触发了NodeChildrenChanged事件
image.pngzk集群
在集群中哪个节点先启动哪个节点就是master节点
集群最少是三台机子,一般集群数量都是为奇数台,也就是3、5、7、9.....
接下来我们要搭建一个伪分布式集群
先拷贝三个zk安装目录
image.png
配置安装目录下的zoo.cfg 在配置文件中最底下的三个ip和端口是,例如:server.1=192.168.200.128:2888:3888 server.1:代表第一台机子,第一台机子是默认是集群的master 192.168.200.128:j机子的ip 2888:节点同步数据的端口号 3888:选举的端口号
image.png配置好zoo.cfg后,需要在zoo.cfg里配置的dataDir配置的位置增加一个myid文件配置,myid中配置相应的serverid,然后其他节点以此类推
image.png image.png最后启动各个zk,查看状态
image.png测试,在主节点创建一个节点,数据同步到了其他节点,其他的节点修改了相应的数据时,各个节点也会跟着改变,各个节点的数据是保存一致的,伪分布式搭建成功
真分布式集群搭建
买了台新电脑,配置够开好几个虚拟机了,哈哈哈
image.png三台机子的ip
image.png
配置第一台机子,myid依然是1,
image.png
第二台机子的zoo.cfg配置是一样的,只有改 dataLogDir和dataDir的位置就好了,myid设置为2,所以第三台机子也以此类推
配置好后,我把02先启动,02成为了主节点,其他的01 03都是从节点
image.png测试时,任意节点修改后每个节点的数据依旧是保持一致的,所以真分布式集群搭建成功
zookeeper的选举
完成真分布式搭建后,我们把02主节点关机,让01 03进行选举
关闭02的电源
我们可以看到,01依旧是从节点,但是03已经成为了主节点了
image.png
然后我们在重新把02开启,并且启动zk,此时我们发现02变成了从节点
网友评论