一、Zookeeper 简介
1、Zookeeper 概念
Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。分布式系统中数据存在一致性的问题!!!
- ZooKeeper 本质上是一个分布式的**小**文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。
ZooKeeper 提供给客户端监控存储在zk内部数据的功能,从而可以达到基于数据的集群管理。 诸如: 统一命名服务(dubbo)、分布式配置管理(solr的配置集中管理)、分布式消息队列(sub/pub)、分布式锁、分布式协调等功能。
2、zookeeper 架构组成
Leader
- Zookeeper 集群工作的核心角色
- 集群内部各个服务器的调度者。
- 事务请求(写操作)的**唯一**调度和处理者,保证集群事务处理的顺序性;对于 create,update, delete 等有写操作的请求,则需要统一转发给leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务。
- Follower
- 处理客户端非事务(读操作) 请求,
- 转发事务请求给 Leader;
- 参与集群 Leader 选举投票 2n-1台可以做集群投票(后面介绍)
此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者角色 Observer
Observer
- 观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给 Leader服务器进行处理。
- 不会参与任何形式的投票,只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。增加了集群增加并发的读请求
注:ZK也是Master/slave架构,但是与之前不同的是:zk集群中的Leader不是指定而来,而是通过选举产生
3、Zookeeper 特点
- Zookeeper:一个leader,多个 follower 组成的集群
- Leader 负责进行投票的发起和决议,更新系统状态(内部原理后面介绍)
- Follower 用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
- 集群中只要有**半数**以上节点存活,Zookeeper集群就能正常服务
- 全局数据一致:每个server保存一份相同的数据副本,Client无论连接到哪个server,数据都是一致的
- 更新请求顺序进行(内部原理后面介绍)
- 数据更新原子性,一次数据更新要么成功,要么失败。
二、环境搭建
1、Zookeeper 搭建方式介绍
Zookeeper安装方式有三种,单机模式和集群模式以及伪集群模式。
■ 单机模式:Zookeeper只运行在一台服务器上,适合测试环境
■ 伪集群模式:就是在一台服务器上运行多个Zookeeper 实例
■ 集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”
2、Zookeeper 集群搭建
- 下载稳定版本的zookeeper http://zookeeper.apache.org/releases.html
上传、解压:
- 任意选择一个节点先进行安装,如 linux121
- 下载完成后,将 zookeeper压缩包
zookeeper-3.4.14.tar.gz
上传到linux系统/opt/lagou/software
tar -zxvf zookeeper-3.4.14.tar.gz -C /opt/lagou/servers/
修改配置文件创建data和logs目录【重要!】
- data目录:存储数据
- log目录:存储日志信息
- 注:以下的配置对所有节点来说都是相同的
```shell
创建zk存储数据目录
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data
创建zk日志文件目录
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data/logs
修改zk配置文件
cd /opt/lagou/servers/zookeeper-3.4.14/conf
文件改名
mv zoo_sample.cfg zoo.cfg vim zoo.cfg
更新datadir
dataDir=/opt/lagou/servers/zookeeper-3.4.14/data
增加logdir
dataLogDir=/opt/lagou/servers/zookeeper-3.4.14/data/logs
增加集群配置
server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
server.1=linux121:2888:3888 server.2=linux122:2888:3888 server.3=linux123:2888:3888
打开以下的注释
ZK提供了自动清理事务日志和快照文件的功能,这个参数指定了清理频率,单位是小时
autopurge.purgeInterval=1
- 之后,将包含配置文件的ZooKeeper安装包**分发**给其他节点
- `rsync-script /opt/lagou/servers/zookeeper-3.4.14`
**添加 myid 配置**
- **注:每个节点的myid配置是唯一的**
- 在zookeepe r的 data 目录下创建一个 `myid` 文件,这个文件就是记录每个服务器的ID
- linux121、linux122、linux123的 myid 文件中记录的值可分别设置为 1、2、3
- linux121:`echo 1 > /opt/lagou/servers/zookeeper-3.4.14/data/myid`
- linux121:`echo 2 > /opt/lagou/servers/zookeeper-3.4.14/data/myid`
- linux121:`echo 3 > /opt/lagou/servers/zookeeper-3.4.14/data/myid`
**依次****启动这三个节点**
- 启动命令【所有节点都要执行此命令】
- `/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh start`
- 查看启动情况
- `/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh status`
- 停止命令【所有节点都要执行此命令】
- `/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh stop`
- 为了方便,创建集群启动和停止的**脚本**
- 可将脚本路径设置在目录:`/opt/lagou/servers/zookeeper-3.4.14/bin/`
- `vim zk.sh`
- 启动集群:`sh zk.sh start`
- 关闭集群:`sh zk.sh stop`
- 查看集群情况:`sh zk.sh status`
- 通过命令 jps 查看可知 zk 的进程名是:**QuorumPeerMain**
```shell
#!/bin/sh
echo "start zookeeper server..."
if(($#==0));then
echo "no params";
exit;
fi
hosts="linux121 linux122 linux123"
for host in $hosts
do
ssh $host "source /etc/profile; /opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh $1"
done
四、ZooKeeper 基本使用
1、ZK 命令行操作
- 首先,进入到zookeeper的bin目录之后,通过zkClient进入zookeeper客户端命令行
./zkCli.sh
连接本地的zookeeper服务器./zkCli.sh -server ip:port(2181)
连接指定的服务器- 如:
./zkCli.sh -server linux122:2181
- 如:
- 连接成功之后,系统会输出Zookeeper的相关环境及配置信息等信息。
- 输入help之后,屏幕会输出可用的Zookeeper命令,如下所示:
[zk: linux122:2181(CONNECTED) 0] help ZooKeeper -server host:port cmd args stat path [watch] set path data [version] ls path [watch] delquota [-n|-b] path ls2 path [watch] setAcl path acl setquota -n|-b val path history redo cmdno printwatches on|off delete path [version] sync path listquota path rmr path get path [watch] create [-s] [-e] path data acl addauth scheme auth quit getAcl path close connect host:port
创建节点
create [-s][-e] path data
使用
create -s /zk-test 123
命令创建zk-test顺序节点,其中内容为 123- [zk: localhost:2181(CONNECTED) 4]
create -s /zk-test 123
- 执行完后,就在根节点下创建了一个叫做
/zk-test
的节点,该节点内容就是 123,同时可以看到创建的zk-test节点后面添加了一串数字以示区别- Created /zk-test0000000000
- 此时从根目录查看节点
ls /
,有以下内容- [k-test0000000000, zookeeper]
- [zk: localhost:2181(CONNECTED) 4]
② 创建临时节点
- 使用
create -e /zk-temp 123
命令创建zk-temp临时节点 - 临时节点在客户端会话结束后,就会自动删除,使用 quit 命令退出客户端后,通过
ls /
查看根目录下的节点,发现临时节点不存在(已被删除)
③ 创建永久节点
- 使用
create /zk-permanent 123
命令创建zk-permanent永久节点 ,可以看到永久节点不同于顺序节点,不会自动在后面添加一串数字
读取节点
- 与读取相关的命令有
ls
命令和get
命令ls
命令可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点ls path
其中,path表示的是指定数据节点的节点路径
get
命令可以获取Zookeeper指定节点的数据内容和属性信息get path
- 若获取根节点下面的所有子节点,使用
ls /
命令即可 - 若想获取某节点如 /zk-permanent 的数据内容和属性,可使用如下命令:
get /zk-permanent
- 从输出信息中可以看到,第一行是节点 /zk-permanent 的数据内容,其他几行则是创建该节点的事务ID(cZxid)、最后一次更新该节点的事务ID(mZxid)和最后一次更新该节点的时间(mtime)等属性信息
更新节点
- 使用
set
命令,可以更新指定节点的数据内容,用法如下set path data
- 其中,data就是要更新的新内容,version表示数据版本,在zookeeper中,节点的数据是有版本概念的,这个参数用于指定本次更新操作是基于Znode的哪一个数据版本进行的,如将/zk-permanent节点的数据更新为456,可以使用如下命令:
set /zk-permanent 456
- 如下,现在 dataVersion已经变为1了,表示进行了更新
[zk: localhost:2181(CONNECTED) 4] set /zk-permanent 456 cZxid = 0x300000008 ctime = Thu Jul 16 04:33:41 EDT 2020 mZxid = 0x300000009 mtime = Thu Jul 16 05:07:00 EDT 2020 pZxid = 0x300000008 cversion = 0 dataVersion = 1 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 3 numChildren = 0
- 如下,现在 dataVersion已经变为1了,表示进行了更新
删除节点
- 使用
delete
命令可以删除Zookeeper上的指定节点,用法如下delete path
- 其中 version 也是表示数据版本,使用
delete /zk-permanent
命令即可删除/zk-permanent节点
注:若删除节点存在子节点,那么无法删除该节点,必须先删除子节点,再删除父节点
2、ZK-开源客户端
ZkClient
ZkClient是Github上一个开源的zookeeper客户端,在Zookeeper原生API接口之上进行了包装,是一个更易用的Zookeeper客户端,同时,zkClient在内部还实现了诸如Session超时重连、Watcher反复注册等功能
- 接下来,还是从创建会话、创建节点、读取数据、更新数据、删除节点等方面来介绍如何使用zkClient这个zookeeper客户端
添加依赖
- 注:Maven中导入依赖后,如果还是找不到该类,需要进行刷新操作
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> </dependency> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.2</version> </dependency>
① 创建会话
- 使用 ZkClient 可以轻松的创建会话,连接到服务端
- 运行结果:zkClient is ready
- 结果表明已经成功创建会话。
public class ZkDemo { public static void main(String[] args) { // 先获取zkClient对象,client与zk集群通信端口为2181 ZkClient zkClient = new ZkClient("linux121:2181"); System.out.println("zkClient is ready"); } }
② 创建节点
- ZkClient 提供了递归创建节点的接口,即其帮助开发者先完成父节点的创建,再创建子节点
运行结果:success create znode
结果表明已经成功创建了节点,值得注意的是,ZkClient通过设置createParents参数为true可以递归的先创建父节点,再创建子节点
public class ZkDemo { public static void main(String[] args) { // 先获取zkClient对象,client与zk集群通信端口为2181 ZkClient zkClient = new ZkClient("linux121:2181"); System.out.println("zkClient is ready"); //1、创建节点 // 第二个参数设置为true,可以递归创建节点 zkClient.createPersistent("/lg-zkClient/lg-c1", true); System.out.println("success create ZNode"); } }
③ 删除节点
ZkClient提供了 递归删除 节点的接口,即其帮助开发者先删除所有子节点(存在),再删除父节点
public class ZkDemo { public static void main(String[] args) { // 先获取zkClient对象,client与zk集群通信端口为2181 ZkClient zkClient = new ZkClient("linux121:2181"); System.out.println("zkClient is ready"); //1、创建节点 // createParents的值设置为true,可以递归创建节点 zkClient.createPersistent("/lg-zkClient/lg-c1", true); System.out.println("success create ZNode."); //2、删除节点 // 递归删除:可以删除非空节点,先删除子节点再删除父节点 zkClient.deleteRecursive("/lg-zkClient"); System.out.println("delete path successfully."); } }
④ 监听节点变化
监听器可以对不存在的目录进行监听
- 监听目录下子节点发生改变,可以接收到通知,携带数据有子节点列表
监听目录创建和删除本身也会被监听到
public class GetChildChange { public static void main(String[] args) throws InterruptedException { //获取到zkClient ZkClient zkClient = new ZkClient("linux121:2181"); //zkClient对指定目录进行监听(不存在的目录:/lg-client),指定收到通知后的逻辑 zkClient.subscribeChildChanges("/lg-client", new IZkChildListener() { //以下方法是接收到通知后的执行逻辑定义 public void handleChildChange(String path, List<String> childs) throws Exception { //打印节点信息 System.out.println(path + " childs changes, current childs " + childs); } }); //使用zkClient创建节点后,删除节点,验证监听器是否运行 zkClient.createPersistent("/lg-client"); Thread.sleep(1000);//此处只是为了方便观察结果数据 zkClient.createPersistent("/lg-client/c1"); Thread.sleep(1000); zkClient.delete("/lg-client/c1"); Thread.sleep(1000); zkClient.delete("/lg-client"); Thread.sleep(Integer.MAX_VALUE); } }
⑤ 获取数据
监听节点是否存在、更新、删除
注:需要设置自定义的序列化类型,否则会报错!!
public class GetDataChange { public static void main(String[] args) throws InterruptedException { //获取到zkClient ZkClient zkClient = new ZkClient("linux121:2181"); //设置自定义的序列化类型,否则会报错!! zkClient.setZkSerializer(new ZkStrSerializer()); //判断节点是否存在,不存在则创建节点并赋值 boolean exists = zkClient.exists("/lg-client"); if (!exists) { zkClient.createEphemeral("/lg-client", "2020"); } //注册监听器,节点数据改变的类型,接收通知后的处理逻辑定义 zkClient.subscribeDataChanges("/lg-client", new IZkDataListener() { @Override public void handleDataChange(String path, Object data) throws Exception { // 定义接收通知后的处理逻辑 System.out.println(path + " data is changed ,new data " + data); } @Override public void handleDataDeleted(String path) throws Exception { //数据删除 --> 节点删除 System.out.println(path + " is deleted!!"); } }); //更新节点的数据,删除节点,验证监听器是否正常运行 Object o = zkClient.readData("/lg-client"); System.out.println(o); zkClient.writeData("/lg-client", "I am new data"); Thread.sleep(1000); //删除节点 zkClient.delete("/lg-client"); Thread.sleep(Integer.MAX_VALUE); } }
自定义的序列化方法
public class ZkStrSerializer implements ZkSerializer { // 序列化方法 string --> byte @Override public byte[] serialize(Object o) throws ZkMarshallingError { return String.valueOf(o).getBytes(); } //反序列化方法 byte --> string @Override public Object deserialize(byte[] bytes) throws ZkMarshallingError { return new String(bytes); } }