zookeeper
一、zookeeper 概述和特性
1.1 zookeeper 的概述
zookeeper 官网:https://zookeeper.apache.org/
1.2 zookeeper 的应用场景
zookeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能,高可用,且具有严格顺序访问控制能力的分布式协调存储服务。
- 维护配置信息
- 分布式锁服务
- 集群管理
- 生成分布式唯一ID
1.2.1 维护配置信息
配置中心
1.2.2 分布式锁服务
1.2.3 集群管理
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、时间戳等数据结构
* 又像目录一样可以作为路径标识的一部分。
一个 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
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 可以轻松完成,当数据库发生变化时自动完成缓存同步。
设计思路:
- 连接zookeeper服务器
- 读取 zookeeper 中的配置信息,注册 watcher 监听器,存入本地变量
- 当 zookeeper 中的配置信息发生变化时,通过 watcher 的回调方法捕获数据变化事件
- 重新获取配置信息
代码示例:
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。
设计思路:
- 连接 zookeeper 服务器
- 指定路径生成临时有序节点
- 取序列号及分布式环境下的唯一 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 中如何实现排他锁。
设计思路:
- 每个客户端往 /Locks 下创建临时有序节点 /Locks/Lock_,创建成功后 /Locks 下面会有每个客户端对应的节点,如 /Locks/Lock_0000000001
- 客户端取得 /Locks 下子节点,并进行排序,判断排在最前面的是否为自己,如果自己的锁节点在第一位,代表获取锁成功
- 如果自己的锁节点不在第一位,则监听自己前一位的锁节点。例如,自己锁节点 Lock_0000000002,那么则监听Lock_0000000001
- 当前一位锁节点(Lock_0000000001)对应的客户端执行完成,释放了锁,将会触发监听客户端(Lock_0000000002)的逻辑
- 监听客户端重新执行第 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
步骤如下:
- 基于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
- 修改 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
- 在上一步 dataDir 指定的目录下,创建 myid 文件,然后在该文件添加上一步 server 配置的对应 A 数字。
#zookeeper2181 对应的数字为 1
#/home/zookeeper/zookeeper2181/data 目录下执行命令
echo "1" > myid
- zookeeper2182、zookeeper2183 参照步骤2/3进行相应配置
- 分别启动三台服务器,检验集群状态
./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
- 登录命令
./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 集群中的角色主要有以下三类,如下表所示:
zab 广播模式工作原理,通过类似两阶段提交协议的方式解决数据一致性:
写入数据到 zookeeper 分为以下6个步骤:
- leader 从客户端收到一个写请求,如果是 follower 节点接收到写请求, follower 节点会将请求转发给 leader 节点处理
- leader 生成一个新的事务并为这个事务生成一个唯一的 ZXID
- leader 将这个事务提议(propose)发送给所有的 follows 节点
- follower 节点将收到的事务请求加入到历史队列(history queue)中,并发送 ack 给 leader
- 当 leader 收到大多数 follower(半数以上节点)的 ack 消息,leader 会发送 commit 请求
- 当 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 选举过程。选举过程如下:
- 每个 server 发出一个投票。由于是初始情况,server1 和 server2 都会将自己作为 leader 服务器来进行投票,每次投票会包含所选举的服务器的 myid 和 zxid(事务id),使用(myid,zxid)来表示,此时 server1 的投票为(1,0),server2 的投票为(2,0)然后各自将这个投票发给集群中其他机器。
- 集群中的每台服务器接收来自集群中各个服务器的投票。
- 处理投票。针每一个投票,服务器都需要将别人的投票和自己的投票进行pk,pk规则如下:
- 优先检查 zxid。zxid 比较大的服务器优先作为 leader。
- 如果 zxid 相同,那么就比较 myid。myid 较大的服务器作为 leader 服务器。
对于 server1 而言,它的投票是(1,0),接收 server2 的投票为(2,0),首先会比较两者的 zxid,均为0,再比较myid,此时server2 的myid最大,于是更新自己的投票为(2,0),然后重新投票,对于 server2 而言,其必须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
- 统计投票。每次投票后,服务器都会统计投票信息,判断是否有过半机器接收到相同的投票信息,对于 server1、server2 而言,都停机集群中已经有两台机器接受了(2,0)的投票信息,此时便认为选出了 leader。
- 改变服务器状态。一旦确认了 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 选举。选举过程如下:
- 变更状态。leader 挂后,余下的服务器都会将自己的服务器状态变更为 looking,然后开始进入 leader 选举过程。
- 每个 server 会发出一个投票。在运行期间,每个服务器上的 zxid 可能不同,此时假设 server1 的 zxid 为122,server3 的zxid 为122,在第一轮投票中,server1 和 server3 都会投自己,产生投票(1,122),(3,122),然后各自将投票发送给集群中所有机器。
- 接收来自各个服务器的投票。与启动时过程相同。
- 处理投票。与启动时过程相同,此时 server3 将会成为 leader。
- 统计投票。与启动时过程相同。
- 改变服务器的状态。与启动时过程相同。
十一、observer(观察者)角色及其配置
观察者角色的目的:为了扩展系统,提高读取速度。
observer 角色特点:
- 不参与集群的 leader 选举
- 不参与集群有写数据的 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
[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
[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
14.6 envi 命令
envi:输出相关服务器的环境个详细信息
shell 终端输入:echo envi| nc localhost 2181
[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
[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
[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
解决方法:
shell 终端输入:echo wchc| nc localhost 2181
14.12 wchp 命令
wchp:通过路径分组,列出所有的 watch session id 信息
shell 终端输入:echo wchp| nc localhost 2181
14.12 mntr 命令
mntr:列出服务器的健康状态
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
- 下载数据库脚本
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;
- 下载主程序
wget https://github.com/downloads/alibaba/taokeeper/taokeeper-monitor.tar.gz
- 下载配置文件
wget https://github.com/downloads/alibaba/taokeeper/taokeeper-monitor-config.properties
- 配置 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
- 配置 tomcat,修改 catalina.sh
- 部署工程启动