ZooKeeper
简介
概述
- Zookeeper是Yahoo!(雅虎)公司开发的后来贡献给了Apache的一套用于进行分布式协调的机制
- Zookeeper提供了中心化服务 - 充当了注册中心:统一配置信息,统一命名,提供了分布式锁服务,提供了分布式组服务
- Zookeeper是仿照了Google的The Chubby Lock Servie来实现的
安装
单机安装
-
关闭防火墙
# 临时关闭 systemctl stop firewalld # 永久关闭 systemctl disable firewalld
-
安装JDK
-
下载Zookeeper的安装包
cd /home/software/ wget http://bj-yzjd.ufile.cn-north-02.ucloud.cn/apache-zookeeper-3.5.7-bin.tar.gz
-
解压
tar -xvf apache-zookeeper-3.5.7-bin.tar.gz
-
重命名
mv apache-zookeeper-3.5.7-bin zookeeper-3.5.7
-
配置环境变量
vim /etc/profile.d/zookeeperhome.sh # 在文件中添加 export ZOOKEEPER_HOME=/home/software/zookeeper-3.5.7 export PATH=$PATH:$ZOOKEEPER_HOME/bin # 保存退出,重新生效 source /etc/profile.d/zookeeperhome.sh
-
进入Zookeeper的配置目录
cd zookeeper-3.5.7/conf/
-
复制并编辑文件
cp zoo_sample.cfg zoo.cfg # 编辑文件 vim zoo.cfg # 修改属性 dataDir=/home/software/zookeeper-3.5.7/tmp
-
启动Zookeeper
zkServer.sh start
-
查看状态
zkServer.sh status
基本细节
特点
- Zookeeper本身是一个树状结构,根节点是/ - 在Zookeeper中,将每一个节点称之为Znode节点,因此Zookeeper的结构也称之为Znode树
- Zookeeper默认客户端端口是2181,可以通过clientPort属性来修改
- Zookeeper启动的时候,自带一个默认的子节点/zookeeper,这个节点用于存储Zoopeepr本身的配置信息的
- 在Zookeeper中,每一个节点都要求携带数据,这个数据可以是配置信息或者是对于这个节点描述信息
- Zookeeper的节点携带的数据会维系在内存以及磁盘中。数据在磁盘上的存储位置由dateLogDir属性来决定。如果没有单独指定,那么
dataLogDir
的值和dataDir
的值一致 - 在Zookeeper中,不存在相对路径,所有路径必须从根路径开始计算
- 在Zookeeper中,每一个写操作(create/set/deleteall/connect/quit/close)看作是一个事务,会给每一个事物分配一个全局递增的编号,称之为事务id,简写为Zxid
扩展:查看log.xxx文件
-
复制jar包
cd /home/software/zookeeper-3.6.3/lib # 复制 cp zookeeper-3.6.3.jar ../tmp/version-2/ cp zookeeper-jute-3.6.3.jar ../tmp/version-2/ cp slf4j-api-1.7.25.jar ../tmp/version-2/
-
回到version-2目录下
cd ../tmp/version-2/
-
查看日志文件
java -cp .:zookeeper-3.6.3.jar:zookeeper-jute-3.6.3.jar:slf4j-api-1.7.25.jar org.apache.zookeeper.server.LogFormatter log.1
命令
命令 | 解释 |
---|---|
zkServer.sh start | 启动Zookeeper |
zkServer.sh status | 查看Zookeeper的状态 |
zkCli.sh | 启动客户端 |
ls / | 查看Zookeeper的根目录的子节点 |
create /video 'manage video servers' | 创建子节点 |
get /video | 获取video节点的数据 |
set /video 'testing' | 修改video的数据 |
delete /log | 删除节点 |
deleteall /video | 递归删除 |
stat /music | 查看music节点状态(属性信息) |
create -e /test | 构建临时节点 |
create -s /test | 创建顺序节点 |
create -e -s /test | 创建临时顺序节点 |
节点属性
属性 | 解释 | 案例 |
---|---|---|
cZxid | 当前节点被创建的事务Id | cZxid = 0x3 |
ctime | 当前节点被创建的时间 | ctime = Wed Feb 23 14:05:49 CST 2022 |
mZxid | 当前节点数据被修改的时间 | mZxid = 0x3 |
mtime | 当前节点数据被修改的时间 | mtime = Wed Feb 23 14:05:49 CST 2022 |
pZxid | 当前节点的子节点个数变化的事务id | pZxid = 0x3 |
cversion | 当前节点的子节点个数变化的次数 | cversion = 0 |
dataVersion | 当前节点的数据被修改的次数 | dataVersion = 0 |
aclVersion | 当前节点权限变化次数 | aclVersion = 0 |
ephemeralOwner | 如果是持久节点,则此项的值为0 如果是临时节点,则此项值为sessionid | ephemeralOwner = 0x0 |
dataLength | 当前节点的数据的字节个数 | dataLength = 0 |
numChildren | 当前节点的子节点个数 | numChildren = 0 |
节点类型
持久节点 | 临时节点 | |
---|---|---|
非顺序节点 | Persistent | Ephemeral |
顺序节点 | Persistent_Sequential | Ephemeral_Sequential |
API操作
ZooKeeper Api
-
连接ZooKeeper
//连接Zookeeper @Test public void connect() throws IOException, InterruptedException { // 构建Zookeeper对象 // String connectString - 连接地址+端口号 // int sessionTimeout - 超时时间 // Watacher watcher - 监控者,用于监控ZooKeeper是否正确建立连接 // Zookeeper底层依靠了Netty来实现连接过程 // Netty底层NIO来实现 - Zookeeper的连接过程是一个异步非阻塞 // @Test所测试的方法会对应一个单独的测试线程,Zookeeper又利用Netty启动单独的连接模型 CountDownLatch cdl = new CountDownLatch(1); ZooKeeper zk = new ZooKeeper("192.168.65.101:2181", 5000,//这个值现阶段的范围必须是4000~40000之间 e ->{ // 判断连接知否建立 if (e.getState() == Watcher.Event.KeeperState.SyncConnected){ System.out.println("连接成功"); cdl.countDown(); } } ); cdl.await(); System.out.println("finish"); }
-
创建节点
// 创建节点 @Test public void createNode() throws InterruptedException, KeeperException { // String path - 节点数据 // byte[] data - 节点路径 // List<ACL> acl - 节点权限 // CreateMode createMode - 节点类型 String path = zk.create("/log", "testing".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println(path); }
-
查看节点数据
@Test public void getData() throws InterruptedException, KeeperException { // 如果需要节点信息/属性 // Stat s = new Stat(); // byte[] data = zk.getData("/log",null,s); // 如果不需要节点信息 byte[] data = zk.getData("/log", null, null); System.out.println(new String(data)); }
-
修改节点数据
@Test public void setData() throws InterruptedException, KeeperException { // String path - 路径 // byte[] data - 要改的数据 // int version - 数据版本,对应了dataVersion // 在修改数据的时候,会先检查version和dataversion是否一致 // 如果一致,则修改数据,如果不一致,会报错 // -1表示忽略版本校验 Stat data = zk.setData("/log", "changing~~~".getBytes(), 0); System.out.println(data); }
-
获取子节点
@Test public void getChildren() throws InterruptedException, KeeperException { List<String> children = zk.getChildren("/", null); for (String child : children) { System.out.println(child); } }
-
删除节点
@Test public void deleteNode() throws InterruptedException, KeeperException { zk.delete("/log", -1); }
-
判断节点是否存在
@Test public void exists() throws InterruptedException, KeeperException { // 如果节点存在,则返回这个节点的属性信息 // 如果不存在,则返回null Stat stat = zk.exists("/log", null); System.out.println(stat); }
-
监控节点的数据是否被修改
@Test public void nodeDataChanged() throws InterruptedException, KeeperException { CountDownLatch cdl = new CountDownLatch(1); zk.getData("/test", e -> { if (e.getType() == Watcher.Event.EventType.NodeDataChanged) System.out.println("节点数据被修改"); cdl.countDown(); },null); cdl.await(); }
-
监控子节点的个数变化
@Test public void childrenChanged() throws InterruptedException, KeeperException { CountDownLatch cdl = new CountDownLatch(1); zk.getChildren("/", e->{ if(e.getType() == Watcher.Event.EventType.NodeChildrenChanged) System.out.println("子节点个数发生变化"); cdl.countDown(); }); cdl.await(); }
-
监控节点的创建/删除
@Test public void nodeChanged() throws InterruptedException, KeeperException { CountDownLatch cdl = new CountDownLatch(1); zk.exists("/test",e ->{ if (e.getType() == Watcher.Event.EventType.NodeCreated) System.out.println("节点被创建!!!"); else if (e.getType() == Watcher.Event.EventType.NodeDeleted) System.out.println("节点被删除!!!"); cdl.countDown(); }); cdl.await(); }
Curator
-
创建节点
@Before public void startClient() { // 重试策略 RetryPolicy rp = new ExponentialBackoffRetry(5000, 3); // 构建客户端 // String connectString - 连接地址+端口号 // RetryPolicy retryPolicy - 重试策略 client = CuratorFrameworkFactory.newClient("192.168.125.128:2181", rp); // 开启客户端 client.start(); } // 创建节点 @Test public void createNode() throws Exception { client.create() .creatingParentsIfNeeded() .withMode(CreateMode.PERSISTENT) // 节点类型 .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE) // 权限 .forPath("/video/movie/comedy","testing".getBytes()); } // 关闭客户端 @After public void closeClient() { if (client != null) client.close(); }
-
获取数据
@Test public void getData() throws Exception { byte[] data = client.getData().forPath("/video"); System.out.println(new String(data)); }
-
修改数据
@Test public void setData() throws Exception { client.setData().withVersion(-1).forPath("/video","test".getBytes()); }
-
获取子节点
@Test public void getChildren() throws Exception { List<String> list = client.getChildren().forPath("/"); for (String s : list) { System.out.println(s); } }
-
删除节点
@Test public void deleteNode() throws Exception { client.delete() .deletingChildrenIfNeeded() // 递归删除 .withVersion(-1).forPath("/video"); }
-
监控
@Test public void watch() throws Exception { // 构建要监控的节点以及其子节点的树状结构 TreeCache tree = new TreeCache(client, "/test"); // 添加监听器 tree.getListenable().addListener( ( curatorFramework, treeCacheEvent) -> { // 获取事件类型 TreeCacheEvent.Type type = treeCacheEvent.getType(); // 判断类型的具体操作 switch (type){ case NODE_ADDED: System.out.println("节点被创建!"); break; case NODE_UPDATED: System.out.println("数据被修改!"); break; case NODE_REMOVED: System.out.println("节点被删除!"); break; default: System.out.println("其他操作!"); } }); // 开始监听 tree.start(); while (true); }
集群安装
安装
-
关闭防火墙
systemctl stop firewalld systemctl disable firewalld
-
安装JDK
-
下载解压ZooKeeper
tar -xvf apache-zookeeper-3.6.3-bin.tar.gz
-
重命名
mv apache-zookeeper-3.6.3-bin zookeeper-3.6.3
-
进入ZooKeeper目录
cd zookeeper-3.6.3/conf/
-
复制文件,并编辑
cp zoo_sample.cfg zoo.cfg vim zoo.cfg # 修改属性 dataDir=/home/software/zookeeper-3.6.3/tmp # 文件末尾添加 server.1=192.168.65.101:2888:3888 server.2=192.168.65.102:2888:3888 server.3=192.168.65.103:2888:3888
-
创建数据目录并进入
cd /home/software/zookeeper-3.6.3/ mkdir tmp cd tmp
-
编辑文件,并且需要在文件中添加对应的编号
vim myid # 给定对应编号
-
远程分发给其他节点
scp -r zookeeper-3.6.3/ root@192.168.65.102:$PWD scp -r zookeeper-3.6.3/ root@192.168.65.103:$PWD # 确定连接,输入yes # 输入密码
-
修改其他节点上的myid
cd /home/software/zookeeper-3.6.3/tmp/ vim myid # 修改成对应的编号
-
添加环境变量
vim /etc/profile.d/zookeeperhome.sh # 在文件中添加 export ZOOKEEPER_HOME=/home/software/zookeeper-3.6.3 export PATH=$PATH:$ZOOKEEPER_HOME/bin # 保存退出,重新生效 source /etc/profile.d/zookeeperhome.sh
-
启动ZooKeeper
zkServer.sh start # 查看状态 zkServer.sh status #如果出现了1个leader+2个follower,则表示成功
概述
- ZooKeeper启动的时候,所有的节点进入选举状态,并且所有节点都会推荐自己成为leader,会将自己的选举信息发送给其他节点
- 一个节点收到其他节点的选举信息之后,就会进行比较。经过多轮比较之后,最终获胜的节点才会成为leader
细节
- 选举信息
- 当前节点(服务器)的最大事务id
- 选举编号 - myid
- 逻辑时钟值 - 用于控制比较轮数
- 比较原则
- 比较最大事务id,谁大谁赢
- 如果事务id一致,则比较myid,谁大谁赢
- 如果一个节点胜过一半及以上的节点,才会成为leader - 过半性
- 如果leader宕机,那么整个ZooKeeper集群会再次进行选举,重新选出一个新的leader
- 在集群中,如果已经选举出来leader的前提下,此时新增节点,新增节点的事务id或者myid无论是多少,都只能成为follwer
- 在ZooKeeper集群中,出现多个leader的现象称之为脑裂
- ZooKeeper集群中脑裂产生的原因
- 集群产生分裂
- 裂完之后进行了选举
- 在ZooKeeper集群中,如果存活(相互通信)的节点个数不足一半,则剩余存活的节点停止提供服务(对外停止接受操作,对内停止选举和原子广播) - 过半性
- ZooKeeper会给每一次选举出来的leader分配一个递增的编号,称之为epochid。如果集群中出现了多个leader,那么ZooKeeper会将epochid较小的leader切换为follower状态
- ZooKeeper集群的节点数量一般是奇数个
- ZooKeeper集群中节点的状态
- voting/looking - 选举状态
- follower - 追随者/跟随者
- leader - 领导者
- observer - 观察者
observer
-
observer既不参与选举也不参与投票(原子广播),但是需要监听选举或者投票结果,根据结果来执行对应的操作
-
在实际开发中,在集群数量庞大的情况下,绝大部分节点都会设置成observer,能够有效提高集群的效率(选举和原子广播)
-
observer没有决策权,所以在计算过半的时候不考录observer。例如,一个集群中有21个节点,其中包含了1个leader,6个follower,14个observer,即使所有的observer全部宕机,集群也会继续提供服务。其中有4个follower宕机,集群就会停止对外服务
-
设置
peerType=observer server.3=192.168.65.103:2888:3888:observer
ZAB协议
概述
- ZAB(ZooKeeper Atomic Broadcast)是专门为ZooKeeper设计的一套用于进行原子广播的协议,在广播过程中能够兼顾奔溃恢复
- ZAB是基于2PC算法来进行设计,加入了过半性和PAXOS算法进行了改进
原子广播
-
原子广播的作用是保证ZooKeeper集群的数据一致性(保证的是强一致性和最终一致性)
- 强一致性:集群中某一个节点上的数据发生变化,其他节点能够立即发现这个变化且做出对应操作
- 弱一致性:集群中某一个节点上的数据发生变化,其他节点能够部分感知这个变化或者没有感知
- 最终一致性:不关心中间过程变化,只要求最后的结果相同 - 不问过程只要结果
-
原子广播是基础2PC算法来实现的 -> 2PC(Two Phase Commit) - 二阶段提交,顾名思义,将请求提交过程拆分成两个阶段
-
请求阶段:协调者收到请求之后,会将请求分发给每一个参与者,并且会要求所有的参与者在规定时间内返回消息等待每一个参与者的反馈信息
-
提交阶段:如果协调者收到所有参与者返回的yes,那么就会认为这个请求可执行,就会命令所有参与者执行刚才的请求
-
中止阶段:如果收到参与者返回No,就会中止执行;或者参与者返回yes大于10s未导致未收到yes,也会中止执行。
-
2PC要么执行请求 - 提交阶段,要么执行请求 - 中止阶段
-
2PC的核心思想:“一票否决”。2PC的优势在于理解和实现过程相对简单,同样,2PC的劣势也非常明显:非常容易受外界环境影响
-
原子广播在2PC的基础上,加入了过半性进行改进:
- 在ZooKeeper中,当leader收到请求之后,会先试图将这个请求记录到本地日志文件log.xxx中。如果记录失败,则直接结束;如果记录成功,则leader会将这个请求放到队列中分发给每一个follower,等待每一个follower的反馈消息
- follower收到队列之后,会将请求从队列中取出,试图记录到本地日志文件log.xxx中。如果记录成功,则follower会给leader返回一个yes;如果记录失败,则follower就会给leader返回一个no
- 如果leader收到了半数及以上的follower返回的yes,则认为这个请求可以执行;如果leader没有收到半数的yes,则认为这个请求不能执行
-
日志记录失败的情况:
- 磁盘空间已满
- 磁盘产生损坏
-
如果follower记录日志失败但是还需要执行请求的时候,follower会给leader发送信号,leader会将请求重新发送给follower,follower会再次试图记录日志,记录成功则执行这个请求,如果记录失败,则会再次给leader发送信号
-
一个节点重新连入集群之后,这个节点会先寻找自己的最大事务id,会将自己最大事务id发送给leader。leader在收到信号之后,会检查事务id,如果发现事务id不一致,则leader会将欠缺的事务放到队列中发送给这个follower。follower收到队列之后,需要在限定时间(默认20s)内补齐事务。在follower补齐事务期间,这个节点展示不对外接受写操作
-
崩溃恢复
- 在Zookeeper集群中,当leader丢失或者宕机的时候,整个ZooKeeper集群不会停止对外提供服务,会选举一个新的leader,这个过程称之为奔溃恢复
- 在Zookeeper中,会为每一次选举出来的leader分配一个递增的编号,称之为epochid。leader会将自己的epochid发送给每一个follower,follower收到epochid之后,会存储到本地文件acceptedEpoch中
- Zxid实际上是由64位二进制(16位十六进制)数字构成,其中高32位对应epochid,底32位对应事务id。例如0x300000006表示第3个leader接受的第6个写操作
其他
特性
- 过半性 - 选举过半,服务过半,存活过半
- 一致性 - 通过原子广播来保证
- 原子性 - 一个请求要么所有节点都执行要么都不执行
- 顺序性 - 所有节点执行请求的顺序一定是相同的
- 实时性 - 在网络条件好的情况下,可以对ZooKeeper集群来进行实时监控
- 可靠性 - 依靠崩溃恢复来保证
配置信息
参数 | 默认值 | 说明 |
---|---|---|
clientPort | 2181 | 客户端连接服务器端的端口,即Zookeeper的对外服务端口 |
dataDir | /tmp/zookeeper | 数据目录,即存储快照文件的目录 |
dataLogDir | /tmp/zookeeper | 事务日志输出目录。在不指定的情况下,和dataDir一致 正常运行过程中,针对所有事务操作,在返回客户端ACK的响应前,Zookeeper会确保已经将本次事务操作的事务日志写到磁盘上,只有这样,事务才会生效 |
tickTime | 2000 | Zookeeper中的一个时间单元。Zookeeper中所有时间都是以这个时间单元为基础,进行整数倍配置,单位是毫秒 |
initLimit | 10*tickTime | follower在启动过程中,会从leader同步所有最新数据,然后确定自己能够对外服务的起始状态。leader允许follower在initLimit 时间内完成这个工作 如果Zookeeper集群的数据量较大,follower在启动的时候,从leader上同步数据的时间也会相应变长,因此在这种情况下,可以考虑适当调大这个参数 |
syncLimit | 5*tickTime | 表示leader与follower 之间发送消息,请求和应答时间长度 如果follower在设置的时间内不能与leader进行通信,那么此follower将被丢弃 当集群网络环境不好时,可以适当调大 |
minSessionTimeout maxSessionTimeout | 2 * tickTime ~ 20 * tickTime | Session超时时间限制,如果客户端设置的超时时间不在这个范围,那么会被强制设置为最大或最小时间 |
server.x=hostname:端口号1:端口号2 | 配置集群中的主机,其中:x是主机编号,即myid;端口号1是原子广播端口,用于leader和follower之间的通信;端口号2是选举端口,用于选举过程中的通信 | |
jute.maxbuffer | 1M | 用于控制每一个节点的最大存储的数据量 |
globalOutstandingLimit | 1000 | 最大请求堆积数 |
preAllocSize | 64M | 预先开辟磁盘空间,用于后续写入事务日志 |
leaderServers | yes | 默认情况下,leader是会接受客户端连接,并提供正常的读写服务。但是,如果需要leader专注于集群中机器的事务协调(原子广播),那么可以将这个参数设置为no,这样一来,会提高整个zk集群性能 |
maxClientCnxns | 60 | 控制的每一台zk服务器能处理的客户端并发请求数 |
集群操作
-
需要nc工具包来完成集群操作。nc(netcat)实际上是将命令打包成一个TCP请求发送到对应的节点上来执行对应的命令
-
安装
rpm -ivh ncat-7.92-1.x86_64.rpm
-
在zoo.cfg中添加
4lw.commands.whitelist=* # 保存之后远程发送给其他节点 # 重新启动ZooKeeper
-
常见命令
-
判断是否存活
echo ruok | nc 192.168.65.101 2181
-
查看配置
echo conf | nc 192.168.65.101 2181
-
查看状态
echo stat | nc 192.168.65.101 2181
-