Loading... # zookeeper搭建、PAXOS、ZAB及基本API ## 官网 http://zookeeper.apache.org/ ## 架构 ![01.png][1] 如图,zookeeper是一套分布式架构,分布式有复制与分片两个维度,zookeeper属于复制集群,也就是说,集群中所有的节点都保留全部的数据,但是zookeeper中是存在leader的,leader可以读写,follower则只能读,写操作会传递给leader执行。zookeeper是一个主从集群,只要有主的存在就会有单点问题。 1. 只要存在主,那么主就必然有故障的时候 2. 主节点挂掉就会造成服务不可用 3. 服务不可用也就是集群不可靠 4. 但是事实上zk集群极其高可用 以前,我们做HA都是通过第三方进程来监控主节点的状态,代替人来进行换主的操作,类似keepalived和redis的哨兵,但是zookeeper并没有第三方进程来监控它,如果有一个方式可以在leader挂掉之后快速恢复出一个leader,那么就可以保证集群的高可用性了。因此zk在提供服务时,可能有两种状态: - 可用状态(leader存在) - 不可用状态(leader挂掉) zookeeper从不可用状态恢复到可用状态(选举出新的leader)的时间小于200ms。 ## 数据结构 ![02.png][2] zookeeper是以目录树的方式存储数据的,数据存储于内存中,因为目录树层级关系,因此有命名空间的概念,类似文件夹及其子文件(夹),不同于文件系统中文件夹上不能存文本,zookeeper中每个节点都可以存文本且可以包含子节点。zookeeper中每个节点存储数据大小上限是1MB,如此设计的初衷就是作者并不希望我们将zookeeper用作数据库,zookeeper的绝大多数情况下应该做读操作而不应该做写操作,这与它数据复制的方式有关。 ### 节点类型 zookeeper有两种节点类型,持久节点与临时节点,同时两种类型又分别存在普通节点与序列节点两种模式。持久节点顾名思义就是创建之后除非主动删除,否则就会一直存在,而临时节点依托于session,每个client连接zookeeper都会生成一个session,当连接断开时,session失效,同时临时节点被删除,由于zookeeper的统一视图,集群中每台zookeeper上都会存有所有client的session以保证leader挂掉重新选出leader后,session不会失效。序列节点就是创建相同名称的节点时,不会失败也不会覆盖,zookeeper会在名称后面添加一串递增的数字。 ## 特征/保障 ZooKeeper非常快速且非常简单。但是,由于其目标是作为构建更复杂的服务(例如分布式锁)的基础,因此它提供了一组保证。这些是: - 顺序一致性-来自客户端的更新将按照其发送顺序执行。 - 原子性-更新要么成功,要么失败,没有中间结果。 - 单个系统映像-无论客户端连接到哪个服务器,客户端都将看到相同的服务视图。 - 可靠性-应用更新后,此更新将一直持续到客户端覆盖更新为止。 - 及时性-确保系统的客户视图在特定时间范围内是最新的(最终一致性)。 ## 安装 以4台服务器作为示例(实际应用中最好是奇数台) 首先去官网下载zookeeper,上传服务器并解压,比如我解压到根目录/root/apache-zookeeper-3.5.6-bin 修改环境变量 ``` export ZOOKEEPER_HOME=/root/apache-zookeeper-3.5.6-bin ``` 在export PATH=这行的最后面追加 ``` :$ZOOKEEPER_HOME/bin ``` 修改配置文件 ``` cd ./root/apache-zookeeper-3.5.6-bin/conf cp zoo_sample.cfg zoo.cfg vi zoo.cfg ``` tickTime表示心跳的时间间隔,initLimit表示初始连接时在该数值乘以tickTime的时间内follwer没有连接上leader,则连接失败,syncLimit表示节点在syncLimit乘以tickTime的时间内follower没有与leader进行通信就放弃这个follower。 修改dataDir ``` dataDir=/var/zookeeper/zk ``` 添加节点信息 ``` server.1=node01:2888:3888 server.2=node02:2888:3888 server.3=node03:2888:3888 server.4=node04:2888:3888 ``` 所有节点,有几台写几条,每台需要监听两个端口,2888是leader接收follower的write请求的端口,3888是选举要用到的端口。 创建dataDir指定的目录 ``` mkdir -p /var/zookeeper/zk ``` 在dataDir中创建一个myid文件,内容是当前服务器配置的server的序号,比如node1是server.1=node01:2888:3888,所以node01的myid中就写1,server.几就是几。 ``` echo 1 > /var/zookeeper/zk/myid ``` 其它几台服务器做相同的配置,只有myid文件中的数字不一样。 启动,在4台机器上执行 ``` zkServer.sh start ``` 操作 ``` zkCli.sh ``` 基本命令 ``` help ls / create /aaa # 创建持久节点 create -s /abc/aaa # 创建序列节点 sequential create -e /abc/aaa # 创建临时节点 ephemeral create -s -e /abc/aaa # 创建临时序列节点 get /aaa # 获取节点上的数据 ``` ## zookeeper通信 在4台机器上分别执行 ``` netstat -natp | egrep '(2888|3888)' ``` 查看2888和3888的占用 发现node01上监听了一个3888端口,并且node02-node04连接上了该端口,然后node01本地开启了一个端口练到了node04的2888,因此,node04一定是leader,而node02上监听了3888,并且node03和node04连接了该端口,node02开启了两个端口分别连接了node01的3888与node04的2888,node03上监听了3888,但是没有人连接它,而是它分别连接了node01,02,04的3888并且连接了04的2888,node04同时监听了2888与3888,2888同时被其他3台连接,3888被node03连接,它连接了node01与02的3888,也就是说zookeeper的所有节点之间都是两两连接的。 ![03.png][3] ## 分布式协调 ### 扩展性 zookeeper有三种角色,leader、follower、observer,读写分离,只有leader可以写,follower和observer只能读,只有follower可以参加选举,observer只用来放大查询性能。 配置observer ``` server.1=node04:2888:3888:observer ``` ### 可靠性 zookeeper在leader挂掉选出新的leader之前会停止对外提供服务,因为无法保证数据是最新的结果。zookeeper不是强一致性,而是最终一致性,但是所有角色都对外可读,为了保证获得的结果是最新的,zookeeper提供了sync方式,就是先去主节点同步数据,同步完成后返回,如果没有leader就无法保证数据的准确性了。 ### PAXOS [Zookeeper全解析——Paxos作为灵魂](https://www.douban.com/note/208430424/) ### ZAB PAXOS是唯一的分布式一致性算法,其它的算法都是Paxos的改进或简化,zookeeper用的ZAB算法就是PAXOS的简化版。zookeeper执行写操作时,会记录Zxid,它们是递增的,cZxid表示该节点创建操作的id,mZxid表示节点最后一次数据修改操作的id,pZxid是该节点最后创建的子节点操作的id。当有写操作时,会转给leader,leader生成Zxid,然后将其放入follower的操作队列中,每个follower去各自的队列中取,取到之后将Zxid记录到自己的log中(log是存在磁盘上的),并给leader返回ok,当leader收到过半数的ok时,就会在队列中放入write的操作,follower会从队列中得到write操作,也就是说,只要所有队列都被消费完成,所有节点数据就都一致了,也就是最终一致性。 ## leader选举 如果国家选举领导人,有什么条件?首先,肯定是经验最丰富的(数据最完整),如果几个候选人经验差不多,那就论资排辈,看谁资格最老。实际上zookeeper的leader并不是选出来的,而是让出来的。我们知道,每次写操作都有Zxid,他是以16进制表示的,总共64位,前32位表示第几代leader,后32位表示最后一次操作的id,当前32位相同时,后面的数越大就是数据越完整,当几台机器Zxid相同时,就以myid最大的作为leader。 最坏情况 1. node04leader挂掉被node02首先发现,node02的Zxid最低 2. node02会发起选主投票将自己的Zxid和myid发给node01和node03,并首先投自己一票。 3. node01和node03收到之后会跟自己对比会否决node02并把自己的Zxid和myid发给自己之外的其他机器,并投自己一票。 4. node01收到node03的消息,发现myid比自己大,就投node03一票,发给其他的机器。 5. node02收到后也发现node03大,也投给了03 6. 最终每台机器都知道node03有三票 7. node03直接开启2888端口将自己变成leader,其他节点直接就会追随node03,不需要再去通知了。 8. 也就是说,不管谁发起了选主投票,都一定会触发其他人的被动投票。 9. 首次启动时,zXid都是0,myid大的就是leader,那么node04一定是leader吗?并不是,如果01,02,03先启动了,满足了过半条件就会先选出03,然后04启动发现有leader了只能追随,如果02,03,04先启动那么04就是leader,所以leader肯定是03或04。 ## Watch zookeeper在执行操作的时候,会有事件产生并可以回调给客户端。比如客户端监听了某个目录的创建子目录事件,当事件发生的时候,该客户端就会收到信息,当然这也可以自己实现,比如,我轮询(心跳)每隔一段事件去访问一次,看看有没有新的目录被创建。但是会有时效性问题,比如我3s轮询一次,那么在这3s之间创建的不能立刻返回,还有方向性,我有10000个客户端都去轮询zookeeper,会造成压力过大,而watch是zookeeper来通知客户端。 ## 简单API 引入对应版本的依赖,zk是什么版本,依赖就选什么版本 ``` dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.5.6</version> </dependency> ``` ``` public class App { public static void main(String[] args) throws Exception { System.out.println("Hello World!"); //zk是有session概念的,没有连接池的概念 //watch 观察、回调 有两类 //watch的注册只发生在读类型的调用 //第一类 new zk时候,传入的watch,这个watch是session级别的,跟path、node没有关系 final CountDownLatch latch = new CountDownLatch(1); final ZooKeeper zk = new ZooKeeper("192.168.106.3:2181,192.168.106.4:2181,192.168.106.5:2181,192.168.106.6:2181", 3000, new Watcher() { //Watch的回调方法 @Override public void process(WatchedEvent watchedEvent) { Event.KeeperState state = watchedEvent.getState(); Event.EventType type = watchedEvent.getType(); String path = watchedEvent.getPath(); System.out.println("new zk watch: " + watchedEvent.toString()); switch (state) { case Unknown: break; case Disconnected: break; case NoSyncConnected: break; case SyncConnected: System.out.println("connected"); latch.countDown(); break; case AuthFailed: break; case ConnectedReadOnly: break; case SaslAuthenticated: break; case Expired: break; case Closed: break; } switch (type) { case None: break; case NodeCreated: break; case NodeDeleted: break; case NodeDataChanged: break; case NodeChildrenChanged: break; case DataWatchRemoved: break; case ChildWatchRemoved: break; } } }); //zookeeper获取连接是异步的,不一定及时返回,所以要await等待连接成功 latch.await(); ZooKeeper.States state = zk.getState(); switch (state) { case CONNECTING: System.out.println("ing........"); break; case ASSOCIATING: break; case CONNECTED: System.out.println("ed........"); break; case CONNECTEDREADONLY: break; case CLOSED: break; case AUTH_FAILED: break; case NOT_CONNECTED: break; } String pathName = zk.create("/ooxx", "olddata".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); final Stat stat = new Stat(); byte[] node = zk.getData("/ooxx", new Watcher() { @Override public void process(WatchedEvent watchedEvent) { System.out.println("getData watch: " + watchedEvent); //获取之后重新注册事件 try { //true default watch 被重新注册 new zk的那个watch // zk.getData("/ooxx",true,stat); //传入当前watcher zk.getData("/ooxx", this, stat); } catch (KeeperException | InterruptedException e) { e.printStackTrace(); } } }, stat); System.out.println(new String(node)); //触发回调 Stat stat1 = zk.setData("/ooxx", "newdata".getBytes(), 0); //还会触发吗?事件是一次性的,想要再次触发必须重新注册 Stat stat2 = zk.setData("/ooxx", "newdata01".getBytes(), stat1.getVersion()); System.out.println("----------- async start ---------------"); zk.getData("/ooxx", false, new AsyncCallback.DataCallback() { @Override public void processResult(int i, String s, Object o, byte[] bytes, Stat stat) { System.out.println("----------- async call back ---------------"); System.out.println(o); System.out.println(new String(bytes)); } }, "abc"); System.out.println("----------- async over ---------------"); TimeUnit.SECONDS.sleep(2222); } } ``` [1]: https://www.princelei.club/usr/uploads/2019/12/1433008099.png [2]: https://www.princelei.club/usr/uploads/2019/12/804326905.png [3]: https://www.princelei.club/usr/uploads/2019/12/882082495.png Last modification:June 11th, 2020 at 06:10 pm © 允许规范转载