zookeeper从入门到精通

1. 概念

1.1 什么是ZooKeeper?

ZooKeeper是给分布式应用使用的高性能协调服务。它可以提供以一些简单的接口提供常见的功能,类似于:配置管理、注册中心、命名服务、同步、分组管理。因此你不用再从0开始重新写编写这些代码了。

zookeeper的特点:

简单:通过一个类似文件系统的共享命名空间就可以让分布式应用与各个节点进行协调。命名空间由znode组成。它比较像文件或者目录。但是又和文件系统不一样,它们是保存在内存中的,这意味着zookeeper可以有一个很高的吞吐量和很低的延迟。

zookeeper注重于高性能、高可用性、严格顺序访问。高性能意味着它能被用于大型的分布式系统之中。高可用则让它避免了单点故障。而严格顺序意味着能在客户端实现复杂的同步。

有副本的:zookeeper会将数据复制到多个主机。多个主机上的zookeeper必须相互知晓各个节点的信息。他们在内存中维护状态,在持久化设备上维护事务日志和快照。只要大多数服务可用,那么zookeeper就可以持续提供服务。

客户端会连接到其中一个zookeeper的server。client会维护一个tcp连接,并且通过这个tcp连接来发送请求、获得返回信息、监听事件,发送心跳。如果tcp连接断开了,客户端会尝试连接其他zookeeper。

zookeeper集群架构

有序:zookeeper会给每一个update记录一个数字(事务版本号),这个数字反映了整个zookeeper的事务顺序。接下来的操作可以使用这个数字来实现更高层面的抽象,比如同步原语

什么是原语?

https://www.cnblogs.com/hualalasummer/p/3704225.html

:特别是读请求占主导地位的时候。zookeeper运行在数千台服务器上,当读请求大于写请求时,比例在10:1d的时候,速度最快。

1.2 数据模型和有层次的命名空间

zk提供的命名空间很像标准的文件系统。一个命名空间就是一系列的路径元素通过反斜杠(/)构成。在zk命名空间中每一个节点(node)由唯一的路径(path)标识。就像这样:

zookeeper有层次的命名空间

1.3 持久节点和零时节点

和标准的文件系统不一样,在zk的命名空间中每个节点都可以保存数据同时拥有子节点。就像一个文件系统,它允许一个文件拥有目录的特点。(zk被设计用于存储协调数据:状态信息、配置、地址信息,等等。因此储存在每个节点中的内容通常很小,大概是byte到kilobyte这个范围)我们是使用名词”znode“来表示zookeeper中的数据节点。

默认情况下我们创建的是持久节点。zk有会有一个临时节点的概念。当创建这个临时节点的客户端不再活跃时,零时节点会被自动删除。

一个节点中除了存储协调数据,本身还会保存很多元信息。

字段 释义
cZxid 创建时的事务id
ctime 创建时间
mZxid 最后一次修改时的事务id
mtime 最后一次修改时间
pZxid 该节点的子节点列表最后一次修改时的事务id,只有子节点列表变更才会更新pZxid,子节点内容变更不会更新
cversion 子节点版本号,当前节点的子节点每次变化时值增加1
dataVersion 数据节点内容版本号,节点创建时为0,每更新一次节点内容(不管内容有无变化)该版本号的值增加1
aclVersion 节点的ACL版本号,表示该节点ACL信息变更次数
ephemeralOwner 创建临时节点的会话id
dataLength 节点存储的数据长度
numChildren 子节点数量

1.4 监视节点

zk支持对节点(znode)的监视(watches)。你首先需要对一个节点注册一个监视事件。当这个节点的内容发生变化或者内容有改动只会,这个监视的客户端会收到一个通知,说你监视的这个节点发生了变动。一旦监听事件被触发,就不会监听就会被移除。换句话说,监听是一次性的,如果下次还需要监听,就必须重新创建一个监听事件。不过在zk的3.6.0版本之后,允许创建持久的、递归的(监听所有子节点)监听。

1.5 保证

zk提供以下保证:

  • 顺序一致性:命令会按照客户端的发送顺序执行。

    这篇文章解释了顺序一致性,和数据写入zk的过程。

  • 原子性:更新要么成功,要么失败。不会有部分更新结果。

  • 单一视图:无论这个客户端连接的是集群中哪台服务器,他们看到的视图都是一样的。也就是说,一个客户端永远都不会看到一个过时的视图,即使这个客户端在同一个会话中因为故障转移连接到了其他服务器。

    Zookeeper 集群由多个节点构成,写入数据时只要多数节点确认就算成功,那些没有确认的节点此时存放的就是老数据。(潜台词:zk是最终一致性)Zookeeper 的“单一视图(single system image)”保证说的是客户端如果读到了新数据,则再也不会读到老数据。(潜台词:单一视图不保证你读到的是最新数据, 但是保证你读到的数据是全局一致的)

    如果重新连接连上了老的节点,老的节点会拒绝新客户端的连接,客户端会寻找其他的节点尝试连接。

    这个特性是通过建立连接时传递zxid,服务端校验zxid来判断是否允许客户端连接做到的。具体可以看这篇文章

  • 可靠性:一旦更新被应用,它就会持续存在。直到一个客户端重写了这个更新。

  • 实时性:客户端的视图保证在一定时间内能获取最新的(最终一致性)。

1.6 简单的API

zk一个主要设计目标就是提供超级简单的编程接口。因此我们只支持下面这些操作:

  • create : 在树的一个位置创建一个节点。
  • delete : 删除一个节点。
  • exists : 查看指定位置的节点是否存在。
  • get data : 从节点中获取数据。
  • set data : 向节点中写数据。
  • get children : 获取这个节点的子节点。
  • sync : 等待数据在集群中广播完毕。

1.7 zk的消息同步

下面这张图从高维度视角展示了zk的构成组件。 除开Request Processor,其他几个组件都是可以被复制的。

zk的组件

Replicated Database是一个内存数据库,它包含了所有的树的数据。所有的更新操作都会被记录到硬盘以备恢复之用。所有的数据在写到内存之前会先序列化到硬盘。

每一个zk的客户端都只会精确的连接到一个zk服务器,读请求会直接在这个服务器就地执行,而写请求通过一致性协议执行。

作为一致性协议规定的一部分,写请求会从客户端转发到一个叫做leader的服务器执行。集群中的其他服务器叫做followers(另外还有一个角色叫observer,它也会同步leader的数据,但是不参与选举)。followers从leader接收数据同步信息。具体同步数据的过程是一个两阶段提交。

  • 第一阶段:leader收到写请求,将数据写到硬盘,给自己返回一个ack。再将这个消息同步给所有的follower。收到同步消息的follower也会将数据从写到硬盘,然后返回一个ack给leader。
  • 第二阶段:等待超过半数的节点返回ack(包括leader自己),leader发起一个commit请求,要求所有的节点(包括它自己)把数据写入内存中(写入数据正式生效)。

1.8 zk的选举

zk选举过程可以参考下面这张图和这篇文章

需要注意的是,投票箱并不是全局共享的,每个server单独维护自己的投票箱。因此当一个server投票之后,需要将自己投的票广播出去,这样自己的投票箱中的票才是准确的(投票箱中有所有server已经投过的票)。而且自己投过的票可以删除重投。每次收到别人投的票之后,都需要重新检查自己是不是要重新投票(重新考虑最好的选票)。

zk选举leader

2 实操

2.1 集群的搭建

我们现在创建一个由3个zk server组成的zk集群。

第一步:安装好jdk环境,目前只支持jdk1.8或更高版本 (JDK 8 LTS, JDK 11 LTS, JDK 12,但 Java 9 和 10 不支持)。

第二步:调整好堆内存大小,避免使用虚拟内存,否则会极大的降低zk的性能。

第三步:zk官网中下载安装包,并解压到/usr/local目录下。

第四步:编辑配置文件,将/usr/local/apache-zookeeper-3.8.0-bin/conf/zoo_sample.cfg复制一份重命名为zoo_1.cfg,并开始修改这个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 基本时间单位(毫秒),用于心跳和超时的计算,比如最小的会话超时时间是两次tick。
tickTime=2000
# follower在进行初始化时,follower连接和同步leader数据的最大时间(按tickTime计算,也就是tickTime * initLimit),如果要同步的数据很大,这个值可以响应的调大。
initLimit=5
# 允许follower与leader数据不同步的最大时间,如果follower落后leader太多,在规定时间内无法同步完成,这个follower会被抛弃。
syncLimit=2
# 数据存储目录,如果没特别指定日志目录(dataLogDir)的话(单独指定日志目录到专用硬盘能提高zk的吞吐量和降低延迟),事务日志也保存在这里。
dataDir=/var/lib/zookeeper/zoo_1
# 客户端连接端口
clientPort=2181
# 所有的集群节点的位置信息。
# 格式为:server.<myid>=<hostname>:<leaderport>:<electionport>
# myid:zk服务的唯一标识,在dataDir目录下创建一个名为myid的文件,文件内容是一个1-255的范围内数字,标识这个zk(除了数字不需要任何额外的内容)。
# hostname:zk服务的地址,可以是ip也可以是域名
# leaderport:follower用于连接leader的端口
# electionport:用于集群中选举的端口
server.1=127.0.0.1:28888:38888
server.2=127.0.0.1:28889:38889
server.3=127.0.0.1:28890:38890

第五步:在dataDir目录下创建myid文件,并且写入zk的唯一标识数字。

第六步:重复步骤四和步骤五,为其他两个zk server创建不同的dataDir和myid,并在各自的配置文件中修改对应的位置(如果部署在同一台机器,clientPort也要改,否则会冲突)。

第七步:进入bin目录,执行启动命令。

1
2
3
./zkServer.sh start ../conf/zoo_1.cfg
./zkServer.sh start ../conf/zoo_2.cfg
./zkServer.sh start ../conf/zoo_3.cfg

第八步:查看运行状态

1
./zkServer.sh status ../conf/zoo_1.cfg

2.2 客户端操作zk服务(zkCli)

zkCli是zk自带的客户端,进入bin目录下可以找到zkCli.sh来连接zk server集群。如果需要集成到Java环境,可以使用curator

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

正如前文描述的那样,zk内部相当于是一个文件系统的实现,那么我们所有的操作都是和文件系统类似的。我们可以在进入zk命令行之后,输入help,它会展示出所有可以执行的命令。下面只列出常见的一些命令,要查看所有的命令的用法可以查阅官方文档

2.2.1 create

create可以创建节点,它的命令格式为:

1
2
3
4
5
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
#-s 序列节点(sequential node)
#-e 零时节点(ephemeral node)
#-c 容器节点
#-t 带生存时间的节点
  • 持久节点

    持久节点一旦创建就会一直存在,create方法默认创建持久节点。

    1
    create /cluster
  • 临时节点

    如果创建零时节点的那个会话断开,零时节点就会被自动删除。临时节点不可拥有子节点。

    1
    2
    3
    create -e /temp-node
    #创建节点的同时,往节点里面写内容
    create -e /cluster/order 127.0.0.1:8080
  • 持久序列节点

    创建文件之后,会自动给文件名追加一个自增的序号。比如我们创建一个/book序列节点,它实际上创建的是/book0000000002

    1
    create -s /book

    序列节点

  • 零时序列节点

    1
    create -e -s /cup
  • 容器节点

    当容器节点最后一个子节点被删除之后,容器节点也会被自动删除。

    1
    2
    3
    4
    5
    6
    create -c /container
    create /container/sub1
    create /container/sub2
    delete /container/sub1
    delete /container/sub2
    # 此时/container也被自动删除了
    2.3 delete

删除节点,如果节点有子节点,则会拒绝执行。

1
2
create /map
delete /map
2.4 deleteall

删除节点,如果节点有子节点,则一并删除。

1
2
3
create /cluster
create -e /cluster/order 127.0.0.1:8080
deleteall /cluster
2.5 get

获取节点的内容或元数据,而且还能监听节点。

1
2
3
get [-s] [-w] path
# -s 展示节点的元信息
# -w 给节点添加监听事件

获取元信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create /pic myson
# 元信息包括以下内容
myson
cZxid = 0x100000021
ctime = Thu Apr 14 11:49:42 CST 2022
mZxid = 0x100000021
mtime = Thu Apr 14 11:49:42 CST 2022
pZxid = 0x100000021
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

监听节点变化:

1
2
create -w /pic
#开启另外一个ssh连接,删除这个/pic节点,这个监听的ssh连接就会收到消息。
2.6 set

设置节点的内容

1
set /pic mymom
2.7 ls

展示指定目录下的节点列表

1
ls /cluster

3 应用

3.1 配置中心

把配置保存在znode节点中,客户端通过watch机制监听znode中内容变化,一旦内容发生变化,就拉去最新的配置。

3.2 注册中心

要实现注册中心一般可以分为3层:/集群/服务/实例

第一层:集群,持久节点。

第二层:服务,持久节点。

第三层:实例,临时序列节点, 把实例的元信息保存到这个节点。

核心的实现在第三层,当启动一个新实例时,创建一个临时序列节点。一旦这个新实例离线,临时序列节点将会被自动删除。

对于客户端来说,可以使用watch来监听服务节点,一旦服务节点下的实例有变动,可以马上感知到。

3.3 分布式锁

通过临时序列节点实现,具体参考这篇文章