Zookeeper 学习笔记
1、Zookeeper 介绍
1.1、什么是Zookeeper
ZooKeeper 是⼀种分布式协调服务,⽤于管理⼤型主机。在分布式环境中协调和管理服务是⼀个复杂的过程。ZooKeeper 通过其简单的架构和 API 解决了这个问题。ZooKeeper 允许开发⼈员专注于核⼼应⽤程序逻辑,⽽不必担⼼应⽤程序的分布式特性。
1.2、Zookeeper的应⽤场景
场景一:分布式协调组件
分布式协调中心 ZK 的 znode 节点:flag = true;
一旦节点发生改变,就会通知所有监听方改变自己的值 — watch机制、分布式锁、可以让分布式服务处于无状态
在分布式系统中,需要有 zookeeper 作为分布式协调组件,协调分布式系统中的状态。
场景二:分布式锁
zk在实现分布式锁上,可以做到强⼀致性,关于分布式锁相关的知识,在之后的 ZAB 协议中介绍。
场景三:无状态化的实现
2、搭建Zookeeper服务器
2.1、安装Zookeeper
下载地址:https://www.apache.org/dyn/closer.lua/zookeeper/zookeeper-3.7.1/apache-zookeeper-3.7.1-bin.tar.gz
cd /usr/local/
# 将zookeeper安装包放在该目录下
mkdir zookeeper
tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz
# 进入conf目录
cd apache-zookeeper-3.7.1-bin/conf/
# 修改配置文件模板
vim zoo_sample.cfg
# 重命名配置文件名称
mv zoo_sample.cfg zoo.cfg
# 进入bin目录
cd ../bin/
./zkServer.sh start ../conf/zoo.cfg
2.2、zoo.cfg 配置⽂件说明
# zookeeper时间配置中的基本单位 (毫秒)
tickTime=2000
# 允许follower初始化连接到leader的最⼤时⻓,它表示tickTime时间倍数 即:initLimit * tickTime
initLimit=10
# 允许follower与leader数据同步的最⼤时⻓,它表示tickTime时间倍数 即:syncLimit * tickTime
syncLimit=5
# zookeper数据存储⽬录及⽇志保存⽬录(如果没有指明dataLogDir,则⽇志也保存在这个⽂件中)
dataDir=/tmp/zookeeper
dataLogDir=/tmp/zookeeper
# 对客户端提供的端⼝号
clientPort=2181
# 单个客户端与zookeeper最⼤并发连接数
maxClientCnxns=60
# 保存的数据快照数量,之外的将会被清除
autopurge.snapRetainCount=3
# ⾃动触发清除任务时间间隔,⼩时为单位。默认为0,表示不⾃动清除。
autopurge.purgeInterval=1
2.3、Zookeeper服务器的操作命令
1、启动zk服务器:
./zkServer.sh start ../conf/zoo.cfg
2、查看zk服务器状态:
./zkServer.sh status ../conf/zoo.cfg
3、停⽌zk服务器:
./zkServer.sh stop ../conf/zoo.cfg
3、Zookeeper内部的数据模型
3.1、zk是如何保存数据的?
zk中的数据是保存在节点上的,节点就是znode,多个 znode 之间构成一棵树的目录结构。
Zookeeper 的数据模型是什么样子呢?类似于数据结构中的树,同时也很像文件系统的目录。
树是由节点所组成的,Zookeeper 的数据存储也同样是基于节点,这种节点叫做 Znode
但是,不同于树的节点,Znode 的引⽤⽅式是路径引⽤,类似于⽂件路径:
/动物/猫
/汽⻋/宝⻢
这样的层级结构,让每⼀个 Znode 节点拥有唯⼀的路径,就像命名空间⼀样对不同信息作出清晰的隔离。
3.2、zk中的znode是什么样的结构?
zk 中的 znode,包含了四个部分:
1、data:保存数据
2、acl:权限,定义了什么样的⽤户能够操作这个节点,且能够进⾏怎样的操作。
- c:create 创建权限,允许在该节点下创建⼦节点
- w:write 更新权限,允许更新该节点的数据
- r:read 读取权限,允许读取该节点的内容以及⼦节点的列表信息
- d:delete 删除权限,允许删除该节点的⼦节点
- a:admin 管理者权限,允许对该节点进⾏ acl 权限设置
3、stat:描述当前 znode 的元数据
4、child:当前节点的⼦节点
3.3、zk中节点znode的类型
1、持久节点:
创建出的节点,在会话结束后依然存在。保存数据
2、持久序号节点:
创建出的节点,根据先后顺序,会在节点之后带上⼀个数值,越往后执⾏数值越⼤,适⽤于分布式锁的应⽤场景- 单调递增
3、临时节点:
临时节点是在会话结束后,⾃动被删除的,通过这个特性,zk可以实现服务注册与发现的效果。那么临时节点是如何维持⼼跳呢?
4、临时序号节点:
跟持久序号节点相同,适⽤于临时的分布式锁。
5、Container节点(3.5.3版本新增):
Container 容器节点,当容器中没有任何⼦节点时,该容器节点会被 zk 定期删除(60s)。
6、TTL节点:
可以指定节点的到期时间,到期后被 zk 定时删除。只能通过系统配置 zookeeper.extendedTypesEnabled=true
开启
3.4、zk的数据持久化
zk 的数据是运⾏在内存中的,zk 提供了两种持久化机制:
1、事务日志
zk 把执⾏的命令以⽇志形式保存在 dataLogDir 指定的路径中的⽂件(如果没有指定dataLogDir,则按 dataDir 指定的路径)。
2、数据快照
zk 会在⼀定的时间间隔内做⼀次内存数据的快照,把该时刻的内存数据保存在快照⽂件中。
zk 通过两种形式的持久化,在恢复时先恢复快照⽂件中的数据到内存中,再⽤⽇志⽂件中的数据做增量恢复,这样的恢复速度更快。
4、Zookeeper客户端(zkCli)的使⽤
# 启动命令
./zkCli.sh
4.1、多节点类型创建
- 创建持久节点
- 创建持久序号节点
- 创建某个节点的子节点
- 创建临时节点
- 创建临时序号节点
- 创建容器节点
4.2、查询节点
1、普通查询
2、查询节点相关信息
- cZxid:创建节点的事务ID
- mZxid:修改节点的事务ID
- pZxid:添加和删除⼦节点的事务ID
- ctime:节点创建的时间
- mtime: 节点最近修改的时间
- dataVersion:节点内数据的版本,每更新⼀次数据,版本会+1
- aclVersion:此节点的权限版本
- ephemeralOwner:如果当前节点是临时节点,则该值是当前节点所有者的 sessionId。如果当前节点不是临时节点,则该值为零。
- dataLength:节点内数据的⻓度
- numChildren:该节点的⼦节点个数
4.3、删除节点
1、普通删除
2、乐观锁删除
4.4、权限设置
# 在会话一窗口设置账号和密码,并设置权限
addauth digest xiaoming:123456
create /test-node abc auth:xiaoming:123456:cdrwa
会话一:
会话二:
注意:
在另⼀个会话中必须先使⽤账号密码,才能拥有操作该节点的权限。
5、Curator客户端的使⽤
5.1、Curator介绍
Curator 是 Netflix 公司开源的⼀套 Zookeeper 客户端框架,Curator 是对 Zookeeper ⽀持最好的客户端框架。Curator 封装了⼤部分 Zookeeper 的功能,⽐如Leader 选举、分布式锁等,减少了技术⼈员在使⽤ Zookeeper 时的底层细节开发⼯作。
5.2、引⼊Curator
1、引入依赖:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
2、修改 application.properties
配置⽂件
curator.retryCount=5
curator.elapsedTimeMs=5000
curator.connectString=119.91.21.45:2183
curator.sessionTimeoutMs=60000
curator.connectionTimeoutMs=5000
3、注⼊配置Bean
@Data
@Component
@ConfigurationProperties(prefix = "curator")
public class WrapperZK {
private int retryCount;
private int elapsedTimeMs;
private String connectString;
private int sessionTimeoutMs;
private int connectionTimeoutMs;
}
4、注⼊CuratorFramework
@Configuration
public class CuratorConfig {
@Autowired
WrapperZK wrapperZk;
@Bean(initMethod = "start")
public CuratorFramework curatorFramework() {
return CuratorFrameworkFactory.newClient(
wrapperZk.getConnectString(),
wrapperZk.getSessionTimeoutMs(),
wrapperZk.getConnectionTimeoutMs(),
new RetryNTimes(wrapperZk.getRetryCount(), wrapperZk.getElapsedTimeMs()));
}
}
5.3、具体操作
创建节点
/**
* 创建节点
* @throws Exception
*/
@Test
void createNode() throws Exception {
// 添加持久节点
String path = curatorFramework.create().forPath("/curator-node"); // 节点名称,注意需要以/开头
// 添加临时序号节点
// String path1 = curatorFramework.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/curator-node", "some-data".getBytes());
System.out.println(String.format("curator create node :%s successfully.", path));
System.in.read();
}
获取节点数据
/**
* 获取节点数据
*
* @throws Exception
*/
@Test
public void testGetData() throws Exception {
byte[] bytes = curatorFramework.getData().forPath("/curator-node");
System.out.println(new String(bytes));
}
设置节点数据
/**
* 设置节点数据
*
* @throws Exception
*/
@Test
public void testSetData() throws Exception {
curatorFramework.setData().forPath("/curator-node", "changed!".getBytes());
byte[] bytes = curatorFramework.getData().forPath("/curator-node");
System.out.println(new String(bytes));
}
创建节点同时创建⽗节点
/**
* 创建节点同时创建⽗节点
*
* @throws Exception
*/
@Test
public void testCreateWithParent() throws Exception {
String pathWithParent = "/node-parent/sub-node-1";
String path = curatorFramework.create().creatingParentsIfNeeded().forPath(pathWithParent);
System.out.println(String.format("curator create node :%s successfully.", path));
}
删除节点
/**
* 删除节点
*
* @throws Exception
*/
@Test
public void testDelete() throws Exception {
String pathWithParent = "/node-parent";
curatorFramework.delete().guaranteed().deletingChildrenIfNeeded().forPath(pathWithParent);
}
6、zk实现分布式锁
6.1、zk中锁的种类
- 读锁:⼤家都可以读,要想上读锁的前提:之前的锁没有写锁
- 写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁。
6.2、zk如何上读锁
1、创建⼀个临时序号节点,节点的数据是read,表示是读锁
2、获取当前zk中序号⽐⾃⼰⼩的所有节点
3、判断最⼩节点是否是读锁:
- 如果不是读锁的话,则上锁失败,为最⼩节点设置监听。阻塞等待,zk 的 watch 机制会当最⼩节点发⽣变化时通知当前节点,于是再执⾏第⼆步的流程
- 如果是读锁的话,则上锁成功
6.3、zk如何上写锁
1、创建⼀个临时序号节点,节点的数据是write,表示是写锁
2、获取zk中所有的⼦节点
3、判断⾃⼰是否是最⼩的节点:
- 如果是,则上写锁成功
- 如果不是,说明前⾯还有锁,则上锁失败,监听最⼩的节点,如果最⼩节点发生变化,则回到第⼆步。
6.4、羊群效应
如果⽤上述的上锁⽅式,只要有节点发⽣变化,就会触发其他节点的监听事件,这样的话对zk的压⼒⾮常⼤,——⽺群效应。可以调整成链式监听。解决这个问题。
6.5、curator实现读写锁
@SpringBootTest
public class TestReadWriteLock {
@Autowired
private CuratorFramework client;
/**
* 获取读锁
*
* @throws Exception
*/
@Test
void testGetReadLock() throws Exception {
// 读写锁
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client, "/lock1");
// 获取读锁对象
InterProcessLock interProcessLock = interProcessReadWriteLock.readLock();
System.out.println("等待获取读锁对象!");
// 尝试获得锁
interProcessLock.acquire();
for (int i = 1; i <= 100; i++) {
Thread.sleep(3000);
System.out.println(i);
}
// 释放锁
interProcessLock.release();
System.out.println("等待释放锁!");
}
/**
* 获取写锁
*
* @throws Exception
*/
@Test
void testGetWriteLock() throws Exception {
// 读写锁
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client, "/lock1");
// 获取写锁对象
InterProcessLock interProcessLock = interProcessReadWriteLock.writeLock();
System.out.println("等待获取写锁对象!");
// 尝试获得锁
interProcessLock.acquire();
for (int i = 1; i <= 100; i++) {
Thread.sleep(3000);
System.out.println(i);
}
// 释放锁
interProcessLock.release();
System.out.println("等待释放锁!");
}
}
7、zk的watch机制
7.1、Watch机制介绍
我们可以把 Watch 理解成是注册在特定 Znode 上的触发器。当这个 Znode 发生改变,也就是调用了 create,delete,setData 方法的时候,就会触发 Znode 上注册的对应事件,请求 Watch 的客户端将会收到异步通知。
具体交互过程如下:
- 客户端调用 getData 方法,watch参数是true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。
客户端一:
客户端二:
- 当被 Watch 的 Znode 已删除,服务端会查找哈希表,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 key-value。
客户端使⽤了 NIO 通信模式监听服务端的调⽤。
7.2、zkCli客户端使⽤watch
create /test xxx
get -w /test # ⼀次性监听节点
ls -w /test # 监听⽬录,创建和删除⼦节点会收到通知。⼦节点中新增节点不会收到通知
ls -R -w /test # 监听⼦节点中⼦节点的变化,但内容的变化不会收到通知
客户端一:
客户端二:
7.3、curator客户端使⽤watch
/**
* 节点监听
*
* @throws Exception
*/
@Test
public void addNodeListener() throws Exception {
NodeCache nodeCache = new NodeCache(curatorFramework, "/curator-node");
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
log.info("{} path nodeChanged: ", "/curator-node");
printNodeData();
}
});
nodeCache.start();
System.in.read();
}
public void printNodeData() throws Exception {
byte[] bytes = curatorFramework.getData().forPath("/curator-node");
log.info("data: {}", new String(bytes));
}
8、Zookeeper集群实战
8.1、Zookeeper集群⻆⾊
zookeeper 集群中的节点有三种⻆⾊:
- Leader:处理集群的所有事务请求,集群中只有⼀个Leader。
- Follower:只能处理读请求,参与Leader选举。
- Observer:只能处理读请求,提升集群读的性能,但不能参与Leader选举。
8.2、集群搭建
搭建4个节点,其中⼀个节点为Observer
1、创建4个节点的myid,并设值
在 /usr/local/zookeeper
中创建以下四个⽂件:
2、编写4个zoo.cfg
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# 修改成对应的zk1 zk2 zk3 zk4
dataDir=/usr/local/zookeeper/zkdata/zk1
# 修改成对应的端⼝ 2181 2182 2183 2184
clientPort=2181
# 2001为集群通信端⼝,3001为集群选举端⼝,observer表示不参与集群选举
server.1=0.0.0.0:2001:3001
server.2=0.0.0.0:2002:3002
server.3=0.0.0.0:2003:3003
server.4=0.0.0.0:2004:3004:observer
3、启动4台Zookeeper
8.3、连接Zookeeper集群
./zkCli.sh -server 0.0.0.0:2181,0.0.0.0:2182,0.0.0.0:2183,0.0.0.0:2184
9、ZAB协议
9.1、什么是ZAB协议
Zookeeper 作为⾮常重要的分布式协调组件,需要进⾏集群部署,集群中会以⼀主多从的形式进⾏部署。Zookeeper 为了保证数据的⼀致性,使⽤了ZAB(Zookeeper Atomic Broadcast)协议,这个协议解决了 Zookeeper 的崩溃恢复和主从数据同步的问题。
9.2、ZAB协议定义的四种节点状态
- Looking :选举状态。
- Following :Follower 节点(从节点)所处的状态。
- Leading :Leader 节点(主节点)所处状态。
- Observing:观察者节点所处的状态。
9.3、集群上线时的Leader选举过程
Zookeeper 集群中的节点在上线时,将会进⼊到 Looking 状态,也就是选举 Leader 的状态,这个状态具体会发⽣什么?
把选票投给对方,应该理解成交换各自的选票信息。然后比较谁的选票更大(先比较zXid,再比较myId)
注意:zXid表示事务Id,每执行一条语句,事务Id就会加1
9.4、崩溃恢复时的Leader选举
Leader建⽴完成后,Leader 周期性地不断向 Follower 发送⼼跳(ping命令,没有内容的socket)。当 Leader 崩溃后,Follower 发现 socket 通道已关闭,于
是 Follower 开始进⼊到 Looking 状态,重新回到上⼀节中的 Leader 选举过程,此时集群不能对外提供服务。
9.5、主从服务器之间的数据同步
9.6、Zookeeper中NIO与BIO的应⽤
NIO
- ⽤于被客户端连接的 2181 端⼝,使⽤的是NIO模式与客户端建⽴连接。
- 客户端开启 Watch 时,也会使⽤ NIO,等待 Zookeeper 服务器的回调。
BIO
- 集群在选举时,多个节点之间的投票通信端⼝,使⽤ BIO 进⾏通信。
10、CAP理论
2000 年 7 ⽉,加州⼤学伯克利分校的 Eric Brewer 教授在 ACM PODC 会议上提出 CAP 猜想。2年后,麻省理⼯学院的 Seth Gilbert 和 Nancy Lynch 从理论上
证明了 CAP。之后,CAP 理论正式成为分布式计算领域的公认定理。
10.1、CAP理论
CAP理论为:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的其中两项。
- —致性(Consistency)
一致性指"all nodespsee the same data at the same time",即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。
- 可用性(Availability)
可用性指"Reads and writes always succeed",即服务一直可用,而且是正常响应时间。
- 分区容错性(Partition tolerance)
分区容错性指 “the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某个节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。——避免单点故障,就要进行冗余部署,冗余部署相当于是服务的分区,这样的分区就具备了容错性。
10.2、CAP 权衡
通过 CAP 理论,我们知道⽆法同时满⾜⼀致性、可⽤性和分区容错性这三个特性,那要舍弃哪个呢?
对于多数⼤型互联⽹应⽤的场景,主机众多、部署分散,⽽且现在的集群规模越来越⼤,所以节点故障、⽹络故障是常态,⽽且要保证服务可⽤性达到 N 个 9,即保证 P 和 A,舍弃C(退⽽求其次保证最终⼀致性)。虽然某些地⽅会影响客户体验,但没达到造成⽤户流程的严重程度。
对于涉及到钱财这样不能有⼀丝让步的场景,C 必须保证。⽹络发⽣故障宁可停⽌服务,这是保证 CA,舍弃 P。貌似这⼏年国内银⾏业发⽣了不下 10 起事故,但影响⾯不⼤,报到也不多,⼴⼤群众知道的少。还有⼀种是保证 CP,舍弃 A。例如⽹络故障是只读不写。
孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的。
10.3、BASE 理论
eBay 的架构师 Dan Pritchett 源于对⼤规模分布式系统的实践总结,在 ACM 上发表⽂章提出 BASE 理论,BASE 理论是对 CAP 理论的延伸,核⼼思想是即使⽆法做到强⼀致性(StrongConsistency,CAP 的⼀致性就是强⼀致性),但应⽤可以采⽤适合的⽅式达到最终⼀致性(Eventual Consitency)。
- 基本可⽤(Basically Available)
基本可⽤是指分布式系统在出现故障的时候,允许损失部分可⽤性,即保证核⼼可⽤。
电商⼤促时,为了应对访问量激增,部分⽤户可能会被引导到降级⻚⾯,服务层也可能只提供降级服务。这就是损失部分可⽤性的体现。
- 软状态(Soft State)
软状态是指允许系统存在中间状态,⽽该中间状态不会影响系统整体可⽤性。分布式存储中⼀般⼀份数据⾄少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysqlreplication 的异步复制也是⼀种体现。
- 最终⼀致性(Eventual Consistency)
最终⼀致性是指系统中的所有数据副本经过⼀定时间后,最终能够达到⼀致的状态。弱⼀致性和强⼀致性相反,最终⼀致性是弱⼀致性的⼀种特殊情况。
4、Zookeeper追求的⼀致性
Zookeeper 在数据同步时,追求的并不是强⼀致性,⽽是顺序⼀致性(事务id的单调递增)。