深度剖析 ZooKeeper 核心原理 学习笔记

什么是 ZooKeeper

假设对 ZooKeeper 中的数据做了变更(比如新增了一台 Kafka 或者挂掉了一个 Kafka 节点),这时候 ZooKeeper 会主动通知其他监听这个数据的客户端,立即告诉其他客户端说这份元数据有变更。

ZooKeeper 的设计十分巧妙,它的主动通知机制采取的是 Watcher 机制,至于什么是 Watcher 机制,后面文章会详细地剖析其思想和源码。

知道了 ZooKeeper 的应用场景后,再来想想目前哪些流行框架使用了 ZooKeeper

  • Dubbo:非常流行的 RPC 框架,它就采取了 ZooKeeper 作为注册中心,来管理分布式集群的元数据存储。当然也可以采取 Nacos 作为注册中心。
  • Kafka:消息中间件,它采取了 ZooKeeper 做分布式集群的元数据存储以及分布式协调功能。
  • HBase:大数据领域的技术,它也采取了 ZooKeeper 做分布式集群的元数据存储。
  • HDFS:大数据领域的技术,它采取了 ZooKeeper 做 Master 选举实现 HA 高可用的架构。
  • Canal:ETL 工具,监控 binlog 做数据同步,它采取了 ZooKeeper 做分布式集群的元数据存储,也用 ZooKeeper 做 Master 选举实现 HA 高可用的架构。
  • ……

多说一句,其实不管是 RPC 也好,消息中间件也罢,它们都需要注册中心,只是技术选型到底该用哪个的事情。比如,Kafka 用了 ZooKeeper,但是 RocketMQ 就用了自研的 Nameserver;再比如,SpringCloud 的 Feign 就采取了 Eureka 和 Nacos。

那么 ZooKeeper 和 Eureka/Nacos 有什么区别呢?

当然有很多区别,但是我这里只介绍一个:ZooKeeper 是 CP,而 Eureka 是 AP,Nacos 既可以是 AP 又可以是 CP。那什么是 CP?什么又是 AP?

我们的 ZooKeeper 集群不是 Leader 负责写,写成功后不是同步到各个 Follower 从节点吗?那么问题来了,如果这时候 Leader 挂了,Follower 会进行选举,但是选举也需要时间的,选举过程中如果进来了读写请求,那么是无法进行的。所以会有部分流量的丢失,这就是所谓的 CP 模型用服务的可用性来换取数据的相对强一致性。

再比如:一个集群 5 个节点,按照过半原则来讲,那么 3 个节点是半数以上,假设集群内挂了 3 台,只保留了 2 台存活节点,那么这时候集群也是无法提供读写请求的,因为不符合过半原则了,这也是 CP 的特征之一。

这样有什么问题?很明显,整个集群的可用性相对较低,因为假设我就 3 个节点,那么挂了 2 个后其实还有 1 个存活,这个存活的节点却不能提供服务。

ZooKeeper 特点

我们在上文中已经说了 ZooKeeper 是分布式的注册中心、分布式的协调中心。只要是分布式,就会有几个通用的特点,比如:一致性、实时性、高可用性等。那 Zookeeper 这个中间件都实现了哪些特点呢?接下来我们逐个分析。

第一个特点就是支持分布式集群部署

第二个特点是顺序一致性。什么叫顺序一致性?就是客户端发送的每一次请求到 ZooKeeper 的时候都是有序的,在整个集群内也是有序的。

比如:client1 先发送了一个请求到 ZooKeeper,client2 也发送了一个请求,那么这两个请求在 ZooKeeper 服务端处理的时候是有序的,且 Leader 同步给其他 Follower 的时候也是有序的。ZooKeeper 是如何实现这个特点的呢?ZooKeeper 给每个请求都编了一个号,比如第一次请求就叫 zxid-1,第二次请求就递增,叫 zxid-2……以此类推。可参考下面的示意图:

image.png

第三个特点是原子性,要么写入成功,要么失败。这里指的是一次请求在整个分布式集群中具备原子性,即要么整个集群中所有 ZooKeeper 节点都成功地处理了这个请求,要么就都没有处理,绝对不会出现集群中一部分 ZooKeeper 节点处理了这个请求,而另一部分节点没有处理的情况。

第四个是可靠性,如果某台机器宕机,要保证数据绝对不能丢失。这里又分为两种情况。

  • 第一种情况:如果同步给 Follower 后,Leader 挂机了怎么办?这种简单呀,Follower 选举为 Leader 就好啦,反正数据同步给 Follower 了。

  • 第二种情况:如果 Leader 收到半数以上的 Follower 的 ack 了,自己先写入了 znode,然后还没发 Commit 呢,这时候宕机了,怎么办?这时候会先选举 Leader,选举规则是 zxid 最大的优先被选举,然后当老 Leader 恢复后会和新 Leader 对比,发现多了一条无用消息(宕机之前写入 znode 的那条),这时候会丢掉这个消息,且自己退位为 Follower 重新和新 Leader 同步数据。

第五个是实时性,这个很好理解了,也就是说一旦数据发生变化,那要及时通知给客户端。这个采取的是 Watcher 监听机制。

ZooKeeper 角色

第一种角色,Leader。那 Leader 是干嘛的?看这个名字就知道很牛了,领导者。Leader 是核心,也就是分布式系统中最常用的 Master。Leader 提供读写能力,且写请求只能由 Leader 来完成;每一次写请求都会生成一个 zxid,然后将此请求同步给各个 Follower 和 Observer,这个 zxid 也是决定顺序一致性的关键所在(顺序一致性我们前面已经分析过了)。

一言以蔽之:Leader 是集群之首,读写都可以提供,且写请求只能由 Leader 来完成,也负责将写请求的数据同步给各个节点

第二种角色,Follower。Follower 是分布式系统中常说的从节点,它只提供读请求的能力;如果写请求到 Follower 上了,那么 Follower 会将此请求转发到 Leader,由 Leader 来完成写入,再同步给 Follower。Follower 不只提供读能力,还额外负责选举,也就是比如 Leader 挂了的话,Follower 是有资格成为 Leader 的。

一言以蔽之:Follower 是从节点的概念,仅提供读能力,且有资格参与选举

第三种角色,Observer。这个角色我之前从未提及过,那是它不重要吗?错,它太重要了!它可以直接提升我们 ZooKeeper 集群的并发读能力。Observer 也只提供读能力,它和 Follower 的差别在于 Observer 没资格竞争 Leader 的选举,也就是 Leader 挂了的话,Observer 是不会被选举为 Leader 的,并且也没资格给选举投票。

它只会接收 Leader 的同步数据,然后提供给读请求。为了提升高并发读能力,肯定是不能太多 Follower 的,因为每次写操作都要同步到 Follower,所以 Follower 一般 3~4 个就好了,多增加 Observer 来提升读的并发能力。但是写的 QPS 能力是无法提升的,只有 Leader 来写,同步到其他节点。

一言以蔽之:Observer也是从节点的概念,仅提供读能力,但是没有资格参与选举。这是和 Follower 的差别所在。合理利用 Observer 可以提供集群的读并发能力。

follower会参与Leader选举。如果一百个Follower节点,那么会拖慢Leader选举的时间,假设Leader挂了,那么也就会降低服务可用性。

下面用一张简图总结下这三种角色:

image.png

ZooKeeper 节点类型

可以将节点理解成 ZooKeeper 存储数据的数据结构。ZooKeeper 把数据都存储到内存里了,数据格式为 znode,至于 znode 长什么样我们先不关心,只需知道 ZooKeeper 的节点就是 znode,znode就是存数据的地方。这就够了。

那再来看什么是节点类型。ZooKeeper 有两种节点类型:持久节点和临时节点。

  • 持久节点:即使客户端断开连接,那么此节点也一直存在。
  • 临时节点:如果客户端断开连接,此时它之前创建的临时节点就会自动消失、自动删除掉。所以说,如果客户端采取 ZooKeeper 的 watch 机制监听了这个临时节点,那么这个临时节点一旦被删除,客户端就会立即收到一个“临时节点已删除”的通知,这时候就可以处理一些自己的业务逻辑。

那什么时候用持久节点?什么时候用临时节点呢?

  • 如果你是做元数据存储,那肯定是持久节点。比如 Kafka,用 ZooKeeper 来维护 topic,那么连接断开后 topic 就被删除了吗?肯定不可以的!所以如果做元数据存储,建议是持久节点。

  • 如果你是做一些分布式协调和通知工作,那大多数是用临时节点的。比如,我创建一个临时节点,别人来监听这个节点的变化;如果我断开连接,那么临时节点自动删除,此时监听我这个临时节点的客户端就会立即感知到。

可以总结为一句话:持久节点就是持久化的,连接断开后数据也不会丢失;临时节点是基于内存的,连接断开后,节点就被删除了,数据也就随之丢失了

网上部分文章还提出了一个顺序节点的概念,其实这个说法从严格意义上来讲是错误的。所谓的顺序节点只是一种修饰状态,它可以跟我们前面讲的持久节点与临时节点组合成:顺序持久节点、顺序临时节点。

那何为顺序呢?举个例子:在/my/test下创建临时顺序节点,那么就会自动创建子节点,子节点自动进行编号,0000000001、0000000002……每次自增。这就是顺序。

那顺序有什么用呢?公平性可以用。比如,Curator 客户端实现公平的分布式锁就是采取的“顺序临时节点+Watcher 监听”的方式来完成的。

节点和节点类型

节点就是 znode,是 ZooKeeper 保存数据的地方

这里我们重新给它下个定义:ZooKeeper 会维护一个具有层次关系的数据结构,这个数据结构和标准文件系统的名称空间非常相似,类似于一个分布式文件系统,每个节点都是一个路径作为唯一标识,每个节点都可以多层级,且可以包含数据,也就是说每个节点都可以由多层节点+数据组成。其数据部分叫作数据节点 znode,每个 znode 大小有限,可以存储 1MB 数据。

我们先看下图:

znode.png

上图中每个圆圈和每个六边形都叫节点,/,/app1,/app2属于三个根节点,/app1/p_1属于/app1节点的子节点,看起来就跟 Linux 文件系统一样。

znode-helloworld.png

一言以蔽之: ZooKeeper 的数据存储由节点组成,每个节点都可以层级嵌套,类似 Linux 文件系统一样。

在我们讲解节点类型之前,我们需要先来看一个全新的概念:Session。

我们肯定知道 HttpSession,一次请求就会产生一个 Session。那我们这里 ZooKeeper 的 Session 也是类似的道理。在 ZooKeeper 中,客户端启动的时候首先会与服务器建立一个 TCP 连接,通过这个连接,客户端可以通过心跳检测的机制与 ZooKeeper 服务器保持会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该 TCP 连接接收来自服务器的 Watcher 事件通知。

那上面所说的 TCP 连接是什么呢?它就是通过一个 Session 来维持的,服务端在为客户端创建 Session 之前,服务端首先会为每个客户端都分配一个 sessionID。无论是哪台服务器为客户端分配的 sessionID,都是全局唯一的。

当然了,它也和 HttpSession 一样,可以设置超时时间,如果在规定时间内由于某种原因导致客户端没能正常和服务端进行心跳或者通信,那么就认为这个会话失效。

好了,我们已经聊明白了 Session 的机制,我们也知道了客户端要想与 ZooKeeper 服务端建立连接,就必须创建一个 Session,所以大概是如下这样的:

session-1.png

明白了 Session 后,我们才能正式开始聊节点类型。先来看第一个持久节点

持久节点就是客户端断开连接后,持久节点也不会消失,会持久化存储。

那我们再来看第二个节点类型:临时节点

临时节点通过 -e 参数设置,然后我们断开连接,也就是 Session 消失了,最后发现临时节点没了,也就是说临时节点会随着 Session 的消失(客户端连接的断开)而消失,不具有持久化存储功能

最后再来看剩下的一个知识点:“顺序节点”

“顺序节点”会自动创建子节点,子节点自动进行编号,0000000001、0000000002,每次自增。

创建一个顺序节点(-s 参数代表顺序节点)看看效果。

[zk: localhost:2181(CONNECTED) 2] create -s /helloworld/v4
Created /helloworld/v40000000000

注意看,我们只创建一个/helloworld/v4节点,却给我们返回的是创建了/helloworld/v40000000001,自动编号了。我们再创建几个 v4 看看呢?

[zk: localhost:2181(CONNECTED) 3] create -s /helloworld/v4
Created /helloworld/v40000000001
[zk: localhost:2181(CONNECTED) 4] create -s /helloworld/v4
Created /helloworld/v40000000002
[zk: localhost:2181(CONNECTED) 5] create -s /helloworld/v4
Created /helloworld/v40000000003

现在效果很明显了,每次创建顺序节点都自动为我们创建一个带编号的节点,比如我们创建顺序节点叫 v4,那 ZooKeeper Server 就为我们自动创建了一个 v40000000000 节点来替代原有的v4,也就是自动为节点编号。

临时顺序节点。主要应用于在分布式锁的领域, 在加锁的时候,会创建一个临时顺序节点,比如:lock0000000000,这时候其他客户端在尝试加锁的时候会继续自增编号,比如lock0000000001,且会注册 Watcher 监听上一个临时顺序节点,然后如果你客户端断开连接了,由于是临时节点,所以会自动销毁你加的这把锁,那么下一个编号为lock0000000001的客户端会收到通知,然后去尝试持有锁。

创建节点肯定要记录一些信息的,比如创建时间,版本号等。那一个节点都有哪些信息呢?我们可以通过stat命令进行查看:

每个字段解释如下:

cZxid:创建znode节点时的事务ID。
ctime:znode节点的创建时间。
mZxid:最后修改znode节点时的事务ID。
mtime:znode节点的最近修改时间。
pZxid:最后一次更改子节点的事物id(修改子节点的数据节点不算)
cversion:对此znode的子节点进行的更改次数。
dataVersion:该znode的数据所做的更改次数。
aclVersion:此znode的ACL进行更改的次数。
ephemeralOwner:如果znode是ephemeral(临时)节点类型,则这是znode所有者的Session ID。 如果znode不是ephemeral节点,则该字段设置为零。
dataLength:znode数据节点的长度。
numChildren:znode子节点的数量。

如何正确退出客户端?ctrl + cquit有何区别呢?

ctrl + c不会主动通知 ZooKeeper Server,所以它不会立即让临时节点断开连接,只能等到下次心跳的时候发现断连了,才会删除临时节点。而quit是主动通知 ZooKeeper 服务端说要断开连接,会立即删除临时节点。

Server 端核心参数

先来看第一组参数:tickTime、initLimit、syncLimit

  • tickTime:ZooKeeper 服务器与客户端之间的心跳时间间隔。单位是毫秒值。也就是每隔多少毫秒发一次心跳。
  • initLimit:Leader 与 Follower 之间建立连接后,能容忍同步数据的最长时间,单位是n * tickTime,比如配置的是initLimit: 3,那就是 Leader 和 Follower 之间同步数据的最大时间是3*tickTime毫秒。假如你的 ZooKeeper 里存储的数据量已经比较大了,那么 Follower 同步数据需要的时间肯定相对较长,此时可以调大这个参数,防止超时断开同步。超时后,此次同步 Leader 就不会管这个 Follower 了。
  • syncLimit:Leader 和 Follower 之间能容忍的最大请求响应时间,单位是n * tickTime,比如配置的是syncLimit: 3,那就是 Leader 和 Follower 之间一次请求响应的最大时间是3*tickTime毫秒。如果超过3*tickTime毫秒没有心跳,那么 Leader 就把这个 Follower 给踢出去了,认为这个 Follower 已经挂掉了。

总结一个重点:initLimit 和 syncLimit 都是以 tickTime 为基准来进行设置,相当于 tickTime 是这三个里面的最小基本单位。

接下来再看第二组参数:dataDir、dataLogDir、snapCount

  • dataDir:存放 ZooKeeper 里的数据快照文件。ZooKeeper 里会存储很多的数据,内存里有一份快照,在磁盘里其实也会有一份数据的快照,用于重启、异常宕机等情况能正常恢复之前的数据。
  • dataLogDir:存储事务日志。比如:写数据的时候按照 2PC 两阶段提交 proposal,然后每台机器都会写入一个本地磁盘的事务日志,这个事务日志就存放在 dataLogDir,如果没有显示配置 dataLogDir,那么事务日志会存储在 dataDir 目录下。
  • snapCount:多少个事务生成一个快照。默认是 10 万个事务生成一次快照,快照文件存储到 dataDir 目录下。

再来看看第三组参数:端口号

我们在搭建 ZooKeeper 集群的时候都会配置两个端口号,比如如下:

server.1= ZooKeeper 01:2888:3888
server.2= ZooKeeper 02:2888:3888
server.3= ZooKeeper 03:2888:3888

那为啥是两个端口号呢?这里用通俗的语言解释下。

  • 2888 端口:用于 Leader 和 Follower 之间进行数据同步和通信的。
  • 3888 端口:用于集群恢复模式的时候进行 Leader 选举投票的,也就是说所有的机器之间进行选举投票的时候都是基于 3888 端口来的。

我们前面已经知道 Leader 和 Follower 之间通信是有超时时间配置的,现在我们又知道 3888 端口是集群内选举投票通信用的,那这个 3888 端口的作用有没有超时时间呢?肯定也是有的~在进行 Leader 选举的时候,各个机器会基于 3888 那个端口建立 TCP 连接,在这个过程中建立 TCP 连接的超时时间可以通过如下参数来设置。

  • cnxTimeout:5000,毫秒值。

其实很简单:2888 是 Leader 和其他节点进行数据同步和心跳等通信的,3888是 Leader 挂了后用于机器间通信选举投票的。

最后再来看几个参数。

我们上一篇详细讲解过 Znode,那一个 Znode 能保存的数据大小是多少呢?这个大小也可以通过如下参数来指定。

  • jute.maxbuffer:默认是 1mb。也就是 1048575 字节(bytes)。

一台机器上能建立多少个客户端与 ZooKeeper 服务端的连接呢?这个肯定不是无穷大的,也可以通过参数来配置的。

  • maxClientCnxns:默认是 60 个。假设每次请求都创建一个 ZooKeeper 客户端,跟 ZooKeeper 服务端建立连接、通信、销毁 ZooKeeper 客户端的话,那么如果并发有很多个请求一起连接 ZooKeeper 服务端,此时可能会被 ZooKeeper Server 拒绝的,因为可能超出了限制。

本篇最后一个知识点:我们知道 Follower 无法处理写请求,写请求到 Follower 上的时候会转发到 Leader 去处理,那 Leader 一定要处理吗?能不能拒绝?可以的!

  • leaderServers:yes。

服务端的几个核心参数我就说到这里,但是我们大家都知道中间件大多就配一个可视化页面,也就是管理页面来很直观地统计一些信息,比如:性能、连接数、配置等。可视化页面是前端,底层肯定要去查性能、配置、连接数等项,那怎么查呢?ZooKeeper 也为我们提供了一些运维相关的命令,一起看看吧~

四、运维相关命令

这个就很简单易懂了,我们先列举一下有哪些常用的运维相关命令。

  • conf:查看配置信息,也就是查 ZooKeeper 的 zoo.conf 配置文件。
  • cons:查看当前 Server 被哪些 Client 连接。
  • crst:重置客户端的统计信息。
  • srst:重置 Server 服务端的统计信息。
  • wchs:查看 Watcher 信息。
  • wchc:查看 Watcher 的详细信息。
  • wchp:也是查看 Watcher,但是会按照 znode 进行分组。
  • stat:查看 Server 运行时状态。
  • mntr:比上面的 stat 更为强大,mntr 的输出比 stat 更详细。
  • ruok:检查服务是否在运行。
  • dump:输出 dump 相关信息。
  • envi:查看环境变量。

知道了这些命令的含义了,那我们怎么去用呢?很简单,语法echo xxx | nc localhost 2181,比如我们演示几个。

先演示一下第一个:查看配置信息(conf)

[root@softservice ~]# echo conf | nc localhost 2181
clientPort=2181
secureClientPort=-1
dataDir=/home/xxx/devtools/apache- ZooKeeper -3.7.0-bin/data/version-2
dataDirSize=469762160
dataLogDir=/home/xxx/devtools/apache- ZooKeeper -3.7.0-bin/logs/version-2
dataLogSize=154754
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
clientPortListenBacklog=-1
serverId=0

就是我们zoo.conf里的内容,也就是 ZooKeeper 的配置文件。

我们再来看下第二个:查看当前 Server 被哪些 Client 连接(cons )

[root@softservice ~]# echo cons | nc localhost 2181
 /127.0.0.1:36886[1](queued=0,recved=219,sent=219,sid=0x1002103d4cb0003,lop=PING,est=1639801094691,to=18000,lcxid=0x13c,lzxid=0x1aa,lresp=18151419761,llat=1,minlat=0,avglat=0,maxlat=7)
 /127.0.0.1:37148[0](queued=0,recved=1,sent=0)

可以发现目前有两个客户端连接。

我们再演示一个 stat,查看 Server 运行时状态

[root@softservice ~]# echo stat | nc localhost 2181
 ZooKeeper  version: 3.7.0-e3704b390a6697bfdf4b0bef79e3da7a4f6bac4b, built on 2021-03-17 09:46 UTC
Clients:
 /127.0.0.1:36886[1](queued=0,recved=225,sent=225)
 /127.0.0.1:37160[0](queued=0,recved=1,sent=0)

Latency min/avg/max: 0/0.3386/7
Received: 265
Sent: 264
Connections: 2
Outstanding: 0
Zxid: 0x1aa
Mode: standalone
Node count: 182

可以发现里面包含 ZooKeeper 的版本号、当前有哪些客户端连接以及其他统计信息。

有了这些命令,我们还会觉得监控平台很高级吗?没有我们底层命令,它啥也不是。

posted @ 2023-02-19 21:32  Dazzling!  阅读(120)  评论(0编辑  收藏  举报