目录
简介
zookeeper的三种角色
zookeeper两种模式
leader选举算法
API使用案例
zookeeper使用单一主进程Leader用于处理客户端所有事务请求,即写请求。当服务器数据发生变更后,集群采用ZAB原子广播协议,以事务提交proposal的形式广播到所有的副本进程,每一个事务分配 一个全局的递增的事务编号xid。
若客户端提交的请求为读请求时,则接受请求的节点直接根据自己保存的数据响应。若是写请求,且当 前节点不是leader,那么该节点就会将请求转发给leader,leader会以提案的方式广播此写请求,如果 超过半数的节点同意写请求,则该写请求就会提交。leader会通知所有的订阅者同步数据。
为了避免zk的单点问题,zk采用集群方式保证zk高可用
leader
leader负责处理集群的写请求,并发起投票,只有超过半数的节点同意后才会提交该写请求
follower
处理读请求,响应结果。转发写请求到leader,在选举leader过程中参与投票
observer
observer可以理解为没有投票权的follower,主要职责是协助follower处理读请求。那么当整个zk 集群读请求负载很高时,为什么不增加follower节点呢?原因是增加follower节点会让leader在提 出写请求提案时,需要半数以上的follower投票节点同意,这样会增加leader和follower的通信通 信压力,降低写操作效率。
恢复模式
当服务启动或领导崩溃后,zk进入恢复状态,选举leader,leader选出后,将完成leader和其他机 器的数据同步,当大多数server完成和leader的同步后,恢复模式结束
广播模式
一旦Leader已经和多数的Follower进行了状态同步后,进入广播模式。进入广播模式后,如果有 新加入的服务器,会自动从leader中同步数据。leader在接收客户端请求后,会生成事务提案广播 给其他机器,有超过半数以上的follower同意该提议后,再提交事务。
注意在ZAB的事务的二阶段提交中,移除了事务中断的逻辑,follower要么ack,要么放弃,leader无需等待所有的follower的ack。
zxid
zxid是64位长度的Long类型,其中高32位表示纪元epoch,低32位表示事务标识xid。即zxid由两部分 构成:epoch和xid
每个leader都会具有不同的epoch值,表示一个纪元,每一个新的选举开启时都会生成一个新的 epoch,新的leader产生,会更新所有的zkServer的zxid的epoch,xid是一个依次递增的事务编号。
选取原则
zookeeper集群中 只有超过半数以上的服务器启动,集群才能正常工作
在集群在正常工作之前,myid小的服务器会给myid大的服务器进行投票,持续到集群正常工作,选出leader
选择leader之后,之前的服务器状态由looking改变为following,以后得服务器都是follower
启动过程
每一个server发出一个投票给集群中其他节点
收到各个服务器的投票后,判断该投票有效性,比如是否是本轮投票,是否是 looking状态
处理投票,pk别人的投票和自己的投票 比较规则xid>myid “取大原则”
统计是否超过半数的接受相同的选票
确认leader,改变服务器状态
添加新server,leader已经选举出来,只能以follower身份加入集群中
崩溃恢复过程
leader挂掉后,集群中其他follower会将状态从following变为looking,重新进入leader选举
同上启动过程
消息广播算法
一旦进入广播模式,集群中非leader节点接受到事务请求,首先会将事务请求转发给服务器,leader服 务器为其生成对应的事务提案proposal,并发送给集群中其他节点,如果过半则事务提交;
leader接受到消息后,消息通过全局唯一的64位自增事务id,zxid标识
leader发送给follower的提案是有序的,leader会创建一个FIFO队列,将提案顺序写入队列中发送 给follower
follower接受到提案后,会比较提案zxid和本地事务日志最大的zxid,若提案zxid比本地事务id 大,将提案记录到本地日志中,反馈ack给leader,否则拒绝
leader接收到过半ack后,leader向所有的follower发送commit,通知每个follower执行本地事务
API | 功能 |
---|---|
ZooKeeper zk = new ZooKeeper(String connectString, int sessionTimeout,Watcher watcher) | 创建zookeeper连接,connectString表示连接的zookeeper服务器的 地址,sessionTimeOut指定会话的的超时时间,Watcher配置监听 |
String create(String path, byte[] data, List acl,CreateMode createMode) | 创建一个给定的目录节点 path, 并给它设置数据,CreateMode 标识有 四种形式的目录节点,分别是 PERSISTENT:持久化目录节点,这个目 录节点存储的数据不会丢失;PERSISTENT_SEQUENTIAL:顺序自动编 号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1, 然后返回给客户端已经成功创建的目录节点名;EPHEMERAL:临时目 录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超 时,这种节点会被自动删除;EPHEMERAL_SEQUENTIAL:临时自动编号节点 |
Stat exists(String path, boolean watch) | 判断某个 path 是否存在,并设置是否监控这个目录节点,这里的 watcher 是在创建 ZooKeeper 实例时指定的 watcher,exists方法还 有一个重载方法,可以指定特定的watcher |
Stat exists(String path,Watcher watcher) | 重载方法,这里给某个目录节点设置特定的 watcher,Watcher 在 ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化 以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置 在这个目录节点上的 Watcher,从而每个客户端都很快知道它所关注 的目录节点的状态发生变化,而做出相应的反应 |
void delete(String path, int version) | 删除 path 对应的目录节点,version 为 -1 可以匹配任何版本,也就删 除了这个目录节点所有数据 |
ListgetChildren(String path, boolean watch) | 获取指定 path 下的所有子目录节点,同样 getChildren方法也有一个 重载方法可以设置特定的 watcher 监控子节点的状态 |
Stat setData(String path, byte[] data, intversion) | 给 path 设置数据,可以指定这个数据的版本号,如果 version 为 -1 怎可以匹配任何版本 |
byte[] getData(String path, boolean watch,Stat stat) | 获取这个 path 对应的目录节点存储的数据,数据的版本等信息可以通 过 stat 来指定,同时还可以设置是否监控这个目录节点数据的状态 |
引入依赖
org.apache.zookeeper zookeeper 3.4.13 junit junit 4.12
测试类
public class zkApiTest { private static final String connectString = "192.168.193.XXX:2181";//默认端口2181 private static final int sessionTimeout = 2000; private static ZooKeeper zk = null; @Before public void init() throws IOException { // //创建zookeeper的连接 zk = new ZooKeeper(connectString,sessionTimeout,(e)->{ System.out.println("触发了"+e.getType()+"事件");//Watcher }); } @Test public void testZkApi() throws KeeperException, InterruptedException { // 创建父节点 String path = zk.create("/testFather", "v1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println("创建父节点:"+path); //创建子节点 String childrenPath = zk.create("/testFather/children", "v2".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println("创建子节点:"+childrenPath); //判断节点是否存在 如果指定路径的节点存在返回节点的状态,如果节点不存在返回null Stat exists = zk.exists("/testFather", true); System.out.println("判断/testFather节点是否存在:"+exists); //获取指定路径下的所有子节点的名称 Listchildren = zk.getChildren("/testFather", true); for (int i = 0; i < children.size(); i++) { String node = children.get(i); System.out.println(node); } //设置节点的数据 Stat stat1 = zk.setData("/testFather/children", "update".getBytes(), -1); System.out.println("/testFather/children节点数据为:"+stat1); //获取节点的数据 byte[] data = zk.getData("/testFather/children", true, new Stat()); System.out.println("/testFather/children节点数据为:"+new String(data)); //删除节点 zk.delete("/testFather/children",-1); //判断节点是否存在 如果指定路径的节点存在返回节点的状态,如果节点不存在返回null Stat exists1 = zk.exists("/testFather/children", true); System.out.println("判断/testFather/children节点是否存在:"+exists1); } }
运行结果
触发了None事件 创建父节点:/testFather 创建子节点:/testFather/children 判断/testFather节点是否存在:4294967365,4294967365,1681723459811,1681723459811,0,1,0,0,2,1,4294967366 children /testFather/children节点数据为:4294967366,4294967367,1681723459828,1681723459872,1,0,0,0,6,0,4294967366 /testFather/children节点数据为:update 触发了NodeDeleted事件 触发了NodeChildrenChanged事件 判断/testFather/children节点是否存在:null