集群操作

集群安装

集群规划

在linux102、linux103、linux104三个节点上都部署ZK

集群操作

安装ZK

  1. # 解压Zookeeper安装包
  2. tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module
  3. # 修改apache-zookeeper-3.5.7-bin名称为zookeeper-3.5.7
  4. mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7
  5. # 配置服务器编号
  6. cd /opt/module/zookeeper-3.5.7/
  7. mkdir zkData # 创建zkData文件夹
  8. touch myid # 创建myid的文件,在文件里面添加与server对应的编号(注:只能有数字,不要有其他内容)
  9. # 向myid写入
  10. 2
  11. # 分发Zookeeper到集群种其他机器上
  12. xsync /opt/module/zookeeper-3.5.7
  13. # 分发到集群种其他机器上,将/opt/module/zookeeper-3.5.7/zkData/myid 文件中的内容梗概为机器名的id(比如linux102->2)

配置zoo.cfg

# 重命名 /opt/module/zookeeper-3.5.7/conf 中zoo_sample.cfg为zoo.cfg
mv /opt/module/zookeeper-3.5.7/conf/zoo_sample.cfg /opt/module/zookeeper-3.5.7/conf/zoo.cfg

# 修改zoo.cfg文件
    # 修改数据存储路径配置
dataDir=/opt/module/zookeeper-3.5.7/zkData
    # 增加配置
###################### cluster #########################
server.2=linux102:2888:3888
server.3=linux103:2888:3888
server.4=linux104:2888:3888

# 同步zoo.cfg配置文件
xsync /opt/module/zookeeper-3.5.7/conf/zoo.cfg

配置参数解读
server.A=B:C:D

  • A是一个数字,表示这个是第几个服务器
    • 集群模式下配置一个文件myid,这个文件在dataDir目录下,这个文件里面有一个数据就是A的值,Zookeeper启动时读取此文件,拿到里面的数据与zoo.cfg里面的配置信息比较从而判断到底是哪个server。
  • B是这个服务器的地址
  • C是这个服务器Follower与集群中的Leader服务器交换信息的端口
  • D是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的端口。

集群操作

# 启动Zookeeper;分别在集群中所有的服务器上启动服务 
/opt/module/zookeeper-3.5.7/bin/zkServer.sh start

# 查看集群状态
/opt/module/zookeeper-3.5.7/bin/zkServer.sh status

选举机制(面试重点)

ZK-第一次启动

C-ZK集群操作 - 图1

  • SID:服务器ID。用来唯一标识一台Zookeeper集群中的机器,每台机器不能重复,和myid一致。
  • ZXID:事务ID,ZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这个Zookeeper服务器对于客户端“更新请求”的处理逻辑有关。
  • Epoch:每个Leader任期的代号,没有Leader时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票就会增加。
  1. 服务器1启动,发起一次选举,服务器1投自己一票,此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING;
  2. 服务器2启动,再发起一次选举,服务器1和2分别投自己一票并交换选票信息;此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)大,更改为推举服务器2,此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2保持状态LOOKING;
  3. 服务器3启动,发起一次选举,此时服务器1和2都会更改选票为服务器3,此次投票结果:服务器1为0票,服务器2为0票,服务器3位3票,此时服务器3的票数已经超过半数,服务器3当选Leader,服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
  4. 服务器4启动,发起一次选举,此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息结果。交换选票信息结果;服务器3为3票,服务器4为1票,此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;
  5. 服务器5启动,同4一样当小弟。

ZK-非第一次启动

C-ZK集群操作 - 图2

  1. 当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举:
    1. 服务器初始化启动。
    2. 服务器运行期间无法和Leader保持连接。
  2. 而当一台机器进入Leader选举流程时,当前集群也可能是会处于以下两种装填:
    1. 集群中本来就已经存在一个Leader。对于第一种已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连接,并进行状态同步即可。
    2. 集群中确实不存在Leader。假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是leader,某一时刻,3和5服务器出现故障,因此开始进行Leader选举。

C-ZK集群操作 - 图3

ZK集群启停脚本

/home/linux目录下创建脚本zk.sh,增加脚本执行权限chmod u+x zk.sh,后改名为zk并移到/usr/bin中,mv /home/linux/zk /usr/bin/

#!/bin/bash

case $1 in
"start") {
    for i in linux102 linux103 hadoop104
  do
      echo ---------------------- zookeeper $i 启动 ----------------------
    ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
  done
};;
"stop") {
    for i in linux102 linux103 hadoop104
  do
      echo ---------------------- zookeeper $i 停止 ----------------------
    ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
  done
};;
"status") {
    for i in linux102 linux103 hadoop104
  do
      echo ---------------------- zookeeper $i 状态 ----------------------
    ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
  done
};;
esac

客户端命令行操作

命令行语法

命令基本语法 功能描述
help 显示所有操作命令
ls path 使用ls命令来查看当前znode的子节点【可监听】
-w 监听子节点变化
-s 附加次级信息
create 普通创建
-s 含有序列
-e 临时(重启或者超时消失)
get path 获得节点的值【可监听】
-w 监听节点内容变化
-s 附加次级信息
set 设置节点的具体值
stat 查看节点状态
delete 删除节点
deleteall 递归删除节点

启动客户端

[linux@linux102 zookeeper-3.5.7]$ pwd
/opt/module/zookeeper-3.5.7
[linux@linux102 zookeeper-3.5.7]$ bin/zkCli.sh -server linux102:2181

显示所有操作命令

[zk: linux102:2181(CONNECTED) 0] help
ZooKeeper -server host:port cmd args
    addauth scheme auth
    close 
    config [-c] [-w] [-s]
    connect host:port
    create [-s] [-e] [-c] [-t ttl] path [data] [acl]
    delete [-v version] path
    deleteall path
    delquota [-n|-b] path
    get [-s] [-w] path
    getAcl [-s] path
    history 
    listquota path
    ls [-s] [-w] [-R] path
    ls2 path [watch]
    printwatches on|off
    quit 
    reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
    redo cmdno
    removewatches path [-c|-d|-a] [-l]
    rmr path
    set [-s] [-v version] path data
    setAcl [-s] [-v version] [-R] path acl
    setquota -n|-b val path
    stat [-w] path
    sync path
Command not found: Command not found help

znode节点数据信息

查看当前znode中所包含的内容

[zk: linux102:2181(CONNECTED) 1] ls /
[zookeeper]

查看当前节点详细数据

[zk: linux102:2181(CONNECTED) 2] ls -s /
[zookeeper]cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
  • czxid:创建节点的事务zxid
    • 每次修改ZooKeeper状态都会产生一个ZooKeeper事务ID。事务ID是ZooKeeper中所有修改总的次序。每次修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
  • ctime:znode被创建的毫秒数(从1970年开始)
  • mZxid:znode最后更新的事务zxid
  • mtime:znode最后修改的毫秒数(从1970年开始)
  • pZxid:znode最后更新的子节点zxid
  • cversion:znode子节点变化号,znode子节点修改次数
  • dataVersion:znode数据变化号
  • aclVersion:znode访问控制列表的变化号
  • ephemeralOwner:如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0
  • dataLength:znode的数据长度
  • numChildren:znode子节点数量

节点类型(持久/短暂/有序号/无序号)

C-ZK集群操作 - 图4

持久(Persistent):客户端和服务器断开连接后,创建的节点不删除。
短暂(Ephemeral):客户端和服务器断开连接后,创建的节点自己删除。

说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护。 注意:在分布式系统中,顺序号可以被用于为所有的时间进行全局排序,这样客户端可以通过顺序号推断事件的顺序

  1. 持久化目录节点
    1. 客户端与Zookeeper断开连接后,该节点依旧存在
  2. 持久化顺序编号目录节点
    1. 客户端与ZooKeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
  3. 临时目录节点
    1. 客户端与ZooKeeper断开连接后,该节点被删除
  4. 临时顺序编号目录节点
    1. 客户端与ZooKeeper断开连接后,该节点被删除,只是ZooKeeper给该节点名称进行顺序编号。

分别创建2个普通节点(永久节点 + 不带序号)

[zk: localhost:2181(CONNECTED) 1] create /sanguo "diaochan"
Created /sanguo
[zk: localhost:2181(CONNECTED) 2] create /sanguo/shuguo "liubei"
Created /sanguo/shuguo

注意:创建节点时,要赋值

获得节点的值

[zk: localhost:2181(CONNECTED) 4] get -s /sanguo
diaochan
cZxid = 0x600000002
ctime = Mon Feb 28 19:01:06 CST 2022
mZxid = 0x600000002
mtime = Mon Feb 28 19:01:06 CST 2022
pZxid = 0x600000003
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 1
[zk: localhost:2181(CONNECTED) 5] get -w /sanguo
diaochan
[zk: localhost:2181(CONNECTED) 6] get -s /sanguo/shuguo
liubei
cZxid = 0x600000003
ctime = Mon Feb 28 19:01:21 CST 2022
mZxid = 0x600000003
mtime = Mon Feb 28 19:01:21 CST 2022
pZxid = 0x600000003
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0

创建带序号的节点(永久节点 + 带序号)

  1. 先创建一个普通的跟节点/sanguo/weiguo

    [zk: localhost:2181(CONNECTED) 17] create /sanguo/weiguo "caocao"
    Created /sanguo/weiguo
    
  2. 创建带序号的节点

    [zk: localhost:2181(CONNECTED) 18] create -s /sanguo/weiguo/zhangliao "zhangliao"
    Created /sanguo/weiguo/zhangliao0000000000
    [zk: localhost:2181(CONNECTED) 19] create -s /sanguo/weiguo/zhangliao "zhangliao"
    Created /sanguo/weiguo/zhangliao0000000001
    [zk: localhost:2181(CONNECTED) 20] create -s /sanguo/weiguo/zhangliao "zhangliao"
    Created /sanguo/weiguo/zhangliao0000000002
    

    如果原来没有序号节点,序号从0开始依次递增。如果原节点下已有2个节点,则再排序时从2开始,依次类推。

创建短暂节点(短暂节点 + 不带序号 or 带序号)

  1. 创建短暂的不带序号的节点

    [zk: localhost:2181(CONNECTED) 0] create -e /sanguo/wuguo "zhouyu"
    Created /sanguo/wuguo
    
  2. 创建短暂的带序号的节点

    [zk: localhost:2181(CONNECTED) 1] create -e -s /sanguo/wuguo "zhouyu"
    Created /sanguo/wuguo0000000004
    
  3. 在当前客户端是能查看到的

    [zk: localhost:2181(CONNECTED) 2] ls /sanguo
    [shuguo, wuguo, wuguo0000000004]
    
  4. 退出当前客户端然后再重启客户端

    [zk: localhost:2181(CONNECTED) 3] quit
    [linux@linux102 zookeeper-3.5.7]$ bin/zkCli.sh
    
  5. 再次查看根目录下短暂节点已经删除

    [zk: localhost:2181(CONNECTED) 0] ls /sanguo
    [shuguo]
    

修改节点数据值

[zk: localhost:2181(CONNECTED) 2] get -w /sanguo/shuguo
liubei
[zk: localhost:2181(CONNECTED) 3] set /sanguo/shuguo "liubei001"

WATCHER::

WatchedEvent state:SyncConnected type:NodeDataChanged path:/sanguo/shuguo
[zk: localhost:2181(CONNECTED) 4] get -w /sanguo/shuguo
liubei001

监听器原理

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper会通知客户端。监听机制保证ZooKeeper保存的任何数据的任何变化都能快速的响应到监听了该节点的应用程序。

C-ZK集群操作 - 图5

监听原理详解

  1. 首先要有一个main()线程
  2. 在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责连接通信(connect),一个负责监听(listener)。
  3. 通过connect线程将注册的监听事件发送给Zookeeper。
  4. 在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
  5. Zookeeper监听到有数据或者路径变化,就会将这个消息发送给listener线程。
  6. listener线程内部调用了process()方法

常见的监听

  1. 监听节点数据的变化get path [watch]
  2. 监听子节点增减的变化ls path [watch]

节点的值变化监听

  1. 在linux104主机上注册监听/sanguo节点数据变化

    [zk: localhost:2181(CONNECTED) 0] ls -w /sanguo
    diaochan
    
  2. 在linux103主机/sanguo节点上创建子节点

    [zk: localhost:2181(CONNECTED) 6] create /sanguo/jin "simayi"
    
  3. 观察linux104主机收到子节点变化的监听 ```shell [zk: localhost:2181(CONNECTED) 7] WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/sanguo

注意:节点的路径变化,也是注册一次,生效一次。想多次生效,就需要多次注册。

<a name="LPd2n"></a>
## 节点删除与查看

<a name="RPDnR"></a>
### 删除节点
```shell
[zk: localhost:2181(CONNECTED) 5] delete /sanguo/jin

递归删除节点

[zk: localhost:2181(CONNECTED) 6] deleteall /sanguo/shuguo

查看节点状态

[zk: localhost:2181(CONNECTED) 7] stat /sanguo
cZxid = 0x600000002
ctime = Mon Feb 28 19:01:06 CST 2022
mZxid = 0x600000019
mtime = Mon Feb 28 19:27:42 CST 2022
pZxid = 0x600000024
cversion = 16
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

客户端API操作

保证linux102、linux103、linux104服务器上Zookeeper集群服务启动。

IDEA环境搭建

创建一个工程:zookeeper

添加pom文件

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.2</version>
</dependency>

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.5.7</version>
</dependency>

拷贝log4j.properties文件到项目根目录

需要在项目src/main/resources目录下创建一个文件,命名为log4j.proprties,在文件中填入

log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/sprint.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n

创建包名com.zh.zk

创建类名称ZkClient

创建ZooKeeper客户端

private static String connectString = "linux102:2181,linux103:2181,linux104:2181";
private static int sessionTimeout = 2000;
private ZooKeeper zkClient = null;

@Before
public void init() throws Exception {
    zkClient = new ZooKeeper(connectString, sessionTimeout, event -> {
        // 收到事件通知后的回调函数(用户的业务逻辑)
        System.out.println(event.getType() + "---" + event.getPath());

        // 再次启动监听
        try {
            List<String> children = zkClient.getChildren("/", true);
            for (String child : children) {
                System.out.println(child);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}

创建子节点

// 创建子节点
@Test
public void create() throws Exception {
    // 参数1:要创建的节点的路径;参数2:节点数据;参数3:节点权限;参数4:节点的类型
    String nodeCreated = zkClient.create("/zk", "shuaige".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}

测试:在linux102的zk客户端上查看创建节点情况

[zk: localhost:2181(CONNECTED) 3] get -s /zk
shuaige
cZxid = 0x700000003
ctime = Mon Feb 28 23:31:08 CST 2022
mZxid = 0x700000003
mtime = Mon Feb 28 23:31:08 CST 2022
pZxid = 0x700000003
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 7
numChildren = 0

获取子节点并监听节点变化

// 获取子节点并监听节点变化
@Test
public void getChildren() throws Exception {
    List<String> children = zkClient.getChildren("/", true);
    for (String child : children) {
        System.out.println(child);
    }

    // 延时阻塞
    Thread.sleep(Long.MAX_VALUE);
}
  1. 在IDEA控制台上看到如下节点:

    zk
    zookeeper
    sanguo
    
  2. 在linux102的客户端上创建再创建一个节点/zh1,观察IDEA控制台

    [zk: localhost:2181(CONNECTED) 2] create /zh1 "zh1"
    NodeChildrenChanged---/
    zk
    zh1
    zookeeper
    sanguo
    
  3. 在linux102的客户端上删除节点/zh1,观察IDEA控制台

    [zk: localhost:2181(CONNECTED) 1] delete /zh1
    NodeChildrenChanged---/
    zk
    zookeeper
    sanguo
    

    判断Znode是否存在

    // 判断znode是否存在
    @Test
    public void exist() throws Exception {
     Stat stat = zkClient.exists("/zk", false);
     System.out.println(stat == null ? "not exist" : "exist");
    }
    

客户端向服务端写数据流程

写流程之写入请求直接发送给Leader节点

C-ZK集群操作 - 图6

写流程之写入请求发送给follower节点

C-ZK集群操作 - 图7