分布式——基于zookeeper构建分布式应用

1. 概述

之前在学校一直都没怎么关注分布式的的应用,在实际的开发场景中,因为都是一些小的项目,也没类似的需求。来到公司后才知道,几乎所有的大的应用都是在分布式环境下运行,就是每个业务都是独立开始部署的,同时每个应用都部署了N台机器。分布式环境一个非常大的区别就是应用并不是运行在一个 JVM 上的,那么所有以本地内存执行为前提的相关技术都将不适用,都需要重写。
所以我一直打算从零开始整理所有在分布式环境下所需要应用或改造的技术,于是就有了这个系列——分布式应用开发与学习。

关于分布式,其中非常重要的一点就是分布环境中的配置管理与状态同步,关于这一点是 ZooKeeper 的强项。今天就整理了一下基于 ZooKeeper 的相关使用方式。

人们普遍认为,您不应该设计或实施自己的加密算法,而应使用经过充分测试的第三方库。分布式系统也是如此:不要总想着构建自己的协议来协调集群,因为要考虑的因素实在太多了,当然前提是已经有成熟的第三方库支持。

构建分布式系统不是一个小问题,它很容易出现竞争条件,死锁和不一致。使集群协调快速且可扩展与使其可靠一样困难。而Apache ZooKeeper的出现为我们提供编写正确的分布式应用程序所需的工具的协调服务。

Apache HBase,HDFS和其他Apache Hadoop项目已经使用ZooKeeper来提供高可用性服务,ZooKeeper 通常可以使分布式编程更容易,这个已经成为事实上的业界标准。

2. ZooKeeper的工作原理

ZooKeeper在服务器集群上运行,该集群共享数据的状态。每当进行更改时,需要通过集群仲裁(至少一半服务器来进行评估),只能仲裁通过,更改才会生效。集群中将会通过选举来找到一个 Leader ,如果同时进行两次相互冲突的变化,率先通过 Leader 支持的变更会生效,另一个将失败。另外,ZooKeeper保证来自同一客户端的写入将按照该客户端发送的顺序进行处理,这个特性可以使系统用于实现分布式排队的锁,队列和其他重要操作。

单个节点服务器无法判断其他服务器是否实际关闭,所以当它自己在配置好的超时时间内,无法连接到仲裁节点时,服务器就会断开所有客户端会话。这种无法连接的原因也可能是由于网络分区而刚刚与它们断开离,因此整个集群并无法保证所有的节点状态都是一样的。所以尽管有一些服务器节点出现故障,但只要整数的一半以上的节点可用,集群就可以继续服务。当故障服务器恢复在线时,它将与集群的其余部分同步并可以恢复服务。

最好使用奇数个服务器运行ZooKeeper集群,比如三,五或七。例如,如果您运行五个服务器而三个服务器已关闭,则集群将不可用(因此您可以关闭一个服务器以进行维护,并且仍能在意外故障中继续运行)。但是,如果运行六台服务器,则在三次故障后集群仍然不可用,但三次同时发生故障的可能性现在略高。还要记住,当添加更多服务器时,您可能能够容忍更多故障,但也可能开始具有较低的写入吞吐量。(Apache的文档 很好地说明了各种ZooKeeper集群大小的性能特征。)

3. 安装ZooKeeper

ZooKeeper依赖Java环境,(当然,客户端是可以支持其他几种语言的)。从ZooKeeper发行版本页面下载好相应版本(目前是3.3.0),并解压

1
2
3
4
$ wget http://mirrors.estointernet.in/apache/zookeeper/zookeeper-3.3.0/zookeeper-3.3.0.tar.gz
$ tar -zxvf zookeeper-3.3.0.tar.gz
$ cd zookeeper-3.3.0/conf
$ touch zoo.cfg

我们创建了一个配置文件 zoo.cfg ,内容如下:

1
2
3
4
tickTime=2000  
dataDir=/Users/ivan/zookeeper/data
dataLogDir=/Users/ivan/zookeeper/logs
clientPort=4180

参数说明:

  • tickTime: ZooKeeper中使用的基本时间单位, 毫秒值.
  • dataDir:数据目录,可以是任意目录
  • dataLogDir: log目录,同样可以是任意目录,如果没有设置该参数,将使用和dataDir相同的设置
  • clientPort:监听的端口号

执行启动脚本:

1
$ ./bin/zkServer.sh start

执行结果如下:

1
2
3
ZooKeeper JMX enabled by default
Using config: /root/DevTools/zookeeper-3.3.0/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

表示一个单机版的 ZooKeeper 启动成功,ZooKeeper 支持集群和伪集群模式启动。生产模式一般为集群模式以保持高可用,伪集群模式通常用于本地开发测试。安装方法都比较简单,今天主要整理一些常见的场景和命令,这里就不演示其它的安装过程了。

4. ZooKeeper CLI

ZooKeeper附带了一个用于交互式使用的命令行客户端,但实际上您可以 直接从应用程序中使用其中一种编程语言绑定。我们将演示将ZooKeeper与命令行客户端一起使用的基本原则。

执行 ./bin/zkCli.sh 后,看见如下的输出即表示已经连接上了 ZooKeeper

1
2
3
Welcome to ZooKeeper!

[zk: localhost:2181(CONNECTED) 0]

注意:这里省略了其它的一些输出

在执行命令时,某些日志消息可能隐藏了初始提示,因此如果您没有看到它,只需按<ENTER>即可。尝试输入ls /help查看其他可能的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[zk: localhost:2181(CONNECTED) 2] ls /
[cluster, controller_epoch, brokers, zookeeper, admin, isr_change_notification, consumers, log_dir_event_notification, latest_producer_id_block, config]
[zk: localhost:2181(CONNECTED) 3] help
ZooKeeper -server host:port cmd args
stat path [watch]
set path data [version]
ls path [watch]
delquota [-n|-b] path
ls2 path [watch]
setAcl path acl
setquota -n|-b val path
history
redo cmdno
printwatches on|off
delete path [version]
sync path
listquota path
rmr path
get path [watch]
create [-s] [-e] path data acl
addauth scheme auth
quit
getAcl path
close
connect host:port

这类似于类UNIX系统中的shell和文件系统。ZooKeeper将其数据存储在znode结构中。每个znode都可以包含数据(如文件)并拥有子节点(如目录)。ZooKeeper旨在处理每个znode中的小块数据:默认限制为1MB

5. 读写数据

5.1 创建——create

前面说了,ZooKeeper 的数据结构类似于文件系统,所以创建znode就像创建一个目录或者文件一样简单。比如如下的命令创建一个空的节点作为父目录,另一个节点作为其子目录:

1
2
3
4
[zk: localhost:2181(CONNECTED) 6] create /zk-demo ''
Created /zk-demo
[zk: localhost:2181(CONNECTED) 7] create /zk-demo/node-1 'Hello World'
Created /zk-demo/node-1

然后我们再通过 ls命令查看一下我们刚刚创建的节点:

1
2
3
4
[zk: localhost:2181(CONNECTED) 8] ls /
[cluster, controller_epoch, brokers, zookeeper, zk-demo, admin, isr_change_notification, consumers, log_dir_event_notification, latest_producer_id_block, config]
[zk: localhost:2181(CONNECTED) 9] ls /zk-demo
[node-1]

5.2 读取数据——get

节点创建成功后,可以使用get命令读取这些节点的内容。节点中包含的数据打印在第一行,之后列出元数据(<metadata>),注意它的 dataVersion 属性,这个属性表示版本信息,我们一会再要查看这个属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
[zk: localhost:2181(CONNECTED) 10] get /zk-demo/node-1
Hello World
cZxid = 0x9b
ctime = Sat Apr 13 03:01:24 UTC 2019
mZxid = 0x9b
mtime = Sat Apr 13 03:01:24 UTC 2019
pZxid = 0x9b
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 0

5.3 修改——set

当然,您可以在创建后修改节点,修改的过程也非常简单,重要设置一下即可。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[zk: localhost:2181(CONNECTED) 11] set /zk-demo/node-1 'Yesterday once more'
cZxid = 0x9b
ctime = Sat Apr 13 03:01:24 UTC 2019
mZxid = 0x9c
mtime = Sat Apr 13 03:06:19 UTC 2019
pZxid = 0x9b
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 19
numChildren = 0
[zk: localhost:2181(CONNECTED) 12] get /zk-demo/node-1
Yesterday once more
cZxid = 0x9b
ctime = Sat Apr 13 03:01:24 UTC 2019
mZxid = 0x9c
mtime = Sat Apr 13 03:06:19 UTC 2019
pZxid = 0x9b
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 19
numChildren = 0

请注意,dataVersion 值已被修改(以及修改后的时间戳)。

5.4 删除——delete

当然,你也可以删除节点。无法删除有孩子的节点(除非他们的孩子先被删除)。删除还有一个rmr命令,用于递归删除节点以及节点下的所有孩子节点,可用于代替delete命令,如果发现指定节点拥有子节点时, rmr命令会首先删除子节点。

1
2
3
[zk: localhost:2181(CONNECTED) 13] delete /zk-demo/node-1
[zk: localhost:2181(CONNECTED) 14] get /zk-demo/node-1
Node does not exist: /zk-demo/node-1

6. 顺序节点和临时节点

除了标准的节点类型,还有两种特殊类型的节点:顺序节点(sequential)临时节点(ephemeral)。您可以通过分别将-s-e标志传递给create命令来创建它们。

6.1 顺序节点——sequence

创建顺序节点时,我们只需要指定名称前缀,ZooKeeper 在创建时会自动给我们添加顺序号,并且ZooKeeper保证同时创建的两个节点将不会被赋予相同的数字。

1
2
3
4
[zk: localhost:2181(CONNECTED) 0] create -s /zk-demo/fruit one
Created /zk-demo/fruit0000000001
[zk: localhost:2181(CONNECTED) 1] create -s /zk-demo/fruit two
Created /zk-demo/fruit0000000002

注意,我们使用了相同的 path(/zk-demo/fruit) 来创建节点,ZooKeeper 自动为我们添加了0000000001的序号后缀。

顺序节点是一个非常好的特性,比如我们可以用它来创建分布式互斥锁。如果客户端想要保留互斥锁,则会创建一个顺序节点。如果它是具有该名称的最小数字节点,则它保持锁定。如果没有,它等待。要释放互斥锁,它会删除该节点,允许下一个节点来保持锁定。

6.2 临时节点——ephemeral

临时节点是相对于持久化节点(默认创建的节点就是持久化节点)而言的。它会和客户端会话绑定,如果创建该节点的session结束了,该节点就会被自动删除。节点不能拥有子节点. 虽然临时节点与创建它的session绑定,但只要该该节点没有被删除,其他session就可以读写该节点中关联的数据。使用-e参数指定创建临时节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[zk: localhost:2181(CONNECTED) 2] create -e /zk-demo/item1 fromOne
Created /zk-demo/item1
[zk: localhost:2181(CONNECTED) 3] ls /zk-demo
[item1, fruit0000000001, fruit0000000002]
[zk: localhost:2181(CONNECTED) 4] ls /zk-demo/item1
[]
[zk: localhost:2181(CONNECTED) 5] get /zk-demo/item1
fromOne
cZxid = 0xa4
ctime = Sat Apr 13 03:27:09 UTC 2019
mZxid = 0xa4
mtime = Sat Apr 13 03:27:09 UTC 2019
pZxid = 0xa4
cversion = 0
dataVersion = 0
aclVersion = 0
# 注意这里记录了临时节点的所有者,如果该客户端与服务器断开连接,这个节点将会被自动删除
ephemeralOwner = 0x100030399c90005
dataLength = 7
numChildren = 0
[zk: localhost:2181(CONNECTED) 6]

注意:严格来说,顺序节点并不能算是一个类型,它和临时节点也并不冲突,一个节点可以同时是顺序节点临时节点

您可以通过使临时顺序节点来实现一个非常简单的选举系统。当创建它们的客户端断开连接时,会自动删除临时节点(这意味着ZooKeeper也可以帮助您进行故障检测 - 分布式系统中的另一个难题)。客户端可以在关闭时故意断开连接,或者可以认为集群已断开连接,因为客户端超出了配置的超时而未发送心跳。创建编号最小的临时顺序节点的节点采用“Master”角色。如果计算机崩溃,或者JVM暂停太长时间以进行垃圾收集,则会删除临时节点,并且下一个符合条件的节点可以占据其位置。

7. 监听器——Watch

ZooKeeper还可以通知您节点内容的变化或节点孩子节点的变化。这个是通过在节点的数据上注册Watch来实现的,在使用getstat命令来访问当前内容或元数据时,添加Watch参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[zk: localhost:2181(CONNECTED) 6] create /zk-demo/watch-this
[zk: localhost:2181(CONNECTED) 7] create /zk-demo/watch-this 'Watch Test'
Created /zk-demo/watch-this
[zk: localhost:2181(CONNECTED) 8] get /zk-demo/watch-this true
Watch Test
cZxid = 0xa5
ctime = Sat Apr 13 03:34:36 UTC 2019
mZxid = 0xa5
mtime = Sat Apr 13 03:34:36 UTC 2019
pZxid = 0xa5
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 0

我们在执行 get 命令时,添加一个 true 参数,表示除了要获取节点信息外,我还要监控这个节点的变化。之后我们修改这个节点的内容(从当前的ZooKeeper客户端或再开启一个,或者通过程序 API 来修改都是一样),您将看到写入终端的以下消息:

1
2
3
4
[zk: localhost:2181(CONNECTED) 13]
WATCHER::

WatchedEvent state:SyncConnected type:NodeDataChanged path:/zk-demo/watch-this

注意:这个 Watcher 只会生效一次,就是说它只监听一次修改,后续的修改它将收不到事件消息。如果您希望将来收到更改通知,则必须在每次触发时重置 WatcherWatcher允许您使用ZooKeeper实现基于事件的异步系统,并在ZooKeeper中的数据本地副本过时时通知节点。

8. 版本控制和ACL

如果查看以前命令中列出的元数据,您将看到许多文件系统中常见的项目以及上面讨论过的功能:创建时间,修改时间(和相应的事务ID),内容大小(以字节为单位),和节点的创建者(如果是临时节点)。您还将看到一些有助于保护数据完整性和安全性的功能元数据:数据版本控制和ACL。例如:

1
2
3
4
5
6
7
8
9
10
11
12
Watch Test
cZxid = 0xa5
ctime = Sat Apr 13 03:34:36 UTC 2013
mZxid = 0xa9
mtime = Sat Apr 13 03:36:08 UTC 2013
pZxid = 0xa5
cversion = 0
dataVersion = 3
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 10
numChildren = 0

每次读取或写入时都会提供当前版本的数据,也可以将版本信息作为写入命令(测试和设置操作)的一部分,这样如果尝试使用指定的过期版本进行写入,则会失败(这有助于确保您不会覆盖客户端尚未处理的更改)。

ZooKeeper还支持使用访问控制列表(ACL)和各种身份验证系统。ACL允许您指定细粒度的权限,以定义允许创建,读取,更新或删除每个节点的用户和组。这里就不分析了,如果你感兴趣的话,可以到它的官网去查阅ZooKeeperAccessControl

9. 其它

ZooKeeper支持各类语言的客户端,上述所有机制都可以通过各种编程语言客户端来完成,通过 API 编写更好的分布式应用程序。几个Hadoop项目已经在使用ZooKeeper协调集群并提供高可用性的分布式服务,最著名的就是Apache HBase,它使用ZooKeeper来跟踪主服务器,区域服务器以及分布在整个集群中的数据状态。

以下是ZooKeeper可能对项目有用的其他一些示例,您可以在此处找到许多这些用例所需的算法的详细信息 。

    • 分组与命名服务

      通过让每个节点为自己注册一个临时节点(以及它可能正在履行的任何角色),您可以使用ZooKeeper替代集群中的DNS。关闭的节点会自动从列表中删除,并且您的集群始终具有活动节点的最新目录。
      
      • 分布式互斥锁和 Leader 选举

        我们在上面讨论了与顺序节点相关的ZooKeeper的这些潜在用途。这些功能可以帮助您在集群中实现自动故障转移,协调对资源的并发访问,以及安全地在集群中做出其他决策。

      • 异步消息传递和事件广播

        如果吞吐量是主要考量的时候,其他工具更适合于消息传递,但我发现ZooKeeper对于在构建一些简单的pub/sub系统非常有用。

      • 集中的配置管理

        使用ZooKeeper存储配置信息有两个主要好处。首先,只需要告知新节点如何连接到ZooKeeper,然后可以下载所有其他配置信息并确定它们应该在集群中自己扮演的角色。其次,您的应用程序可以订阅配置中的更改,允许您通过ZooKeeper客户端调整配置并在运行时修改集群的行为。

10. 常用命令

对于服务管控而言,有如下的常用命令:

  1. stat: 查看ZK结点follower或leader情况。
  2. srst: 重置通过stat查看的统计结果信息。
  3. ruok: 测试Server,若回复imok表示已经启动。
  4. dump: 列出未经处理的会话和临时节点。
  5. kill: 关掉server, 必须在ZK服务运行的机器上执行。
  6. conf: 输出相关服务配置的详细信息。
  7. cons: 列出所有连接到服务器的客户端的完全的连接 / 会话的详细信息。
  8. crst: 重置 connection/session 所有连接的统计信息。
  9. envi: 输出关于服务环境的详细信息。
  10. reqs: 列出未经处理的请求。
  11. wchs: 列出服务器 watch 的详细信息。
  12. wchc: 通过 session 列出服务器 watch 的详细信息,它的输出是一个与 watch 相关的会话的列表。
  13. wchp: 通过路径列出服务器 watch 的详细信息。它输出一个与 session 相关的路径。
  14. srvr: 列出服务的详细信息。
  15. mntr: 打印所有可以用来监控集群服务健康状态信息的变量。

对于客户端操作,有如下的常用命令:

进入zkCli后敲入help,或者随便敲几个字符ZK会打印出如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
get path [watch]
ls path [watch]
set path data [version]
rmr path
delquota [-n|-b] path
quit
printwatches on|off
create [-s] [-e] path data acl
stat path [watch]
close
ls2 path [watch]
history
listquota path
setAcl path acl
getAcl path
sync path
redo cmdno
addauth scheme auth
delete path [version]
setquota -n|-b val path

注:path 要以 / 打头;使用监听如:get /t watch