1. zookeeper使用详解(命令、客户端、源码)
1.1. 前言
zookeeper我们常用来做分布式协调中间件,很多时候我们都接触不到它的原理和用法,我对他的了解也仅限于知道它可以做分布式协调、配置管理、分布式锁,并且有个watch节点监听常常能听到。接下来我要系统的学下zookeeper的功能和原理,一起走进zookeeper的世界
1.2. 概述
zookeeper主要目的就是为了分布式应用提供协同服务,zookeeper的节点管理机制,当节点发生变化时(创建、删除、数据变更),可以通知各个客户端,利用这种特性,zk的主要场景就如我前面说的:
1. 统一配置:把配置放在ZooKeeper的节点中维护,当配置变更时,客户端可以收到变更的通知,并应用最新的配置。2. 集群管理:集群中的节点,创建ephemeral的节点,一旦断开连接,ephemeral的节点会消失,其它的集群机器可以收到消息。3. 分布式锁:多个客户端发起节点创建操作,只有一个客户端创建成功,从而获得锁。
1.3. zk基本操作
- ZNode ZNode是ZK树形结构的一个节点,它可以包含或者不包含数据。
- ZK提供了如下API,用于操作ZNode。
- create path data
- delete path data
- exists path
- getdata path
- putdata path data
- getChildren path
- ZK客户端通过建立一个Session会话,来连接ZK服务,通过这些API来操作ZNode。
1.4. zk模式
- ZNode模式 目前ZNode包含持久模式和短暂模式ephemeral。
- ephemeral模式指的是这个节点在session断了之后就会消失。
- 持久模式和ephemeral模式外,ZNode还可以是有序的(自动分配自增ID到节点上,比如task-1,task-2,task-3)
- 因此ZNode一共有四种形态:
- 持久
- 持久有序
- ephemeral
- ephemeral有序
1.5. watch机制
- Watch和Notifications Watch可以避免每次主动去请求数据是否变化,而是当ZNode变化时,来通知。
- Watch是个一次性操作,每次收到通知后,必须重新watch,如果时间比较久或者ZNode更新频繁,在此时间之间,可能会有更新没有被通知到(还没来得急watch)。
- ZNode的创建、删除和修改都可以被watch到。
1.6. FAQ
1.6.1. 客户端对服务器列表的轮询机制
- 客户端建立与zk会话的时候需要我们填写zk服务器列表,之后该列表会被随机打散然后每次请求轮询该列表,这种打散是一次性的,之后每次都是这个顺序
- 知道客户端轮询原理,可以知道列表是可以重复填写的,这样也可以通过重复填写配置地址来增加权重,但也会有风险,可能使server切换耗时过长,倒是session_expired
参考 https://blog.51cto.com/nileader/932948
1.6.2. 客户端常遇到的浴场Connectionloss(连接断开)和Sessionexpired(session过期)处理
- 在ZooKeeper中,服务器和客户端之间维持的是一个长连接,在 SESSION_TIMEOUT 时间内,服务器会确定客户端是否正常连接(客户端会定时向服务器发送heart_beat),服务器重置下次SESSION_TIMEOUT时间。因此,在正常情况下,Session一直有效,并且zk集群所有机器上都保存这个Session信息。在出现问题情况下,客户端与服务器之间连接断了(客户端所连接的那台zk机器挂了,或是其它原因的网络闪断),这个时候客户端会主动在地址列表(初始化的时候传入构造方法的那个参数connectString)中选择新的地址进行连接。
- connectionloss通常发生在连接的zk挂了,这个时候只要等待客户端连接上新的zk机器(zk必须集群),然后确认操作是否执行成功
- sessionexpired通常发生在zk客户端和服务器的连接断了,视图连上新的zk机器,如果这个过程耗时过长,超过session_timeout时间,那么服务器认为这个session已经结束(服务器无法确认时因为其他异常原因还是客户端主动结束会话),开始清除和这个会话相关的信息,包括会话创建的临时节点和注册的watcher。这时客户端重新连接上服务器,服务器会报sessionexpired。这个时候解决办法要看业务情况了,只能重新实例化zk对象,重新操作节点数据
1.6.3. 创建的临时节点什么时候会被删除,是连接一断就删除吗?
- 连接断了之后,ZK不会马上移除临时数据,只有当SESSIONEXPIRED之后,才会把这个会话建立的临时数据移除。因此,用户需要谨慎设置Session_TimeOut
1.6.4. zk日志清理
- zk不会自动清理日志,参考:https://blog.51cto.com/nileader/932156
1.7. zkClient命令行(包含了全部命令)
1.7.1. 创建节点
- 语法
create [-s] [-e] path data acl
- -s 创建有序节点 -e创建临时节点
acl专门一节讲
[zk: localhost:2181(CONNECTED) 4] create -s -e /mynode/subnode hellpNode does not exist: /mynode/subnode[zk: localhost:2181(CONNECTED) 5] create -s -e /mynode/ hellp Node does not exist: /mynode/[zk: localhost:2181(CONNECTED) 6] create -s -e /mynode hellp Created /mynode0000000001[zk: localhost:2181(CONNECTED) 7] create -s -e /mynode/subnode helloNode does not exist: /mynode/subnode[zk: localhost:2181(CONNECTED) 8] ls /mynodeNode does not exist: /mynode[zk: localhost:2181(CONNECTED) 9] ls /[mycat, mynode0000000001, zookeeper][zk: localhost:2181(CONNECTED) 10] create -s -e /mynode0000000001/subnode helloEphemerals cannot have children: /mynode0000000001/subnode
- 上面的命令可以看出
- 第一不能直接创建多级几点
- 第二创建临时节点不能有子节点
- 第三有序节点节点名后会加上序号
1.7.2. 列出节点 ls
ls path [watch]
ls2 path [watch]
[zk: localhost:2181(CONNECTED) 17] ls2 /persistence[]cZxid = 0x12ctime = Tue Mar 26 06:52:28 GMT 2019mZxid = 0x12mtime = Tue Mar 26 06:52:28 GMT 2019pZxid = 0x12cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x0dataLength = 3numChildren = 0[zk: localhost:2181(CONNECTED) 18] ls /persistence []
- 可以看出,ls2能给出更详细的路径信息
[zk: localhost:2181(CONNECTED) 2] ls / 1[persistence, temporary, mycat, zookeeper][zk: localhost:2181(CONNECTED) 3] create -e /temp 123WATCHER::WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/Created /temp[zk: localhost:2181(CONNECTED) 4] create -e /temp2 123Created /temp2
- 根节点创建监听器1,之后根节点的改变会触发监听器,但只有一次
1.7.3. 获取节点信息 get
get path [watch]
[zk: localhost:2181(CONNECTED) 5] get /temp2123cZxid = 0x17ctime = Tue Mar 26 06:59:20 GMT 2019mZxid = 0x17mtime = Tue Mar 26 06:59:20 GMT 2019pZxid = 0x17cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x103c831d4dc0003dataLength = 3numChildren = 0
- 可以看到,get到路径有详细信息,和ls2获得的信息一样
- 每一个对znode树的更新操作,都会被赋予一个全局唯一的ID,我们称之为zxid(ZooKeeper Transaction ID)
- 更新操作的ID按照发生的时间顺序升序排序。例如,z1大于z2,那么z1的操作就早于z2操作。
- 每个 znode 的状态信息包含以下内容:
- czxid,创建(create)该 znode 的 zxid
- mzxid,最后一次修改(modify)该 znode 的 zxid
- pzxid,最后一次修改该 znode 子节点的 zxid
- ctime,创建该 znode 的时间
- mtime,最后一次修改该 znode 的时间
- dataVersion,该节点内容的版本,每次修改内容,版本都会增加
- cversion,该节点子节点的版本
- aclVersion,该节点的 ACL 版本
- ephemeralOwner,如果该节点是临时节点(ephemeral node),会列出该节点所在客户端的 session id;如果不是临时节点,该值为 0
- dataLength,该节点存储的数据长度
- numChildren,该节点子节点的个数
1.7.4. 检查状态 stat
stat path [watch]
- 与 get 的区别是,不会列出 znode 的值。
1.7.5. 修改节点 set
set path data [version]
- 修改已经存在的节点的值
[zk: localhost:2181(CONNECTED) 10] set /temp2 456cZxid = 0x17ctime = Tue Mar 26 06:59:20 GMT 2019mZxid = 0x18mtime = Tue Mar 26 07:12:27 GMT 2019pZxid = 0x17cversion = 0dataVersion = 1aclVersion = 0ephemeralOwner = 0x103c831d4dc0003dataLength = 3numChildren = 0
- 可以看到,在修改节点值之后,mZxid、mtime、dataVersion 都发生了变化。
1.7.6. 删除节点 rmr
rmr path
[zk: localhost:2181(CONNECTED) 12] rmr /temp2[zk: localhost:2181(CONNECTED) 13] get /temp2Node does not exist: /temp2
- 删除 /mynode,不会返回任何内容。如果有子节点的时候,连带子节点也一起删除。
1.7.7. 删除节点 delete
delete path [version]
- 调用delete和set操作时,如果指定znode版本号,需要与当前的版本号匹配。如果版本号不匹配,操作将会失败。失败的原因可能是在我们提交之前,该znode已经被修改过了,版本号发生了增量变化。如果不指定版本号,就是直接操作最新版本的 znode。
- 如果要删除的节点有子节点,不能删除
[zk: localhost:2181(CONNECTED) 17] create /per 1Created /per[zk: localhost:2181(CONNECTED) 18] create /per/subper 2Created /per/subper[zk: localhost:2181(CONNECTED) 19] delete /perNode not empty: /per[zk: localhost:2181(CONNECTED) 20] delete /per/subper[zk: localhost:2181(CONNECTED) 21] delete /per[zk: localhost:2181(CONNECTED) 22] ls /perNode does not exist: /per
1.7.8. 历史记录 history
history
列出最近的10条历史记录
[zk: localhost:2181(CONNECTED) 23] history13 - get /temp214 - ls /15 - ls /temp16 - get /temp17 - create /per 118 - create /per/subper 219 - delete /per20 - delete /per/subper21 - delete /per22 - ls /per23 - history
1.7.9. 重复之前的命令 redo
redo cmdno
根据 cmdno 重复之前的命令,cmdno 就是方括号里面最后的数字,每次执行命令都会自增。
[zk: localhost:2181(CONNECTED) 25] redo 22Node does not exist: /per[zk: localhost:2181(CONNECTED) 26] redo 17Created /per
1.7.10. 是否输出 watch 事件(printwatches)
printwatches on|off
[zk: localhost:2181(CONNECTED) 28] printwatchesprintwatches is on[zk: localhost:2181(CONNECTED) 29] ls /mynodeNode does not exist: /mynode[zk: localhost:2181(CONNECTED) 30] create /mynode 123Created /mynode[zk: localhost:2181(CONNECTED) 31] ls /mynode watch[][zk: localhost:2181(CONNECTED) 34] create /mynode/subnode 234WATCHER::WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mynodeCreated /mynode/subnode[zk: localhost:2181(CONNECTED) 35] printwatches off[zk: localhost:2181(CONNECTED) 36] ls /mynode 2[subnode][zk: localhost:2181(CONNECTED) 37] create /mynode/subnode2 567Created /mynode/subnode2
- 可以看到设置off后,监听打印输出就没有了
1.7.11. 关闭连接 close
close
[zk: localhost:2181(CONNECTED) 38] close 2019-03-26 07:26:59,240 [myid:] - INFO [main:ZooKeeper@693] - Session: 0x103c831d4dc0003 closed[zk: localhost:2181(CLOSED) 39] 2019-03-26 07:26:59,241 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@522] - EventThread shut down for session: 0x103c831d4dc0003lsNot connected[zk: localhost:2181(CLOSED) 40] ls /Not connected
1.7.12. 打开连接 connect
connect host:port
[zk: localhost:2181(CLOSED) 42] connect2019-03-26 07:28:18,093 [myid:] - INFO [main:ZooKeeper@442] - Initiating client connection, connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@782830e[zk: localhost:2181(CONNECTING) 43] 2019-03-26 07:28:18,096 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1029] - Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)2019-03-26 07:28:18,097 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@879] - Socket connection established to localhost/127.0.0.1:2181, initiating session2019-03-26 07:28:18,100 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1303] - Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x103c831d4dc0004, negotiated timeout = 30000[zk: localhost:2181(CONNECTED) 43] ls /[mycat, mynode, zookeeper, persistence, per]
- 可以看到默认连接的是本地的2181
1.7.13. 退出连接 quit
quit
直接退出当前的 zkCli 命令行。
1.7.14. 强制同步 sync
sync path
- sync方法会强制客户端所连接的服务器状态与leader的状态同步,这样在读取 path 的值就是最新的值了
1.7.15. ACL 操作
- 一个znode中不仅包含了存储的数据,还有 ACL(Access Control List)。znode的创建时,可以给它设置一个ACL(Access Control List),来决定谁可以对znode做哪些操作。
- ACL 具有以下特点:
- ZooKeeper的权限控制是基于每个znode节点的,需要对每个节点设置权限
- 每个znode支持设置多种权限控制方案和多个权限
- 子节点不会继承父节点的权限,客户端无权访问某节点,但可能可以访问它的子节点
- 所以任何一个客户端都可以通过exists 操作来获得任何znode的状态,从而得知znode是否真的存在。
1.7.16. ACL Permissions
ACL 权限 | ACL 简写 | 允许的操作 |
---|---|---|
CREATE | c | 创建子节点 |
READ | r | 获取节点的数据和它的子节点 |
WRITE | w | 设置节点的数据 |
DELETE | d | 删除子节点 (仅下一级节点) |
ADMIN | a | 设置 ACL 权限 |
1.7.17. 权限相关命令
命令 | 语法 | 描述 |
---|---|---|
getAcl | getAcl path | 读取ACL权限 |
setAcl | setAcl path acl | 设置ACL权限 |
addauth | addauth scheme auth | 添加认证用户 |
create | create [-s] [-e] path data acl | 创建节点时指明 ACL 权限 |
1.7.18. ACL Schemes
- ZooKeeper内置了一些权限控制方案,可以用以下方案为每个节点设置权限:
方案 | 描述 |
---|---|
world | 只有一个用户:anyone,代表所有人(默认) |
ip | 使用IP地址认证 |
auth | 使用已添加认证的用户认证 |
digest | 使用“用户名:密码”方式认证 |
- ACL是由鉴权方式、鉴权方式的ID和一个许可(permession)的集合组成。例如,我们想通过一个ip地址为10.0.0.1的客户端访问一个znode。那么,我们需要为znode设置一个ACL,鉴权方式使用IP鉴权方式,鉴权方式的ID为10.0.0.1,只允许读权限。那么 ACL 的格式就是:ip:10.0.0.1:w
1.7.18.1. world 方案
setAcl <path> world:anyone:<acl>
[zk: localhost:2181(CONNECTED) 7] getAcl /world'world,'anyone: cdrwa[zk: localhost:2181(CONNECTED) 8] setAcl /world world:anyone:cdrcZxid = 0x27ctime = Tue Mar 26 07:41:43 GMT 2019mZxid = 0x27mtime = Tue Mar 26 07:41:43 GMT 2019pZxid = 0x27cversion = 0dataVersion = 0aclVersion = 1ephemeralOwner = 0x0dataLength = 1numChildren = 0[zk: localhost:2181(CONNECTED) 9] set /world 234Authentication is not valid : /world
- 可以看出,在修改权限为 cdr 之后,不能再设置节点数据了。注意 aclVersion 也发生了变化。
1.7.18.2. IP 方案
setAcl <path> ip:<ip>:<acl>
[zk: localhost:2181(CONNECTED) 10] create /ip helloCreated /ip[zk: localhost:2181(CONNECTED) 11] getAcl /ip 'world,'anyone: cdrwa[zk: localhost:2181(CONNECTED) 12] setAcl /ip ip:52.231.163.100:cdrwacZxid = 0x2actime = Tue Mar 26 07:56:21 GMT 2019mZxid = 0x2amtime = Tue Mar 26 07:56:21 GMT 2019pZxid = 0x2acversion = 0dataVersion = 0aclVersion = 1ephemeralOwner = 0x0dataLength = 5numChildren = 0[zk: localhost:2181(CONNECTED) 13] getAcl /ip 'ip,'52.231.163.100: cdrwa[zk: localhost:2181(CONNECTED) 14] get /ipAuthentication is not valid : /ip
1.7.18.3. auth 方案
addauth digest: #添加认证用户setAcl auth: :
[zk: localhost:2181(CONNECTED) 15] create /auth helloCreated /auth[zk: localhost:2181(CONNECTED) 16] addauth digest admin:tom[zk: localhost:2181(CONNECTED) 17] setAcl /auth auth:tom:cdrwacZxid = 0x2cctime = Tue Mar 26 08:04:23 GMT 2019mZxid = 0x2cmtime = Tue Mar 26 08:04:23 GMT 2019pZxid = 0x2ccversion = 0dataVersion = 0aclVersion = 1ephemeralOwner = 0x0dataLength = 5numChildren = 0[zk: localhost:2181(CONNECTED) 18] getAcl /auth'digest,'admin:cFk4QI8k/ZVgHVEnb06Vtoc651o=: cdrwa
断开以后再连上,需要重新认证
WatchedEvent state:SyncConnected type:None path:null[zk: localhost:2181(CONNECTED) 0] get /authAuthentication is not valid : /auth
1.7.18.4. digest 方案
setAcl <path> digest:<user>:<password>:<acl>
这里的密码是经过SHA1及BASE64处理的密文,在SHELL中可以通过以下命令计算:
echo -n : | openssl dgst -binary -sha1 | openssl base64
[root@izbp1itlw36onyj4m9b4hiz ~]# echo -n admin:123456 | openssl dgst -binary -sha1 | openssl base640uek/hZ/V9fgiM35b0Z2226acMQ=
[zk: localhost:2181(CONNECTED) 21] create /digest helloCreated /digest[zk: localhost:2181(CONNECTED) 22] setAcl /digest digest:admin:0uek/hZ/V9fgiM35b0Z2226acMQ=:cdrw cZxid = 0x39ctime = Tue Mar 26 08:22:04 GMT 2019mZxid = 0x39mtime = Tue Mar 26 08:22:04 GMT 2019pZxid = 0x39cversion = 0dataVersion = 0aclVersion = 1ephemeralOwner = 0x0dataLength = 5numChildren = 0[zk: localhost:2181(CONNECTED) 23] getAcl /digest'digest,'admin:0uek/hZ/V9fgiM35b0Z2226acMQ=: cdrw[zk: localhost:2181(CONNECTED) 24] get /digestAuthentication is not valid : /digest[zk: localhost:2181(CONNECTED) 25] addauth digest admin:123456[zk: localhost:2181(CONNECTED) 26] get /digesthellocZxid = 0x39ctime = Tue Mar 26 08:22:04 GMT 2019mZxid = 0x39mtime = Tue Mar 26 08:22:04 GMT 2019pZxid = 0x39cversion = 0dataVersion = 0aclVersion = 1ephemeralOwner = 0x0dataLength = 5numChildren = 0
1.7.18.5. 创建节点时指定 ACL
[zk: localhost:2181(CONNECTED) 27] addauth digest admin:tim[zk: localhost:2181(CONNECTED) 28] create /createnode hello auth:tim:cdrwa Created /createnode[zk: localhost:2181(CONNECTED) 29] getAcl[zk: localhost:2181(CONNECTED) 30] getAcl /createnode'digest,'admin:0uek/hZ/V9fgiM35b0Z2226acMQ=: cdrwa'digest,'admin:H4JbicQawMpoqvA2LI0LFNFSMNE=: cdrwa[zk: localhost:2181(CONNECTED) 31] get /createnodehellocZxid = 0x3bctime = Tue Mar 26 08:27:27 GMT 2019mZxid = 0x3bmtime = Tue Mar 26 08:27:27 GMT 2019pZxid = 0x3bcversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x0dataLength = 5numChildren = 0[zk: localhost:2181(CONNECTED) 32] close[zk: localhost:2181(CLOSED) 33] connect[zk: localhost:2181(CONNECTED) 34] get /createnodeAuthentication is not valid : /createnode[zk: localhost:2181(CONNECTED) 35] getAcl /createnode'digest,'admin:0uek/hZ/V9fgiM35b0Z2226acMQ=: cdrwa'digest,'admin:H4JbicQawMpoqvA2LI0LFNFSMNE=: cdrwa[zk: localhost:2181(CONNECTED) 36] addauth digest admin:tim[zk: localhost:2181(CONNECTED) 37] get /createnodehellocZxid = 0x3bctime = Tue Mar 26 08:27:27 GMT 2019mZxid = 0x3bmtime = Tue Mar 26 08:27:27 GMT 2019pZxid = 0x3bcversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x0dataLength = 5numChildren = 0
- 关闭会话后需要重新授权
1.7.19. zookeeper quota
- zookeeper quota 机制支持节点个数(namespace)和空间大小(bytes)的设置。
- zookeeper quota 保存在 /zookeeper/quota 节点下,可以设置该节点的 ACL 权限,以防其他人修改。
- 语法
listquota path
、setquota -n|-b val path
、delquota [-n|-b] path
- -n表示设置znode count限制,包含自身节点
[zk: localhost:2181(CONNECTED) 10] ls /zookeeper/quota[][zk: localhost:2181(CONNECTED) 11] get /zookeeper/quotacZxid = 0x0ctime = Thu Jan 01 00:00:00 GMT 1970mZxid = 0x0mtime = Thu Jan 01 00:00:00 GMT 1970pZxid = 0x0cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x0dataLength = 0numChildren = 0[zk: localhost:2181(CONNECTED) 12] listquota /persistenceabsolute path is /zookeeper/quota/persistence/zookeeper_limitsquota for /persistence does not exist.[zk: localhost:2181(CONNECTED) 13] setquota -n 3 /persistenceComment: the parts are option -n val 3 path /persistence[zk: localhost:2181(CONNECTED) 14] create /persistence/node1 123Created /persistence/node1[zk: localhost:2181(CONNECTED) 15] create /persistence/node2 124Created /persistence/node2[zk: localhost:2181(CONNECTED) 16] create /persistence/node3 125Created /persistence/node3[zk: localhost:2181(CONNECTED) 17] listquota /persistenceabsolute path is /zookeeper/quota/persistence/zookeeper_limitsOutput quota for /persistence count=3,bytes=-1Output stat for /persistence count=4,bytes=12
- 可以看到,超出节点数zk也不会报错,只会在listquato里打下日志
[zk: localhost:2181(CONNECTED) 18] delquota -n /persistence[zk: localhost:2181(CONNECTED) 19] listquota /persistence absolute path is /zookeeper/quota/persistence/zookeeper_limitsOutput quota for /persistence count=-1,bytes=-1Output stat for /persistence count=4,bytes=12
- 删除节点quota,count变成-1
1.8. java操作
1.8.1. 异步操作
主流异步操作
参考 https://github.com/llohellohe/zookeeper/blob/master/src/main/java/yangqi/zookeeper/example/masterworker/AsynMaster.java
1.8.2. 状态变更
- Watcher优势 通过watcher,可以避免主动轮询导致的额外负担,更加实时和有效率。
- Watcher接口,仅有一个实现接口
public void process(WatchedEvent event)
- WatchedEvent代表watcher到的事件,它包含发生了什么事件,ZooKeeper的当前连接状态以及产生事件的ZNode路径。
- KeeperState
- EventType
- path
- KeeperState包含Disconnected\SyncConnected\AuthFailed\ConnectedReadOnly\SaslAuthenticated\Expired等6种状态。
- EventType包含五种状态:
- None
- NodeCreated
- NodeDeleted
- NodeDataChanged
- NodeChildrenChanged
- 其中后四种用于表示ZNode的状态或者数据变更,而None则用于会话的状态变更。
- EventType为None的Watch SessionWatch实例描述了,初始化一个ZooKeeper实例时注册的Watcher接口
- 将在连接时收到EventType为None,KeeperState为SyncConnected,path为null的Event
- 将在失去连接时收到EventType为None,KeeperState为:Disconnected,path为null的Event
- ChildrenCallback 通过getChildren方法,可以设置ChildrenCallback,以便获得获得当子节点发生变化时的相关信息。
- ChildrenCallback 的唯一接口:
public void processResult(int rc, String path, Object ctx, List
children) - getChildren可以设置对应的Watcher,一旦发现节点的事件类型为NodeChildrenChanged后,可以继续设置watch
- 例子:https://github.com/llohellohe/llohellohe.github.com/blob/master/readers/ZooKeeper/04-%E7%8A%B6%E6%80%81%E5%8F%98%E6%9B%B4.md
- 上述这篇例子我用zookeeper3.4.13运行会报NullPointException,我dubug后发现,该版本zookeeper在进入链接刚建立状态(None)时,会添加默认defaultWatcher,而示例代码初始化时,watch填了null,导致了后续,从set中得到的watcher为null,我认为这是个bug,初始化连接不放watch应该也是允许的。
@Override public Setmaterialize(Watcher.Event.KeeperState state, Watcher.Event.EventType type, String clientPath) { Set result = new HashSet (); switch (type) { case None://初始化连接时进入 result.add(defaultWatcher);//defaultWatcher如果填空,后续会报错
private void processEvent(Object event) { try { if (event instanceof WatcherSetEventPair) { // each watcher will process the event WatcherSetEventPair pair = (WatcherSetEventPair) event; for (Watcher watcher : pair.watchers) { try { watcher.process(pair.event);//watcher为null
- processEvent方法就是EvenetThread线程处理watcher监听的地方
1.8.3. Curator
- Curator是构建在ZooKeeper上的API,它屏蔽一些复杂的ZooKeeper操作,并提供了一些扩展。
1.8.3.1. 流式API
- 一般的ZooKeeper创建节点的代码如下:
zk.create("/mypath", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
1.8.3.2. 使用Curator后
zkc.create().withMode(CreateMode.PERSISTENT).forPath("/mypath", new byte[0]);
1.8.3.3. leader latch 和leader selection
- 通过Curator可以方便的进行leader的选举和控制
1.8.3.4. Leader Elections
在ZK集群中,Leader的作用是保证变更操作(create\setData\delete)的顺序性。
它将接收到的请求转换成事务,然后提议followers按照顺序应用这些事务。在初始阶段,所有的ZK服务端都处于LOOKING状态,要么找到已经存在Leader结点,要么自己选举出Leader。成为Leader的节点将进入LEADING状态,其它则将进入FOLLOWING状态。1.8.3.5. 选举过程
- 进入LOOKING状态的server将广播消息,称为vote。
- vote包含serverId和ZXID,比如(1,5)表示server id为1的服务端,它的ZXID为5。
- 每个server将比较自己的vote和收到的vote,如果:
- 收到vote的zxid大于自己的,则使用这个vote
- 如果zxid相等,则sid大的获胜
- 当推选超过半数以上,则确定leader
1.9. 源码分析
1.9.1. 客户端组成
- Zookeeper客户端主要由如下核心部件构成。
- Zookeeper实例,客户端入口。
- ClientWatchManager, 客户端Watcher管理器。
- HostProvider,客户端地址列表管理器。
- ClientCnxn,客户端核心线程,内部包含了SendThread和EventThread两个线程,SendThread为I/O线程,主要负责Zookeeper客户端和服务器之间的网络I/O通信;EventThread为事件线程,主要负责对服务端事件进行处理。
1.9.2. 初始化
- 客户端在初始化和启动过程中大体可以分为如下3个步骤
- 设置默认Watcher
- 设置Zookeeper服务器地址列表
- 创建ClientCnxn。
- 若在Zookeeper构造方法中传入Watcher对象时,那么Zookeeper就会将该Watcher对象保存在ZKWatcherManager的defaultWatcher中,并作为整个客户端会话期间的默认Watcher。
1.9.3. 会话的创建
- 客户端与服务端会话建立的整个过程,包括初始化阶段(第一阶段)、会话创建阶段(第二阶段)、响应处理阶段(第三阶段)三个阶段。 细节:
1.9.4. 服务器地址列表
- 在实例化Zookeeper时,用户传入Zookeeper服务器地址列表,如192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181
- Chroot:客户端可以设置自己的命名空间,若客户端设置了Chroot,此时,该客户端对服务器的任何操作都将被限制在自己的命名空间下,如设置Choot为/app/X,那么该客户端的所有节点路径都是以/app/X为根节点。具体设置可以使输入地址列表的时候加上
192.168.0.1:2181/app/X
- 地址列表管理:Zookeeper使用StaticHostProvider打散服务器地址(shuffle),并将服务器地址形成一个环形循环队列,然后再依次取出服务器地址。
- Chroot:客户端可以设置自己的命名空间,若客户端设置了Chroot,此时,该客户端对服务器的任何操作都将被限制在自己的命名空间下,如设置Choot为/app/X,那么该客户端的所有节点路径都是以/app/X为根节点。具体设置可以使输入地址列表的时候加上
1.10. 总结
- zookeeper比较重要的概念就是选主算法,这里尽可能简单的简述一下,分两类:
- 全新集群选主:按照服务器启动顺序,判断server_id大小,根据过半选举的规则选主;比如服务器1-5对应server_id1-5,按顺序启动,每启动一台会有个选主过程,服务器会交换选主信息,id大的胜出,启动2台时,由于没有超过半数以上的机器,所以继续保持LOOKING,当第三台机器启动,id最大,且选票结果超过半数,则确定leader为server_id=3的机器,后续4、5启动,由于已经存在leader,只能当following
- 非全新集群选主:这种情况说明zookeeper集群leader机器宕机,需要重新选举,需要根据数据的票选轮数epoch、zxid和server_id判断,先进行选举信息的交换,票选轮数小的忽略,zxid大的胜出,zxid相同情况看server_id,server_id大的胜出
- zookeeper3.4.13查看源码可以发现,
protected int electionAlg = 3;
case 3: qcm = createCnxnManager(); QuorumCnxManager.Listener listener = qcm.listener; if(listener != null){ listener.start(); le = new FastLeaderElection(this, qcm); } else { LOG.error("Null listener when initializing cnx manager"); } break;
- 也就是说zk默认使用的是FastLeaderElection选举算法,参考
参考
https://blog.csdn.net/feixiang2039/article/details/79810102#zookeeper-%E5%91%BD%E4%BB%A4 zookeeper 客户端 zkCli 命令详解
https://github.com/llohellohe/llohellohe.github.com/blob/master/readers/ZooKeeper llohellohe/llohellohe.github.com
http://www.cnblogs.com/leesf456/p/6098255.html Zookeeper客户端
https://blog.csdn.net/cxhzqhzq/article/details/6568040 Zookeeper全解析——Paxos作为灵魂
http://blog.chinaunix.net/uid-26726125-id-4038581.html zookeeper跟经典paxos的对比(附源码)
https://blog.csdn.net/panxj856856/article/details/80403487 FastLeaderElection选举算法