zookeeper02-了解ZooKeeper
1、ZooKeeper基础
- 很多用于协作的原语常常在很多应用之间共享。因此,设计一个用于协作的服务的方法往往是提供原语列表,暴露出每个原语的实例化调用方法,并直接操作这些实例。比如,我们可以说分布式锁组成了一个重要的原语,暴露出创建(create)、获取(acquire)和释放(release)锁的三个调用方法。
- ZooKeeper另辟蹊径。ZooKeeper并不直接暴露原语,相反,它暴露了一个类似于文件系统的API(由一些方法组成),使应用程序能够实现它们自己的原语。我们通常使用方法(recipes)来表示这些原语的实现。应用程序调用这些方法来操作和维护ZooKeeper上的多个小型的数据节点,这些节点被称为znode,并采用类似于文件系统的树状层级结构进行管理。
- 图2-1描述了一个znode树的结构,根znode(节点)包含4个子znode(节点),其中三个子znode拥有下一级子znode,叶子znode存储了数据信息。
- znode如果没有数据常常表达了重要的信息。比如,在主-从模式的例子中,如果/master znode没有数据,表示主-从应用还没有选举出主节点。
- 而图2-1中涉及的一些其他znode,在主-从模式的配置中非常有用:
- /workers:其下每个子znode保存了系统中一个可用从节点信息。如图2-1所示,有一个从节点(foot.com:2181)。如果一个从节点变得不可用,它对应znode应该从/workers中移除。
- /tasks:其下每个子znode保存了所有已经创建并等待从节点执行的任务的信息,主-从应用的客户端在/tasks下添加一个子znode,用来表示一个新任务,并等待执行。
- /assign:其下每个子znode保存了分配到某个从节点的任务信息,当主节点分配给从节点一个任务,就会在/assign下增加一个znode。
1.1、API概述
- znodes可能包含数据也可能不包含。如果znode包含数据,该数据将以字节数组的形式存储。字节数组的确切格式是特定于每个应用程序,并且ZooKeeper不直接提供解析它的支持。我们可以使用如Protocol Buffers、Thrift、Avro或MessagePack等序列化(Serialization)包来处理保存于znode中的数据格式,不过有些时候,使用UTF-8或ASCI编码的字符串就可以了。
- ZooKeeper API提供如下操作:
- create /path data:创建一个名为/path并包含数据的znode(节点)。
- delete /path:删除/path znode。
- exists /path:检查/path是否存在。
- setData /path data:设置/path的数据为data。
- getData /path:返回/path中的数据。
- getChildren /path:返回/path下的子znode列表。
- 需要注意的是,ZooKeeper不允许对znode中保存的数据进行部分读写。当设置或读取znode中的数据时,znode中的内容将被完全替换或读取。
- ZooKeeper客户端要连接到ZooKeeper服务,是通过API调用来建立会话(session)。
1.2、znode的不同类型
- 创建znode时,还需要指定该znode的类型(mode),类型决定了znode的行为方式。
- znode一共有4种类型:持久的(persistent)、临时的(ephemeral)、持久有序的(persistent-sequential)和临时有序的(ephemeralsequential)。
1.2.1、持久节点和临时节点
- znode可以是持久的(persistent),也可以是临时的(ephemeral)。
- 持久的znode,只能通过调用delete来进行删除。
- 临时的znode,当创建该znode的客户端崩溃或关闭了与ZooKeeper的连接时,这个znode就会被删除。
- 持久znode是一种非常有用的,可以通过持久znode为应用保存一些数据,即这个znode的创建者不再属于应用系统时,保存于这个znode的数据也可以保存下来而不丢失。例如,在主-从模式中,即使分配任务的主节点已经崩溃了,也需要保存从节点的任务分配情况。
- 临时znode传递关于应用程序的某些方面的信息,这些信息只在其创建者的会话有效时才存在。例如,在主-从模式中,当一个节点创建一个名为/master的临时znode时,该znode的存在意味着系统现在有一个主节点了,且该主节点处于正常运行中。如果主节点崩溃后,该znode仍然存在,那么系统将无法监测到主节点已经崩溃。因此这个znode需要和主节点一起消失,这样就可以阻止系统继续运行。从节点也可以使用临时的znode,如果一个从节点失效,它的会话将会过期,并且它在/workers中的子znode将自动消失。
- 一个临时znode,在以下两种情况下将会被删除:
- 当创建该znode的客户端的会话因超时或主动关闭而中止时。
- 当某个客户端(不一定是创建者)主动删除该znode时。
- 因为临时的znode在其创建者的会话过期时被删除,所以我们现在不允许临时znode拥有子znode。
1.2.2、有序节点
- znode还可以设置为有序的(sequential) 。一个有序znode被分配一个唯一的单调递增的整数。当创建有序znode时,一个序号会被追加到路径之后。例如,如果一个客户端创建了一个有序znode,其路径为/tasks/task-,那么Zookeeper将会为其分配一个数字序号并追加到其路径之后,例如1,最后该znode为/tasks/task-1。
- 通过这种简单的方式,为有序znode提供唯一的名称,同时也可以直观地查看有序znode的创建顺序。
1.3、监视与通知
- 因为ZooKeeper通常是作为远程服务被访问,所以客户端每次访问znode时,客户端都需要获得znode中的内容,这样的代价就非常大:这会导致更高的延迟和更多的操作。以图2-2为例,第二次调用getChildren /tasks,返回了相同的值(一个空的集合),其实这是没有必要的。
- 为了取代客户端轮询,我们选择了基于通知(notification)的机制:客户端向ZooKeeper注册监视znode变化的监视点(watch),znode有任何变化就会触发监视点,然后通知客户端。监视点是一个单次触发的操作,即一个监视点只会触发一个通知。为了接收多个通知,客户端必须在每次收到通知后重新设置一个监视点。如图2-3所示,当节点/tasks发生变化时,客户端会收到一个通知,并从ZooKeeper读取一个新值。
- 因为监视点是单次触发操作,所以在客户端接收到关于znode的通知和设置新监视之间,znode可能会发生新的更改(但不要担心,您不会错过对状态的更改)。
- 让我们看一个简单的例子,看看它是如何工作的。假设以下事件按此顺序发生:
- (1)客户端C1设置监视点来监控/tasks数据的变化。
- (2)客户端C2连接后,向/tasks中添加了一个新的任务。
- (3)客户端C1接收到/tasks有变化的通知。
- (4)客户端C1设置新的监视点,在设置完成前,第三个客户端C3连接后,向/tasks中添加了一个新的任务。
- 客户端C1最终设置了新的监视点,但是C3所做的更改没有触发任何通知。为了观察C3所做的变更,实际上在C1设置新的监视点前需要读取/tasks节点的状态,通过在设置监视点前读取ZooKeeper的状态,C1就不会错过任何变更。
- 通知机制的一个重要保障是,在对同一个znode进行任何其他更改之前,将先通知客户端。
- 例如,如果客户端对一个znode设置了监视点,并且该znode发生了两个连续更新。在第一次更新后,第二次更新前,客户端将收到通知,然后读取znode中的数据。(即在第一次变更后,会先通知客户端,然后再做第二次变更)
- 我们要满足的关键点是保证客户端观察到更新顺序。尽管ZooKeeper状态的变化最终会传播到其他给定的客户端(不论快慢),但我们保证客户端可以根据全局顺序观察到ZooKeeper状态的变化。
- ZooKeeper可以定义不同类型的监视点,不同类型的监视点有不同的通知。
- 客户端可以设置多种监视点,如监视znode的数据变化、监视znode的子znode的变化、监视znode的创建或删除。
- 为了设置监视点,可以使用API中的任何方法来读取Zookeeper的状态,在使用这些API方法时,传入一个watcher对象或使用默认的watcher对象。
1.4、znode的版本号
- 每一个znode都有一个版本号,该版本号随着每次数据更改而递增。
- API中的两个操作是有条件地执行:setData和delete。
- 这两个调用都以znode的版本号作为参数,只有当客户端传递的版本与服务器上的版本号一致时,调用才会成功。
- 当多个ZooKeeper客户端对同一个znode进行操作时,版本的使用就会显得尤为重要。
- 例如,假设客户端C1对/config写入了一些配置信息,如果另一个客户端C2同时更新这个znode,此时C1的版本号已经过期,C1调用setData一定不会成功。
- C1使用的版本不匹配,操作失败,如图2-4所示。
2、ZooKeeper架构
- 我们已经在一个较高的层次上讨论了ZooKeeper向应用程序公开的操作,我们需要更多地了解该服务的实际工作方式。应用程序通过客户端库调用ZooKeeper。客户端库负责与ZooKeeper服务器的交互。
- 图2-5展示了客户端与服务器端之间的关系。每个客户端导入客户端库之后,就可以与任意一个ZooKeeper节点通信。
- ZooKeeper服务器有两种运行模式:独立模式((standalone)和仲裁模式(quorum)。
- 独立模式:只有一个服务器,ZooKeeper状态不会被复制。
- 仲裁模式:有一组ZooKeeper服务器,我们称之为ZooKeeper集群,它们之间可以进行状态的复制,并一起服务客户端请求。
2.1、ZooKeeper仲裁模式
- 在仲裁模式下,ZooKeeper可以在集群中的所有服务器上复制数据树。但如果让一个客户端等待每个服务器完成数据保存后再继续,那么延迟问题将是不可接受的。
- 在公共管理领域,法定人数是指进行一项投票所需的投票人的最少数量。而在ZooKeeper中,它是为了使ZooKeeper能够正常工作而必须运行并可用的服务器的最小数量。这个数量也是在告知客户端数据已被安全存储之前,必须存储客户端数据的服务器的最小数量。
- 例如,我们一共有5个ZooKeeper服务器,但法定人数为3个,只要任意3个服务器保存了数据,客户端就可以继续,而其他两个服务器最终也将捕获到数据,并保存数据。
- 选择适当大小的法定人数很重要。法定人数的数量需要保证不管系统发生延迟或崩溃,服务主动确认的任何更新请求都将持续存在,直到另一个请求代替它。
- 为了明白这到底是什么意思,让我们先来通过一个例子来看看,如果法定人数太小,将会出现什么问题。假设有5个服务器并设置法定人数为2,如果有两台服务器s1和s2确认它们已经复制了一个创建/z znode的请求,并回复客户端,但是在服务器s1和s2将新的znode复制到其他服务器之前,这两台服务器与其他服务器、客户端发生了长时间的分区隔离,此时ZooKeeper服务的状态仍然正常,因为法定人数为2,而现在还有3个服务器,但这3个服务器将无法发现新的/z znode。因此,对创建节点/z的请求是非持久化的。(如果s1和s2仅是与另外3台服务器发生了网络分区,但与客户端通信正常,此时的ZooKeeper服务就是发生了脑裂)
- 通过使用多数方案,可以避免上面的问题,法定人数要大于所有服务器数量的一半,也就是我们能够容忍小于1/2数量的服务器崩溃。
- 例如,如果我们有5个服务器,我们可以容忍2台服务器崩溃。
- 服务器的数量并不一定是奇数,但偶数实际上会使系统更加脆弱。(建议使用奇数个服务器)
2.2、会话
- 会话的概念对于ZooKeeper的运行是非常重要。
- 在对ZooKeeper执行任何请求之前,客户端必须与Zookeeper服务建立会话。客户端提交给ZooKeeper的所有操作都会关联到一个会话。当会话因某种原因结束时,在该会话期间创建的临时节点将消失。
- 当客户端使用特定的语言绑定创建一个ZooKeeper句柄时,它会与服务建立一个会话。客户端最初连接到集群中的任意一台服务器,并且只连接到单个服务器。它使用TCP连接与服务器通信,但如果客户端有一段时间没有收到来自当前服务器的消息,会话可能会移动到另一个服务器。将会话移动到不同的服务器是由ZooKeeper客户端库透明地处理的。
- 会话提供顺序保证,这意味着会话中的请求是按照FIFO(先进先出)顺序执行的。通常,客户端只有一个打开的会话,所以它的所有请求都是按照FIFO顺序执行的。如果客户端有多个并发会话,那么FIFO顺序在多个会话之间未必能够保持。如果同一客户端有多个会话,就算这些会话分别在不同的时间进行请求,也不一定保持FIFO顺序。在这种情况下,它是如何发生的:
- (1)客户端建立一个会话,并通过两个连续的异步调用来创建/tasks和/workers。
- (2)第一个会话到期。
- (3)客户端建立另一个会话,并通过异步调用来创建/assign。
- 在这个调用序列中,可能只创建了/tasks和/assign,这是因为第一个会话保持了FIFO顺序,但在跨会话时就违反了FIFO顺序。
3、开始使用ZooKeeper
3.1、安装ZooKeeper
1、下载ZooKeeper
官网:https://zookeeper.apache.org 下载地址:https://archive.apache.org/dist/zookeeper/ 安装包:apache-zookeeper-3.5.9-bin.tar.gz --注意,要下载二进制安装包(名称带bin的)
2、安装ZooKeeper
]# tar zvfx ./apache-zookeeper-3.5.9-bin.tar.gz -C /usr/local/ --解压之后在/usr/local/目录下生成一个名为apache-zookeeper-3.5.9-bin的文件夹 ]# cd /usr/local/apache-zookeeper-3.5.9-bin/
3、配置zookeeper的环境变量
--向/etc/profile文件中添加如下配置 export ZOOKEEPER_HOME=/usr/local/apache-zookeeper-3.5.9-bin/ export PATH=$PATH:$ZOOKEEPER_HOME/bin --使配置生效 ]# source /etc/profile
4、修改ZooKeeper的配置文件
--将zoo_sample.cfg文件修改为zoo.cfg
]# cp /usr/local/apache-zookeeper-3.5.9-bin/conf/{zoo_sample.cfg,zoo.cfg}
--修改zoo.cfg文件
]# vim /usr/local/apache-zookeeper-3.5.9-bin/conf/zoo.cfg
#ZooKeeper对外服务端口
clientPort=2181
#ZooKeeper服务法心跳时间,单位为ms
tickTime=2000
#投票选举新leader的初始化时间
initLimit=10
#leader与follower心跳检测最大容忍时间,响应超过syncLimit*tickTime,leader认为follower“死掉”,从服务器列表中删除follower
syncLimit=5
#数据目录
dataDir=/tmp/zookeeper/data
#日志目录
dataLogDir=/tmp/zookeeper/log
--创建目录
]# mkdir -p /tmp/zookeeper/{data,log}
5、添加ZooKeeper服务器编号
- 在${dataDir}目录(也就是ZooKeeper的数据目录/tmp/zookeeper/data )下创建一个myid文件,并写入一个数值,比如0。
- myid文件里存放的数值就是ZooKeeper服务器的编号。(集群中每台服务器的编号都必须唯一)
]# echo '1' > /tmp/zookeeper/data/myid
6、启动Zookeeper服务
]# zkServer.sh start --启动zookeeper服务(因为加了zookeeper的环境变量,因此不用使用绝对路径/usr/local/apache-zookeeper-3.5.9-bin/bin/zkServer.sh) ]# zkServer.sh stop --停止zookeeper服务 ]# zkServer.sh status --查看zookeeper服务的状态
7、ZooKeeper集群
- 以上是关于ZooKeeper单机模式的安装与配置,一般在生产环境中使用的都是集群模式,集群模式的配置也比较简单,相比单机模式只需要修改一些配置即可。
--假设一个ZooKeeper集群有三台机器,IP地址分别是: 10.1.1.10 10.1.1.11 10.1.1.12 --依次在三台机器的数据目录中分别添加一个myid文件,其内容是服务器的编号(在同一个集群中,编号唯一): ]# echo '1' > /tmp/zookeeper/data/myid ]# echo '2' > /tmp/zookeeper/data/myid ]# echo '3' > /tmp/zookeeper/data/myid --在三台机器的zoo.cfg文件中添加以下配置: server.1=10.1.1.10:2888:3888 server.2=10.1.1.11:2888:3888 server.3=10.1.1.12:2888:3888
- 为了便于讲解上面的配置,这里抽象出一个公式,即server.A=B:C:D
- A是一个数字,代表服务器的编号,就是myid文件里面的值。集群中每台服务器的编号都必须唯一,所以要保证每台服务器中的myid文件中的值不同。
- B代表服务器的IP地址。
- C表示服务器与集群中的leader服务器交换信息的端口。
- D表示选举时服务器相互通信的端口
3.2、第一个ZooKeeper会话
- 使用ZooKeeper中bin/目录下的zkServer和zkCi工具进行调试和管理。
- 使用客户端建立一个会话
]# zkCli.sh -- ... --客户端启动程序来建立一个会话。 2021-12-22 09:37:37,464 [myid:] - INFO [main:ZooKeeper@868] - Initiating client connection, connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@73a8dfcc 2021-12-22 09:37:37,476 [myid:] - INFO [main:X509Util@79] - Setting -D jdk.tls.rejectClientInitiatedRenegotiation=true to disable client-initiated TLS renegotiation 2021-12-22 09:37:37,492 [myid:] - INFO [main:ClientCnxnSocket@237] - jute.maxbuffer value is 4194304 Bytes 2021-12-22 09:37:37,505 [myid:] - INFO [main:ClientCnxn@1653] - zookeeper.request.timeout value is 0. feature enabled= --客户端尝试连接到localhost/127.0.0.1:2181 2021-12-22 09:37:37,518 [myid:localhost:2181] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1112] - Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error) Welcome to ZooKeeper! JLine support is enabled --客户端连接成功,服务器开始初始化这个新会话。 2021-12-22 09:37:37,675 [myid:localhost:2181] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@959] - Socket connection established, initiating session, client: /127.0.0.1:35332, server: localhost/127.0.0.1:2181 --会话初始化成功完成。会话ID(sessionid)是0x20000103b190002 2021-12-22 09:37:37,710 [myid:localhost:2181] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1394] - Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x20000103b190002, negotiated timeout = 30000 WATCHER:: ----服务器向客户端发送一个SyncConnected事件。 WatchedEvent state:SyncConnected type:None path:null [zk: localhost:2181(CONNECTED) 0]
- 简单使用会话
--列出根(root)下的所有znode。此刻出/zookeeper之外,znode树为没有其他znode [zk: localhost:2181(CONNECTED) 0] ls / [zookeeper] --创建一个名为/workers的znode [zk: localhost:2181(CONNECTED) 1] create /workers "" Created /workers --查看/workers [zk: localhost:2181(CONNECTED) 2] ls / [workers, zookeeper] [zk: localhost:2181(CONNECTED) 3] ls /workers [] --删除znode [zk: localhost:2181(CONNECTED) 4] delete /workers [zk: localhost:2181(CONNECTED) 5] ls / [zookeeper] --退出会话 [zk: localhost:2181(CONNECTED) 6] quit WATCHER:: WatchedEvent state:Closed type:None path:null 2021-12-22 09:56:02,382 [myid:] - INFO [main:ZooKeeper@1422] - Session: 0x20000103b190002 closed 2021-12-22 09:56:02,390 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@524] - EventThread shut down for session: 0x20000103b190002
3.3、会话的状态和周期
3.2.1、会话状态转换
- 会话的生命周期是指会话从创建到结束之间的时间,无论它是正常关闭还是由于超时而过期。为了讨论会话中发生的事情,我们需要考虑会话可能处于的状态以及可能改变其状态的事件。
- 会话的主要状态:CONNECTING、CONNECTED、CLOSED和NOT_CONNECTED。状态的转换依赖于发生在客户端与服务之间的各种事件(图2-6)。
- 当要开始一个ZooKeeper会话时,会话从NOT_CONNECTED状态开始,并过渡到CONNECTING(图2-6中的箭头1)。正常情况下,与ZooKeeper服务器连接成功,会话转换到CONNECTED状态(箭头2)。当客户端与ZooKeeper服务器断开连接或无法收到服务器的响应时,会话就会转换到CONNECTED状态(箭头3)并尝试寻找其他ZooKeeper服务器。如果它能够找到另一个服务器或重新连接到原来的服务器,一旦服务器确认会话仍然有效,会话就会转换到CONNECTED状态。否则,服务器将声明会话过期,会话转换到CLOSED状态(箭头4)。应用程序也可以显式关闭会话(箭头4和5)。
- 注意,发生网络分区时等待CONNECTING
- 当客户端因超时断开与服务器连接时,客户端仍处于“CONNECTING”状态。如果因网络分区问题导致客户端与Zookeeper集群隔离而发生连接断开时,它将一直保持这种状态,直到显式地关闭这个会话,或者分区恢复后,客户端从ZooKeeper服务器获悉会话已经过期。
- 发生这种行为是因为ZooKeeper集群负责声明会话是否过期,而不是客户端。直到客户端获悉一个ZooKeeper会话已经过期,客户端才能声明该会话已经过期。然而,客户端可以选择关闭会话。
3.2.2、会话重连
- 创建会话时需要设置一个重要的参数是会话超时时间,即ZooKeeper服务允许会话超时之前存在的时间。
- 如果经过时间t之后ZooKeeper服务接收不到这个会话的任何消息,ZooKeeper服务就会声明会话过期。
- 在客户端,如果它在1/3 t时没有收到来自服务器的任何消息,它就向服务器发送一个心跳消息。在2/3 t处,客户端开始寻找一个不同的服务器,而此时它还有1/3 t的时间去寻找。
- 注意,客户端会尝试连接到哪一个ZooKeeper服务器?
- 在仲裁模式下,客户端有多个服务器可以连接,而在独立模式下,它必须尝试重新连接到可用的单个服务器。在仲裁模式中,应用程序需要传递可用的服务器列表给客户端,客户端选择一个进行连接。
- 当尝试连接到一个不同的服务器时,重要的是这个服务器的ZooKeeper状态至少要和客户端最后连接的服务器的ZooKeeper状态一致。客户端不能连接到这样的服务器:客户端已经发现的更新而服务器却未发现。
- ZooKeeper通过在服务中排序更新来决定状态是否最新。ZooKeeper中的每一个状态的变化都与其他所有已经执行的更新完全一致。在ZooKeeper中,系统根据更新的顺序分配事务标识符。因此,如果一个客户端已经更新到了i(事务标识符),那么这个客户端就不能连接到只更新到i'(i'
- 图2-7描述了在重连情况下事务标识符(zkid)的使用。当客户端因超时与S1断开连接后,客户端开始尝试连接S2,但延迟于客户端所知的变化。S3对这个变化的情况与客户端保持一致,所以S3可以安全连接。
3.4、实现一个原语:通过ZooKeeper实现锁
- 关于ZooKeeper的功能,一个简单的例子就是通过锁来实现临界区域。我们知道锁有很多形式(如:读/写锁、全局锁),通过ZooKeeper来实现锁也有多种方式。这里讨论一个简单的方式来说明应用中如何使用ZooKeeper,我们不再考虑其他形式的锁。
- 假设有一个应用程序由n个进程组成,这些进程尝试获取一个锁。再次强调,ZooKeeper并未直接暴露原语,因此我们使用ZooKeeper的接口来管理znode,并以此来实现锁。为了获得一个锁,进程p尝试创建一个名为/lock的znode,如果进程p成功创建了znode,就表示它获得了锁并可以继续执行其临界区域的代码。不过一个潜在的问题是进程p可能崩溃,导致这个锁永远无法释放。在这种情况下,没有任何其他进程可以再次获得这个锁,整个系统可能因死锁而崩溃。为了避免这种情况,我们不得不在创建这个节点时指定/lock为临时znode。
- 其他进程将因这个znode存在而创建/lock失败。每个监听/lock变化的进程,在检测到/lock已被删除时将尝试创建znode来获得锁。例如,当进程p?收到/lock被删除的通知时,它将尝试创建/lock试图获取锁,如果其他进程已经创建了,就继续监听znode。
4、一个主-从模式例子的实现
- 本示例仅用于教学目的,我们不建议使用zkCli.sh构建系统。
- 主-从模型中包括三个角色:
- Master(主节点):主节点负责监视新的从节点和任务,分配任务给可用的从节点。
- Worker(从节点):从节点会通过系统注册自己,以确保主节点看到它们可以执行任务,然后开始监视新任务。
- Client(客户端):客户端创建新任务并等待系统的响应。
4.1、主节点角色
- 因为只有一个进程会成为master,所以一个进程必须在ZooKeeper中锁定/master才能成为master。为此,进程会创建一个临时的znode,名为/master:
- 一个临时节点会在会话过期或关闭时自动被删除。
--开启第一个会话(zkCli.sh),将作为主节点会话 --创建名为/master的znode,以便获得管理权。使用-e标志来表示创建的znode为临时性的。 --添加主机信息并不是必需的,这样做仅仅是为了说明我们可以在需要时添加数据。 [zk: localhost:2181(CONNECTED) 0] create -e /master "master1.example.com:2223" Created /master --列出ZooKeeper树的根。 [zk: localhost:2181(CONNECTED) 1] ls / [master, zookeeper] --获取/master的数据。 [zk: localhost:2181(CONNECTED) 2] get /master master1.example.com:2223
- 假如其他进程想成为master,并尝试创建一个/master节点。让我们看看会发生什么:
- ZooKeeper告诉我们/master已经存在。这样,第二个进程就知道已经有一个master了。
--开启第二个会话(zkCli.sh),将作为从节点会话 [zk: localhost:2181(CONNECTED) 0] create -e /master "master2.example.com:2223" Node already exists: /master
- 主节点可能会崩溃,而从节点可能需要接替主节点的角色。为了检测到这一点,从节点需要在/master上设置一个监视点:
- stat命令可以得到znode节点的属性,并可以在已经存在的znode节点上设置监视点。-w选项表示添加监视点。
--第二个会话(目前是从节点会话) [zk: localhost:2181(CONNECTED) 1] stat -w /master cZxid = 0x100000028 ctime = Wed Dec 22 23:06:11 CST 2021 mZxid = 0x100000028 mtime = Wed Dec 22 23:06:11 CST 2021 pZxid = 0x100000028 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x100004099d30008 dataLength = 24 numChildren = 0
- 在主节点崩溃的情况下,我们观察到以下情况:
- NodeDeleted事件指出主节点的会话已经关闭或过期。
--关闭第一会话,观察第二个会话 [zk: localhost:2181(CONNECTED) 2] WATCHER:: WatchedEvent state:SyncConnected type:NodeDeleted path:/master
- 主节点已经不存在了,现在从节点可以通过创建/master来成为主节点。
--第二个会话(将成为主节点会话) [zk: localhost:2181(CONNECTED) 2] create -e /master "master2.example.com:2223" Created /master
4.2、从节点,任务和任务分配
- 在讨论从节点和客户端所采取的步骤之前,先创建三个重要的父znode:/workers、/tasks和/assign。
- 这三个新的znode是持久的znode,不包含任何数据。在这个例子中,使用这些znode可以告诉我们哪些从节点是可用的,告诉我们什么时候有任务要分配,并向从节点分配任务。
--开启一个会话创建持久znode,创建后关闭该会话 [zk: localhost:2181(CONNECTED) 0] create /workers "" Created /workers [zk: localhost:2181(CONNECTED) 1] create /tasks "" Created /tesks [zk: localhost:2181(CONNECTED) 2] create /assign "" Created /assign
- 在实际的应用程序中,这三个znode需要由主进程在开始分配任务之前创建,或者由某个引导过程创建。无论它们是如何创建的,一旦它们存在,主节点需要监视/workers和/tasks的子znode的变化。
- ls命令使用-w选项表示在相应znode上设置监视点。
--第二个会话(目前是主节点会话) --监控/workers的变化 [zk: localhost:2181(CONNECTED) 3] ls -w /workers [] --监控/tesks的变化 [zk: localhost:2181(CONNECTED) 4] ls -w /tasks []
4.3、从节点角色
- 从节点第一步是通知主节点它可以执行任务了。它通过在/workers下创建一个临时的znode来表示它。从节点使用主机名来标识自己。
--重新开启第一个会话(zkCli.sh),将作为从节点会话 [zk: localhost:2181(CONNECTED) 0] create -e /workers/worker1.example.com "worker1.example.com:2223" Created /workers/worker1.example.com
- 之前主节点已经监视了/workers的子节点变化情况。一旦从节点在/workers下创建了一个znode,主节点就会观察到以下通知信息。
--第二个会话(目前是主节点会话) [zk: localhost:2181(CONNECTED) 5] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/workers
- 接下来,从节点需要创建一个父znode(/assing/worker1.example.com)来接收分配的任务。并通过ls命令的-w选项来监视这个节点的变化,以便等待新的任务。
--第一个会话(目前是从节点会话) [zk: localhost:2181(CONNECTED) 1] create /assign/worker1.example.com "" Created /assign/worker1.example.com [zk: localhost:2181(CONNECTED) 2] ls -w /assign/worker1.example.com []
- 从节点现在已经准备就绪,可以接收分配的任务了
4.4、客户端角色
- 客户端向系统添加任务。就本例而言,任务是什么并不重要。这里我们假设客户端要求主从系统运行cmd命令。要向系统中添加任务,客户端需要执行以下操作:
--开启第三个会话(zkCli.sh),将作为客户端会话 [zk: localhost:2181(CONNECTED) 0] create -s /tasks/task- "cmd" Created /tasks/task-0000000000
- 我们需要按照任务添加的顺序来添加znode,其本质上是一个队列。客户端现在必须等待任务执行完毕。执行任务的从节点将任务执行完毕后,会在“/tasks/task-”下创建一个子znode来表示任务状态。客户端通过查看这个子znode是否创建来确定任务是否执行完毕,因此客户端需要监视“/tasks/task-”的创建事件。
--第三个会话(目前是客户端会话) [zk: localhost:2181(CONNECTED) 1] ls -w /tasks/task-0000000000 []
- 之前主节点已经监视了/tasks的子znode。一旦客户端在/tasks下创建了一个znode,主节点就会观察到以下通知信息。
--第二个会话(目前是主节点会话) [zk: localhost:2181(CONNECTED) 5] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks
- 主节点之后会检查这个新的任务,然后获取可用的从节点列表,并任务分配给可用的从节点。
--第二个会话(目前是主节点会话) [zk: localhost:2181(CONNECTED) 5] ls /tasks [task-0000000000] [zk: localhost:2181(CONNECTED) 6] ls /workers [worker1.example.com] [zk: localhost:2181(CONNECTED) 7] create /assign/worker1.example.com/task-0000000000 "" Created /assign/worker1.example.com/task-0000000000
- 从节点接收到分配任务的通知(之前从节点已经监视了/assign/worker1.example.com):
--第一个会话(目前是从节点会话) [zk: localhost:2181(CONNECTED) 3] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/assign/worker1.example.com
- 从节点检查新任务,并确认该任务是否分配给自己:
--第一个会话(目前是从节点会话) [zk: localhost:2181(CONNECTED) 3] ls /assign/worker1.example.com [task-0000000000]
- 一旦从节点完成任务的执行,它就会在“/tasks/task-”中添加一个表示状态的znode:
--第一个会话(目前是从节点会话) [zk: localhost:2181(CONNECTED) 4] create /tasks/task-0000000000/status "done" Created /tasks/task-0000000000/status
- 客户端接收到任务执行完成的通知,并检查执行结果:
--第三个会话(目前是客户端会话) [zk: localhost:2181(CONNECTED) 2] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks/task-0000000000 [zk: localhost:2181(CONNECTED) 2] get /tasks/task-0000000000 cmd [zk: localhost:2181(CONNECTED) 3] get /tasks/task-0000000000/status done
- 客户端检查状态znode的内容,并确认任务的执行结果。在本例中,它已经成功执行,结果是“done”。当然任务也可能非常复杂,甚至涉及另一个分布式系统。最终不管是什么样的任务,执行任务和通过Zookeeper传递结果的机制,本质上都是一样的。
4.5、三个会话
4.5.3、第一个会话
--第一个会话(目前是主节点会话) [zk: localhost:2181(CONNECTED) 0] create -e /master "master1.example.com:2223" Created /master [zk: localhost:2181(CONNECTED) 1] ls / [master, zookeeper] [zk: localhost:2181(CONNECTED) 2] get /master master1.example.com:2223 --第一个会话(目前是从节点会话) [zk: localhost:2181(CONNECTED) 0] create -e /workers/worker1.example.com "worker1.example.com:2223" Created /workers/worker1.example.com [zk: localhost:2181(CONNECTED) 1] create /assign/worker1.example.com "" Created /assign/worker1.example.com [zk: localhost:2181(CONNECTED) 2] ls -w /assign/worker1.example.com [] [zk: localhost:2181(CONNECTED) 3] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/assign/worker1.example.com [zk: localhost:2181(CONNECTED) 3] ls /assign/worker1.example.com [task-0000000000] [zk: localhost:2181(CONNECTED) 4] create /tasks/task-0000000000/status "done" Created /tasks/task-0000000000/status
4.5.2、第二个会话
--第二个会话(目前是从节点会话) [zk: localhost:2181(CONNECTED) 0] create -e /master "master2.example.com:2223" Node already exists: /master [zk: localhost:2181(CONNECTED) 1] stat -w /master cZxid = 0x100000004 ctime = Thu Dec 23 10:51:28 CST 2021 mZxid = 0x100000004 mtime = Thu Dec 23 10:51:28 CST 2021 pZxid = 0x100000004 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x1000011abdf0000 dataLength = 24 numChildren = 0 --第二个会话(目前是主节点会话) [zk: localhost:2181(CONNECTED) 2] create -e /master "master2.example.com:2223" Created /master [zk: localhost:2181(CONNECTED) 3] ls -w /workers [] [zk: localhost:2181(CONNECTED) 4] ls -w /tasks [] [zk: localhost:2181(CONNECTED) 5] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/workers [zk: localhost:2181(CONNECTED) 5] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks [zk: localhost:2181(CONNECTED) 5] ls /tasks [task-0000000000] [zk: localhost:2181(CONNECTED) 6] ls /workers [worker1.example.com] [zk: localhost:2181(CONNECTED) 7] create /assign/worker1.example.com/task-0000000000 "" Created /assign/worker1.example.com/task-0000000000
4.5.3、第三个会话
--第三个会话(目前是客户端会话) [zk: localhost:2181(CONNECTED) 0] create -s /tasks/task- "cmd" Created /tasks/task-0000000000 [zk: localhost:2181(CONNECTED) 1] ls -w /tasks/task-0000000000 [] [zk: localhost:2181(CONNECTED) 2] WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks/task-0000000000 [zk: localhost:2181(CONNECTED) 2] get /tasks/task-0000000000 cmd [zk: localhost:2181(CONNECTED) 3] get /tasks/task-0000000000/status done
5、zkCli.sh脚本的使用方法
1、连接和退出ZooKeeper服务器
1、连接到ZooKeeper服务器
- 使用ZooKeeper客户端(zkCli.sh)连接到ZooKeeper服务器的语法是:
zkCli.sh -timeout 5000 -r -server ip1:port1[,ip2:port2,ip3:port3]
-timeout:表示客户端向ZooKeeper服务器发送心跳的时间间隔,单位为毫秒。因为客户端与服务器的连接状态是通过心跳检测来维护的,如果在指定的时间间隔内,客户端没有向服务器发送心跳包,服务器则会断开与客户端的连接。参数5000,表示客户端向服务器发送心跳的间隔为5秒。
-r:表示客户端以只读模式连接
-server:指定客户端要连接的服务器IP与端口,服务器默认的端口为2181
示例:
]# zkCli.sh #相当于zkCli.sh -server 127.0.0.1:2181 ]# zkCli.sh -timeout 5000 -r -server 10.1.1.10:2181 --会随机连接到后面三个ZooKeeper服务器之一 ]# zkCli.sh -server 10.1.1.10:2181,10.1.1.11:2181,10.1.1.12:2181
2、关闭连接
- close命令会关闭当前客户端连接
[zk: localhost:2181(CONNECTED) 0] close WATCHER:: WatchedEvent state:Closed type:None path:null 2021-12-26 21:48:31,454 [myid:] - INFO [main:ZooKeeper@1422] - Session: 0x100001117980000 closed 2021-12-26 21:48:31,459 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@524] - EventThread shut down for session: 0x100001117980000
3、连接到ZooKeeper服务器
connect host:port
host:port:ZooKeeper服务器IP和端口
示例:
[zk: localhost:2181(CLOSED) 1] connect 127.0.0.1:2181
4、退出zkCli.sh终端
[zk: 127.0.0.1:2181(CONNECTED) 2] quit WATCHER:: WatchedEvent state:Closed type:None path:null 2021-12-26 21:49:06,266 [myid:] - INFO [main:ZooKeeper@1422] - Session: 0x100001117980001 closed 2021-12-26 21:49:06,269 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@524] - EventThread shut down for session: 0x100001117980001
2、查询znode
1、查询子znode列表
ls [-s] [-w] [-R] path
path:znode路径
-s:显示path状态(元数据)的详细信息
-w:监听path下子znode列表变化(如果path下子znode的数量有变化,就会触发监视点)
-R:递归显示path下的所有znode
--ls2已弃用
ls2 path [watch]
示例:
[zk: localhost:2181(CONNECTED) 0] ls / [zookeeper] [zk: localhost:2181(CONNECTED) 1] ls -s / [zookeeper]cZxid = 0x0 ctime = Thu Jan 01 08:00:00 CST 1970 mZxid = 0x0 mtime = Thu Jan 01 08:00:00 CST 1970 pZxid = 0x0 cversion = -1 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 0 numChildren = 1 [zk: localhost:2181(CONNECTED) 2] ls -R / / /zookeeper /zookeeper/config /zookeeper/quota [zk: localhost:2181(CONNECTED) 3] ls -w / [zookeeper]
2、获取znode状态
stat [-w] path
path:znode路径
-w:监听path znode的状态(元数据)变化
示例:
[zk: localhost:2181(CONNECTED) 8] stat /test1 cZxid = 0x100000011 --cZxid:创建znode的事务id ctime = Sun Dec 26 19:33:43 CST 2021 --ctime:创建znode的时间 mZxid = 0x100000011 --mZxid:修改znode的事务id mtime = Sun Dec 26 19:33:43 CST 2021 --mtime:修改znode的时间 pZxid = 0x100000011 --pZxid:子znode列表最后一次修改的事务id。删除或添加子znode,不包含修改子znode的数据。 cversion = 0 --cversion:子znode的版本号,删除或添加子znode,版本号会自增 dataVersion = 0 --dataVersion:znode数据版本号,数据写入操作,版本号会递增 aclVersion = 0 --aclVersion:znode ACL权限版本,权限写入操作,版本号会递增 ephemeralOwner = 0x0 --ephemeralOwner:临时znode创建时的事务id,如果是永久znode,则它的值为0 dataLength = 0 --dataLength:znode数据长度(单位:byte),中文占3个byte numChildren = 0 --numChildren:子znode数量
3、创建、删除znode和设置znode数据
1、创建znode
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
path:znode路径
-s:创建有序znode。(自动为有序znode加上编号)
-e:创建临时znode,默认是持久znode。(会话断开时,该会话的临时znode被删除)
data:存储在znode中的数据
acl:设置子znode访问权限,默认所有人都可以对该znode进行读写操作
示例:
--创建持久znode [zk: localhost:2181(CONNECTED) 0] create /test1 Created /test1 --创建临时znode [zk: localhost:2181(CONNECTED) 1] create -e /test2 Created /test2 --创建持久有序的znode [zk: localhost:2181(CONNECTED) 2] create -s /test3 Created /test30000000001 --创建临时有序的znode [zk: localhost:2181(CONNECTED) 3] create -e -s /test4 Created /test40000000001
2、删除znode
--删除znode,该znode不能有子znode
delete [-v version] path
path:znode路径
-v:znode版本号(znode状态cversion的值)。如果传递了版本号,则必须保证和服务器的版本号一致,否则会报错:version No is not valid
--删除znode,包含其子znode(会递归删除子znode,再删除znode本身)
deleteall path
--已弃用
rmr path
示例:
[zk: localhost:2181(CONNECTED) 0] delete /test1 [zk: localhost:2181(CONNECTED) 1] delete /test2 Node not empty: /test2 [zk: localhost:2181(CONNECTED) 2] deleteall /test2
3、获取znode数据
get [-s] [-w] path
path:znode路径
-s:显示path状态(元数据)的详细信息
-w:监视path znode中的数据变化
4、设置znode数据
set [-s] [-v version] path data
path:znode路径
data:znode数据
-s:显示path状态(元数据)的详细信息
-v:数据版本号(znode状态dataVersion的值)
4、znode配额
1、查询znode配额
listquota path
path:znode路径
2、设置znode配额
- znode可以存储数据,也可以创建子znode,但是如果不做控制,znode数据可以无限大,子znode数量也可以创建无数个,所以在有些场景下需要对znode的数据和子znode的数量需要做一些限制,zk为我们提供了setauota命令实现对子znode的限制功能。但是,zk并不是真正在的物理上对znode做了限制,而是如果超过了znode限制,会在zk的日志文件中记录配额超限的警告信息。
setquota -n|-b val path
path:znode路径
-n:限制子znode的数量
-b:限制znode的数据长度
val:如果参数是-n,val表示限制子znode的数量。如果参数是-b,val表示限制znode的数据长度
3、删除znode配额
delquota [-n|-b] path
path:znode路径
-n:删除子znode数量配额限制
-b:删除znode数据长度配额限制
示例:
[zk: localhost:2181(CONNECTED) 0] create /test1 Created /test1 --查看/test1的配额 [zk: localhost:2181(CONNECTED) 1] listquota /test1 absolute path is /zookeeper/quota/test1/zookeeper_limits quota for /test1 does not exist. --设置/test1的配额 [zk: localhost:2181(CONNECTED) 2] setquota -n 2 /test1 [zk: localhost:2181(CONNECTED) 3] listquota /test1 absolute path is /zookeeper/quota/test1/zookeeper_limits Output quota for /test1 count=2,bytes=-1 --表示znode的配额信息,限制该znode最多有2个子znode,znode数据为-1,表示不限制 Output stat for /test1 count=1,bytes=0 --表示当前znode的状态信息,该znode有1个子znode,znode数据长度为0 --删除/test1的配额 [zk: localhost:2181(CONNECTED) 4] delquota /test1 [zk: localhost:2181(CONNECTED) 5] listquota /test1 absolute path is /zookeeper/quota/test1/zookeeper_limits quota for /test1 does not exist.
5、znode的ACL
1、获取znode ACL
getAcl [-s] path
path:znode路径
-s:显示path状态(元数据)的详细信息
2、设置znode ACL
- 创建znode时如果没有设置acl权限,默认为所有用户都可以对该znode进行读写操作。
setAcl [-s] [-v version] [-R] path acl
path:znode路径
acl:设置子znode访问权限,默认所有人都可以对该znode进行读写操作
-s:显示path状态(元数据)的详细信息
-v:acl版本号(znode状态aclVersion的值)
-R:递归设置acl
示例:
[zk: localhost:2181(CONNECTED) 0] create /test1 Created /test1 [zk: localhost:2181(CONNECTED) 1] getAcl /test1 'world,'anyone : cdrwa [zk: localhost:2181(CONNECTED) 2] setAcl /test1 ip:10.1.1.12:cdrwa [zk: localhost:2181(CONNECTED) 3] getAcl /test1 --要操作该znode,需要登录zkCli.sh -server 10.1.1.12:2181 Authentication is not valid : /test1
6、历史命令
1、查看历史命令
history
2、执行历史命令
redo cmdno
cmdno:历史命令编号
示例:
[zk: localhost:2181(CONNECTED) 0] ls / [test1, zookeeper] [zk: localhost:2181(CONNECTED) 1] get /zookeeper ss [zk: localhost:2181(CONNECTED) 2] history --查看历史命令 0 - ls / 1 - get /zookeeper 2 - history [zk: localhost:2181(CONNECTED) 3] redo 0 --执行历史命令 [test1, zookeeper]
7、其他命令
1、给当前客户端添加授权信息
addauth scheme auth
scheme:授权方式
auth:权限
2、与leader同步数据
sync path
path:znode路径
3、打开或关闭监听日志
- 在获取znode数据、子znode列表等操作时,都可以添加watch参数监听znode的变化,从而znode数据更改、子znode列表变更时收到通知,并输出到控制台。默认是打开,可以设置参数将其关闭。
printwatches on|off on:打开 off:关闭
4、配置信息
config [-c] [-w] [-s]
5、重新配置信息
reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
6、删除监听器
removewatches path [-c|-d|-a] [-l]