Zookeeper 入门到实战 看这一篇文章就够了 !

324 阅读21分钟

鄙视屎山,理解屎山,堆砌屎山,超越屎山 !!!

image.png

聊到分布式协调服务,相信大家肯定能够想到Zookeeper。那么Zookeeper是怎么实现的呢,正好最近上班很无聊,这几天利用摸鱼的时间梳理了一遍 Zookeeper 相关的内容。今天就给大家分享一下我这几天摸鱼的成果,本文主要内容有以下几个部分
1、Zookeeper单机版的安装,以及基本命令的使用。
2、Zookeeper内部数据的存储方式以及Znode的特点。
3、Zookeeper Java API 的基本使用
4、Zookeeper 的 Watch 机制
5、Zookeeper的应用场景
6、Zookeeper 集群环境搭建

本文中使用的软件版本如下:

                                软件                                                                              版本号                                 
Zookeeper3.9.2
JDK17.02
SpringBoot3.2.10

1、安装和配置

1.1、下载安装包

 wget https://dlcdn.apache.org/zookeeper/zookeeper-3.9.2/apache-zookeeper-3.9.2-bin.tar.gz   

下载完成后解压后即可,这里我将解压后的目录放到了 /usr/local 目录下了

image.png

tar zxvf apache-zookeeper-3.9.2-bin.tar.gz 
mv apache-zookeeper-3.9.2-bin zookeeper-3.9.2
mv zookeeper-3.9.2 /usr/local/ 

1.2、修改配置文件

官方文档连接: zookeeper.apache.org/doc/current…

image.png

这里我们参照官方文档上的说明 新建一个数据目录,然后修改zoo.cfg配置文件
这里解释一下几个参数:
tickTime: ZooKeeper使用的基本时间单位(毫秒)。它用于执行心跳,最小会话超时将是tickTime的两倍。
dataDir: 存储内存中数据快照的路径
clientPort: 监听客户端连接的端口 这里我是将原来的 文件复制了一份然后在原来的基础上修改,内容如下

image.png

2、基本命令

这里参照官网上给大家梳理了一部分命令

2.1、服务端主要的命令

服务的的命令主要包含服务的启停以及状态查看

# 启动 ZooKeeper 服务
./zkServer.sh start
# 查看 ZooKeeper 服务状态
./zkServer.sh status
# 停止 ZooKeeper 服务
./zkServer.sh stop 
# 重启 ZooKeeper 服务
./zkServer.sh restart 

zookeeper启动如下

[root@VM-4-9-centos zookeeper-3.9.2]# ./bin/zkServer.sh stop                                                                                                                                                                                                                
ZooKeeper JMX enabled by default                                                                                                                                                                                                                                            
Using config: /usr/local/zookeeper-3.9.2/bin/../conf/zoo.cfg                                                                                                                                                                                                                
Stopping zookeeper ... STOPPED                                                                                                                                                                                                                                              
[root@VM-4-9-centos zookeeper-3.9.2]# ./bin/zkServer.sh start                                                                                                                                                                                                               
ZooKeeper JMX enabled by default                                                                                                                                                                                                                                            
Using config: /usr/local/zookeeper-3.9.2/bin/../conf/zoo.cfg                                                                                                                                                                                                                
Starting zookeeper ... STARTED                                                                                                                                                                                                                                              
[root@VM-4-9-centos zookeeper-3.9.2]# ./bin/zkServer.sh status                                                                                                                                                                                                              
ZooKeeper JMX enabled by default                                                                                                                                                                                                                                            
Using config: /usr/local/zookeeper-3.9.2/bin/../conf/zoo.cfg                                                                                                                                                                                                                
Client port found: 2181. Client address: localhost. Client SSL: false.                                                                                                                                                                                                      
Mode: standalone                                                                                                                                                                                                                                                            
[root@VM-4-9-centos zookeeper-3.9.2]# ps -ef | grep zookeeper                                                                                                                                                                                                               
root     27055     1  6 18:40 pts/1    00:00:01 /usr/local/jdk-21.0.4/bin/java -Dzookeeper.log.dir=/usr/local/zookeeper-3.9.2/bin/../logs -Dzookeeper.log.file=zookeeper-root-server-VM-4-9-centos.log -XX:+HeapDumpOnOutOfMemoryError -XX:OnOutOfMemoryError=kill -9 %p -cp
 /usr/local/zookeeper-3.9.2/bin/../zookeeper-metrics-providers/zookeeper-prometheus-metrics/target/classes:/usr/local/zookeeper-3.9.2/bin/../zookeeper-server/target/classes:/usr/local/zookeeper-3.9.2/bin/../build/classes:/usr/local/zookeeper-3.9.2/bin/../zookeeper-met
rics-providers/zookeeper-prometheus-metrics/target/lib/*.jar:/usr/local/zookeeper-3.9.2/bin/../zookeeper-server/target/lib/*.jar:/usr/local/zookeeper-3.9.2/bin/../build/lib/*.jar:/usr/local/zookeeper-3.9.2/bin/../lib/zookeeper-prometheus-metrics-3.9.2.jar:/usr/local/z
ookeeper-3.9.2/bin/../lib/zookeeper-jute-3.9.2.jar:/usr/local/zookeeper-3.9.2/bin/../lib/zookeeper-3.9.2.jar:/usr/local/zookeeper-3.9.2/bin/../lib/snappy-java-1.1.10.5.jar:/usr/local/zookeeper-3.9.2/bin/../lib/slf4j-api-1.7.30.jar:/usr/local/zookeeper-3.9.2/bin/../lib
/simpleclient_servlet-0.9.0.jar:/usr/local/zookeeper-3.9.2/bin/../lib/simpleclient_hotspot-0.9.0.jar:/usr/local/zookeeper-3.9.2/bin/../lib/simpleclient_common-0.9.0.jar:/usr/local/zookeeper-3.9.2/bin/../lib/simpleclient-0.9.0.jar:/usr/local/zookeeper-3.9.2/bin/../lib/
netty-transport-native-unix-common-4.1.105.Final.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-transport-native-epoll-4.1.105.Final-linux-x86_64.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-transport-classes-epoll-4.1.105.Final.jar:/usr/local/zookeeper-3.9.2/bin/
../lib/netty-transport-4.1.105.Final.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-tcnative-classes-2.0.61.Final.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-tcnative-boringssl-static-2.0.61.Final-windows-x86_64.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-tcn
ative-boringssl-static-2.0.61.Final-osx-x86_64.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-tcnative-boringssl-static-2.0.61.Final-osx-aarch_64.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-tcnative-boringssl-static-2.0.61.Final-linux-x86_64.jar:/usr/local/zookee
per-3.9.2/bin/../lib/netty-tcnative-boringssl-static-2.0.61.Final-linux-aarch_64.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-tcnative-boringssl-static-2.0.61.Final.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-resolver-4.1.105.Final.jar:/usr/local/zookeeper-3.9.
2/bin/../lib/netty-handler-4.1.105.Final.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-common-4.1.105.Final.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-codec-4.1.105.Final.jar:/usr/local/zookeeper-3.9.2/bin/../lib/netty-buffer-4.1.105.Final.jar:/usr/local/zookee
per-3.9.2/bin/../lib/metrics-core-4.1.12.1.jar:/usr/local/zookeeper-3.9.2/bin/../lib/logback-core-1.2.13.jar:/usr/local/zookeeper-3.9.2/bin/../lib/logback-classic-1.2.13.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jline-2.14.6.jar:/usr/local/zookeeper-3.9.2/bin/../lib/j
etty-util-ajax-9.4.53.v20231009.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jetty-util-9.4.53.v20231009.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jetty-servlet-9.4.53.v20231009.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jetty-server-9.4.53.v20231009.jar:/usr/local/zookee
per-3.9.2/bin/../lib/jetty-security-9.4.53.v20231009.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jetty-io-9.4.53.v20231009.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jetty-http-9.4.53.v20231009.jar:/usr/local/zookeeper-3.9.2/bin/../lib/javax.servlet-api-3.1.0.jar:/usr/lo
cal/zookeeper-3.9.2/bin/../lib/jackson-databind-2.15.2.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jackson-core-2.15.2.jar:/usr/local/zookeeper-3.9.2/bin/../lib/jackson-annotations-2.15.2.jar:/usr/local/zookeeper-3.9.2/bin/../lib/commons-io-2.11.0.jar:/usr/local/zookeep
er-3.9.2/bin/../lib/commons-cli-1.5.0.jar:/usr/local/zookeeper-3.9.2/bin/../lib/audience-annotations-0.12.0.jar:/usr/local/zookeeper-3.9.2/bin/../zookeeper-*.jar:/usr/local/zookeeper-3.9.2/bin/../zookeeper-server/src/main/resources/lib/*.jar:/usr/local/zookeeper-3.9.2
/bin/../conf: -Xmx1000m -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/local/zookeeper-3.9.2/bin/../conf/zoo.cfg                                                                     
root     27249 27414  0 18:40 pts/1    00:00:00 grep --color=auto zookeeper                                                                                                                                                                                                 
[root@VM-4-9-centos zookeeper-3.9.2]#         

2.2、客户主要命令

关于客户端的命令我们先来看和连接相关的命令 连接服务端

# 格式  -server ip:port
/zkCli.sh -server 127.0.0.1:2181

# 断开连接
quit 

image.png

这里先给大家整理出一些常用的命令

                                命令                                                                                                释意
create /node_path node_value创建节点 /node_path 是节点名称(全路径) node_value:存放在节点的数值
set /node_path node_value给/node_path 节点设置 值
get /node_path node_value获取/node_path 节点的数值
delete /node_path删除节点(单个节点) 节点下不能 有子节点
deleteall /node_path删除节点(包括该节点下的子节点)
create -e /node_path创建临时节点 该节点只在当前会话中生效
create -s /node_path创建顺序节点

以上就是关于 Zookeeper 中对节点的crud操作,我们可以使用命令实操一下

[zk: 127.0.0.1:2181(CONNECTED) 0] ls /                                                                                                                                                                                                                                      
[zookeeper]                                                                                                                                                                                                                                                                 
[zk: 127.0.0.1:2181(CONNECTED) 1] create /tom tomValue                                                                                                                                                                                                                      
Created /tom                                                                                                                                                                                                                                                                
[zk: 127.0.0.1:2181(CONNECTED) 2] get /tom                                                                                                                                                                                                                                  
tomValue                                                                                                                                                                                                                                                                    
[zk: 127.0.0.1:2181(CONNECTED) 3] set /tom 22                                                                                                                                                                                                                               
[zk: 127.0.0.1:2181(CONNECTED) 4] get /tom                                                                                                                                                                                                                                  
22                                                                                                                                                                                                                                                                          
[zk: 127.0.0.1:2181(CONNECTED) 5] create /jerry  jerryValue                                                                                                                                                                                                                 
Created /jerry                                                                                                                                                                                                                                                              
[zk: 127.0.0.1:2181(CONNECTED) 6] ls /                                                                                                                                                                                                                                      
[jerry, tom, zookeeper]                                      

3、Zookeeper数据模型

到这里大家可能并不太理解 Zookeeper 到底是怎么组织数据的,这里我们可以查看文档的数据模型章节

image.png

上面大概的意思就是
ZooKeeper 的命名空间是有一个层次结构的,类似分布式文件系统。唯一的区别是名称空间中的每个节点都可以有与其关联的数据以及子节点。这就类似在文件系统,它既是一个文件也是一个目录。到节点的路径总是斜杠分隔的路径。

支持所有的unicode字符,但是有几个比较特殊
1、空字符 (\u0000) 不能使用
2、\u0001 - \u001F and \u007F \u009F 、\ud800 - uF8FF, \uFFF0 - uFFFF这几个字符也不能使用
3、最后 zookeeper 作为保留令牌 也不能使用。

到这里我们就可以把 zookeeper中的数据模型理解成下图所示的样子

image.png

需要注意的是 每个节点上可以存放数据,但是数据大小不能超过1M。

4、Java API

前面我们知道了 Zookeeper 的数据组织形式 也知道了 基本的CRUD命令了,下面我们就来学习一个操作Zookeeper的Java API 首先 引入依赖

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

然后我们新建一个工程,测试代码如下

public class ZooKeeperPractice {  
  
private static final int SESSION_TIMEOUT = 3000;  
private static ZooKeeper zooKeeper;  
  
public static void main(String[] args) {  
        try {  
            // 创建 ZooKeeper 实例  
            zooKeeper = new ZooKeeper("ip:port", SESSION_TIMEOUT, new Watcher() {  
            @Override  
            public void process(WatchedEvent event) {  
            System.out.println("Watch event: " + event);  
            }  
        });  
  
        String path = "/myZnode"; // 节点路径  
        // String data = "Hello ZooKeeper"; // 节点数据  

        // 创建节点  
        // createNode(path, data);  

        // 获取节点数据  
        getNodeData(path);  

        // 删除节点  
        // deleteNode(path);  

        } catch (IOException | KeeperException | InterruptedException e) {  
        e.printStackTrace();  
        } finally {  
        try {  
            if (zooKeeper != null) {  
            zooKeeper.close();  
            }  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
     }  
}  
  
    private static void createNode(String path, String data) throws KeeperException, InterruptedException {  
        // 创建持久节点  
        String createdPath = zooKeeper.create(path, data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);  
        System.err.println("Node created: " + createdPath);  
    }  
  
    private static void getNodeData(String path) throws KeeperException, InterruptedException {  
        byte[] data = zooKeeper.getData(path, false, null);  
        System.err.println("Node data: " + new String(data));  
    }  

    private static void deleteNode(String path) throws KeeperException, InterruptedException {  
        zooKeeper.delete(path, -1); // -1 表示删除最新版本的节点  
        System.err.println("Node deleted: " + path);  
    }  
}

先将创建节点的代码注释放开,运行上述代码 我们即可查看zookeeper里面创建的节点和节点存储的数值了,接着getNodeData 方法可以获取该节点的数值。

[zk: localhost:2181(CONNECTED) 4] get /myZnode                                                                                                                                                                                                                              
Hello ZooKeeper                                                                                                                                                                                                                                                             
[zk: localhost:2181(CONNECTED) 5]  

好了,到这里我们已经知道了Zookeeper的 Java API 的一些基本的操作了,但是你可能会很好奇Zookeeper 到底能做什么 ,上面这些CRUD操作也实现不了什么很厉害的功能啊,别急 我们再来学习一个很重要的特性 Zookeeper的Watch机制

5、Zookeeper的Watch机制

5.1、Watch机制概述

Zookeeper 的 Watch 机制主要是用于监控节点状态变化的机制。简而言之就是它允许客户端在zookeeper上注册某个节点的事件监听器,当该节点发生了变更的时候Zookeeper会通知到该客户端。 这个功能就很厉害了,我们先来做一个小案例,体会下这个过程,之后那你就能很快的理解这个机制的工作原理了

5.2、Watch机制原理

直接上代码。 首先我们编写一个类,这个类的功能是创建一个临时节点,并且隔段时间修改一次这个节点上存放的数值,最后等待一段时间后结束整个会话。相关代码如下:

public class EventRegister {  
    private ZooKeeper zooKeeper;  
    private String eventNodePath;  

    public EventRegister(String zkAddr, String eventNodePath) throws IOException {  
        this.zooKeeper = new ZooKeeper(zkAddr, 3000, null);  
        this.eventNodePath = eventNodePath;  
    }  
    // 注册事件  
    public void registerEvent(String eventData) {  
        try {  
            String createdPath = zooKeeper.create(eventNodePath, eventData.getBytes(),  
            ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);  
            System.out.println("Event registered at path: " + createdPath);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  

    // 修改节点的值  
    public void updateServiceData(String newServiceData) {  
        try {  
            zooKeeper.setData(eventNodePath, newServiceData.getBytes(), -1);  
            System.out.println("Service data updated to: " + newServiceData);  
        } catch (KeeperException | InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  

    // 关闭 ZooKeeper 连接  
    public void close() {  
        try {  
            zooKeeper.close();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  

    public static void main(String[] args) {  
        String zkAddr = "ip:port";  //修改成自己的ip和端口
        String eventNodePath = "/test_node";  
        // 事件数据  
        try {  
            // 创建注册器并注册事件  
            EventRegister eventRegister = new EventRegister(zkAddr, eventNodePath);  
            eventRegister.registerEvent("hi i'm tom" );  
            System.out.println("Event registration completed.");  
            //模拟10s后修改节点数据  
            Thread.sleep(10000);  
            eventRegister.updateServiceData("fuck i'm jerry");  
            //模拟2后修改节点数据  
            Thread.sleep(4000);  
            eventRegister.updateServiceData("你们见鬼去吧 ");  
            //30后下线  
            Thread.sleep(30000);  
            // 关闭连接  
            eventRegister.close();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

接着我们还需要一个Watcher监听类,这个类需要实现的功能是监听上面创建的临时节点,每当节点的值发生了变更后zookeeper就会下发通知,接着就重新获取该节点的值,相关代码如下:

public class EventListener implements Watcher {  
    private ZooKeeper zooKeeper;  
    private String eventNodePath;  

    public EventListener(String zkAddr, String eventNodePath) throws IOException {  
        this.zooKeeper = new ZooKeeper(zkAddr, 3000, this);  
        this.eventNodePath = eventNodePath;  
        // 初始时设置 watcher  
        watchEventNode();  
    }  

    // 处理事件  
    @Override  
    public void process(WatchedEvent event) {  
        if (event.getType() == Event.EventType.NodeDeleted) {  
            System.out.println("服务下线了: " + eventNodePath);  
        } else if (event.getType() == Event.EventType.NodeDataChanged) {  
            System.out.println("服务变更了: " + eventNodePath);  
        }  
        // 重新设置 watcher  
        watchEventNode();  
    }  

    // 监视事件节点  
    private void watchEventNode() {  
        try {  
            byte[] data = zooKeeper.getData(eventNodePath, this, null);  
            System.out.println("节点中最新版本的数据内容: " + new String(data));  
        } catch (KeeperException e) {  
            e.printStackTrace();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  

    // 关闭 ZooKeeper 连接  
    public void close() {  
        try {  
            zooKeeper.close();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
    public static void main(String[] args) {  
        String zkAddr = "ip:port";  //修改成自己的ip和端口
        String eventNodePath = "/test_node"; // 事件节点路径  
        // 创建并启动监听器  
        try {  
            EventListener eventListener = new EventListener(zkAddr, eventNodePath);  
            // 模拟事件监听一段时间  
            Thread.sleep(10000000);  
            // 关闭连接  
            eventListener.close();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

完成上述代码后,我们先启动 EventRegister,等待 2s 后再启动 EventListener 然后观察他们的控制台的输出

EventRegister 日志输出

Event registered at path: /test_node
Event registration completed.
23:30:01.372 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for session id: 0x1000637a0d10042 after 12ms.
23:30:02.707 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for session id: 0x1000637a0d10042 after 12ms.
23:30:10.059 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Reading reply session id: 0x1000637a0d10042, packet:: clientPath:null serverPath:null finished:false header:: 2,5  replyHeader:: 2,354,0  request:: '/test_node,#6675636b202069276d20206a65727279,-1  response:: s{352,354,1729092599985,1729092610005,1,0,0,72064430028947522,16,0,352} 
Service data updated to: fuck  i'm  jerry
23:30:14.075 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Reading reply session id: 0x1000637a0d10042, packet:: clientPath:null serverPath:null finished:false header:: 3,5  replyHeader:: 3,355,0  request:: '/test_node,#ffffffe4ffffffbdffffffa0ffffffe4ffffffbbffffffacffffffe8ffffffa7ffffff81ffffffe9ffffffacffffffbcffffffe5ffffff8effffffbbffffffe5ffffff90ffffffa720,-1  response:: s{352,355,1729092599985,1729092614020,2,0,0,72064430028947522,19,0,352} 
Service data updated to: 你们见鬼去吧 
23:30:15.406 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for session id: 0x1000637a0d10042 after 12ms.

EventListener 日志输出:

节点中最新版本的数据内容: hi  i'm  tom
23:30:06.651 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Reading reply session id: 0x1000637a0d10043, packet:: clientPath:null serverPath:null finished:false header:: 2,4  replyHeader:: 2,353,0  request:: '/test_node,T  response:: #6869202069276d2020746f6d,s{352,352,1729092599985,1729092599985,0,0,0,72064430028947522,12,0,352} 
节点中最新版本的数据内容: hi  i'm  tom
23:30:07.982 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for session id: 0x1000637a0d10043 after 12ms.
23:30:10.059 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got WatchedEvent state:SyncConnected type:NodeDataChanged path:/test_node zxid: 354 for session id 0x1000637a0d10043
服务变更了: /test_node
23:30:10.071 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Reading reply session id: 0x1000637a0d10043, packet:: clientPath:null serverPath:null finished:false header:: 3,4  replyHeader:: 3,354,0  request:: '/test_node,T  response:: #6675636b202069276d20206a65727279,s{352,354,1729092599985,1729092610005,1,0,0,72064430028947522,16,0,352} 
节点中最新版本的数据内容: fuck  i'm  jerry
23:30:11.410 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got ping response for session id: 0x1000637a0d10043 after 18ms.
23:30:14.074 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got notification session id: 0x1000637a0d10043
23:30:14.074 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got WatchedEvent state:SyncConnected type:NodeDataChanged path:/test_node zxid: 355 for session id 0x1000637a0d10043
服务变更了: /test_node
23:30:14.086 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Reading reply session id: 0x1000637a0d10043, packet:: clientPath:null serverPath:null finished:false header:: 4,4  replyHeader:: 4,355,0  request:: '/test_node,T  response:: #ffffffe4ffffffbdffffffa0ffffffe4ffffffbbffffffacffffffe8ffffffa7ffffff81ffffffe9ffffffacffffffbcffffffe5ffffff8effffffbbffffffe5ffffff90ffffffa720,s{352,355,1729092599985,1729092614020,2,0,0,72064430028947522,19,0,352} 
节点中最新版本的数据内容: 你们见鬼去吧 
23:30:46.598 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got notification session id: 0x1000637a0d10043
23:30:46.598 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Got WatchedEvent state:SyncConnected type:NodeDeleted path:/test_node zxid: 356 for session id 0x1000637a0d10043
服务下线了: /test_node
23:30:46.610 [main-SendThread(ip:port)] DEBUG org.apache.zookeeper.ClientCnxn - Reading reply session id: 0x1000637a0d10043, packet:: clientPath:null serverPath:null finished:false header:: 5,4  replyHeader:: 5,356,-101  request:: '/test_node,T  response::  
org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /test_node
	at org.apache.zookeeper.KeeperException.create(KeeperException.java:117)
	at org.apache.zookeeper.KeeperException.create(KeeperException.java:53)
	at org.apache.zookeeper.ZooKeeper.getData(ZooKeeper.java:1972)
	at org.wcan.zkdemo.EventListener.watchEventNode(EventListener.java:36)
	at org.wcan.zkdemo.EventListener.process(EventListener.java:30)
	at org.apache.zookeeper.ClientCnxn$EventThread.processEvent(ClientCnxn.java:564)
	at org.apache.zookeeper.ClientCnxn$EventThread.run(ClientCnxn.java:539)

我们查看上述控制台的信息,发现每一次test_node上的数据的变更zookeeper都会通知EventListener,并且EventListener也能获取到最新的值,EventRegister会话结束后临时节点被删除也能感知到。到这里相信你已经理解了这种机制了。Zookeeper 之所以被广泛的应用就是因为他的这个特有的机制。

相信大家很容易就理解了上面的这个小案例的代码,不过这里我还是推荐大家去看看官方文档上给出的案例,相信你肯定会有更深刻的理解 地址: zookeeper.apache.org/doc/current…

6、Zookeeper 的应用场景

从上面的案例中我们可以知道 Zookeeper 的监听机制大致的工作流程,这里给大家梳理成2张图

image.png

image.png

上面两张图就是Zookeeper的Watch机制的流程了。那么基于这种机制我们就可以好好聊聊 zookeeper的应用场景了

6.1 分布式配置中心

假设我们有一个应用,服务端是由3个服务节点组成的集群,某一天我们需要修改服务端的某项配置的时候,我们需要 分别在3个节点上修改,假如后期应用体量变大了 服务端需要扩容,增加到了100个节点,那我们想修改某个配置的时候 就需要去修改100个节点,想想就头皮发麻。这个时候我们就可以使用 Zookeeper 来管理这些配置了

image.png

image.png

6.2、统一集群管理

在一个服务端集群的环境中 我们需要实时的掌握每个服务的状态,当某个节点发生了变更或者故障的时候我们要能及时的发现并作出对应的措施,这个时候我们同样的也可以使用Zookeeper来实现。实现步骤也很简单,如下图所示

image.png 每个服务启动的时候向zookeeper注册一个临时节点,监控服务分别监听所有服务的节点,当某个节点发生了故障断开会话,这个时候它所对应的临时节点就不存在了。这个时候Zookeeper就会通知监控中心,我们就能捕获到异常的服务了。

6.3、服务注册中心

这个相信大家对注册中心都不陌生,早些年 Zookeeper 配合 Dubbo 构建分布式系统有很多成功的案例, 同样的Zookeeper作为服务注册中心的原理也是类似的,服务提供者将自己的服务地址、方法签名存到Zookeeper上指定的节点,服务消费者从这个节点上拉取服务提供者的元信息,进行远程调用。当某个服务挂掉了,Zookeeper就会通知监控中心。

6.4、分布式锁

在分布式环境中如果多个服务实例需要访问 某个共享资源,这个时候我们就需要引入分布式锁了,同样的Zookeeper也是一个不错的选择。实现原理是临时顺序节点+watch机制。还记得主要命令的那个章节里介绍的 临时节点和顺序节点吗,同样的其实还有一个临时顺序节点,他就是分布式锁的最佳实践。 直接上代码吧

public class DistributedLock implements Watcher {  
    private static final String LOCK_NODE = "/lock"; // 锁节点  
    private final ZooKeeper zooKeeper;  
    private String lockId; // 当前实例的锁节点 ID  

    public DistributedLock(String zkHost) throws IOException {  
        this.zooKeeper = new ZooKeeper(zkHost, 3000, this);  
    }  

    @Override  
    public void process(WatchedEvent event) {  
        // 处理事件(可根据需要实现)  
    }  

    public boolean lock() throws KeeperException, InterruptedException {  
        // 创建临时顺序节点  
        lockId = zooKeeper.create(LOCK_NODE + "/lock-", new byte[0],  
        ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);  

        // 检查是否为最小节点  
        return tryLock();  
    }  

    // 尝试获取锁  
    private boolean tryLock() throws KeeperException, InterruptedException {  
    // 获取当前锁节点的所有子节点  
        List<String> children = zooKeeper.getChildren(LOCK_NODE, false);  
        if (children.isEmpty()) {  
            return true; // 成功获取锁  
        }  

        // 检查自己是否为最小节点  
        String minNode = getMinNode(children);  
        return minNode != null && lockId.equals(LOCK_NODE + "/" + minNode);  
    }  

    // 获取最小的节点名称  
    private String getMinNode(List<String> children) {  
        return children.stream()  
            .sorted() // 按节点名称排序  
            .findFirst()  
            .orElse(null);  
    }  

    // 释放锁  
    public void unlock() throws KeeperException, InterruptedException {  
        if (lockId != null) {  
            zooKeeper.delete(lockId, -1);  
            lockId = null; // 清空锁 ID  
        }  
    }  

    public void close() throws InterruptedException {  
        zooKeeper.close();  
    }  
}

相比长篇大论的文字,相信大家肯定觉得还是代码好理解,上面的代码就是 在Zookeeper的/lock 节点下创建一个临时的顺序节点,当有一次请求过来的时候就会新创建一个节点,然后判断自己的节点是不是顺序值最小的。如果是就算是获取到了锁,处理完后业务逻辑后,就将该节点删掉也就是释放了锁。后面的请求进来了也是同样的判断。 下面再来写一段测试一下这个过程

public class LockTest {  
    public static void main(String[] args) {  
        try {  
        DistributedLock lock = new DistributedLock("ip:port");  

        if (lock.lock()) {  
            try {  
                // 访问共享资源  
                System.out.println(args[0]+" 成功获取到锁,开始访问共享资源。。。。。。");  
                // 模拟对共享资源的访问 等待一段时间  
                Thread.sleep(6000);  
            } finally {  
                lock.unlock(); // 确保释放锁  
                System.out.println(args[0]+" 处理业务结束 开始释放分布式锁。。。。。。");  
            }  
        } else {  
            //未获取到锁 为了方便查看 zookeeper 上的临时数据节点 等待3s  
            Thread.sleep(3000);  
            System.out.println(args[0]+" 未获取到锁 。。。。。。");  
        }  
            lock.close();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

我们就启动2个进程吧

image.png

image.png

然后我们在zk上查看就可以看到一面这个过程(注意手速,过程有点快)

[zk: 127.0.0.1:9009(CONNECTED) 28] ls /lock                                                                             
[]                                                                                                                      
[zk: 127.0.0.1:9009(CONNECTED) 29] ls /lock                                                                             
[lock-0000000008]                                                                                                       
[zk: 127.0.0.1:9009(CONNECTED) 30] ls /lock                                                                             
[lock-0000000008]                                                                                                       
[zk: 127.0.0.1:9009(CONNECTED) 31] ls /lock                                                                             
[lock-0000000008, lock-0000000009]                                                                                      
[zk: 127.0.0.1:9009(CONNECTED) 35] ls /lock                                                                             
[lock-0000000008, lock-0000000009]                                                                                      
[zk: 127.0.0.1:9009(CONNECTED) 36] ls /lock                                                                             
[lock-0000000008]                                                                                                       
[zk: 127.0.0.1:9009(CONNECTED) 37] ls /lock                                                                             
[lock-0000000008]                                                                                                                                                                                 
[zk: 127.0.0.1:9009(CONNECTED) 39] ls /lock                                                                             
[]                                                 

我们可以观察到 中间出现了2个子节点,这个时候就是产生了竞争,这里可能不是很好观察,大家可以使用SpringBoot 快速构建一个http服务

@RestController  
public class UserInfoController {  
  
    private final DistributedLock distributedLock;  

    public UserInfoController(DistributedLock distributedLock) {  
        this.distributedLock = distributedLock;  
    }  

    @RequestMapping("/getUserInfo")  
    public Map getUserInfo() throws InterruptedException, KeeperException {  
        Map jerry = new HashMap<String, Object>();  
        String msg = "";  
        if (distributedLock.lock()) {  
            msg = "当前线程: " + Thread.currentThread().getId() + " 获取到锁";  
            try {  
                jerry.put("name", Thread.currentThread().getName());  
                jerry.put("age", 18);  
                jerry.put("sex", "男");  
                jerry.put("address", "深圳");  
             } finally {  
                distributedLock.unlock();  
            }  
        } else  
             msg = "当前线程: " + Thread.currentThread().getName() + " 没有获取到锁";  
        jerry.put("msg", msg);  
        return jerry;  
    }  
}


@SpringBootApplication  
public class JerryStoreApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(JerryStoreApplication.class, args);  
        System.out.println("jerry-store started");  
    }  

    @Bean  
    public DistributedLock distributedLock() throws IOException {  
        return new DistributedLock("ip:port");  
    }  
  
}

然后我们可以利用jemter工具 进行测试 就能看到明显的过程了。

image.png

7、Zookeeper集群搭建

7.1、配置集群

上面我们都是基于单节点的环境下实验的,接下来我们搭建一个Zookeeper集群的环境 首先 我们zookeeper安装包解压的目录复制三份,再在zkCluster目录下

image.png

我们要启动3个zookeeper进程,他们的端口分别是9002、9003、9004。我们需要修改对应目录下的配置文件,其中还需要对应的数据存储目录。需要注意的是 这里还需要在每个data 目录下创建一个 myid 文件,内容分别是1、2、3 这个文件就是记录每个服务器的ID

image.png

mkdir -p  /opt/zkCluster/zookeeper-9002/data 
mkdir -p  /opt/zkCluster/zookeeper-9003/data 
mkdir -p  /opt/zkCluster/zookeeper-9004/data 

echo 1 > /opt/zkCluster/zookeeper-9002/data/myid
echo 2 > /opt/zkCluster/zookeeper-9003/data/myid
echo 3 > /opt/zkCluster/zookeeper-9004/data/myid

接着修改配置文件 zoo.cfg,这里给出每份配置文件的内容

# /usr/local/zkCluster/zookeeper-9002/conf/zoo.cfg 内容

dataDir=/opt/zkCluster/zookeeper-9002/data                                                                                                                                       
clientPort=9002  
server.1=127.0.0.1:9012:3881                                                     
server.2=127.0.0.1:9013:3882                                                                  
server.3=127.0.0.1:9014:3883 

# /usr/local/zkCluster/zookeeper-9003/conf/zoo.cfg 内容

dataDir=/opt/zkCluster/zookeeper-9003/data                                                                                                     
clientPort=9003 
server.1=127.0.0.1:9012:3881                                         
server.2=127.0.0.1:9013:3882                                                 
server.3=127.0.0.1:9014:3883 

# /usr/local/zkCluster/zookeeper-9004/conf/zoo.cfg 内容

dataDir=/opt/zkCluster/zookeeper-9004/data                                                                                     
clientPort=9004  
server.1=127.0.0.1:9012:3881                                              
server.2=127.0.0.1:9013:3882                                  
server.3=127.0.0.1:9014:3883 

其中server配置的含义是: server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口

7.2、启动集群

配置完成后我们分别启动三个节点

./zookeeper-9002/bin/zkServer.sh start
./zookeeper-9003/bin/zkServer.sh start
./zookeeper-9004/bin/zkServer.sh start

查看状态

[root@VM-4-9-centos zkCluster]# ./zookeeper-9004/bin/zkServer.sh status                                                                                                                                                                                                     
ZooKeeper JMX enabled by default                                                                                                                                                                                                                                            
Using config: /usr/local/zkCluster/zookeeper-9004/bin/../conf/zoo.cfg                                                                                                                                                                                                       
Client port found: 9004. Client address: localhost. Client SSL: false.                                                                                                                                                                                                      
Mode: follower                                                                                                                                                                                                                                                              
[root@VM-4-9-centos zkCluster]# ./zookeeper-9002/bin/zkServer.sh status                                                                                                                                                                                                     
ZooKeeper JMX enabled by default                                                                                                                                                                                                                                            
Using config: /usr/local/zkCluster/zookeeper-9002/bin/../conf/zoo.cfg                                                                                                                                                                                                       
Client port found: 9002. Client address: localhost. Client SSL: false.                                                                                                                                                                                                      
Mode: follower                                                                                                                                                                                                                                                              
[root@VM-4-9-centos zkCluster]# ./zookeeper-9003/bin/zkServer.sh status                                                                                                                                                                                                     
ZooKeeper JMX enabled by default                                                                                                                                                                                                                                            
Using config: /usr/local/zkCluster/zookeeper-9003/bin/../conf/zoo.cfg                                                                                                                                                                                                       
Client port found: 9003. Client address: localhost. Client SSL: false.                                                                                                                                                                                                      
Mode: leader                                                                                                                                                                                                                                                                
[root@VM-4-9-centos zkCluster]#      

其中9003被选举成了主节点,9002和9004是从节点。至此整个集群环境已经搭建完成了

8、总结

本文从Zookeeper安装开始,由基础命令、JavaAPI的使用逐步过渡到Zookeeper的监听机制的特性,然后又通过多个代码示例讲解了 Zookeeper怎么实现分布式配置中心、统一集群管理和服务注册中心的,接着又通过临时顺序节点和Watch机制实现了一个简单的分布式锁。最后搭建了一个Zookeeper集群环境。 到这里你肯定已经对Zookeeper 有了一定的认知了,假设面试官问你 有没有用过Zookeeper 之类的问题 相信你肯定知道怎么去回答了。甚至可以按照本文的脉络去回答 它是什么,怎么用、能做什么,具体怎么去落地的大致流程。