Blog

Zookeeper

Begin

介绍

ZooKeeper是一种分布式协调服务,用于管理大型主机。在分布式环境中协调和管理服务一个复杂的过程。ZooKeeper通过其简单的架构和API解决了这个问题。ZooKeeper允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。

应用场景

  • 分布式协调组件:在分布式系统中,需要有zookeeper作为分布式协调组件,协调分布式系统中的状态。
  • 分布式锁:zk在实现分布式锁上,可以做到强一致性,关于分布式锁相关的知识,在之后的ZAB协议中介绍。
  • 无状态化的实现

搭建Zookeeper服务器

命令

brew install zookeeper
brew list zookeeper
brew info zookeeper

# 前台
zkServer start  
zkServer stop
# 后台
brew services start zookeeper   
brew services stop zookeeper

zkServer status

# 连接
zkCli

# 配置文件在:/opt/homebrew/etc/zookeeper/zoo.cfg

zoo.cfg 配置文件说明

# zookeeper时间配置中的基本单位 (毫秒)
tickTime=2000
# 允许follower初始化连接到leader最大时⻓,它表示tickTime时间倍数。即:initLimit*tickTime
initLimit=10
# 允许follower与leader数据同步最大时⻓,它表示tickTime时间倍数
syncLimit=5
#zookeper 数据存储目录及日志保存目录(如果没有指明dataLogDir,则日志也保存在这个文件中)
dataDir=/tmp/zookeeper
#对客户端提供的端口号
clientPort=2181
#单个客户端与zookeeper最大并发连接数
maxClientCnxns=60
# 保存的数据快照数量,之外的将会被清除
autopurge.snapRetainCount=3
#自动触发清除任务时间间隔,小时为单位。默认为0,表示不自动清除。
autopurge.purgeInterval=1

Zookeeper内部的数据模型

zk如何保存数据的

zk中的数据是保存在节点上的,节点就是znode,多个znode之间构成一颗树的目录结构。

树是由节点所组成,Zookeeper的数据存储也同样是基于节点,这种节点叫做Znode但是,不同于树的节点,Znode的引用方式是路径引用,类似于文件路径:

/动物/猫
/汽⻋/宝⻢

这样的层级结构,让每一个Znode节点拥有唯一的路径,就像命名空间一样对不同信息作出清晰的隔离。

znode的结构

zk中的znode,包含了四个部分:

  • data:保存数据
  • acl:权限,定义了什么样的用户能够操作这个节点,且能够进行怎样的操作。
    • c: create 创建权限,允许在该节点下创建子节点
    • w:write 更新权限,允许更新该节点的数据
    • r:read 读取权限,允许读取该节点的内容以及子节点的列表信息
    • d:delete 删除权限,允许删除该节点的子节点
    • a:admin 管理者权限,允许对该节点进行acl权限设置
  • stat:描述当前znode的元数据(节点相关信息)
  • child:当前节点的子节点

znode的类型

Znode被分为持久(persistent)节点,顺序(sequential)节点和临时(ephemeral)节点。

  • 持久节点:即使在创建该特定znode的客户端断开连接后,持久节点仍然存在。默认情况下,除非另有说明,否则所有znode都是持久的。
  • 临时节点:客户端活跃时,临时节点就是有效的。当客户端与ZooKeeper集合断开连接时,临时节点会自动删除。因此,只有临时节点不允许有子节点。如果临时节点被删除,则下一个合适的节点将填充其位置。临时节点在leader选举中起着重要作用。
  • 顺序节点:顺序节点可以是持久的或临时的。当一个新的znode被创建为一个顺序节点时,ZooKeeper通过将10位的序列号附加到原始名称来设置znode的路径。例如,如果将具有路径 /myapp 的znode创建为顺序节点,则ZooKeeper会将路径更改为 /myapp0000000001 ,并将下一个序列号设置为0000000002。如果两个顺序节点是同时创建的,那么ZooKeeper不会对每个znode使用相同的数字。顺序节点在锁定和同步中起重要作用。

zk可以实现服务注册与发现的效果。临时节点是如何维持心跳?

  • Container节点(3.5.3版本新增:Container容器节点,当容器中没有任何子节点,该容器节点会被zk定期删除(60s)。
  • TTL节点:可以指定节点的到期时间,到期后被zk定时删除。只能通过系统配置。zookeeper.extendedTypesEnabled=true开启。

znode的相关信息

状态属性 节点说明
cZxid 数据节点创建时的事务ID
ctime 数据节点创建世的时间
mZxid 数据节点最后一个更新是的事务ID
mtime 数据节点最后一个跟新时的时间
pZxid 数据节点的子节点最后一个被修改时的事务ID
cversion 子节点的更改次数
dataVerion 节点数据的更改次数
aclVersion 节点ACL的更改次数
ephemeralOwner 如果节点是临时节点,则表示创建该节点的会话的SeeesionID。如果是持久节点,则该属性值为0
dataLength 数据内容的长度
numChildren 数据节点当前的子节点个数

数据持久化

zk的数据是运行在内存中,zk提供了两种持久化机制:

  • 事务日志:zk把执行的命令以日志形式保存在dataLogDir指定的路径中的文件中(如果没有指定 dataLogDir,则按dataDir指定的路径)。
  • 数据快照:zk会在一定的时间间隔内做一次内存数据的快照,把该时刻的内存数据保存在快照文件中。

zk通过两种形式的持久化,在恢复时先恢复快照文件中的数据到内存中,再用日志文件中的数据做增量恢复,这样的恢复速度更快。

客户端(zkCli)命令

ZooKeeper -server host:port -client-configuration properties-file cmd args
	addWatch [-m mode] path # optional mode is one of [PERSISTENT, PERSISTENT_RECURSIVE] - default is PERSISTENT_RECURSIVE
	addauth scheme auth
	close 
	config [-c] [-w] [-s]
	connect host:port
	create [-s] [-e] [-c] [-t ttl] path [data] [acl]
	delete [-v version] path
	deleteall path [-b batch size]
	delquota [-n|-b|-N|-B] path
	get [-s] [-w] path
	getAcl [-s] path
	getAllChildrenNumber path
	getEphemerals path
	history 
	listquota path
	ls [-s] [-w] [-R] path
	printwatches on|off
	quit 
	reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
	redo cmdno
	removewatches path [-c|-d|-a] [-l]
	set [-s] [-v version] path data
	setAcl [-s] [-v version] [-R] path acl
	setquota -n|-b|-N|-B val path
	stat [-w] path
	sync path
	version 
	whoami 

创建节点

创建持久节点

create /test [data]

创建持久序号节点

create -s /test

创建临时节点

create -e /test

创建临时序号节点

create -e -s /test

创建容器节点

create -c /test

查询节点

查看节点列表有ls path和ls2 path两个命令,后者是前者的增强。不仅可以查看指定路径下的所有节点,还可以查看当前节点的信息。

普通查询

ls
ls -R /test # 递归查询

查询节点相关信息

# 获取数据
get /test
# 获取详细信息
get -s /test

删除节点

普通删除

delete /test
# 有子节点
deleteall /test

乐观锁删除

delete -v [dataVersion] /test

权限设置

注册当前会话的账号和密码:

addauth digest xiaowang:123456

创建节点并设置权限:

create /test-node abcd auth:xiaowang:123456:cdrwa

在另一个会话中必须先使用账号密码,才能拥有操作该节点的权限。

查看节点状态

可以使用stat命令查看节点状态,它的返回值和get命令类似,但不会返回节点数据

stat /test

更新节点

//使用set命令来更新hadoop节点
[zk: localhost:2181(CONNECTED) 1] set /hadoop "345"
//根据版本号来更新节点
[zk: localhost:2181(CONNECTED) 1] set /hadoop "345" 2

可以基于版本号来进行更改,此时类似于乐观锁机制,当传入的数据版本号(dataVersion)和当前节点的数据版本号不符合时,zookeeper会拒绝本次修改。

监听器

我们可以把Watch理解成是注册在特定Znode上的触发器。当这个Znode发生改变,也就是调用了create、delete、setData方法的时候,将会触发Znode上注册的对应事件,请求Watch的客户端会接收到异步通知。

使用get -w path注册的监听器能够在节点内容发生改变的时候,向客户点发出通知。需要注意的是zookeeper的触发器是一次性的(One-time trigger),触发一次后就会立即失效。

使用stat -w path注册的监听器能够在节点发生改变的时候,向客户端发出通知。

使用ls -w path或者ls2 -w path注册的监听器能够监听该节点下所有子节点的增加和删除操作。

具体交互过程如下:

  • 客户端调用getData方法,watch参数是true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被Watch的Znode路径,以及Watcher列表。
  • 当被Watch的Znode已删除,服务端会查找哈希表,找到该Znode对应的所有Watcher,异步通知客户端,并且删除哈希表中对应的Key-Value。

实现分布式锁

zk中锁的种类

  • 读锁:大家都可以读,要想上读锁的前提是之前的锁没有写锁
  • 写锁:只有得到写锁的才能写。要想上写锁的前提是之前没有任何锁

上读锁

  • 创建一个临时序号节点,节点的数据是read,表示是读锁
  • 获取当前zk中序号比自己小的所有节点
  • 判断最小节点是否是读锁:
    • 如果不是读锁的话,则上锁失败,为最小节点设置监听。阻塞等待,zk的watch机制会当最小节点发生变化时通知当前节点,于是再执行第二步的流程
    • 如果是读锁的话,则上锁成功

上写锁

  • 创建一个临时序号节点,节点的数据是write,表示是写锁
  • 获取zk中所有的子节点
  • 判断自己是否是最小的节点:
    • 如果是,则上写锁成功
    • 如果不是,说明前面还有锁,则上锁失败,监听最小的节点,如果最小节点有变化,则回到第二步。

羊群效应

如果用上述的上锁方式,只要有节点发生变化,就会触发其他节点的监听事件,这样的话对zk的压力非常大。

羊群效应,可以调整成链式监听,解决这个问题。

Zookeeper集群

Zookeeper集群⻆色

  • Leader:处理集群的所有事务请求,集群中只有一个Leader。
  • Follower:只能处理读请求,参与Leader选举。
  • Observer:只能处理读请求,提升集群读的性能,但不能参与Leader选举。

搭建集群

1. 创建4个节点的myid,并设值

在/usr/local/zookeeper中创建以下四个文件。

/usr/local/zookeeper/zkdata/zk1# echo 1 > myid
/usr/local/zookeeper/zkdata/zk2# echo 2 > myid
/usr/local/zookeeper/zkdata/zk3# echo 3 > myid
/usr/local/zookeeper/zkdata/zk4# echo 4 > myid

2. 编写4个zoo.cfg

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement 
syncLimit=5
# 修改对应的zk1 zk2 zk3 zk4 
dataDir=/usr/local/zookeeper/zkdata/zk1
# 修改对应的端口 2181 2182 2183 2184
clientPort=2181
# 2001为集群通信端口,3001为集群选举端口,observer表示不参与集群选举 
server.1=172.16.253.54:2001:3001 
server.2=172.16.253.54:2002:3002 
server.3=172.16.253.54:2003:3003 
server.4=172.16.253.54:2004:3004:observer

3. 启动4台Zookeeper

./bin/zkServer.sh status ./conf/zoo1.cfg
./bin/zkServer.sh status ./conf/zoo2.cfg
./bin/zkServer.sh status ./conf/zoo3.cfg
./bin/zkServer.sh status ./conf/zoo4.cfg

4. 连接Zookeeper集群

./bin/zkCli.sh 
-server 172.16.253.54:2181,172.16.253.54:2182,172.16.253.54:2183

ZAB协议

介绍

Zookeeper作为非常重要的分布式协调组件,需要进行集群部署,集群中会以一主多从的形式进行部署。Zookeeper为了保证数据的一致性,使用了ZAB(Zookeeper Atomic Broadcast)协议,这个协议解决了Zookeeper的崩溃恢复和主从数据同步的问题。

ZAB协议定义的四种节点状态

  • Looking:选举状态。
  • Following:Follower节点(从节点)所处的状态。
  • Leading:Leader节点(主节点)所处状态。
  • Observing:观察者节点所处的状态

集群上线时的Leader选举过程

Zookeeper集群中的节点在上线时,将会进入到Looking状态,也就是选举Leader的状态。

任何增删改查,zXid加一。选票优先对比zXid。第一轮后没有超过一半的选票,进入第二轮。

崩溃恢复时的Leader选举

Leader建立完后,Leader周期性地不断向Follower发送心跳(ping命令,没有内容的socket)。当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进入到Looking状态,重新回到上一节中的Leader选举过程,此时集群不能对外提供服务。

主从服务器之间的数据同步

  1. 数据准备写入集群。
  2. 集群leader将数据写入数据文件,再把数据广播给所有的follower。此时数据还在数据文件中。
  3. follower完成写入数据文件后返回ACK给leader。
  4. leader得到半数以上ACK则发送commit。
  5. follower将数据文件中的数据写入内存。

半数以上是整个集群的半数,为了提高写数据的性能。

Zookeeper中的NIO与BIO的应用

  • NIO
    • 用于被客户端连接的2181端口,使用的是NIO模式与客户端建立连接。
    • 客户端开启Watch时,也使用NIO,等待Zookeeper服务器的回调。
  • BIO
    • 集群在选举时,多个节点之间的投票通信端口,使用BIO进行通信。

定义:

  • BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
  • NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  • AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

CAP理论

CAP定理

2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。

CAP理论为:一个分布式系统最多只能同时满足一致性(Consistency)、可用性 (Availability)和分区容错性(Partition tolerance)这三项中的两项。

一致性(Consistency)

一致性指 “all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。

可用性(Availability)

可用性指“Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。

分区容错性(Partition tolerance)

分区容错性指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。避免单点故障,就要进行冗余部署,冗余部署相当于是服务的分区,这样的分区就具备了容错性。

CAP权衡

通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个?

对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,即保证P和A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流失的严重程度。

对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。还有一种是保证CP,舍弃A。例如网络故障是只读不写。

孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的。

BASE理论

eBay的架构师Dan Pritchett 源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性 (Eventual Consitency)。

基本可用(Basically Available)

基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

电商大促时,为了应对访问量激增,部分用户可能会被引导到降级⻚面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

软状态(Soft State)

软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中 一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。

最终一致性(Eventual Consistency)

最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

Zookeeper的一致性

Zookeeper在数据同步时,并不是强一致性,而是顺序一致性(事务ID的单调递增)。


参考链接: