ZooKeeper 入门篇
1. 简介
1.1 概述
ZooKeeper 是一个开源的分布式的,为分布式框架提供协调服务的Apache项目。简单来说,就是在分布式框架中,协调各个服务节点,包括节点的状态和节点之间的调用等等。
官网地址,https://zookeeper.apache.org/index.html
提供的功能包括:统一命名服务,统一配置管理,统一集群管理,服务节点动态上下线,软负载均衡。
1.2 特点
在分布式架构中,为了保证高可用,ZooKeeper通常采用集群架构,在CAP原理中,它更倾向于CP架构。
ZooKeeper集群中有一个leader,和多个follower
集群中只要有半数以上(强调一下不包含半数)节点存活,ZooKeeper集群就能正常提供服务,所以它适合安装奇数台服务器。
全局数据一致,每个server保存一份相同的数据副本,Client无论链接到哪个server,数据都是一致的。
更新请求顺序执行,来自同一个Client的请求按其发送顺序依次执行
数据更新原子性,一次数据更新要不成功,要不失败。
实时性,一定时间范围内,Client能读到最新数据。
题外话,
为什么是安装奇数台服务器?
假设是3台服务器,挂了2台,zookeeper就停止服务了,所以3台服务器的容忍度就是1,允许一台服务器挂了。
假设是4台服务器,挂了2台,zookeeper就停止服务了,因为没有超过半数,所以4台服务器的容忍度也是1。
所以从效率和高可用角度考虑,3台和4台没什么区别,所以最好选用奇数台服务器。
zookeeper是cp还是ap?
zookeeper保证的是cp,eruka是ap。准确来说zookeeper保证的是写是强一致性,读是顺序一致性。
写是强一致性,读是顺序一致性?
zookeeper只有Leader节点能写数据,其他的Follower节点读数据;写的时候,保证只有Leader节点写完并同步完其他Follower节点,才会响应客户端,所以是写强一致性;但是读的时候,Follower节点也可以提供服务,zookeeper是直接返回响应结果的;这个时候Follower节点可能还没同步主节点数据,对于同一个节点数据zookeeper是按版本号记录的,那么这个时候就会返回该节点最新版本号的数据,所以读取是顺序一致性;
1.3 数据结构
ZooKeeper数据模型的结构与Unix文件系统类似,整体上可以看作一棵树,每个节点称为一个ZNode。
每个ZNode默认能够存储1MB的数据,所以不是存储过大的数据。
每个ZNode都可以通过其路径唯一标识。
2. 下载安装和参数介绍
2.1 下载安装
下载地址,https://archive.apache.org/dist/zookeeper/
解压
tar -zvxf apache-zookeeper-3.9.2-bin.tar.gz
修改配置文件,进入配置文件夹,将zoo_sample.cfg
复制一份到zoo.cfg
,修改一下默认的dataDir
地址
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/opt/zookeeper/apache-zookeeper-3.9.2-bin/zkdata
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
启动,进入bin目录
./zkServer.sh start
其他命令
# 启动客户端
./zkCli.sh
# 查看服务状态
./zkServer.sh status
# 停止服务
./zkServer.sh stop
2.2 参数介绍
在配置文件中,包含如下默认参数
#这是 ZooKeeper 中的基本时间单位,以毫秒为单位。也就是通信心跳时间
tickTime=2000
# LF初始通信时限,Leader和Follower 初始连接 时能容忍的最多心跳数。也就是10个tickTime
initLimit=10
# LF同步通信时限,Leader和Follower 同步数据 时能容忍的最多心跳数。也就是5个tickTime
syncLimit=5
# 存储zookeeper中的数据,默认是在/tmp目录下,容易被定期清除,所以必须要修改
dataDir=/opt/zookeeper/apache-zookeeper-3.9.2-bin/zkdata
# 客户端连接端口,通常不修改
clientPort=2181
其他
# 这个参数配置了 ZooKeeper 能接受的最大客户端连接数。
maxClientCnxns=60
# 这个参数配置了 session 的最小超时时间,这个值是以 tickTime 为单位的。
minSessionTimeout=10
# 这个参数配置了 session 的最大超时时间,这个值也是以 tickTime 为单位的。
maxSessionTimeou=20
# 这是集群模式下配置的
# A是服务器的编号也就是myid的值,B是这个服务器的IP地址,C是这个服务器与其他ZooKeeper服务器通信的端口,D是这个服务器用来选举Leader的端口。
# 如 server.0=192.168.137.139:2888:3888
# 如 server.1=192.168.137.140:2888:3888
server.A=B:C:D
3. 集群搭建与操作
3,1 集群搭建
首先按照前面安装的步骤成功安装好后,再执行下面的操作步骤
配置服务器编号
在zkdata
下创建myid
文件,注意文件名必须是myid
,且文件中编号不能有空格,空行等,执行vi myid
,输入服务器编号即可,可以是ip的后三位
139
配置zoo.cfg文件
修改zoo.cfg配置,增加如下配置
# cluster
server.139=192.168.137.139:2888:3888
server.140=192.168.137.140:2888:3888
依次启动
./zkServer.sh start
通过查看zookeeper的状态确定是否启动
139服务器,follower
[root@wuqiongda bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper-3.9.2/apache-zookeeper-3.9.2-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
140服务器,leader
[root@localhost bin]# ./zkServer.sh status
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/apache-zookeeper-3.9.2-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader
3.2 选举机制
选举机制分为两种,全新集群选举(第一次启动)和 非全新集群选举(非第一次启动)
当服务器一台服务器出现以下两种情况之一时,就会进入选举
服务器初始化启动
服务器运行期间无法和leader保持连接
选举时对应的状态
LOOKING:选举中,正在寻找Leader
FOLLOWING:随从状态,同步leader状态,参与投票
LEADING:领导者,差不多是master,在zookeeper中只有leader才有写的权限,following只有读的权限
OBSERVING:观察者状态,不同leader状态,不参与投票
选举的与三个参数有关
SID,服务器ID,用来唯一标识一台zookeeper集群中的机器,每台机器不能重复,和myid一致
ZXID,事务ID。ZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这和 ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。
Epoch,每个Leader任期的代号。没有Leader时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加。
选举核心原则
Zookeeper集群中只有超过半数以上的服务器启动,集群才能正常工作;
在集群正常工作之前,myid小的服务器给myid大的服务器投票,直到集群正常工作,选出Leader;
选出Leader之后,之前的服务器状态由Looking改变为Following,以后的服务器都是Follower。
如果集群没有Leader(非全新选举)
Epoch大的服务器当选leader;
如果Epoch相等,比较ZXID(事物ID),事物ID大的,当选leader;
如果Epoch相等,ZXID相等,则比较myId(服务器id),服务器id大的当选Leader,服务器id是不重复;
3.2.1 第一次启动
通俗来说,就是初始化时,每个节点都是LOOKING状态,每个节点都有一张选票,默认是给自己投的,当通过2888端口获取了其他服务信息,就比较myid大小,大的获取选票。此时有两种情况
当某个节点选票小于或等于集群节点数一半时,节点状态都保持为LOOKING,
当某个节点选票大于集群节点数一半时,该节点就当选为leader,状态也变为LEADING,其他节点状态则为FOLLOWING,当已经选出leader后,其他节点只能是follower
3.2.2 非第一次启动
3.3 集群停止脚本
对于脚本不太熟悉,参考链接,https://www.cnblogs.com/yeyuzhuanjia/p/18025531
#!/bin/bash
zookeeper_home=/opt/zookeeper-3.9.2/apache-zookeeper-3.9.2-bin
zookeeper_array=(192.168.137.139 192.168.137.140)
## 启动和停止Zookeeper
function zookeeper_operate(){
zookeeper_operate=$@
if [[ ${zookeeper_operate} == "status" ]]
then
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "About to check zookeeper status ...."
sleep 5
fi
echo "****************************************Zookeeper ${zookeeper_operate}*******************************************"
for zookeeper_node in ${zookeeper_array[@]}
do
echo "======================${zookeeper_node} ${zookeeper_operate}========================"
ssh ${zookeeper_node} "source /etc/profile;${zookeeper_home}/bin/zkServer.sh ${zookeeper_operate}"
done
}
echo "=======Start Zookeeper cluster,please intput : start or 1 ======"
echo "=======Stop Zookeeper cluster,please intput : stop or 0 ======"
read -p "please input : " inputStr
case ${inputStr} in
start|START|1)
## 启动zookeeper
zookeeper_operate start
## 检查所有节点的状态
## zookeeper_operate status
;;
stop|STOP|0)
zookeeper_operate stop
## zookeeper_operate status
;;
*)
echo "Input wrong,please check!"
break
esac
注意这里的ssh命令,会需要输入密码
ssh ${zookeeper_node} -l root -o StrictHostKeyChecking=no
如果不想输入密码,可以通过,使用ssh密钥对验证:
首先,生成一对密钥对,包括公钥和私钥。可以使用以下命令生成密钥对:ssh-keygen -t rsa
这将在默认目录(一般在用户目录下的.ssh文件夹)中生成id_rsa
和id_rsa.pub
两个文件,其中id_rsa为私钥,id_rsa.pub为公钥。
然后,将公钥拷贝到远程服务器上的~/.ssh/authorized_keys文件中,可以使用以下命令完成拷贝:ssh-copy-id user@hostname
其中,user为登录服务器的用户名,hostname为服务器的IP地址或域名。执行该命令后,系统会要求输入密码进行验证,之后会自动将公钥拷贝到远程服务器。
完成以上操作后,再次使用ssh命令登录远程服务器时,系统会自动使用私钥进行验证,无需再输入密码。
4. 客户端命令行操作
4.1 命令行语法
4.2 znode节点数据信息
当我们依次执行了下面的命令后,通过 stat 命令查看节点状态信息
[zk: localhost:2181(CONNECTED) 10] create /dubbo
Created /dubbo
[zk: localhost:2181(CONNECTED) 11] set /dubbo "127.0.0.1:8081"
[zk: localhost:2181(CONNECTED) 12] ls /
[dubbo, zookeeper]
[zk: localhost:2181(CONNECTED) 13] create -s /dubbo/UserService
Created /dubbo/UserService0000000000
[zk: localhost:2181(CONNECTED) 14] create -s /dubbo/UserService
Created /dubbo/UserService0000000001
[zk: localhost:2181(CONNECTED) 15] create -s /dubbo/UserService
Created /dubbo/UserService0000000002
[zk: localhost:2181(CONNECTED) 16] stat /dubbo
cZxid = 0x30000000d
ctime = Sun Oct 06 06:48:39 CST 2024
mZxid = 0x30000000e
mtime = Sun Oct 06 06:49:41 CST 2024
pZxid = 0x300000011
cversion = 3
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 14
numChildren = 3
czxid:创建节点的事务 zxid
每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。
事务 ID 是 ZooKeeper 中所有修改总的次序。
每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生。
ctime:znode 被创建的毫秒数(从 1970 年开始)
mzxid:znode 最后更新的事务 zxid
mtime:znode 最后修改的毫秒数(从 1970 年开始)
pZxid:znode 最后更新的子节点 zxid
cversion:znode 子节点变化号,znode 子节点修改次数
dataversion:znode 数据变化号
aclVersion:znode 访问控制列表的变化号
ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0。
dataLength:znode 的数据长度
numChildren:znode 子节点数量
4.3 节点类型
节点类型(短暂、持久,有序号,无序号)
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
有序号,就是通过create创建节点是,通过
create -s
创建的节点,znode会有一个附加的序号值,这个序号单调递增,由父节点维护。无序号,默认创建都是无序号的
注意,在分布式系统中,顺序号可以用于为所有创建事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
4.4 监听器原理
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper 会通知客户端;
监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序
监听器原理
监听器使用
注意监听器,注册一次,生效一次。同一个节点连续注册,也只生效一次。
监听节点数据变化
通过
get -w path
命令,当节点数据变化时,会收到回复[zk: localhost:2181(CONNECTED) 18] get -w /dubbo 127.0.0.1:8081 [zk: localhost:2181(CONNECTED) 19] set /dubbo "8082" WATCHER:: WatchedEvent state:SyncConnected type:NodeDataChanged path:/dubbo zxid: 12884901907 [zk: localhost:2181(CONNECTED) 20] set /dubbo "8083" [zk: localhost:2181(CONNECTED) 21]
监听子节点增减的变化
通过
ls -w path
命令,当子节点数量发送变化时,也会收到回复[zk: localhost:2181(CONNECTED) 25] ls -w /dubbo [RoleService0000000003, UserService0000000000, UserService0000000001, UserService0000000002] [zk: localhost:2181(CONNECTED) 26] create -s /dubbo/RoleService WATCHER:: Created /dubbo/RoleService0000000004 WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/dubbo zxid: 12884901911 [zk: localhost:2181(CONNECTED) 27] create -s /dubbo/RoleService Created /dubbo/RoleService0000000005 [zk: localhost:2181(CONNECTED) 28]
4.5 节点删除
delete命令和deleteall命令
delete 删除指定节点
deleteall 递归删除节点,包括自己
[zk: localhost:2181(CONNECTED) 28] delete /dubbo/RoleService0000000005
[zk: localhost:2181(CONNECTED) 29] ls /dubbo
[RoleService0000000003, RoleService0000000004, UserService0000000000, UserService0000000001, UserService0000000002]
[zk: localhost:2181(CONNECTED) 30] delete /dubbo
Node not empty: /dubbo
[zk: localhost:2181(CONNECTED) 31] ls /dubbo
[RoleService0000000003, RoleService0000000004, UserService0000000000, UserService0000000001, UserService0000000002]
[zk: localhost:2181(CONNECTED) 32] deleteall /dubbo
[zk: localhost:2181(CONNECTED) 33] ls /dubbo
Node does not exist: /dubbo
[zk: localhost:2181(CONNECTED) 34] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 35]
4.6 客户端权限-ACL
参考链接,https://blog.csdn.net/2401_84572976/article/details/138679185
# 1.给已有节点赋予权限
setAcl path acl
# 2.在创建节点时候指定权限
create [-s] [-e] path data acl
# 3.查看指定节点的权限
getAcl path
zookeeper 权限组成
zookeeper 的权限由 [scheme : id : permissions] 三部分组成,其中 Schemes 和 Permissions 内置的可选项分别如下: Permissions 可选项:
CREATE:允许创建子节点;
READ:允许从节点获取数据并列出其子节点;
WRITE:允许为节点设置数据;
DELETE:允许删除子节点;
ADMIN:允许为节点设置权限。
Schemes 可选项:
world:默认模式,所有客户端都拥有指定的权限。world 下只有一个 id 选项,就是 anyone,通常组合写法为 world:anyone:[permissons] ;
auth:只有经过认证的用户才拥有指定的权限。通常组合写法为 auth:user:password:[permissons] ,使用这种模式时,你需要先进行登录,之后采用 auth 模式设置权限时, user和 password 都将使用登录的用户名和密码;
digest:只有经过认证的用户才拥有指定的权限。通常组合写法为auth:user:BASE64(SHA1(password)):[permissons] ,这种形式下的密码必须通过 SHA1 和BASE64 进行双重加密;
ip:限制只有特定 IP 的客户端才拥有指定的权限。通常组成写法为 ip:182.168.0.168:[permissions] ;
super:代表超级管理员,拥有所有的权限,需要修改 Zookeeper 启动脚本进行配置
认证信息
可以使用如下所示的命令为当前 Session 添加用户认证信息,等价于登录操作。
# 格式
addauth scheme auth
#示例:添加用户名为heibai,密码为root的用户认证信息
addauth digest heibai:root
4.7 客户端API操作
就是通过代码的方式,执行以上命令,具体实现放到下面应用场景中,通过zookeeper实现分布式锁。
5. 应用场景
5.1 统一命名服务
有点类似于nginx的负载均衡
5.2 统一配置管理
如kafka中就是使用到了这一功能
5.3 统一集群管理
通过watch监听节点状态
5.4 服务器动态上下线
5.5 软负载均衡
5.6 基于ZooKeeper的分布式锁
大致流程,假设有100个线程同时访问锁定资源
zookeeper收到请求后,根据请求依次创建临时的顺序节点
判断自己是不是当前节点下最小的节点,是则获取锁,不是则监听前一个节点
获取到锁后,处理完业务,delete节点释放锁,后面的节点收到监听通知,重复第二步
使用 curator可以使用框架实现分布式锁
参考链接,https://www.jianshu.com/p/f0616449ee72
首先项目中,可以通过AOP的方式实现锁机制
导入依赖
<dependencies>
<!-- zookeeper分布式锁 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
</dependency>
<!-- aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
DistributedLock注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
String lockPath() default "/opt/zookeeper/lock";
}
配置类
@Configuration
public class CuratorConfiguration {
@Bean
public CuratorFramework curatorFramework(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
.connectString("192.168.137.140:2181,192.168.137.139:2181")
.sessionTimeoutMs(5000)
.connectionTimeoutMs(5000)
.retryPolicy(retryPolicy)
.build();
curatorFramework.start();
return curatorFramework;
}
}
切面类
@Aspect
@Component
public class DistributedLockAspect{
@Autowired
private CuratorFramework curatorFramework;
@Pointcut("@annotation(com.wz.consumer.config.DistributedLock)")
public void methodAspect(){};
@Around("methodAspect()")
public Object execute(ProceedingJoinPoint proceedingJoinPoint) throws Exception{
Object object = null;
DistributedLock disLock = getDisLockInfo(proceedingJoinPoint);
String lockPath = disLock.lockPath();
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, lockPath);
try{
boolean locked = mutex.acquire(100, TimeUnit.SECONDS);
if(!locked){
return null;
}else{
object = proceedingJoinPoint.proceed();
}
} catch(Throwable e){
e.printStackTrace();
} finally{
mutex.release();
}
return object;
}
public DistributedLock getDisLockInfo(ProceedingJoinPoint proceedingJoinPoint) {
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
return methodSignature.getMethod().getAnnotation(DistributedLock.class);
}
@PreDestroy
public void destroy(){
CloseableUtils.closeQuietly(curatorFramework);
}
}
测试接口,假设A是分布式中的共享资源
static int a = 100;
@GetMapping("/testLock")
@DistributedLock
public String testLock() {
a--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("a===============" + a);
return "success";
}
6. paxos算法和zab协议
暂时先了解一下
参考链接,https://blog.csdn.net/qq_49511239/article/details/126504244
Paxos算法:是基于消息传递而且具有高度容错性的一致性算法
解决的问题:解决如何快速准确的在分布式系统中对某个数据达成一致性,而且保证不管发生什么异常,都不破坏系统一致性
ZAB协议:支持崩溃恢复的原子广播协议。两种模式:崩溃恢复(选主)、消息广播(同步)
在消息广播中,所有请求都转化为一个proposal提案,这个提案会有一个全局ID,zxid,确保所有服务器最终能正确提交proposal
崩溃恢复中,zab协议满足两个条件
确保已经被leader提交的提案proposal,必须最终被所有的follower服务器提交,所有崩溃恢复时,zxid大的优先当选leader
确保丢弃已经被leader提出,但是没有被提交的proposal
消息广播:写操作
崩溃恢复:
7. 面试
7.1 客户端向服务端写数据流程
第一种,写请求发送给了leader,假设有三台服务器,当leader成功写入后,会先同步给follower,当同步的节点超过半数,就会直接响应
第二种,写请求发送给了follower,请求会先转发给leader,当leader写入成功后,会先将数据同步给follower,当同步的节点超过半数,响应先交给follower,由它去响应客户端的请求(为什么leader不直接响应,因为客户端连接的是follower,最终响应结果得由follower完成)
7.2 生产集群安装多少zk合适
如果是 10 台服务器,需要部署多少台 Zookeeper?
安装奇数台生产经验:
10 台服务器: 3 台 zk
20 台服务器: 5 台 zk
100 台服务器: 11 台 zk
200 台服务器: 11 台 zk
服务器台数多:好处,提高可靠性;坏处:提高通信延时