zookeeper

一、zookeeper 概述和特性

1.1 zookeeper 的概述

zookeeper 官网:https://zookeeper.apache.org/

1.2 zookeeper 的应用场景

zookeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能,高可用,且具有严格顺序访问控制能力的分布式协调存储服务。

  • 维护配置信息
  • 分布式锁服务
  • 集群管理
  • 生成分布式唯一ID
1.2.1 维护配置信息

配置中心

zookeeper kerberos客户端登录_hadoop

1.2.2 分布式锁服务

zookeeper kerberos客户端登录_zookeeper_02

1.2.3 集群管理

zookeeper kerberos客户端登录_zookeeper_03

1.2.4 生成分布式唯一id

在过去的单库单表型系统中,通常可以使用数据库字段自带的 auto_increment 属性来自动为每条记录生成一个唯一的id,但是数据库分表后,就无法再依赖数据库的 auto_increment 属性来唯一标识一条记录了。此时我们就可以用 zookeeper 在分布式环境下生成全局唯一id。
做法如下:每次要生成一个新的id时,创建一个持久顺序节点,创建操作返回的节点序号,即为新id,然后把比自己节点小的删除即可。

1.3 zookeeper 的设计目标

zookeeper致力于为分布式应用提欧共一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。

1.3.1 高性能

zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,尤其适用于以读为主的应用场景

1.3.2 高可用

zookeeper一般以集群的方式对外提供服务,一般3-5台机器就可以组成一个可用的zookeeper集群了,每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都相互保持着通信。只要集群中超过一半的机器都能够正常工作,那么整个集群就能够对外服务。

1.3.3 严格顺序访问

对于来自客户端的每个更新请求,zookeeper都会分配一个全局的递增编号,整个编号反映了所有事物的先后顺序

  • 全局数据一致性
  • 可靠性
  • 顺序性
  • 数据更新原子性
  • 实时性

二、zookeeper的数据模型

2.1 文件系统的数据结构

zookeeper的数据节点可以视为树状结构(或者目录),树中的各节点被称为znode(即zookeeper node),一个znode可以有多个子节点。zookeeper节点在结构上表现为树状;使用路径path来定位某个znode,比如 /appa/p_3,此处的app1、p_3分别为根节点和二级节点,以此类推。

znode,兼具文件和目录两种特点。

* 既像文件一样维护着数据、元信息、ACL、时间戳等数据结构

* 又像目录一样可以作为路径标识的一部分。

zookeeper kerberos客户端登录_hadoop_04


一个 znode 节点大体分为 3 个部分:

  • 节点的数据:即 znode data(节点path、节点data)的关系就像java map 中(key,value)的关系
  • 节点的子节点 children
  • 节点的状态 stat:用来描述当前节点的创建、修改记录,包括 cZxid、ctime等
  • 节点状态 stat 的属性

    属性说明:

属性

说明

cZxid

数据节点创建时的事务id

ctime

数据节点创建的时间

mZxid

数据节点最后一次更新时的事务id

mtime

数据节点最后一次更新的时间

pZxid

数据节点的子节点最后一次被修改时的事务id

cversion

子节点的更改次数

dataVersion

节点数据的更改次数

aclVersion

接地那的 ACL 的更改次数

ephemeralOwner

如果节点是临时节点,则表示创建该节点的会话的 sessionId;如果节点是持久节点,则该属性值为 0

data Length

数据内容的长度

numChildren

数据节点当前的子节点个数

2.2 节点的类型

zookeeper中的节点有两种,分别为临时节点 和 永久节点。节点的类型在创建时即被确定,并且不能改变。

  • 临时节点:该接地那的生命周期依赖于创建他们的会话。一旦会话(session)结束,临时接地那将被自动删除,当然也可以手动删除。虽然每个临时的 Znode 都会绑定到一个客户端会话,但他们对所有的客户端还是可见的。另外,zookeeper 的临时节点不允许拥有子节点。
  • 持久化节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除

2.2 时间监听机制

对节点或目录进行监听

三、安装 zookeeper

apache-zookeeper-3.5.7

zookeeper 目录结构

配置文件
zoo_sample.cfg

./zkCli.sh start	# 本机登录
./zkCli.sh -server ip	# 远程登录

四、zookeeper 的常用命令

4.1 新增节点

语法:

# 其中 -s 为有序节点,-e 为临时节点
create [-s] [-e] path data

创建持久化节点并写入数据:

create /hadoop "123456" # Created /hadooop

创建持久化有序节点,此时创建的节点的节点名为指定节点名 + 自增序号

create -s /a "aaa"  # Created /a0000000001 其中 0000000001 为zookeeper自动加上去的序号

create -s /b "bbb"	# Created /b0000000002	即使是不同的节点,序号也是递增的

create -s /c "ccc"	# Created /c0000000003

创建临时节点:临时节点会在会话过期后被删除,quit 退出命令窗口

create -e /tmp "tmp"	# Created /tmp

创建临时有序节点,临时节点会在会话过期后被删除

create -s -e /aa "aa"	# Created /aa0000000005

新增子节点:

create /hadooop/node "node1"

注意:
当节点 /create1 节点不存在时,不能创建节点 /create1/node1 节点

[zk: localhost:2181(CONNECTED) 17] create /create1/node1 "node"
Node does not exist: /create1/node1

4.2 修改节点

更新节点的命令是 set,可以直接进行修改,如下:

set /hadoop "345"

也可以基于版本号进行更改,此时类似于乐观锁机制,当你传入的数据版本号(dataVersion)和当前节点的数据版本是不符合时,zookeeper 会拒绝本次修改。
首次创建节点时,dataVersion为0,每次修改时 dataVersion 自增1

set /hadoop "3456" 1	# 当版本号不对时,会提示如下信息:version No is not valid : /hadooop

4.3 删除节点

删除节点的语法如下:

delete /path [version]

和更新节点数据一样,也可以传入版本号,当你传入的数据版本号(dataVersion)和当前节点的数据版本号不符合时,zookeeper不会执行删除操作。

delete /hadoop 1	# 当版本号错误时,会提示:version No is not valid : /hadoop

要想删除某个节点及其所有后代节点,可以使用递归删除:

rmr path

4.4 查看节点

返回当前节点的数据 及 属性,语法如下:

get path

4.5 查看节点状态

可以使用 stat 命令查看节点状态,它的返回值和 get 命令类似,但只返回节点的属性,不返回节点数据
语法如下:

stat path

4.6 查看节点列表

查看节点列表有 ls path 和 ls2 path 两个命令,后者是前者的增强,不仅可以查看指定路径下的所有节点,还可以查看当前节点的信息。

例如:

ls /		# 返回当前路径下的所有节点(不包括子节点)[b0000000002, a0000000001, zookeeper, c0000000003, aa0000000005, hadooop]
ls2 path	# 除了返回当前节点下的所有节点,还返回了当前节点的属性

4.7 监听器 get path [watch]

一个监听器的注册,只能捕获一次实践。

使用 get path [watch] 注册的监听器能够在节点内容发生改变的时候,向客户端发出通知。需要注意的是 zookeeper 的触发器是一次性的(one-time trigger),即触发一次后就会立即失效。

get /hadoop watch	# 为节点 /hadoop 注册一个监听器
set /hadoop 2345	# 当节点 内容 发生改变时,zookeeper会通知 WatchedEvent state:SyncConnected type:NodeDataChanged path:/hadoop

4.8 监听器 stat path [watch]

使用 stat path [watch] 注册的监听器能够在节点状态发生改变的时候,向客户端发出通知

stat /hadoop watch		# 注册监听器
set /hadoop 12334		# WatchedEvent state:SyncConnected type:NodeDataChanged path:/hadoop

4.9 监听器 ls/ls2 path [watch]

使用 ls path [watch] 或 ls2 path [watch] 注册的监听器能够监听该节点下所有子节点的增加和删除操作。

增加节点:

ls /hadoop watch
create /hadoop/node2 "aaa"	# WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hadoop

删除节点:

ls2 /hadoop watch		# 注册一个监听器
delete /hadoop/node2 	# WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hadoop

五、zookeeper 的 acl 权限控制

5.1 概述

zookeeper 类似文件系统,client 可以创建节点、更新节点、删除节点,而节点的权限的控制是通过 access control list 访问控制列表可以做到这一点。

acl 权限控制,使用 scheme:id:permission 来标识,主要涵盖 3 个方面:

  • 权限模式(scheme):授权策略
  • 授权对象(id):授权对象
  • 权限(permission):授予的权限

其特征如下:

  • zookeeper 的权限控制是基于每个 znode 节点的,需要对每个节点设置权限
  • 每个 znode 支持设置多种权限控制方案 和 多个权限
  • 子节点不会集成父节点的权限,客户端无权访问某节点,但可能可以访问它的子节点
setAcl /test2 ip:192.168.60.130:crwda // 将节点权限设置为 ip:192.168.60.130 的客户端可以对节点进行增、删、改、查、管理权限

5.2 权限模式

采用何种方式授权

方案

描述

world

只有一个用户:anyone,代表登录zookeeper所有人(默认)

ip

对客户端使用 IP地址认证

auth

使用已添加认证的用户认证

digest

使用 “用户名”,“密码”方式认证

5.3 授权的对象

给谁授予权限
授权对象 ID 是指,权限赋予的实体,例如:ip地址或用户。

5.4 授予的权限

授予什么权限
create、delete、read、writer、admin也就是 增、删、改、查、管理权限,这5种权限简写为 cdrwa,注意:这 5 中权限中,delete 是指对子节点的删除权限,其他 4 种权限指对自身节点的操作权限。

权限

ACL简写

描述

create

c

可以创建子节点

delete

d

可以删除子节点(仅下一级节点)

read

r

可以读取节点数据及显示子节点列表

write

w

可以设置节点数据

admin

a

可以设置节点访问控制列表权限

5.5 授权的相关命令

命令

使用方式

描述

getAcl

getAcl path

读取 ACL 权限

setAcl

setAcl path acl

设置 ACL 权限

addauth

addauth scheme auth

添加认证用户

5.4 授予案例

world 授权模式:通用授权

语法:
setAcl <path> world:anyone:<acl>
示例:
setAcl /hadoop/node3 world:anyone:drwa		# 针对 /hadoop/node3 这个节点去除创建节点的权限
create /hadoop/node5 "node5"	# 该节点可以正常创建成功
create /hadoop/node3/node31 "31"		# /hadoop/node3节点没有创建子节点的权限: Authentication is not valid : /hadooop/node3/node31

IP 授权模式

对某个节点采用 ip 授权模式,某个节点

setAcl /node1 ip:192.168.30.130:cdrwa	# 给某个节点,只对一个ip授予权限

setAcl /node2 ip:192.168.30.130:cdrwa,ip:192.168.60.129:cdrwa	# 给某个节点,对多个ip授予权限

Auth 授权模式:

命令:

addauth digest <user>:<password> 	# 添加认证用户
setAcl <path> auth:<user>:<acl>		# 对用户进行授权

示例:
addauth digest test:123456		# 添加用户 test,密码为 123456
create /node11 “11”		# 创建节点 node11
setAcl /node11 auth:test:crdwa	# 给 node11 节点对用户 test 进行授权
getAcl /node11		# 查看 node11 节点的权限:'digest,'test:hT3UeTemD8EFy6fNX9CwXcvZkLM=: cdrwa ,其中密码123456被加密成了 hT3UeTemD8EFy6fNX9CwXcvZkLM

get /node11		# 可以正常查看节点的信息,因为添加用户后,默认这个用户已经在当前客户端已经登录了
quit	# 退出当前客户端
./zkCli.sh		# 重新登录zookeeper客户端
get /node11		# 没有权限 Authentication is not valid : /node11
addauth digest test:123456		# 重新登录 test用户
get /node11		# 可以正常查看节点信息

Digest 授权模式:

命令:

setAcl <path> digest:<user>:<password>:<acl>

这里的密码是经过 SHA1 及 BASE64 处理的密文,在 SHELL 中可以通过以下命令计算:

echo -n <user>:<password> | openssl dgst -binary -sha1 | openssl base64

示例:先来计算一个密文
echo -n username:123456 | openssl dgst -binsry -sha1 | openssl base64

案例

echo -n test:123456 | openssl dgst -binary -sha1 | openssl base64	# 直接Linux命令窗口下生成密文,hT3UeTemD8EFy6fNX9CwXcvZkLM=
create /node4 "node4"	# 创建 node4 节点
setAcl /node4 digest:test:hT3UeTemD8EFy6fNX9CwXcvZkLM=:cdrwa	# 以digest的方式给node4 节点授权
get /node4	# 用户未登陆时,Authentication is not valid : /node4
addauth digest test:123456	# 登陆用户后,可以正常进行查看节点等操作

多种授权模式:

create /node5 "node5"
addauth digest test:123456		# 登陆test用户
setAcl /node5 ip:192.168.60.129:cdra,auth:test:123456:cdrwa,digest:test:hT3UeTemD8EFy6fNX9CwXcvZkLM=:cdrwa	# 多种模式授权

[zk: localhost:2181(CONNECTED) 14] getAcl /node5	# 查看 /node5 节点的权限,结果如下:
'ip,'192.168.15.30
: cdra
'digest,'hsz:hT3UeTemD8EFy6fNX9CwXcvZkLM=
: cdrwa
'digest,'hsz:hT3UeTemD8EFy6fNX9CwXcvZkLM=
: cdrwa

5.7 acl 超级管理员

zookeeper 的权限管理模式有一种叫做 super,该模式提供一个超管可以方便的访问任何权限的节点

假设这个超管是:super:admin,需要先为超管生成密码的密文

echo -n super:admin | openssl dgst -binary -sha1 | openssl base64

那么打开 zookeeper 目录下的 /bin/zkServer.sh 服务器脚本文件,找到如下一行:

nohup $JAVA "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4_PROP}"

这就是脚本中启动 zookeeper 的命令,默认只有以上两个配置项,我们需要加一个超管的配置项

"-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs="

那么修改以后这条完整命令变成了:

nohup $JAVA "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" 
"-Dzookeeper.root.logger=${ZOO_LOG4_PROP}" 
"-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs="

之后启动zookeeper,输入如下命令添加权限:

addauth digest super:admin	# 添加认证用户

示例:

create /node6 "node6"	# 创建节点
setAcl /node6 ip:192.168.15.30 cdrwa	# 设置节点权限
getAcl /node6	# 使用非授权ip访问 /node6节点,Authentication is not valid : /node6
addauth digest super:admin	# 登录超级管理员用户
get /node6		# 可以正常进行查看节点等操作

六、zookeeper java API

znode 是 zookeeper集合的核心组件,zookeeper API 提供了一小组方法使用 zookeeper 集合来操作 znode 的所有细节。

客户端应该遵循以下步骤,与 zookeeper 服务器进行清晰和干净的交互。

  • 连接到 zookeeper服务器。与 zookeeper 服务器为服务器客户端会话 ID。
  • 定期向服务器发送心跳。否则,zookeeper 服务器将过期会话 ID,客户端需要重新连接。
  • 只要会话 ID,处于活动状态,就可以获取/设置 znode
  • 所有任务完成后,断开 与 zookeeper 服务器的连接。如果客户端长时间不活动,则 zookeeper 服务器将自动断开客户端。

6.1 SpringBoot集成Zookeeper

Zookeeper(String connectionString, int sessionTimeout, Watcher watcher)

  • connectionString - zookeeper 服务器的 IP 和 端口
  • sessionTimeout - 会话超时(以毫秒为单位)
  • watcher - 实现 “监听器”对象。zookeeper 集合通过监视器对象返回连接状态。

添加 pom 依赖:

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

application.yml 配置

zookeeper:
  address: 127.0.0.1:2181
  timeout: 5000

zookeeperConfig 类

package com.example.zookeeper.conf;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.CountDownLatch;

/**
 * @Description zookeeper 配置类
 * @Author try
 * @Date 2020/12/10 20:07
 * @Version 1.0
 */
@Configuration
public class ZookeeperConfig {

    private static final Logger logger = LoggerFactory.getLogger(ZookeeperConfig.class);

    @Value("${zookeeper.address}")
    private String connectString;
    @Value("${zookeeper.timeout}")
    private int timeout;

    @Bean(name = "zooKeeperClient")
    public ZooKeeper zooKeeperClient() {
        ZooKeeper zooKeeper = null;
        try{
            CountDownLatch countDownLatch = new CountDownLatch(1);
            //连接成功后,会回调watcher监听,此连接操作是异步的,执行完new语句后,直接调用后续代码
            //  可指定多台服务地址 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183
            zooKeeper = new ZooKeeper(connectString, timeout, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    if(watchedEvent.getState().equals(Event.KeeperState.SyncConnected)) {
                        //如果收到了服务端的响应事件,连接成功
                        countDownLatch.countDown();
                    }
                }
            });
            countDownLatch.await();
            logger.info("【初始化ZooKeeper连接状态....】={}",zooKeeper.getState());
        }catch (Exception e) {
            logger.error("初始化ZooKeeper连接异常....】{}", e);
        }
        return zooKeeper;
    }
}

6.2 新增节点

新增节点的两种方式

// 同步方式
create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
// 异步方式
create(String path, byte[] data, List<ACL> acl, CreateMode createMode, AsyncCallback.StringCallback callBack, Object ctx)
  • path:znode路径,例如,/node1 /node1/node12
  • data:要存储在指定 znode 路径中的数据
  • acl:要创建的节点的访问控制列表。zookeeper API 提供了一个静态接口 ZooDefs.lds 来获取一些基本的 acl 列表。例如,ZooDefs.lds.OPEN_ACL_UNSAFE 返回打开 znode 的 acl 列表
  • createMode:节点的类型,这是一个枚举
  • callBack:异步回调接口
  • ctx:传递上下参数

代码示例:

6.3 更新节点

更新节点的方式

// 同步方式
setData(String path, byte[] data, int version)
// 异步方式
setData(String path, byte[] data, int version, AsyncCallback.StatCallback callback, Object ctx)
  • path:znode 路径
  • data:要存储在指定节点的 znode 路径中的数据
  • version:znode 的当前版本。每当数据更改时,zookeeper 会更新 znode 的版本号
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.4 删除节点

删除节点的两种方式

// 同步方式
delete(String path, int version)
// 异步方式
delete(String path, int version, AsyncCallback callBack, Object ctx)
  • path:znode路径
  • version:znode 的当前版本
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.5 查看节点

查看节点的两种方式

// 同步方式
getData(String path, boolean b, Stat stat)
// 异步方式
getData(String path, boolean b, AsyncCallback.DataCallback callBack, Object ctx)
  • path:znode 路径
  • b:是否使用连接对象中注册的监视器
  • stat:返回 znode 的元数据
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.6 查看子节点

查看子节点的两种方式

// 同步方式
getChildren(String path, boolean b)
// 异步方式
getChildren(String path, boolean b, AsyncCallback.DataCallback callBack, Object ctx)
  • path:znode 路径
  • b:是否使用连接对象中注册的监视器
  • callBack:异步回调接口
  • ctx:传递上下文参数

6.7 检查节点是否存在

检查节点是否存在的两种方式

// 同步方式
exists(String path, boolean b)
// 异步方式
exists(String path, boolean b, AsyncCallback.DataCallback callBack, Object ctx)
  • path:znode 路径
  • b:是否使用连接对象中注册的监视器
  • callBack:异步回调接口
  • ctx:传递上下文参数

ZookeeperApi 工具类

package com.example.zookeeper.conf;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @Description TODO
 * @Author try
 * @Date 2020/12/10 20:19
 * @Version 1.0
 */
@Component
public class ZookeeperApi {

    private static final Logger logger = LoggerFactory.getLogger(ZookeeperApi.class);

    @Autowired
    private ZooKeeper zooKeeperClient;

    /**
     * 同步方式创建节点
     *  节点的权限列表,world:anyone:cdrwa
     *  节点的类型,持久化节点
     * @param path 节点的路径
     * @param data 节点保存的数据
     * @return
     */
    public boolean createNode(String path, String data){
        try {
            zooKeeperClient.create(path, data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            return true;
        } catch (Exception e) {
            logger.error("【创建持久化节点异常】{},{},{}",path,data,e);
            return false;
        }
    }

    /**
     * 更新指定节点
     * @param path 节点路径
     * @param data 更新的数据
     * @return
     */
    public boolean updateNode(String path, String data) {
        try {
            //zk的数据版本是从0开始计数的。如果客户端传入的是-1,则表示zk服务器需要基于最新的数据进行更新。如果对zk的数据节点的更新操作没有原子性要求则可以使用-1.
            //version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查
            zooKeeperClient.setData(path, data.getBytes(), -1);
            return true;
        } catch (Exception e) {
            logger.error("【修改持久化节点异常】{},{},{}",path,data,e);
            return false;
        }
    }

    /**
     * 删除指定节点
     * @param path 节点路径
     * @return
     */
    public boolean deleteNode(String path) {
        try {
            //version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查
            zooKeeperClient.delete(path, -1);
            return true;
        } catch (Exception e) {
            logger.error("【删除持久化节点异常】{},{}",path,e);
            return false;
        }
    }

    /**
     * 获取指定节点的值
     * @param path 节点路径
     * @param watch 是否使用连接对象中注册的监视器
     * @return 节点中存储的内容
     */
    public String getNode(String path, boolean watch) {
        try {
            Stat stat = new Stat();
            byte[] data = zooKeeperClient.getData(path, watch, stat);
            return new String(data);
        } catch (Exception e) {
            logger.error("【获取节点信息异常】{},{}",path,e);
            return null;
        }
    }

    /**
     * 获取指定节点的子节点列表
     * @param path 节点路径
     * @param watch 是否使用连接对象中注册的监视器
     * @return 子节点名称列表
     */
    public List<String> getChildrenNode(String path, boolean watch) {
        try {
            return zooKeeperClient.getChildren(path, watch);
        } catch (Exception e) {
            logger.error("【获取子节点名称列表异常】{},{}",path,e);
            return null;
        }
    }

    /**
     * 判断节点是否存在
     * @param path 节点路径
     * @param watch 是否使用连接对象中注册的监视器
     * @return 节点属性信息对象,节点不存在时返回 null
     */
    public Stat exists(String path, boolean watch) {
        try {
            return zooKeeperClient.exists(path, watch);
        } catch (Exception e) {
            logger.error("【断指定节点是否存在异常】{},{}",path,e);
            return null;
        }
    }

}

zookeeper 工具类测试:

package com.example.zookeeper;

import com.example.zookeeper.conf.ZookeeperApi;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

/**
 * @Description TODO
 * @Author try
 * @Date 2020/12/10 20:17
 * @Version 1.0
 */
@SpringBootTest
public class CreateNodeTest {

    @Autowired
    private ZookeeperApi zookeeperApi;

    @Test
    public void createNode() {
        boolean node1 = zookeeperApi.createNode("/create/node1", "node1");
        System.out.println("节点创建结果:" + node1);
    }

    @Test
    public void updateNode() {
        boolean node1 = zookeeperApi.updateNode("/set/node1", "node11");
        System.out.println("节点更新结果:" + node1);
    }

    @Test
    public void deleteNode() {
        boolean node1 = zookeeperApi.deleteNode("/set/node1");
        System.out.println("节点更新结果:" + node1);
    }

    @Test
    public void getNode() {
        String node = zookeeperApi.getNode("/get/node1", false);
        System.out.println("节点更新结果:" + node);
    }

    @Test
    public void getChildrenNode() {
        List<String> childrenNode = zookeeperApi.getChildrenNode("/get", false);
        System.out.println("节点更新结果:" + childrenNode.toString());
    }

    @Test
    public void exists() {
        Stat exists = zookeeperApi.exists("/get", false);
        System.out.println("节点更新结果:" + exists);
    }
}

七、zookeeper 事件监听机制

7.1 watcher 概念

zookeeper 提供了数据的发布/订阅功能,多个订阅者可同时监听某一特定主题对象,当该主题对象的自身状态发生变化时(例如节点内容改变、节点下的子节点列表改变等),会实时、主动通知所有订阅者

zookeeper 采用了 watcher 机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时话异步通知客户端,因此客户端不必再 watcher 注册后轮询阻塞,从而减轻了客户端压力。

watcher 机制实际上与观察者模式类似,也可以看做是一种观察者模式在分布式场景下的实现方式。

7.2 watcher 架构

watcher 实现由三个部分组成:

  • zookeeper 服务端
  • zookeeper 客户端
  • 客户端ZKWatchManager 对象

7.3 watcher 特性

一次性:

  • watcher 是一次性的,一旦被触发就会移除,再次使用时需要重新注册
  • 如果需要重复使用,可以在 watcher 对象的 process 方法中重新注册监听事件

客户端顺序回调:

  • watcher 回调是顺序串行化执行的,只有回调后客户端才能看到最新的数据状态。一个 watcher 回调逻辑不应该太多,以免影响别的 watcher 执行。
  • 同一个节点可以同时注册多个 watcher 监听事件

轻量级:

  • watchEvent 是最小通信单元,结构上包含通知状态、事件类型和节点路径,并不会告诉数据节点变化前后的具体内容

时效性:

  • watcher 只有在当前 session 执行彻底失效时才会无效,若在 session 有效期内快速重连成功,则 watcher 依然存在,仍可接受到通知。

7.4 watcher 接口设计

watcher 是一个接口,任何实现了 watcher 接口的类就是一个新的 watcher。watcher 内部包含了两个枚举类:KeeperState、EventType

zookeeper kerberos客户端登录_hadoop_05


watcher 通知状态(KeeperState)

keeperState 是客户端 与 服务端连接状态发生变化时对应的通知类型。路径为 org.apache.zookeeper.Watcher.Event.KeeperState,是一个枚举类,其枚举属性如下:

枚举属性

说明

SyncConnected

客户端与服务器正常连接时

Disconnected

客户端与服务器断开连接时

Expire

会话session失效时,当断开连接后在未超过session失效时间时重新连接时,可以连接成功;当断开连接后超过了session的失效时间重新连接时,该session已经失效,重新连接失败

AuthFailed

身份认证失败时

Watcher 事件类型(EventType)
EventType 是数据节点(znode)发生变化时对应的通知类型。EventType变化时 KeeperState 永远处于 SyncConnented 通知状态下;当KeeperState 发生变化时,EventType 永远为 None。其路径为 org.apache.zookeeper.Watcher.EventType,是一个枚举类,枚举属性如下:

枚举属性

说明

None


NodeCreated

Watcher 监听的数据节点被创建时

NodeDeleted

Watcher 监听的数据节点被删除时

NodeDataChanged

Watcher 监听的数据节点内容发生变更时(无论内容数据是否变化)

NodeChildrenChanged

Watcher 监听的数据节点的子节点列表发生变更时

注:客户端接收到的相关事件通知中只包含状态及类型等信息,不包括节点变化前后的具体内容,变化前的数据需业务自身存储,变化后的数据需调用 get 等方法重新获取。

7.5 捕获相应的事件

建立zookeeper 的 watcher 监听。在 zookeeper 中采用 zk.getChildren(path, watch)、zk.exists(path, watch)、zk.getData(path, watcher, stat) 这样的方式为某个 znode 注册监听。
以 node-x 节点为例,说明调用的注册方法 和 可监听事件间的关系:

注册方式

Created

ChildrenChanged

Changed

Deleted

zk.exists("/node-x", watcher)

可监控

可监控

可监控

zk.getData("/node-x", watcher)

可监控

可监控

zk.getChildren("/node-x", watcher)

可监控

可监控

7.6 注册 watcher 的方法

7.6.1 客户端 与 服务器的连接状态

watcher 机制 exists

自定义 watcher 对象
zk.exists("/node-x", new Watcher() { 
	@Override
	public void process(WatchedEvent event) {
		// 回调逻辑
	}
});

@Test
public void watcherExists() throws Exception {
	// watcher 一次性演示
	Watcher watcher = new Watcher() {
		@Override
		public void process(WatchedEvent event) {
			System.out.println("自定义watcher");
			System.out.println("path = " + event.getPath());
			System.out.println("自定义watcher" + event.getType());
			// 如果需要重复使用监听器,可以再次注册监听器
			zooKeeper.exists("/watcher1" this);
		}
	};

	zookeeper.exists("watcher1", watcher);
	Thread.sleep(80000);
	System.out.println("结束");
}
7.6.2 查看节点
// 使用连接对象的监视器
getData(String path, boolean b, Stat stat);
// 自定义监视器
getData(String path, Watcher w, Stat stat);

// 检测的事件类型有以下两种:
// NodeChanged:节点发生变化
// NodeDeleted:节点删除
  • path:znode 路径
  • b:是否使用连接对象中注册的 watcher
  • w:监视器对象
  • stat:返回 znode 的元数据
@Test
public void watcherExists() throws Exception {
	// watcher 一次性演示
	Watcher watcher = new Watcher() {
		@Override
		public void process(WatchedEvent event) {
			System.out.println("自定义watcher");
			System.out.println("path = " + event.getPath());
			System.out.println("自定义watcher" + event.getType());
			// 如果需要重复使用监听器,可以再次注册监听器
			if(event.getType == Event.EventType.NodeDataChanged) {
				zooKeeper.getData("/watcher2" this);
			}			
		}
	};

	zookeeper.exists("watcher2", watcher);
	Thread.sleep(80000);
	System.out.println("结束");
}
7.6.2 查看子节点
// 使用连接对象的监视器
getChildren(String path, boolean b);
// 自定义监视器
getChildren(String path, Watcher w);

// 检测的事件类型有以下两种:
// NodeChildrenChanged:子节点发生变化
// NodeDeleted:节点删除
  • path:znode 路径
  • b:是否使用连接对象中注册的 watcher
  • w:监视器对象
@Test
public void getChildrenNode2() {
    Watcher watcher = new Watcher() {
        @Override
        public void process(WatchedEvent watchedEvent) {
            if(watchedEvent.getType() == Event.EventType.NodeChildrenChanged) {
                // 如果需要重复使用自定义监听器,则需要在回调函数里再次注册监听器
                zookeeperApi.getChildrenNode("/get", this);
            }
        }
    };
    List<String> childrenNode = zookeeperApi.getChildrenNode("/get", false);
    System.out.println("节点更新结果:" + childrenNode.toString());
}

7.7 配置中心案例

工作中有这样的一个场景:数据库用户名和密码信息放在一个配置文件中,应用读取该配置文件,配置文件信息放诶缓存。

若数据库的用户名和密码改变时,还需要重新加载缓存,比较麻烦,通过 zookeeper 可以轻松完成,当数据库发生变化时自动完成缓存同步。

设计思路:

  1. 连接zookeeper服务器
  2. 读取 zookeeper 中的配置信息,注册 watcher 监听器,存入本地变量
  3. 当 zookeeper 中的配置信息发生变化时,通过 watcher 的回调方法捕获数据变化事件
  4. 重新获取配置信息

代码示例:

package com.example.zookeeper.conf;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.CountDownLatch;

/**
 * @Description TODO
 * @Author try
 * @Date 2020/12/15 21:53
 * @Version 1.0
 */
@Component
public class MyConfigCenter implements Watcher {

    private static final Logger logger = LoggerFactory.getLogger(MyConfigCenter.class);

    @Autowired
    private ZookeeperApi zookeeperApi;

    /** 用于本地化存储的配置信息*/
    private String url;
    private String username;
    private String password;

    @PostConstruct
    private void initValue() {
        this.url = zookeeperApi.getNode("/config/url", this);
        this.username = zookeeperApi.getNode("/config/username", this);
        this.password = zookeeperApi.getNode("/config/password", this);
    }

    @Override
    public void process(WatchedEvent event) {
        // 当配置信息发生变化时,重新加载配置内容
        if(event.getType() == Event.EventType.NodeDataChanged) {
            initValue();
        }
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

单元测试:

package com.example.zookeeper.conf;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
public class MyConfigCenterTest {

    @Autowired
    private MyConfigCenter myConfigCenter;

    @Test
    public void updateDataTest() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            String url = myConfigCenter.getUrl();
            System.out.println("usr : " + url);
            Thread.sleep(3000);
        }
    }
}

使用Springboot2.x+Zookeeper实现简易的分布式配置中心,使用Zookeeper存储配置,本地缓存配置,监听zookeeper的配置更新,本地实时更新。

引入依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/>
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <zk.curator.version>2.12.0</zk.curator.version>
    </properties>
 
    <dependencies>
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>${zk.curator.version}</version>
        </dependency>
    </dependencies>

配置文件:

server.port=8080
zookeeper.url=127.0.0.1:2181

配置中心类

@Component
public class PropertiesCenter {
 
    /**
     * 配置中心
     */
    Properties properties = new Properties();
    CuratorFramework client = null;
    TreeCache treeCache = null;
 
    @Value("${zookeeper.url}")
    private String zkUrl;
 
    private final String CONFIG_NAME = "/config-center";
 
    public PropertiesCenter() {
    }
 
    private void init() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        client = CuratorFrameworkFactory.newClient(zkUrl, retryPolicy);
        treeCache = new TreeCache(client, CONFIG_NAME);
    }
 
    /**
     * 设置属性
     * @param key
     * @param value
     * @throws Exception
     */
    public void setProperties(String key, String value) throws Exception {
        String propertiesKey = CONFIG_NAME + "/" + key;
        Stat stat = client.checkExists().forPath(propertiesKey);
        if(stat == null) {
            client.create().forPath(propertiesKey);
        }
        client.setData().forPath(propertiesKey, value.getBytes());
    }
 
    /**
     * 获取属性
     * @param key
     * @return
     */
    public String getProperties(String key) {
        return properties.getProperty(key);
    }
 
    @PostConstruct
    public void loadProperties() {
        try {
            init();
            client.start();
            treeCache.start();
 
            // 从zk中获取配置放入本地配置中
            Stat stat = client.checkExists().forPath(CONFIG_NAME);
            if(stat == null) {
                client.create().forPath(CONFIG_NAME);
            }
            List<String> configList = client.getChildren().forPath(CONFIG_NAME);
            for (String configName : configList) {
                byte[] value = client.getData().forPath(CONFIG_NAME + "/" + configName);
                properties.setProperty(configName, new String(value));
            }
 
            // 监听属性值变更
            treeCache.getListenable().addListener(new TreeCacheListener() {
                @Override
                public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
                    if (Objects.equals(treeCacheEvent.getType(), TreeCacheEvent.Type.NODE_ADDED) ||
                            Objects.equals(treeCacheEvent.getType(), TreeCacheEvent.Type.NODE_UPDATED)) {
                        String updateKey = treeCacheEvent.getData().getPath().replace(CONFIG_NAME + "/", "");
                        properties.setProperty(updateKey, new String(treeCacheEvent.getData().getData()));
                        System.out.println("数据更新: "+treeCacheEvent.getType()+", key:"+updateKey+",value:"+new String(treeCacheEvent.getData().getData()));
                    }
                }
            });
 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

7.8 分布式唯一 id 案例

在过去的单库单表系统中,通常可以使用数据库字段自带的 auto_increment 属性来自动为每条记录生成一个唯一的 id。但是分库分表后,就无法再依靠数据库的 auto_increment 属性来唯一标识一条记录了。此时我们就可以用 zookeeper 在分布式环境下生成全局唯一id。

设计思路:

  1. 连接 zookeeper 服务器
  2. 指定路径生成临时有序节点
  3. 取序列号及分布式环境下的唯一 id
/**
 * 分布式生成唯一id
 * 创建临时有序节点,截取返回值的序号部分
 * @return 唯一id
 */
public String generateId() {
    try {
        /*
        * ZooDefs.Ids.OPEN_ACL_UNSAFE 设置权限为:cdrwa
        * CreateMode.EPHEMERAL_SEQUENTIAL 节点类型为,临时有序节点
        */
        String generateId = zooKeeperClient.create(DEFAULT_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        //  /generateId0000000017 截取序号部分
        return generateId.substring(9);
    } catch (Exception e) {
        logger.error("【生成id异常】{},{}", DEFAULT_PATH, e);
        return null;
    }
}

7.9 分布式锁案例

分布式锁有多重实现方式,比如通过数据库,redis 都可以实现。作为分布式协同工具 zookeeper,当然也有着标准的实现方式。下面介绍在 zookeeper 中如何实现排他锁。

设计思路:

  1. 每个客户端往 /Locks 下创建临时有序节点 /Locks/Lock_,创建成功后 /Locks 下面会有每个客户端对应的节点,如 /Locks/Lock_0000000001
  2. 客户端取得 /Locks 下子节点,并进行排序,判断排在最前面的是否为自己,如果自己的锁节点在第一位,代表获取锁成功
  3. 如果自己的锁节点不在第一位,则监听自己前一位的锁节点。例如,自己锁节点 Lock_0000000002,那么则监听Lock_0000000001
  4. 当前一位锁节点(Lock_0000000001)对应的客户端执行完成,释放了锁,将会触发监听客户端(Lock_0000000002)的逻辑
  5. 监听客户端重新执行第 2 步逻辑,判断自己是否获得了锁

代码示例:

获取锁,释放锁

/**分布式锁*/
	private static final String LOCK_ROOT_PATH = "/Locks";
	private static final String LOCK_NODE_NAME = "Lock_";
	private String lockPath;

	/**
     * 获取锁
     */
    public void acquireLock() throws Exception {
        // 创建锁节点
        createLock();

        // 尝试获取锁
        attemptLock();
    }

    /**
     * 创建锁节点
      */
    private void createLock() throws Exception {
        // 判断Locks节点是否存在
        Stat exists = zooKeeperClient.exists(LOCK_ROOT_PATH, false);
        if(null == exists) {
            // 不存在则创建,持久化节点
            zooKeeperClient.create(LOCK_ROOT_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        // 创建临时节点
        lockPath = zooKeeperClient.create(LOCK_ROOT_PATH + "/" + LOCK_NODE_NAME, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        logger.info("节点创建成功:" + lockPath);
    }

    /**监视器对象,监视上一个节点是否被删除*/
    final Watcher lockWatcher = new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            // 节点被删除
            if(event.getType() == Event.EventType.NodeDeleted) {
                synchronized (this) {
                    // 唤醒正在等待的线程
                    this.notifyAll();
                }
            }
        }
    };

    /**
     * 尝试获取锁
     */
    private void attemptLock() throws Exception {
        // 获取Locks节点下的所有子节点
        List<String> childrenNodeList = zooKeeperClient.getChildren(LOCK_ROOT_PATH, false);
        // 对子节点进行排序
        Collections.sort(childrenNodeList);
        // 获取当前节点的下标位置
        int index = childrenNodeList.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));
        if(index == 0) {
            // 程序无需处理
            logger.info("获取锁成功");
        }else {
            // 上一个节点的路径
            String preNodePath = childrenNodeList.get(index - 1);
            Stat preNodeExists = zooKeeperClient.exists(LOCK_ROOT_PATH +"/"+ preNodePath, lockWatcher);
            if(preNodeExists == null) {
                // 上一个节点已释放锁,当前节点继续尝试获取锁
                attemptLock();
            }else {
                // 上一个节点未获取到锁,或者锁未释放,当前节点进入等待
                synchronized (lockWatcher) {
                    lockWatcher.wait();
                }
                // 等待被唤醒,当前继续尝试获取锁
                attemptLock();
            }
        }
    }

    /**
     * 释放锁
     */
    public void releaseLock() throws Exception {
        // 删除临时有序节点
        zooKeeperClient.delete(this.lockPath, -1);
        // 关闭客户端
        logger.info("锁已经释放:" + lockPath);
    }

测试代码

package com.example.zookeeper.test;

import com.example.zookeeper.conf.ZookeeperUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Description 售票类,测试zookeeper分布式锁
 * @Author try
 * @Date 2020/12/17 21:13
 * @Version 1.0
 */
@Component
public class TicketSeller {

    @Autowired
    private ZookeeperUtils zookeeperUtils;

    private void sell() throws Exception {
        System.out.println("售票开始");
        Thread.sleep(5000);
        System.out.println("售票结束");
    }

    public void sellTicketWithLock() throws Exception {
        // 获取锁
        zookeeperUtils.acquireLock();

        sell();

        // 释放锁
        zookeeperUtils.releaseLock();
    }
}

单元测试,将以下单元测试类复制两个同时执行,可以看出交替执行的效果

package com.example.zookeeper.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
public class TicketSellerTest {

    @Autowired
    private TicketSeller ticketSeller;

    @Test
    public void sellTicketWithLock() throws Exception {
        for (int i = 0; i < 10; i++) {
            ticketSeller.sellTicketWithLock();
        }
    }
}

八、zookeeper 集群搭建

单机环境下,jdk、zookeeper 安装完毕,基于一台虚拟机,进行zookeeper 伪集群搭建,zookeeper 集群中包含 3 个节点,节点对外提供服务端口分别为 2181、2182、8283

步骤如下:

  1. 基于zookeeper-3.4.10 复制三份zookeeper安装好的服务器文件,目录名称分别为 zookeeper2181、zookeeper2182、zookeeper2183
cp -r zookeeper-3.4.10 zookeeper2181
cp -r zookeeper-3.4.10 zookeeper2182
cp -r zookeeper-3.4.10 zookeeper2183
  1. 修改 zookeeper2181 服务器对应配置文件
# 服务器对应端口号
clientPort=2181
# 数据快照文件所在路径
dataDir=/home/zookeeper/zookeeper2181/data
# 集群配置信息
	#server.A=B:C:D
	#A:是一个数字,表示这个是服务器的编号
	#B:是这个服务器的 ip 地址
	#C:zookeeper 服务器之间的通信端口
	#D:Leader 选举的端口
server.1=192.168.60.130:2287:3387
server.1=192.168.60.130:2288:3388
server.1=192.168.60.130:2289:3389
  1. 在上一步 dataDir 指定的目录下,创建 myid 文件,然后在该文件添加上一步 server 配置的对应 A 数字。
#zookeeper2181 对应的数字为 1
#/home/zookeeper/zookeeper2181/data 目录下执行命令
echo "1" > myid
  1. zookeeper2182、zookeeper2183 参照步骤2/3进行相应配置
  2. 分别启动三台服务器,检验集群状态
./zkServer.sh start	# 启动
./zkServer.sh status	# 检查节点的状态

[root@localhost bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/zookeeper2183/bin/../conf/zoo.cfg
Mode: leader

[root@localhost bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/zookeeper2181/bin/../conf/zoo.cfg
Mode: follower
  1. 登录命令
./zkCli.sh -server 192.168.60.130:2181
./zkCli.sh -server 192.168.60.130:2182
./zkCli.sh -server 192.168.60.130:2183

九、一致性协议:zab 协议

zab 协议的全称是 zookeeper Atomic Broadcast(zookeeper 原子广播)。zookeeper 是通过 zab 协议来保证分布式事务的最终一致性

基于 zab 协议,zookeeper 集群中的角色主要有以下三类,如下表所示:

zookeeper kerberos客户端登录_数据_06


zab 广播模式工作原理,通过类似两阶段提交协议的方式解决数据一致性:

zookeeper kerberos客户端登录_数据_07


写入数据到 zookeeper 分为以下6个步骤:

  1. leader 从客户端收到一个写请求,如果是 follower 节点接收到写请求, follower 节点会将请求转发给 leader 节点处理
  2. leader 生成一个新的事务并为这个事务生成一个唯一的 ZXID
  3. leader 将这个事务提议(propose)发送给所有的 follows 节点
  4. follower 节点将收到的事务请求加入到历史队列(history queue)中,并发送 ack 给 leader
  5. 当 leader 收到大多数 follower(半数以上节点)的 ack 消息,leader 会发送 commit 请求
  6. 当 follower 收到 commit 请求时,从历史队列中将事务请求 commit

十、zookeeper 的 leader 选举

10.1 服务器状态

  • looking:寻找 leader 状态。当服务器处于该状态时,它会认为当前集群中没有 leader,因此需要进入 leader选举状态。
  • leading:领导者状态。表明当前服务器角色是 leader。
  • following:跟随者状态。表明当前服务器角色是 follower。
  • observing:观察者状态。表明当前服务器角色是 observer。

10.2 服务器启动时期的 leader 选举

在集群初始化阶段,当有一台服务器 server1 启动时,其单独无法进行和完成 leader 选举,当第二台服务器 server2 启动时,此时两台机器可以相互通信,每台机器都试图找到 leader,于是进入 leader 选举过程。选举过程如下:

  1. 每个 server 发出一个投票。由于是初始情况,server1 和 server2 都会将自己作为 leader 服务器来进行投票,每次投票会包含所选举的服务器的 myid 和 zxid(事务id),使用(myid,zxid)来表示,此时 server1 的投票为(1,0),server2 的投票为(2,0)然后各自将这个投票发给集群中其他机器。
  2. 集群中的每台服务器接收来自集群中各个服务器的投票。
  3. 处理投票。针每一个投票,服务器都需要将别人的投票和自己的投票进行pk,pk规则如下:
  • 优先检查 zxid。zxid 比较大的服务器优先作为 leader。
  • 如果 zxid 相同,那么就比较 myid。myid 较大的服务器作为 leader 服务器。
           对于 server1 而言,它的投票是(1,0),接收 server2 的投票为(2,0),首先会比较两者的 zxid,均为0,再比较myid,此时server2 的myid最大,于是更新自己的投票为(2,0),然后重新投票,对于 server2 而言,其必须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
  1. 统计投票。每次投票后,服务器都会统计投票信息,判断是否有过半机器接收到相同的投票信息,对于 server1、server2 而言,都停机集群中已经有两台机器接受了(2,0)的投票信息,此时便认为选出了 leader。
  2. 改变服务器状态。一旦确认了 leader,每个服务器就会更新自己的状态,如果是 follower,那么就变更为 following,如果是 leader,就变更为 leading。

选举原则:
优先选择事务id最大的机器:

  • 根据最后一条事务的事务id,找相应的数据,最后将数据同步到其他节点
  • 不同机器事务id不同的场景: leader 宕机前同步数据到其他 follower 时,部分机器同步完了,部分机器未同步完 leader 就宕机了,导致 follower 的事务id(zxid)不同
    其次选择 myid 最大的机器。

10.3 服务器运行时期的 leader 选举

        在 zookeeper 运行期间,leader 与非 leader 服务器各司其职,即便当有非 leader 服务器宕机 或 新加入,此时也不会影响 leader,但是一旦 leader 服务器挂了,那么整个集群将暂停对外服务,进入新一轮 leader 选举,其过程和启动时期的 leader 选举过程基本一致。
        假设正在运行的有 server1、server2、server3 三台服务器,当前 leader 是 server2,若某一时刻 leader 挂了,此时便开始 leader 选举。选举过程如下:

  1. 变更状态。leader 挂后,余下的服务器都会将自己的服务器状态变更为 looking,然后开始进入 leader 选举过程。
  2. 每个 server 会发出一个投票。在运行期间,每个服务器上的 zxid 可能不同,此时假设 server1 的 zxid 为122,server3 的zxid 为122,在第一轮投票中,server1 和 server3 都会投自己,产生投票(1,122),(3,122),然后各自将投票发送给集群中所有机器。
  3. 接收来自各个服务器的投票。与启动时过程相同。
  4. 处理投票。与启动时过程相同,此时 server3 将会成为 leader。
  5. 统计投票。与启动时过程相同。
  6. 改变服务器的状态。与启动时过程相同。

十一、observer(观察者)角色及其配置

观察者角色的目的:为了扩展系统,提高读取速度。

observer 角色特点:

  1. 不参与集群的 leader 选举
  2. 不参与集群有写数据的 ack 反馈
           为了使用 observer 角色,在任何想变成 observer 角色的配置文件中加入如下配置:
peerType=observer

       并在所有节点 server 的配置文件中,配置成 observer 模式的 server 的那行配置追加:observer,例如:

clientPort=2183
server.1=192.168.15.128:2287:3387
server.2=192.168.15.128:2288:3388
server.3=192.168.15.128:2289:3389:observer

peerType=observer	# 该配置只需要加在观察者节点的配置文件中即可,其他节点不需要加

观察者的节点状态如下:

[root@localhost bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper/zookeeper2183/bin/../conf/zoo.cfg
Mode: observer

十二、zookeeper API 连接集群

Zookeeper(String connectionString, int sessionTimeout, Watcher watcher)
  • connectionString:zookeeper 集合主机
  • sessionTimeout:会话超时(以毫秒为单位)
  • watcher:实现 “监视器”界面的对象。zookeeper 集合通过监视器对象返回连接状态。

与连接单个节点类似,只需要将连接 zookeeper 节点的 IP 地址和端口用逗号连接即可。

示例:

zookeeper:
  address: 192.168.15.128:2181,192.168.15.128:2182,192.168.15.128:2183
  timeout: 5000

十三、zookeeper 开源客户端 curator 介绍

13.1 curator 简介

curator 是 Netflix 公司开源的一个 zookeeper 客户端,后捐献给 Apache,curator 框架在 zookeeper 原生 API接口上进行了包装,解决了很多 zookeeper 客户端非常底层的细节开发。提供 zookeeper 各种应用场景(比如:分布式锁服务、集群领导选举、共享计数器、缓存机制、分布式队列等)的抽象封装,实现了 Fluent(链式编程) 风格的 API接口,是最好用,最流行的 zookeeper 的客户端。

原生zookeeper API 的不足:

  • 连接对象异步创建,需要开发自行编码等待
  • 连接没有自动重连超时机制
  • watcher 一次注册生效一次
  • 不支持递归创建树形节点

curator 的特点:

  • 解决 session 会话超时重连
  • watcher 反复注册
  • 简化开发 API
  • 遵循 Fluent(链式编程) 风格的 API
  • 提供了分布式锁服务、共享计数器、缓存机制等机制

13.2 curator 连接对象的创建

引入依赖:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

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

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.6.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.12.0</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>
package com.example.zookeeper.curator;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryOneTime;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @Description TODO
 * @Author try
 * @Date 2020/12/20 11:05
 * @Version 1.0
 */
@SpringBootTest
public class CuratorConnectionTest {

    @Value("${zookeeper.address}")
    private String connectString;
    @Value("${zookeeper.timeout}")
    private int timeout;

    @Test
    public void curatorConnection() {
        CuratorFramework client = CuratorFrameworkFactory.builder()
                // ip地址端口号
                .connectString(connectString)
                // 会话超时时间
                .sessionTimeoutMs(timeout)
                // 重连机制
                .retryPolicy(new RetryOneTime(timeout))
                // 命名空间(根节点),也可以不指定
                .namespace("create")
                // 构建连接对象
                .build();

        // 打开连接
        client.start();
        System.out.println(client.isStarted());
        // 关闭连接
        client.close();
    }

}

13.3 curator 连接对象的重连策略

四种重连方式

方式一:

// 3秒后重连一次,只重连 1 次
RetryPolicy retryPolicy = new RetryOneTime(3000);

方式二:

// 每3秒重连一次,重连 3 次
RetryPolicy retryPolicy = new RetryNTimes(3, 3000);

方式三:

// 每3秒重连一次,总等待时间超过 10秒 后停止重连
RetryPolicy retryPolicy = new RetryUntilElapsed(3000);

方式四:

// 随着重连次数的增加,重连的间隔也会变长,间隔时间计算方式如下所示,假设 baseSleepTimeMs 为 1000
// baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retyrCount + 1)))
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

13.4 curator 创建节点

package com.example.zookeeper.curator;

import com.example.zookeeper.ZookeeperApplication;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.retry.RetryOneTime;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.ArrayList;
import java.util.List;


/**
 * @Description TODO
 * @Author try
 * @Date 2020/12/20 11:05
 * @Version 1.0
 */
@SpringBootTest(classes = {ZookeeperApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class CuratorConnectionTest {

    @Value("${zookeeper.address}")
    private String connectString;
    @Value("${zookeeper.timeout}")
    private int timeout;

    CuratorFramework client;

    @Before
    public  void before() {
        client = CuratorFrameworkFactory.builder()
                // ip地址端口号
                .connectString(connectString)
                // 会话超时时间
                .sessionTimeoutMs(timeout)
                // 重连机制
                .retryPolicy(new RetryOneTime(timeout))
                // 命名空间(根节点)
                .namespace("create")
                // 构建连接对象
                .build();

        // 打开连接
        client.start();
    }

    @After
    public void after() {
        // 关闭连接
        client.close();
    }


    @Test
    public void curatorConnection() {
        CuratorFramework client = CuratorFrameworkFactory.builder()
                // ip地址端口号
                .connectString(connectString)
                // 会话超时时间
                .sessionTimeoutMs(timeout)
                // 重连机制
                .retryPolicy(new RetryOneTime(timeout))
                // 命名空间(根节点)
                .namespace("create")
                // 构建连接对象
                .build();

        // 打开连接
        client.start();
        System.out.println(client.isStarted());
        // 关闭连接
        client.close();
    }

    @Test
    public void crete1() throws Exception {
        // 新增节点
        String forPath = client.create()
                .withMode(CreateMode.PERSISTENT)
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                .forPath("3node", "2node".getBytes());
        System.out.println("节点创建完成" + forPath);
    }

    @Test
    public void crete2() throws Exception {
        // 新增节点,自定义权限列表
        List<ACL> list = new ArrayList<>();
        // 授权模式 和 授权对象
        Id id = new Id("ip", "192.168.15.128");
        list.add(new ACL(ZooDefs.Perms.ALL, id));

        String forPath = client.create()
                .withMode(CreateMode.PERSISTENT)
                .withACL(list)
                .forPath("4node", "4node".getBytes());
        System.out.println("节点创建完成" + forPath);
    }

    @Test
    public void crete3() throws Exception {
        // 新增节点,递归创建节点树
        String forPath = client.create()
                // 当父节点不存在时,自动创建对应的父节点,再创建当前节点
                .creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT)
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                .forPath("/node2/4node", "4node".getBytes());
        System.out.println("节点创建完成" + forPath);
    }

    @Test
    public void crete4() throws Exception {
        // 新增节点,异步方式创建节点
        String forPath = client.create()
                // 当父节点不存在时,自动创建对应的父节点,再创建当前节点
                .creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT)
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                .inBackground(new BackgroundCallback() {
                    // 异步回调接口
                    @Override
                    public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                        // 节点路径
                        System.out.println(curatorEvent.getPath());
                        // 节点类型
                        System.out.println(curatorEvent.getType());
                    }
                })
                .forPath("/node3", "node3".getBytes());
        Thread.sleep(5000);
        System.out.println("节点创建完成" + forPath);
    }
}

13.5 更新节点

// 更新节点
    @Test
    public void set1() throws Exception {
        // 更新节点
        client.setData()
                .forPath("/node2", "node".getBytes());
        System.out.println("结束");
    }

    // 更新节点,指定版本号
    @Test
    public void set2() throws Exception {
        client.setData()
                // 指定版本号
                .withVersion(1)
                .forPath("/node2", "node11".getBytes());
        System.out.println("结束");
    }

    // 更新节点,异步方式修改节点数据
    @Test
    public void set3() throws Exception {
        client.setData()
                .withVersion(-1)
                .inBackground(new BackgroundCallback() {
                    @Override
                    public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                        // 节点路径
                        System.out.println(curatorEvent.getPath());
                        // 节点类型,SET_DATA
                        System.out.println(curatorEvent.getType());
                    }
                })
                .forPath("/node2", "node1111".getBytes());
        System.out.println("结束");
    }

13.6 删除节点

/**
     * 删除节点
     */
    @Test
    public void delete1() throws Exception {
        client.delete()
                // 节点路径
                .forPath("/node2/4node");
        System.out.println("结束");
    }

    /**
     * 删除节点,指定版本号
     */
    @Test
    public void delete2() throws Exception {
        client.delete()
                .withVersion(0)
                // 节点路径
                .forPath("/node2/4node");
        System.out.println("结束");
    }

    /**
     * 删除节点
     *      删除包含子节点的节点
     */
    @Test
    public void delete3() throws Exception {
        client.delete()
                .deletingChildrenIfNeeded()
                .withVersion(0)
                // 节点路径
                .forPath("/node3");
        System.out.println("结束");
    }

    /**
     * 删除节点
     *      异步方式
     */
    @Test
    public void delete4() throws Exception {
        client.delete()
                .deletingChildrenIfNeeded()
                .withVersion(0)
                .inBackground(new BackgroundCallback() {
                    @Override
                    public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                        // 节点路径
                        System.out.println(curatorEvent.getPath());
                        // 事件类型
                        System.out.println(curatorEvent.getType());
                    }
                })
                // 节点路径
                .forPath("/node1");
        System.out.println("结束");
    }

13.7 查看节点

/**
     * 读取节点数据
     */
    @Test
    public void get1() throws Exception {
        byte[] bytes = client.getData()
                // 节点路径
                .forPath("/node1");
        System.out.println(new String(bytes));
    }

    /**
     * 读取节点数据时,读取节点的属性
     */
    @Test
    public void get2() throws Exception {
        Stat stat = new Stat();
        byte[] bytes = client.getData()
                // 读取属性
                .storingStatIn(stat)
                // 节点路径
                .forPath("/node1");
        System.out.println(new String(bytes));
        System.out.println(stat.toString());
    }

    /**
     * 读取节点数据时,异步方式读取
     */
    @Test
    public void get3() throws Exception {
        client.getData()
                .inBackground(new BackgroundCallback() {
                    @Override
                    public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                        // 节点的路径
                        System.out.println(curatorEvent.getPath());
                        // 事件类型
                        System.out.println(curatorEvent.getType());
                        // 数据
                        System.out.println(new String(curatorEvent.getData()));
                    }
                })
                // 节点路径
                .forPath("/node1");
        Thread.sleep(5000);
        System.out.println("结束");
    }

13.8 查看子节点

/**
     * 读取子节点数据
     */
    @Test
    public void getChild1() throws Exception {
        List<String> list = client.getChildren()
                // 子节点路径
                .forPath("/node4");
        list.forEach(System.out::println);
        System.out.println("结束");
    }

    /**
     * 读取节点数据时,异步方式读取
     */
    @Test
    public void getChild2() throws Exception {
        client.getChildren()
                .inBackground(new BackgroundCallback() {
                    @Override
                    public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                        // 节点路径
                        System.out.println(curatorEvent.getPath());
                        // 事件类型
                        System.out.println(curatorEvent.getPath());
                        // 节点
                        List<String> children = curatorEvent.getChildren();
                        children.forEach(System.out::println);
                    }
                })
                // 子节点路径
                .forPath("/node4");
        Thread.sleep(5000);
        System.out.println("结束");
    }

13.9 判断节点是否存在

/**
     * 判断节点是否存在
     */
    @Test
    public void exists1() throws Exception {
        Stat node1 = client.checkExists()
                .forPath("node1");
        if(null != node1){
            System.out.println(node1.toString());
        }
    }

    /**
     * 判断节点是否存在,异步方式
     */
    @Test
    public void exists2() throws Exception {
        client.checkExists()
                .inBackground(new BackgroundCallback() {
                    @Override
                    public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                        // 节点的路径
                        System.out.println(curatorEvent.getPath());
                        // 事件类型
                        System.out.println(curatorEvent.getType());
                        System.out.println(curatorEvent.getStat().toString());
                    }
                })
                .forPath("node1");
    }

13.10 Watcher API

curator 提供了两种 Watcher(Cache)来监听节点的变化

  • Node Cache:只是监听某一个特定的节点,监听节点的新增和修改
  • PathChildren Cache:监控一个 ZNode 的子节点。当一个子节点增加,更新,删除时,Path Cache 会改变它的状态,会包含最新的子节点,子节点的数据和状态。

示例:

/**
     * 监视某个节点的数据变化
     */
    @Test
    public void watcher1() throws Exception {
        final NodeCache nodeCache = new NodeCache(client, "/watcher1");
        // 启动监视器对象
        nodeCache.start();
        nodeCache.getListenable().addListener(new NodeCacheListener() {
            // 节点变化时,回调的方法
            @Override
            public void nodeChanged() throws Exception {
                System.out.println(nodeCache.getCurrentData().getPath());
                System.out.println(new String(nodeCache.getCurrentData().getData()));
            }
        });
        Thread.sleep(10000);
        System.out.println("结束");
        // 关闭监视器对象
        nodeCache.close();
    }

    /**
     * 监视子节点的变化
     */
    @Test
    public void watcher2() throws Exception {
        // arg1:连接对象
        // arg2:监视的节点路径
        // arg3:事件中是否可以获取节点的数据
        PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/watcher1", true);
        // 启动监听
        pathChildrenCache.start();
        pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
            // 当子节点发生变化时,回调的方法
            @Override
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                // 节点事件类型
                System.out.println(pathChildrenCacheEvent.getType());
                // 节点的路径
                System.out.println(pathChildrenCacheEvent.getData().getPath());
                // 节点的数据
                System.out.println(new String(pathChildrenCacheEvent.getData().getData()));
            }
        });

        Thread.sleep(100000);
        System.out.println("结束");
        // 关闭监听
        pathChildrenCache.close();
    }

13.11 curator 事务

/**
     * curator事务
     */
    @Test
    public void tra1() throws Exception {
        // 开启事务
        client.inTransaction()
                .create().forPath("/node11", "node1".getBytes())
                .and()
                // node12 节点不存在,事务提交失败
                .setData().forPath("node12", "node12".getBytes())
                .and()
                // 事务提交
                .commit();
    }

13.12 curator 分布式锁

curator 分布式锁分为两种:

  • 排他锁
  • 同时有且只有一个线程 能够获取到锁
  • 读写锁
  • 读读操作,可以多个线程同时进行
  • 读写操作,需要等待读线程完成后,才能进行写操作
  • 写读操作:需要等待写线程完成后,才能进行读操作
  • 写写操作:同时有且只有一个线程 能够获取到锁
/**
     * 分布式锁,排他锁
     */
    @Test
    public void lock1() throws Exception {
        // arg1:连接对象
        // arg2:节点路径
        InterProcessLock interProcessLock = new InterProcessMutex(client, "/lock1");
        System.out.println("等待获取锁对象");
        // 获取锁
        interProcessLock.acquire();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(3000);
            System.out.println(i);
        }
        // 释放锁
        interProcessLock.release();
        System.out.println("释放锁结束");
    }

    /**
     * 分布式锁,读写锁
     */
    @Test
    public void lock2() throws Exception {
        InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client, "/lock1");
        System.out.println("等待获取锁对象");
        // 获取读锁
        InterProcessLock readLock = interProcessReadWriteLock.readLock();
        // 获取锁
        readLock.acquire();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(3000);
            System.out.println(i);
        }
        // 释放锁
        readLock.release();
        System.out.println("释放锁结束");
    }

    /**
     * 分布式锁,读写锁
     */
    @Test
    public void lock3() throws Exception {
        InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client, "/lock1");
        System.out.println("等待获取锁对象");
        // 获取写锁
        InterProcessLock writeLock = interProcessReadWriteLock.writeLock();
        // 获取锁
        writeLock.acquire();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(3000);
            System.out.println(i);
        }
        // 释放锁
        writeLock.release();
        System.out.println("释放锁结束");
    }

十四、zookeeper 四字监控命令

14.1 监控命令介绍

zookeeper 支持某些特定的四字命令与其的交互。他们大多是查询命令,用来获取 zookeeper 服务的当前状态及相关信息。用户在客户端可以通过 telnet(Linux系统自带的命令) 或 nc 向 zookeeper 提交相应的命令。zookeeper 常用的四字命令如下表所示:

命令

描述

conf

输出相关服务器配置的详细信息。比如端口、zk 数据及日志配置路径、最大连接数、session 超时时间、serverId 等

cons

列出所有连接到这台服务器的客户端连接、会话的详细信息。包括 “接收/发送” 的包数量、session id、操作延迟、最后的操作执行等信息

crst

重置当前这台服务器所有连接/会话 的统计信息

envi

输出关于服务器的环境详细信息

dump

列出未经处理的会话 和 临时节点

ruok

测试服务是否处于正确运行状态。如果正常返回 “imok” ,否则返回空

stat

输出服务器的详细信息:接收/发送包数量、连接数、模式(leader/follower)、节点总数、延迟

srst

重置 server 状态

wchs

列出服务器 watches 的简洁信息:连接总数、watching 节点总数 和 watches 总数

wchc

通过 session 分组,列出 watch 的所有节点,它的输出是一个与 watch 相关的会话的节点列表

mntr

列出集群的健康状态。包括 “接受/发送”的包数量、操作延迟、当前服务模式(leader/follower)、节点总数、watch 总数、临时节点总数

telnet 示例:
如果提示 telnet 命令未找到,则需要安装 telnet,命令如下:
yum install telnet

# 登录 telnet
telnet 192.168.15.128 2181
# 列出集群的健康状态
mntr

nc 方式登录

# 下载
wget http://vault.centos.org/6.6/os/x86_64/Packages/nc-1.84-22.el6.x86_64.rpm
# 安装
rpm -iUv nc-1.84-22.el6.x86_64.rpm
# 使用 nc 方式查看
echo mntr | nc 192.168.15.128 2181

14.2 conf 命令

conf:输出相关服务配置的详细信息

shell 中断输入:echo conf| nc localhost 2181

zookeeper kerberos客户端登录_zookeeper_08

[root@localhost src]# echo conf| nc 192.168.15.128 2181
clientPort=2181
dataDir=/usr/local/zookeeper/zookeeper2181/data/version-2
dataLogDir=/usr/local/zookeeper/log/version-2
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=1
initLimit=10
syncLimit=5
electionAlg=3
electionPort=3387
quorumPort=2287
peerType=0

14.3 cons 命令

cons:列出所有连接到这台服务器的客户端连接/ 会话的详细信息
shell 终端输入:echo cons| nc localhost 2181

zookeeper kerberos客户端登录_zookeeper_09

[root@localhost src]# echo cons| nc 192.168.15.128 2181
 /192.168.15.128:56733[0](queued=0,recved=1,sent=0)
 /192.168.15.128:56694[1](queued=0,recved=18801,sent=18801,sid=0x1003ba3cfa90000,lop=PING,est=1608349270941,to=30000,lcxid=0x31,lzxid=0xffffffffffffffff,lresp=1188622456,llat=0,minlat=0,avglat=0,maxlat=268)

14.4 crst 命令

crst:重置当前这台服务器所有连接/会话的统计信息
shell 终端输入:echo crst| nc localhost 2181

14.5 dump 命令

dump:列出未处理的会话 和 临时节点 信息

shell 终端输入:echo dump| nc localhost 2181

zookeeper kerberos客户端登录_hadoop_10


zookeeper kerberos客户端登录_hadoop_11

14.6 envi 命令

envi:输出相关服务器的环境个详细信息

shell 终端输入:echo envi| nc localhost 2181

zookeeper kerberos客户端登录_数据_12

[root@localhost src]# echo envi | nc 192.168.15.128 2181
Environment:
zookeeper.version=3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
host.name=localhost
java.version=1.8.0_65
java.vendor=Oracle Corporation
java.home=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-3.b17.el7.x86_64/jre
java.class.path=/usr/local/zookeeper/zookeeper2181/bin/../zookeeper-server/target/classes:/usr/local/zookeeper/zookeeper2181/bin/../build/classes:/usr/local/zookeeper/zookeeper2181/bin/../zookeeper-server/target/lib/*.jar:/usr/local/zookeeper/zookeeper2181/bin/../build/lib/*.jar:/usr/local/zookeeper/zookeeper2181/bin/../lib/slf4j-log4j12-1.7.25.jar:/usr/local/zookeeper/zookeeper2181/bin/../lib/slf4j-api-1.7.25.jar:/usr/local/zookeeper/zookeeper2181/bin/../lib/netty-3.10.6.Final.jar:/usr/local/zookeeper/zookeeper2181/bin/../lib/log4j-1.2.17.jar:/usr/local/zookeeper/zookeeper2181/bin/../lib/jline-0.9.94.jar:/usr/local/zookeeper/zookeeper2181/bin/../lib/audience-annotations-0.5.0.jar:/usr/local/zookeeper/zookeeper2181/bin/../zookeeper-3.4.14.jar:/usr/local/zookeeper/zookeeper2181/bin/../zookeeper-server/src/main/resources/lib/*.jar:/usr/local/zookeeper/zookeeper2181/bin/../conf:
java.library.path=/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
java.io.tmpdir=/tmp
java.compiler=<NA>
os.name=Linux
os.arch=amd64
os.version=3.10.0-327.el7.x86_64
user.name=root
user.home=/root
user.dir=/usr/local/zookeeper/zookeeper2181/bin

14.7 ruok 命令

ruok:测试服务是否处于正确运行状态
shell 终端输入:echo ruok| nc localhost 2181

[root@localhost src]# echo ruok| nc 192.168.15.128 2181
imok[root@localhost src]#

14.8 stat 命令

stat:输出服务器的详细信息 与 srvr 相似,但是多了每个链接的会话信息

shell 终端输入:echo stat| nc localhost 2181

zookeeper kerberos客户端登录_数据_13

[root@localhost src]# echo stat| nc 192.168.15.128 2181
Zookeeper version: 3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
Clients:
 /192.168.15.128:56694[1](queued=0,recved=18974,sent=18974)
 /192.168.15.128:56736[0](queued=0,recved=1,sent=0)

Latency min/avg/max: 0/0/496
Received: 90185
Sent: 90189
Connections: 2
Outstanding: 0
Zxid: 0x30000017d
Mode: follower
Node count: 39

14.9 srst 命令

srst:重置 server 的状态
shell 终端输入: echo srst| nc localhost 2181

[root@localhost src]# echo srst| nc 192.168.15.128 2181
Server stats reset.

14.10 wchs 命令

wchs:列出服务器 watches 的简洁信息

shell 终端输入: echo wchs| nc localhost 2181

zookeeper kerberos客户端登录_hadoop_14

[root@localhost src]# echo wchs| nc 192.168.15.128 2181
0 connections watching 0 paths
Total watches:0

14.11 wchc 命令

wchc:通过 session 分组,列出 watch 的所有节点,它的输出的是一个与 watch 相关的会话的节点列表。

可能会出现以下问题:

wchc is not executed because it is not in the whitelist

解决方法:

zookeeper kerberos客户端登录_数据_15


shell 终端输入:echo wchc| nc localhost 2181

14.12 wchp 命令

wchp:通过路径分组,列出所有的 watch session id 信息

zookeeper kerberos客户端登录_hadoop_16


shell 终端输入:echo wchp| nc localhost 2181

14.12 mntr 命令

mntr:列出服务器的健康状态

zookeeper kerberos客户端登录_zookeeper_17


shell 终端输入:echo mntr| nc localhost 2181

[root@localhost src]# echo mntr| nc 192.168.15.128 2181
zk_version	3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
zk_avg_latency	1
zk_max_latency	29
zk_min_latency	0
zk_packets_received	93
zk_packets_sent	93
zk_num_alive_connections	2
zk_outstanding_requests	0
zk_server_state	follower
zk_znode_count	39
zk_watch_count	0
zk_ephemerals_count	0
zk_approximate_data_size	560
zk_open_file_descriptor_count	34
zk_max_file_descriptor_count	4096
zk_fsync_threshold_exceed_count	0

十五、zookeeper图形化的客户端工具(Zooinspector)

ZooInspector 下载地址:

https://issues.apache.org/jira/secure/attachment/12436620/ZooInspector.zip

解压,进入目录ZooInspector\build
运行或mac下双击zookeeper-dev-ZooInspector.jar

十六、taokeeper 监控工具的时候

基于 zookeeper 的监控管理工具 taokeeper,有淘宝团队开源的 zk 管理中间件,安装前要求服务先配置 nc 和 sshd

  1. 下载数据库脚本
wget https://github.com/downloads/alibaba/taokeeper/taokeeper.sql

脚本内容如下:

CREATE DATABASE taokeeper;
USE taokeeper;

-- ----------------------------
-- Table: alarm_settings
-- ----------------------------
DROP TABLE IF EXISTS `alarm_settings`;
CREATE TABLE `alarm_settings` (
  `alarm_settings_id` int(11) NOT NULL auto_increment,
  `cluster_id` int(11) NOT NULL,
  `wangwang_list` varchar(255) default NULL,
  `phone_list` varchar(255) default NULL,
  `email_list` varchar(255) default NULL,
  `max_delay_of_check` varchar(255) default NULL,
  `max_cpu_usage` varchar(255) default NULL,
  `max_memory_usage` varchar(255) default NULL,
  `max_load` varchar(255) default NULL,
  `max_connection_per_ip` varchar(255) default NULL,
  `max_watch_per_ip` varchar(255) default NULL,
  `data_dir` varchar(255) default NULL,
  `data_log_dir` varchar(255) default NULL,
  `max_disk_usage` varchar(255) default NULL,
  PRIMARY KEY  (`alarm_settings_id`),
  UNIQUE KEY `uk_alarm_settings_cid` (`cluster_id`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk;

-- ----------------------------
-- Table taokeeper_settings
-- ----------------------------
DROP TABLE IF EXISTS `taokeeper_settings`;
CREATE TABLE `taokeeper_settings` (
  `settings_id` int(11) NOT NULL auto_increment,
  `env_name` varchar(20) default NULL,
  `max_threads_of_zookeeper_check` int(5) default NULL,
  `description` varchar(255) default NULL,
  PRIMARY KEY  (`settings_id`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk;

-- ----------------------------
-- Table: taokeeper_stat
-- ----------------------------
DROP TABLE IF EXISTS `taokeeper_stat`;
CREATE TABLE `taokeeper_stat` (
  `cluster_id` int(11) NOT NULL,
  `server` varchar(30) NOT NULL COMMENT '127.0.0.1:2181',
  `stat_date_time` datetime NOT NULL COMMENT '统计时间 2012-01-05 14:56:20',
  `stat_date` date NOT NULL,
  `connections` int(11) DEFAULT NULL,
  `watches` int(11) DEFAULT NULL COMMENT '订阅者数目',
  `send_times` bigint(20) unsigned DEFAULT 0,
  `receive_times` bigint(20) unsigned DEFAULT 0,
  `node_count` int(11) DEFAULT 0,
  PRIMARY KEY (`cluster_id`,`server`,`stat_date_time`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk;


-- ----------------------------
-- Table: zookeeper_cluster
-- ----------------------------
DROP TABLE IF EXISTS `zookeeper_cluster`;
CREATE TABLE `zookeeper_cluster` (
  `cluster_id` int(11) NOT NULL auto_increment,
  `cluster_name` varchar(255) NOT NULL,
  `server_list` varchar(255) NOT NULL,
  `description` varchar(255) default NULL,
  PRIMARY KEY  (`cluster_id`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk;
  1. 下载主程序
wget https://github.com/downloads/alibaba/taokeeper/taokeeper-monitor.tar.gz
  1. 下载配置文件
wget https://github.com/downloads/alibaba/taokeeper/taokeeper-monitor-config.properties
  1. 配置 taokeeper-monitor-config.properties
#Daily

systemInfo.envName=DAILY

#DBCP
dbcp.driverClassName=com.mysql.jdbc.Driver
# MySQL 连接的ip地址端口号
dbcp.dbJDBCUrl=jdbc:mysql://1.1.1.1:3306/taokeeper
dbcp.characterEncoding=GBK
dbcp.username=root
dbcp.password=123456
dbcp.maxActive=30
dbcp.maxIdle=10
dbcp.maxWait=10000

#SystemConstant
# 用户存储内部数据的文件夹
# 创建/home/zookeeper/taokeeperdata/ZookeeperClientThroughputStat
SystemConstent.dataStoreBasePath=/home/yinshi.nc/taokeeper-monitor/
# ssh 用户
SystemConstant.userNameOfSSH=admin
# ssh 密码
SystemConstant.passwordOfSSH=123456
#Optional
SystemConstant.portOfSSH=22

zookeeper kerberos客户端登录_数据_18

  1. 配置 tomcat,修改 catalina.sh
  2. zookeeper kerberos客户端登录_hadoop_19

  3. 部署工程启动

zookeeper kerberos客户端登录_hadoop_20


zookeeper kerberos客户端登录_hadoop_21