这里我准备了4台虚拟机,从node1到node4,其myid也从1到4.
一,zk server的启动和选举
zk需要至少启动3台Server,按照配置的myid,选举出参与选举的myid最大的server为Leader。(与redis的master、slave不同,zk的叫leader、follower)。
如果已经选举成功,那么即使新加入的zk server的myid比现有leader的myid更大,也不会成为新的leader,除非现有的Leader挂掉,那么新加入的myid最大的zk server会成为leader。
我们通过zkServer.sh start-foreground
命令来前台阻塞的启动zk server,这样方便看到日志输出。注意,start 和-foreground之间是没有空格的。
启动后,看到如下日志,是已经启动成功。后面的日志是一些同步快照的日志。(前面有异常是因为其他zk server还没有起来)
二、zk cli的启动和基础命令
我们通过zkCLi.sh
,默认的连接Localhost,本机(node1,一个folloer)上的server,连上了follower,就连上了zk集群。你在follower上做的增改,会发送到leader那里,由leader广播给所有zk的follower。
进入客户端后,使用help
查看zk cli的命令。如下所示
[zk: localhost:2181(CONNECTED) 0] help
ZooKeeper -server host:port -client-configuration properties-file cmd args
addWatch [-m mode] path # optional mode is one of [PERSISTENT, PERSISTENT_RECURSIVE] - default is PERSISTENT_RECURSIVE
addauth scheme auth
close
config [-c] [-w] [-s]
connect host:port
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
delete [-v version] path
deleteall path [-b batch size]
delquota [-n|-b|-N|-B] path
get [-s] [-w] path
getAcl [-s] path
getAllChildrenNumber path
getEphemerals path
history
listquota path
ls [-s] [-w] [-R] path
printwatches on|off
quit
reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
redo cmdno
removewatches path [-c|-d|-a] [-l]
set [-s] [-v version] path data
setAcl [-s] [-v version] [-R] path acl
setquota -n|-b|-N|-B val path
stat [-w] path
sync path
version
whoami
刚进入zk,zk的根节点/下会有一个默认的节点zookeeper
[zk: localhost:2181(CONNECTED) 1] ls /
[zookeeper]
create
:当然你可以自己去在根节点下创建一个新的节点Node1。
在创建节点时,可以在节点名字后面跟上你想要赋予节点的信息,这里我跟了一个空字符串,在我这个版本3.7也可以将空串直接不写,存储的信息就是空。
[zk: localhost:2181(CONNECTED) 2] create /node1 ""
Created /node1
[zk: localhost:2181(CONNECTED) 3] ls /
[node1, zookeeper]
在node1下再创建一个节点node2
[zk: localhost:2181(CONNECTED) 4] create /node1/node2 ""
Created /node1/node2
set
:可以更改节点中的信息
[zk: localhost:2181(CONNECTED) 8] set /node1/node2 "this is node2"
get
:可以获取节点中的信息,带上-s可以在信息的下面带上节点的事务id等信息,效果同stat
命令。
[zk: localhost:2181(CONNECTED) 9] get /node1/node2
this is node2
stat
:获取节点的事务id、创建和修改时间等信息。
这些信息每个数据节点如/node1,每个数据节点都会有自己的一份。
其中,cZxid为创建节点事务id,开头的0x表示数据为16进制,x后的2表示集群leader为第几代(集群旧的leader挂掉,新的Leader上台,这个数就会+1)。2后面的8位16进制,表示节点创建的事务id,数值范围为从0~16^8。
类似的,mZxid为修改节点事务id,pZxid为该节点的子节点(或该节点)的最近一次创建 / 删除 的事务id。
//todo cZxid mZxid如何递增
[zk: localhost:2181(CONNECTED) 15] stat /node1
cZxid = 0x200000002
ctime = Fri Sep 22 00:12:28 PDT 2023
mZxid = 0x200000002
mtime = Fri Sep 22 00:12:28 PDT 2023
pZxid = 0x200000003
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
三,zk的三种节点
zk有三种节点,永久节点、临时节点、序列节点
永久节点
我们使用create命令,不加参数,创建的是第一种节点,即永久节点,这种节点不会随着客户端断开连接而消失,也没有序列号。
临时节点
使用create -e,创建的节点为临时节点。
这种节点会随着客户端断开连接,如果未在一定时间内重新连接,临时节点数据将会在集群内被清除。
假设客户端cli连接的server挂掉,导致客户端断开连接,如果客户端在一定时间内重新连接上,临时节点仍然不会被清除。这是因为客户端在连接server的时候,会产生一个会话session,客户端提示如下图所示。
session id = 0x1000014a34d0000, negotiated timeout = 30000
。
session id 0x1000014a34d0000将会被同步到整个集群(这次同步也会增加一次事务id),使得每个server都会持有这个客户端的session id。
negotiated timeout = 30000意为客户端断连后需要在3秒内重新连接,否则视为退出。
node1上的这台服务端也会有日志,session id会作为全局id提交给集群。
创建临时节点后,可以通过stat
命令结果中的ephemeralOwner
查看该临时节点的创建者的session id。
[zk: localhost:2181(CONNECTED) 23] create -e /node4 "this is node4"
Created /node4
[zk: localhost:2181(CONNECTED) 24] stat /node4
cZxid = 0x200000007
ctime = Fri Sep 22 01:03:39 PDT 2023
mZxid = 0x200000007
mtime = Fri Sep 22 01:03:39 PDT 2023
pZxid = 0x200000007
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x1000014a34d0000
dataLength = 13
numChildren = 0
毫无疑问,临时节点天然就可以用来表征其他集群中节点的状态。也很适合做分布式锁,抢占锁,就在某数据节点(如表示某系统的节点)如/DataCenter下创建一个固定名称如Lock临时节点,能创建,即为抢占到锁,且能通过stat查看到锁的主人是哪一台服务器。不能抢占,即为锁被抢占,要进行等待。
zk的分布式锁,相较于redis实现的分布式锁,要更简单。
因为redis想要不死锁,需要指定一个固定的过期时间,指定的过期时间长了,锁性能不高;指定过期时间短了,还没操作完锁就被释放了。
为此,redis分布式锁的实现者们需要考虑诸如引入守护线程或者线程池[1] (因为锁持有者释放锁后守护线程没用了就要被销毁,引入线程池减小线程开销)为锁自动续过期时间,例如Redission的WatchDog看门狗机制[1];而早期的setnx命令+expire命令不具备原子性,为此需要引入Lua脚本等等诸多问题[2](redis 2.7后的版本可以在set 命令上同时指定过期时间参数和nx参数)。
zk就不一样,zk的临时节点销毁由zk自己保证,leader挂掉后可在200毫秒内选举出新的Leader,且性能极高[3],使用者不需要考虑太多。
序列节点
使用create -s创建的节点为序列节点
。这种节点在创建节点如create -s node时,不会直接创建Node,而是会在node后面跟上一个从0开始的序列号,每创建一个,序列号就+1。即使这些序列节点都删除了,再创建序列节点,仍然会再之前的基础上再+1.
可以在某个节点如node下创建序列节点,可以实现一个类似于队列的特性,按照创建顺序来争抢代表了node的锁。
关于zk的暂时就先写这么多,后面有了再继续写或者补充。
参考文章:
[1],面试官:Redis 分布式锁如何自动续期?
[2],面试被问Redis锁🔒的缺点,被打击的扎心了
[3],ZooKeeper