搭建基于ZooKeeper的集群

概述

搭建了集群后,再认真学习下ZooKeeper。

ZooKeeper的存储结构

存储结构

树形结构,类似Linux系统,第一级是/。ZooKeeper不区分目录(普通目录可以拥有子节点,但是不能存储内容)和文件(普通文件能存储内容,但是不能拥有子节点),都叫节点。

宏观上节点指一台机器,微观上节点指代一个文件或者目录。但是ZooKeeper的节点兼容了目录和文本的特征。

节点设计

既可以拥有子节点,也可以存储内容。

不能存储大量的数据,每个节点存储的数据内容不能超过1M。

ZooKeeper的常用命令

由于是解压安装,缺少很多环境变量配置,每次都先cd到安装目录再执行显然不合适。

新建脚本:start-zk-all.sh用于启动全部ZK节点:

#!/bin/bash
ZK_HOME=/export/server/zookeeper-3.4.6

for number in {1..3}
do
	host=node${number}
	echo ${host}
	/usr/bin/ssh ${host} "cd ${ZK_HOME};source /etc/profile;${ZK_HOME}/bin/zkServer.sh start"
	echo "${host} started"
done

新建脚本:status-zk-all.sh用于查看全部ZK节点的状态:

#!/bin/bash
ZK_HOME=/export/server/zookeeper-3.4.6

for number in {1..3}
do
	host=node${number}
	echo ${host}
	/usr/bin/ssh ${host} "cd ${ZK_HOME};source /etc/profile;${ZK_HOME}/bin/zkServer.sh status"
done

新建脚本:stop-zk-all.sh用于关闭全部ZK节点:

#!/bin/bash
ZK_HOME=/export/server/zookeeper-3.4.6

for number in {1..3}
do
	host=node${number}
	echo ${host}
	/usr/bin/ssh ${host} "cd ${ZK_HOME};source /etc/profile;${ZK_HOME}/bin/zkServer.sh stop"
	echo "${host} stoped"
done

node1中切换到/bin目录cd /bin,rz上传3个脚本,使用chmod u+x ./*给这3个脚本添加可执行权限。
在这里插入图片描述
执行后可以看到3个节点的状态。

Client连接Server

切换到ZK的安装目录并查看配置文件:

cd /export/server/zookeeper-3.4.6/conf
cat zoo.cfg

发现Client端口号为2181:
在这里插入图片描述
使用cd ..返回上一级,使用bin/zkCli.sh -server node1:2181,node2:2181,node3:2181,发现:

Connecting to node1:2181,node2:2181,node3:2181
2021-04-21 20:30:05,692 [myid:] - INFO  [main:Environment@100] - Client environment:zookeeper.version=3.4.6-1569965, built on 02/20/2014 09:09 GMT
2021-04-21 20:30:05,696 [myid:] - INFO  [main:Environment@100] - Client environment:host.name=node1
2021-04-21 20:30:05,696 [myid:] - INFO  [main:Environment@100] - Client environment:java.version=1.8.0_241
2021-04-21 20:30:05,700 [myid:] - INFO  [main:Environment@100] - Client environment:java.vendor=Oracle Corporation
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:java.home=/export/server/jdk1.8.0_241/jre
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:java.class.path=/export/server/zookeeper-3.4.6/bin/../build/classes:/export/server/zookeeper-3.4.6/bin/../build/lib/*.jar:/export/server/zookeeper-3.4.6/bin/../lib/slf4j-log4j12-1.6.1.jar:/export/server/zookeeper-3.4.6/bin/../lib/slf4j-api-1.6.1.jar:/export/server/zookeeper-3.4.6/bin/../lib/netty-3.7.0.Final.jar:/export/server/zookeeper-3.4.6/bin/../lib/log4j-1.2.16.jar:/export/server/zookeeper-3.4.6/bin/../lib/jline-0.9.94.jar:/export/server/zookeeper-3.4.6/bin/../zookeeper-3.4.6.jar:/export/server/zookeeper-3.4.6/bin/../src/java/lib/*.jar:/export/server/zookeeper-3.4.6/bin/../conf:.:/export/server/jdk1.8.0_241/lib
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:java.library.path=/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:java.io.tmpdir=/tmp
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:java.compiler=<NA>
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:os.name=Linux
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:os.arch=amd64
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:os.version=3.10.0-1062.el7.x86_64
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:user.name=root
2021-04-21 20:30:05,701 [myid:] - INFO  [main:Environment@100] - Client environment:user.home=/root
2021-04-21 20:30:05,702 [myid:] - INFO  [main:Environment@100] - Client environment:user.dir=/export/server/zookeeper-3.4.6
2021-04-21 20:30:05,703 [myid:] - INFO  [main:ZooKeeper@438] - Initiating client connection, connectString=node1:2181,node2:2181,node3:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@25f38edc
Welcome to ZooKeeper!
2021-04-21 20:30:05,749 [myid:] - INFO  [main-SendThread(node3:2181):ClientCnxn$SendThread@975] - Opening socket connection to server node3/192.168.88.11:2181. Will not attempt to authenticate using SASL (unknown error)
JLine support is enabled
2021-04-21 20:30:05,844 [myid:] - INFO  [main-SendThread(node3:2181):ClientCnxn$SendThread@852] - Socket connection established to node3/192.168.88.11:2181, initiating session
[zk: node1:2181,node2:2181,node3:2181(CONNECTING) 0] 2021-04-21 20:30:05,900 [myid:] - INFO  [main-SendThread(node3:2181):ClientCnxn$SendThread@1235] - Session establishment complete on server node3/192.168.88.11:2181, sessionid = 0x378eeb1e5800000, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null

使用node1作Client连接Server,node1连接到了自己!!!node1还连接到了node2和node3。

使用bin/zkCli.sh -server node1:2181连接node1.或者使用bin/zkCli.sh -server node2:2181或者使用bin/zkCli.sh -server node3:2181只连一台机器也没有任何问题。这是因为ZK是公平节点,每个节点存储的内容是一致的(数量上至少有多一半的机器存储着相同的内容),故只需要一台就可以使用。但是如果只给定一台,则Client只能连接到这台,Server宕机后将无法连接到另外的节点。规范的操作应该是给定多台的地址,当连接一台失败时,自动连接到其它的机器。

ZooKeeper的基本命令

连接后,命令行的状态为:[zk: node1:2181,node2:2181,node3: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

给出了命令格式及参数。要注意:ZK的节点不能使用相对路径,需要使用绝对路径。

列举

标准格式:ls path [watch]

使用ls /可以查看目录下的内容,使用ls /zookeeper还可以看到下一级的内容:
在这里插入图片描述

创建

标准格式:create [-s] [-e] path data
在这里插入图片描述

读取

标准格式:get path [watch]
在这里插入图片描述
可以看到cZxid是递增的。

修改

标准格式:set path data [version]
在这里插入图片描述

查看

标准格式:get path
在这里插入图片描述
可以看出内容已经发生了改变,dataversion也+1。

删除

标准格式:rmr path
在这里插入图片描述
可以看出成功删除了节点文件。

退出

这一步就比较随意了,使用quit或者ctrl+c都可以。

ZooKeeper的特性

节点类型

永久节点

默认创建的节点都是永久节点。create path data这条命令创建的普通的节点(也就是永久节点)如果不手动删除,将会永远存在。上方也看到了不允许创建同名的节点。

永久有序节点

命令create -s path data允许创建同名的节点,会自动编号。

临时节点-e

命令create -e path data创建的临时节点会在Client断开自动删除

临时节点的生命周期随着Client产生,Client断开时,创建的临时节点会自动删除。

临时有序节点

命令create -e -s path data创建的节点包含了临时节点和有序节点的特性,既做编号也时临时节点。

序列节点-s

可以创建同名的节点,ZK会自动对同名节点进行编号。

监听机制

功能

监听:设置监听某个节点,如果这个节点发生变化,可以立即受到这个变化的通知。

实现

ls path [watch] 
get path [watch]

当使用watch时就会开启监听,发生变化时自动提示,貌似只能生效一次。。。

ZooKeeper的选举

所有辅助选举、元数据存储都在分布式工具中封装好了,不用我们干预。

辅助Active Master选举

临时节点的作用:辅助选举。
监听机制的作用:辅助切换。

使用临时节点

这是最常见的方式。

MasterA和MasterB,都启动后,让它们同时创建同一个临时节点file,普通的临时节点不允许重复。假设A创建成功,B就无法创建成功(反之亦然),成功的Master就是Active的Master(Leader),失败的Master就是Standby的Master(Follower)。

假设Active的Master节点A故障,临时节点会被自动删除。让B监听,如果临时节点小时被B监听到,B就会立即创建这个临时节点并成为新的Active的Master。A状态正常后已经无法创建该同名节点(说明已经有Active的Master节点),自己只能是Standby并且监听。

使用临时有序节点

MasterA和MasterB,都启动后,让它们同时创建同一个临时有序节点file,临时有序节点允许重复但是会标记编号。假设A先创建成功,A的编号最小,就会作为Active的Master,B就只能是Standby的Master。

假设Active的Master节点A故障,临时有序节点会被自动删除。让B监听,如果临时有序节点有1个消失,B就会监听到,成为新的Active的Master。A重启以后发现已经存在一个临时节点,说明已经有一个Active,无论A是否创建新的临时有序节点都不可能比当前B的临时有序节点的编号小,故A只能是Standby状态并且设置监听。

内部Leader节点选举

内部Leader节点的选举规则

zxid:数据id,用于标记数据的更新状态,zxid越大,代表数据越新。

myid:权重id(节点标识id),每台ZooKeeper都有一个唯一标识的id(搭建集群时已经给定),选举时也会作为权重的id。

规则:先比较zxid(zxid大的就是leader),如果zxid相同,再去比较myid,谁大(且超过半数)谁就是leader。

内部Leader节点的选举过程

集群第一次启动

由于搭集群时已经设定了node1的myid=1,node2的myid=2,node3的myid=3,集群刚启动时zxid都=0,只能比较myid。
由于是使用node1的命令行通过免密匙登录和shell脚本启动node2和node3的ZooKeeper服务,node1一定是最先启动,先给自己投一票。按照shell脚本的执行顺序,正常情况应该是node2接着启动,也给自己投了一票,此时node1与node2都是1票。由于node2的myid比node1大,node1转而把自己的票投给node2,此时node2拥有2票。此时已有超过半数的机器投了node2的票,node2成为Leader。
node3启动后,还是会发现已经有超过一半的机器投了node2的票,就算给自己投票,票数也不可能比node2更多,于是node3和node1都是Follower。
这就解释了喂猫平时这种启动方式大概率node2是leader。

但是也会有意外!!!

比如node2网络延时比node3高,或者硬盘读取等因素,导致node2启动的比node3慢,leader就是node3。

集群故障恢复启动

假设node2为leader,node1和node3为follower,写入文件时node2的zxid=5,假设node1先同步写入完成(node1的zxid=5),此时超过一半的节点已经完成写入,如果此时leader node2宕机,作为follower的node3的zxid还是4(没有来得及同步写入并使zxid=5),此时就会重新选举,先比较zxid,更大的node1直接就会成为新的leader(数据越新越能保证数据的安全性),并且将node3的数据同步为和自己一致,这样便保证了数据的安全性和一致性。

ZK节点数一般为奇数

虽然节点的个数可以是偶数,但是奇数台和偶数台的容错率是相同的。例如:5台和6台,都是只容许小于一半的机器宕机,5台允许宕机2台(假设宕机了3台且都是已经更新数据的机器,剩下的2台都是未更新数据的机器,如果放任那2台变成leader并继续工作会出现数据回滚),6台也是允许宕机2台(假设6台更新数据的有4台,宕机的3台恰好都是已经更新过数据的[这种做法是为了提高响应速度],只剩3台未更新数据的服务器,此时可能发生脑裂,集群的可靠性得不到保障,破坏了选举机制),超过容错率就没办法保证数据的一致性、安全性、可靠性。显然奇数台和多一台的偶数台相比区别并不大,偶数台还会浪费一台机器的资源,故一般情况都是用奇数台。

ZooKeeper的Java API

环境搭建

新建Maven项目,新建个Module,在pom.xml添加依赖项:

<dependencies>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.12.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.collections</groupId>
            <artifactId>google-collections</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
        </dependency>
    </dependencies>

继续添加编译版本限制:

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

新建Java类public class ZooKeeperClientDemo {},为了方便就不使用psvm构造main主方法入口了,直接配置:
创建客户端连接对象:

CuratorFramework client = null;

构建连接:

@Before
    public void getConnection() {
        //构建重试连接
        ExponentialBackoffRetry retry = new ExponentialBackoffRetry(5000, 3);
        //构建连接客户端
        client = CuratorFrameworkFactory.newClient("node1:2181", retry);
    }

如果这一步漏了连接IP与端口会报错:

java.net.ConnectException: Connection refused: no further information

释放资源:

@After
    public void closeConnect() {
        client.close();
    }

增删改查

创建节点:

@Test
    public void createNode() throws Exception {
        //启动
        client.start();

        //创建节点
        //创建永久节点
        client.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath("/bigdata01", "spark".getBytes());

        //创建临时节点
        client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/bigdata01", "spark".getBytes());
        Thread.sleep(10000);//暂停10s
    }

查询节点:

@Test
    public void getNode() throws Exception {
        //启动
        client.start();
        //查询
        byte[] bytes = client.getData().forPath("/bigdata01");
        System.out.println("/bigdata01的内容是"+new String(bytes));
    }

修改节点:

@Test
    public void setNode() throws Exception {
        //启动
        client.start();
        //修改节点
        client.setData().forPath("/bigdata","digital monster".getBytes());
    }

删除节点:

@Test
    public void rmNode() throws Exception {
        //启动
        client.start();
        //删除
        client.delete().forPath("/bigdata01");

    }

在这里插入图片描述
执行:
在这里插入图片描述
在这里插入图片描述
可以看到创建永久节点成功。

在这里插入图片描述
过了10s后临时节点消失。

在这里插入图片描述
也可以查询。

在这里插入图片描述
也可以删除。

以后就可以使用JavaAPI实现对ZooKeeper节点的增删改查了!!!类似JDBC修改MySQL数据库的骚操作,还是很方便的。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐