Zookeeper分布式过程协同技术详解笔记(二)

下载Zookeeper

笔者这里不对下载部分做过多赘述,从官网下载何时版本的包解压即可。其中bin目录中含有zk的启动脚本,conf中则是启动所需的配置文件,lib目录则是java的jar文件。

第一个zookeeper会话

初学zookeeper,我们使用bin目录下的zkServer和zkClient工具进行简单的调试和管理。
笔者使用的版本是3.4.10,conf目录下的zoo.cfg是zk默认的配置文件,zoo_sample则是包含了更多的配置及其含义的注释,为了简便,我们直接使用zoo.cfg来启动zk服务。

localhost:bin wz$ sudo ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /Users/wz/Developement/zookeeper-3.4.10/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

这个指令可以让zk服务在后台运行,如果需要在前台运行以便查看服务器输出,可以通过以下命令。

sudo ./zkServer.sh start-foreground

好了,启动好了服务端,接着我们启动一个客户端。

./zkCli.sh

......

2018-06-05 21:11:46,487 [myid:] - INFO  [main:ZooKeeper@438] - Initiating client connection, connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@67424e82 ①
Welcome to ZooKeeper!
2018-06-05 21:11:46,511 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1032] - Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
JLine support is enabled ②
2018-06-05 21:11:46,566 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@876] - Socket connection established to localhost/127.0.0.1:2181, initiating session ③
2018-06-05 21:11:46,595 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server  localhost/127.0.0.1:2181, sessionid = 0x163d00dddef0000, negotiated timeout = 30000 ④

WATCHER::

WatchedEvent state:SyncConnected type:None path:null ⑤

下面我们逐个分析这几行日志,其实就是会话建立的过程信息。

① 客户端启动,开始建立会话
② 客户端尝试连接到localhost/127.0.0.1:2181
③ 连接成功建立,开始初始化会话
④ 会话初始化完成
⑤ 服务端向客户端发送一个state:SyncConnected事件,会话建立完成,id为 0x163d00dddef0000
客户端需要实现Watcher对象来处理这个事件。

接下来我们劣列出根节点下的所有znode,然后尝试创建一个znode。

[zk: localhost:2181(CONNECTED) 3] ls /
[zookeeper]

只有zookeeper节点,其中包含了zk服务所需要的元数据树,这里不多赘述,下面我们新建一个workers znode。

[zk: localhost:2181(CONNECTED) 3] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 4] create /workers ""
Created /workers
[zk: localhost:2181(CONNECTED) 5] ls /
[zookeeper, workers]

这里创建时我们指定了一个空串,代表此时这个znode中不保存数据,当然你也可以把""替换为"workers"或是任意内容。

然后我们删除znode,停止这个会话,这样就完成了这第一个小实验。

[zk: localhost:2181(CONNECTED) 6] delete /workers
[zk: localhost:2181(CONNECTED) 7] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 8] quit
Quitting...
2018-06-05 21:45:24,904 [myid:] - INFO  [main:ZooKeeper@684] - Session: 0x163d00dddef0000 closed
2018-06-05 21:45:24,906 [myid:] - INFO  [main-EventThread:ClientCnxn$EventThread@519] - EventThread shut down for session: 0x163d00dddef0000

会话的状态和生命周期

一个zookeeper会话的状态转换大致如下图所示


Zookeeper分布式过程协同技术详解笔记(二)_第1张图片
zookeeper会话状态转换图

一个会话从NOT_CONNECTED开始,当客户端初始化连接完成时转到CONNECTING状态,连接成功建立后会转到CONNECTED状态。倘若此时服务器断开连接或者无法收到服务器的响应时,就会转会CONNECTING状态(箭头3),并尝试重新连接或发现其他zk服务器,如果发现一个服务器或重连成功,状态就会重新回到CONNECTED,否则,会话过期,转到CLOSED状态(箭头4)。当然,应用也可以显示关闭会话(箭头5)。

注意:
如果一个客户端因超时与服务端断开连接,客户端仍然保持CONNECTING状态,此时倘若因为网络分区错误导致客户端与服务端之间连接不可达,那么其状态会一直保持,直到显示的关闭这个会话或者问题修复后客户端悉知会话已过期。这是因为会话的超时由服务集群来控制,客户端无法控制。直到客户端获悉会话超时,否则不能声明自己的回话过期,但是客户端可以显示关闭会话。

因此,我们需要设置会话过期时间这个参数,如果经过t时间服务接收不到会话的消息,就会声明这个会话过期。在客户端侧,如果经过t/3时间后没有收到消息,就会向服务器发送心跳消息。经过2t/3时间后会开始寻找其他服务器,如果在剩下t/3时间内无法找到,就会被声明会话过期。

当客户端尝试连接到一个不同的服务器时,需要保证这个服务的状态要与最后连接的服务器状态一致,如果某个服务获悉状态变更的时间点延迟于客户端,那就要保证这个服务不会被链接。zk通过在服务中排序更新操作发发生的事件来确保这种情况,如果客户端再位置i观察到一个更新,那他就不能连接到只观察到i之前状态的服务。这个过程如下图所示。


Zookeeper分布式过程协同技术详解笔记(二)_第2张图片
客户端重连

zookeeper与仲裁模式

上面我们都是基于独立模式进行的实验,这在实际环境中肯定是非常不靠谱的,如果服务器故障那整个zk服务都将关闭。下面我们通过在一台机器上运行多个zk服务器来演示仲裁模式。
首先将配置文件修改如下:

   tickTime=2000
   initLimit=10
   syncLimit=5
   dataDir=./data
   clientPort=2181
   server.1=127.0.0.1:2222:2223
   server.2=127.0.0.1:3333:3334
   server.3=127.0.0.1:4444:4445

其中server.n指定了编号为n的服务器所使用的地址和端口,每个server.n通过冒号分隔为三部分,第一部分为服务器主机名,第二部分和第三部分为TCP端口号,分别用于仲裁和群首选举。

接着我们还需要为每个服务分别设置data目录

localhost:bin wz$ mkdir z1
localhost:bin wz$ mkdir z2
localhost:bin wz$ mkdir z3
localhost:bin wz$ mkdir z1/data
localhost:bin wz$ mkdir z2/data
localhost:bin wz$ mkdir z3/data

当一个服务器启动时,需要知道启动的是哪个服务器,通过读取data目录下一个名为myid的文件来获取服务器ID信息,我们可以通过以下命令创建这些文件:

localhost:bin wz$ echo 1 > z1/data/myid
localhost:bin wz$ echo 2 > z2/data/myid
localhost:bin wz$ echo 3 > z3/data/myid

接着我们分别创建每台服务器的配置文件,根据上面的配置新建z1.cfg,然后修改端口为2181和2183创建z2/z3.cfg。

现在我们可以启动服务器,先从z1开始

sudo ./zkServer.sh start-foreground ./z1/z1.cfg

此时服务器疯狂尝试连接到其他服务器,报错如下:

018-06-07 22:51:26,169 [myid:1] - WARN  [WorkerSender[myid=1]:QuorumCnxManager@588] - Cannot open channel to 2 at election address /127.0.0.1:3334
java.net.ConnectException: Connection refused (Connection refused)
    at java.net.PlainSocketImpl.socketConnect(Native Method)
    at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
    at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
    at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
    at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
    at java.net.Socket.connect(Socket.java:589)
    at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:562)
    at org.apache.zookeeper.server.quorum.QuorumCnxManager.toSend(QuorumCnxManager.java:538)
    at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.process(FastLeaderElection.java:452)

接着我们启动第二台服务器,我们会看到以下日志

2018-06-07 22:51:42,699 [myid:2] - INFO  [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2182:Leader@371] - LEADING - LEADER ELECTION TOOK - 250

该日志指出服务器2已被选举为群首,再看服务器1的日志

2018-06-07 22:51:42,683 [myid:1] - INFO  [QuorumPeer[myid=1]/0:0:0:0:0:0:0:0:2181:Follower@64] - FOLLOWING - LEADER ELECTION TOOK - 16517

该服务器作为服务器2的追随者被激活,现在我们并没有启动服务器3,也就是说此时构成了允许执行的最小数目(参见一篇博客)。

此刻服务已经可用,现在我们启动一个客户端来连接到服务上,连接字符串需要列出所有组成服务的服务器host:port对。这个例子中,这个连接串为
"127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"(这里我们包含了第三台服务器的信息,但我们并没有启动它,放在这里仅为了说明zk的一些属性)
接下来使用zkCli来访问这个集群:

./zkCli.sh -server 127.0.0.1:2181,127.0.0.2:2182,127.0.0.1:2183

连接成功后,你会看到如下日志

2018-06-07 23:09:16,507 [myid:] - INFO  [main-SendThread(127.0.0.1:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server 127.0.0.1/127.0.0.1:2181, sessionid = 0x163dabb62e60001, negotiated timeout = 30000

如果你通过多次ctrl+c停止并重连,就会发现端口号在2181和2182之间变化,也会看到因为尝试连接2183而报错的日志,之后又成功连接到某一个服务。

一个主从模式例子的实现

主从模式的模型包括三个角色:

  1. 主节点,主要负责监视新的从节点和任务,进行任务的分配。
  2. 从节点, 通过系统注册自己以便可以被主节点所监控,然后开始监视新的任务。
  3. 客户端,创建新任务并等待服务端响应。

因为只有一个进程会成为主节点,所以一旦有一个进程成为主节点后就必须锁定管理权,为此,我们先创建一个临时节点/master。

[zk: localhost:2181(CONNECTED) 0] create -e /master "master1.example.com:2223"
Created /master
[zk: localhost:2181(CONNECTED) 1] ls /
[zookeeper, master]
[zk: localhost:2181(CONNECTED) 2] get /master
master1.example.com:2223
cZxid = 0xf0a
ctime = Tue Jun 12 22:35:20 CST 2018
mZxid = 0xf0a
mtime = Tue Jun 12 22:35:20 CST 2018
pZxid = 0xf0a
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x163f1627cc3000e
dataLength = 24
numChildren = 0

以上我们创建主节点znode,使用 -e标注创建的znode为临时性的。之后列出了zookeeper树的根。最后获取了/master这个znode的数据和元数据。
主节点建立后,倘若其他进程不知道一个主节点已被选举出来,尝试重复创建/master,zk会告诉我们一个/master节点已经存在。但是主节点随时可能崩溃,为了让其他节点能够接替主节点的角色,需要在/master上设置一个监视点。

[zk: localhost:2181(CONNECTED) 4] stat /master true
cZxid = 0xf0a
ctime = Tue Jun 12 22:35:20 CST 2018
mZxid = 0xf0a
mtime = Tue Jun 12 22:35:20 CST 2018
pZxid = 0xf0a
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x163f1627cc3000e
dataLength = 24
numChildren = 0

stat命令可以得到一个znode的属性,并允许我们设置一个监视点。通过路径后添加true来添加监视点,这样以来,当主节点会话结束过崩溃时,我们就可以收到一个NodeDeleted事件。此时备份节点就可以创建/master成为新的活动主节点。

从节点的任务和分配

开始之前,我们先创建3个持久性父znode, /workers, /tasks以及/assign。他们不包含任何数据,用来告诉主节点有哪些节点可以接受任务,哪些任务要分配,并向从节点分配任务。

[zk: localhost:2181(CONNECTED) 5] create /workers ""
Created /workers
[zk: localhost:2181(CONNECTED) 6] create /tasks ""
Created /tasks
[zk: localhost:2181(CONNECTED) 7] create /assign ""
Created /assign
[zk: localhost:2181(CONNECTED) 8] ls /workers true
[]
[zk: localhost:2181(CONNECTED) 9] ls /tasks true
[]

通过ls的可选参数true,来设置对这个znode子节点变化的监视点。

好了,从节点现在首先要通知主节点,自己可以执行任务。通过在/workers子节点下创建临时性的znode来进行通知,并用主机名表示自己。

create -e /workers/worker1.example.com "worker1.example.com:2224"

WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/workers
Created /workers/worker1.example.com

一单创建成功,主节点就会收到type:NodeChildrenChanged的通知。
下一步,从节点需要创建一个znode /assign/worker1.example.com来接受任务分配,
并监视这个节点的变化:

[zk: localhost:2181(CONNECTED) 11] create -e /assign/worker1.example.com ""
Created /assign/worker1.example.com
[zk: localhost:2181(CONNECTED) 12] ls /assign/worker1.example.com true
[]

这样从节点就已经准备就绪,可以接受任务分配。接下来我们通过讨论客户端角色来看一下任务分配的问题。
假设客户端提交了一个请求主从系统来运行cmd的任务,客户端执行一下操作

create -s /tasks/task- "cmd"

因为需要保证任务执行顺序,所以这里是一个有序队列,使用-s创建顺序节点。负责执行该任务的节点执行完成后会在此节点下创建一个新的节点表示任务的状态,因此客户端需要监视这个节点,同样的,使用ls的参数true来设置监视点

ls /tasks/task-0000000000 true

之前我们已经设置了主节点对于/task节点的监视,所以这里一单创建成功,我们就会监视到以下事件:

[zk: localhost:2181(CONNECTED) 6]
 WATCHER::
 WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks

主节点接着会检查这个新任务,获取可处理任务的节点列表,之后分配给work1.example.com

[zk: 6] ls /tasks
[task-0000000000]
[zk: 7] ls /workers
[worker1.example.com]
[zk: 8] create /assign/worker1.example.com/task-0000000000 ""
Created /assign/worker1.example.com/task-0000000000
[zk: 9]

接下来从节点会获取到新增任务的通知:

  [zk: localhost:2181(CONNECTED) 3]
   WATCHER::
   WatchedEvent state:SyncConnected type:NodeChildrenChanged
   path:/assign/worker1.example.com

之后从节点会再次检查任务是否分配给自己

   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]

一旦从节点完成任务,就会向/task/task-0000000000中添加一个状态节点status

   [zk: localhost:2181(CONNECTED) 4] create /tasks/task-0000000000/status "done"
   Created /tasks/task-0000000000/status
   [zk: localhost:2181(CONNECTED) 5]

之后客户端收到通知,检查结果:

   WATCHER::
   WatchedEvent state:SyncConnected type:NodeChildrenChanged
   path:/tasks/task-0000000000
   [zk: localhost:2181(CONNECTED) 2] get /tasks/task-0000000000
   "cmd"
   cZxid = 0x7c
   ctime = Tue Dec 11 10:30:18 CET 2012
   mZxid = 0x7c
   mtime = Tue Dec 11 10:30:18 CET 2012
   pZxid = 0x7e
   cversion = 1
   dataVersion = 0
   aclVersion = 0
   ephemeralOwner = 0x0
   dataLength = 5
   numChildren = 1
   [zk: localhost:2181(CONNECTED) 3] get /tasks/task-0000000000/status
   "done"
   cZxid = 0x7e
   ctime = Tue Dec 11 10:42:41 CET 2012
   mZxid = 0x7e
   mtime = Tue Dec 11 10:42:41 CET 2012
   pZxid = 0x7e
   cversion = 0
   dataVersion = 0
   aclVersion = 0
   ephemeralOwner = 0x0
   dataLength = 8
   numChildren = 0
   [zk: localhost:2181(CONNECTED) 4]

小结

这次我们通过例子了解了很多zk的基础以及api的使用,尽管实际在分布式系统中可能复杂数倍,但本质上是相似的。通过对仲裁模式中主从节点通讯过程的演示,相信你对zk的基本原理已经有一些理解,本文中的演示主要使用的都是zkcli这个命令行工具,它更多的是为了学习和演示,下一章我们将直接使用JAVA来实现一些例子。

你可能感兴趣的:(Zookeeper分布式过程协同技术详解笔记(二))