近期面试经常被问到zookeeper,加上刚好学习大数据也遇到了zookeeper,因此再次做一个总结.
zookeeper是一个开源的分布式协调服务,基于zookeeper可以实现负载均衡,命名服务,集群管理,master选举,分布式锁等功能.
zookeeper集群中存在三个角色:1.leader;2.follower,3.observer
同一时刻,zookeeper集群中只存在一个leader,其余的都是follower或者observer,其中observer默认情况下,没有该角色,如果需要observer模式则需要单独配置.observer与follower功能类似,唯一区别仅仅是observer不参与leader选举和过半写成功策略.
集群中所有的机器通过leader选举来确定一台机器作为leader,leader服务器为客户端提供读/写功能,follower和observer仅仅提供读服务,不提供写功能.由于observer不参与决策所以,其可以在不影响写性能的情况下提升几群的读性能.
zookeeper与客户端之间的连接为TCP长连接,客户端会与zookeeper简历连接,此时会话周期开始,,通过连接客户端会通过心跳检测和服务保持来保持会话.也会通过该链接完成请求的发送和接受,以及接受服务端的WATCH时间通知.session可以设置sessionTimeout来指定超时时间,如果服务器宕机后客户端在sessionTimeout时间内重新链接上一台新的服务器,则原本的session依然有效.
.zookeeper中的节点不同于分布式集群中的节点(每台机器),此处的节点是指zookeeper存储数据的数据模型(Znode Tree)中的每个数据单元.称之为znode.zookeeper的数据存储在内存中,是一个树形结构存储,"/"来进行路径分割,每个路径就是一个znode.(此处可以理解成系统中的文件夹和文件,每个znode既可以作为文件存储数据,也可以作为目录由下一级目录)
持久节点: 是指该znode一旦被创建除非主动移除,否则该znode会一直保持在zookeeper上
持久化顺序编号目录节点 : 也是持久节点,在创建时zookeeper会自动给一个编号拼接在节点名后面
临时节点: 是指该节点存在生命周期,与会话的生命周期相同,一旦客户端会话失效,该节点就会被移除.
临时顺序编号类目节点: 也是临时节点,在创建时zookeeper会自动给一个编号拼接在节点名后面
zookeeper每个节点都可以存数数据和子节点,其存储的形式类似于key_value,key为路径,value为具体的值.在zookeeper的命令客户端,可以通过"get /路径"命令查询节点中存储的数据,"create /路径"可以创建节点."ls /路径"可以查询子节点列表.
每个节点在刚创建时就会包含一些头信息包括了:数据版本,事务id等.
zookeeper在每一个znode上存储数据,同时还会维护一个叫stat的数据结构,记录着这个znode的三个数据版本,version(当前znode版本),cversion(当前znode的子节点版本),aversion(当前znode的父节点版本),version属性是用来实现乐观锁机制中的写入校验的.
每个znode中除了存储数据外,还存储了有些znode本身的状态信息,通过get命令可以获取.
在zookeeper中,更改服务器状态的操作称之为事务操作(节点的创建与删除,数据内容的更新等).对应每一个事务请求,zookeeper都会分配一个全局的唯一事务id(ZXID,通常是64位数字),,每个ZXID都对应一个事务操作.
zookeeper采用ACL策略来进行权限控制.定义了5种权限:
1.create:创建子节点权限
2.read:获取节点和子节点数据的权限
3.write:更新节点数据的权限
4.delete:删除子节点的权限
5.admin:设置节点的ACL的权限
watcher(事件监听器),shizookeeper的一个很重要的特性.zookeeper允许用户在指定的节点上注册一些watcher,并且在一些特定的事件触发的时候,zookeeper服务器会将时间通知到需要的客户端上.该机制是zookeeper实现分布式协调服务的重要特性.
zookeeper的watcher注册可以通过get,ls来注册,注册该监听仅一次有效,如需要长久循环有效,则需要在watcher中进行处理
基于ZAB算法能够很好的保证分布式环境中的数据一致性.
将系统的配置文件放置在zookeeper上,订阅者可以进行数据订阅,从而达到动态获取数据的目的.实现配置文件信息的集中管理和动态更新.
zookeeper采用的推(push)拉(pull)结合的模式实现发布/订阅,客户端向服务端注册自己关注的节点,一旦该节点的数据发生变化,服务端就会向客户端发送watcher事件通知,客户端在接受到消息通知后,需要主动从服务器端获取最新数据
在分布式系统环境中,通过命名服务,客户端可以根据指定的名字获取资源或服务地址.常见的分布式框架(RPC,RIM)中的服务地址列表,通过zookeeper里的创建顺序节点,形成一个全局的唯一路径.(生成全局唯一的ID)
使用zookeeper可以实现分布式环境下的多机器间的心跳检测功能,基于zk的临时节点特性,可以让不同的进程都在zk的同一个节点下创建临时子节点,不同的进程可以直接根据临时节点来判断进程是否依旧存活,该方式不需要将检测和被检测系统直接关联,从而降低耦合.
任务分发系统中,分发任务到各个不同的系统执行,可以在zk中选择一个节点,每个任务客户端都在该节点下创建临时节点.
a.通过判断临时节点的存活来判断任务机器的存活
b. 各个任务机器可以将自己的任务进度写入到临时节点上,任务中心便可以实时获取任务的进度.
HDFS中的namenode的选举,YARN中resourceManager的选举等,都是其使用场景.
利用zk的强一致性,能够很好的保证分布式高并发下,节点的创建一定是全局唯一的.zk可以保证客户端无法创建一个已经存在的znode,因此多个请求中只有一个请求可以创建成功.从而很容易在分布式环境进行master选举.
zk可以将一个znode看作一把锁,当该znode存在是其他请求都无法再次创建获得锁,该znode是临时节点,当获得锁的机器宕机时,临时节点会自动删除,释放锁.正常完成业务的客户端需要注定去删除自己创建的临时节点.未获得锁的客户端需要注册一个该节点变更的watcher监听,实时监听该节点的变动.
本文主要讲述在使用ZooKeeper进行分布式锁的实现过程中,如何有效的避免“羊群效应( herd effect)”的出现。
以下摘录自:https://yq.aliyun.com/articles/427024
一般的分布式锁实现
临时顺序节点。这种类型的节点有几下几个特性:
1. 节点的生命周期和客户端会话绑定,即创建节点的客户端会话一旦失效,那么这个节点也会被清除。
2. 每个父节点都会负责维护其子节点创建的先后顺序,并且如果创建的是顺序节点(SEQUENTIAL)的话,父节点会自动为这个节点分配一个整形数值,以后缀的形式自动追加到节点名中,作为这个节点最终的节点名。
利用上面这两个特性,我们来看下获取实现分布式锁的基本逻辑:
1. 客户端调用create()方法创建名为“_locknode_/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL。
2. 客户端调用getChildren(“_locknode_”)方法来获取所有已经创建的子节点,同时在这个节点上注册上子节点变更通知的Watcher。
3. 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。
4. 如果在步骤3中发现自己并非是所有子节点中最小的,说明自己还没有获取到锁,就开始等待,直到下次子节点变更通知的时候,再进行子节点的获取,判断是否获取锁。
释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可。
问题所在(羊群效应)
上面这个分布式锁的实现中,大体能够满足了一般的分布式集群竞争锁的需求。这里说的一般性场景是指集群规模不大,一般在10台机器以内。
不过,细想上面的实现逻辑,我们很容易会发现一个问题,步骤4,“即获取所有的子点,判断自己创建的节点是否已经是序号最小的节点”,这个过程,在整个分布式锁的竞争过程中,大量重复运行,并且绝大多数的运行结果都是判断出自己并非是序号最小的节点,从而继续等待下一次通知——这个显然看起来不怎么科学。客户端无端的接受到过多的和自己不相关的事件通知,这如果在集群规模大的时候,会对Server造成很大的性能影响,并且如果一旦同一时间有多个节点的客户端断开连接,这个时候,服务器就会像其余客户端发送大量的事件通知——这就是所谓的羊群效应。而这个问题的根源在于,没有找准客户端真正的关注点。
我们再来回顾一下上面的分布式锁竞争过程,它的核心逻辑在于:判断自己是否是所有节点中序号最小的。于是,很容易可以联想的到的是,每个节点的创建者只需要关注比自己序号小的那个节点。
改进后的分布式锁实现
下面是改进后的分布式锁实现,和之前的实现方式唯一不同之处在于,这里设计成每个锁竞争者,只需要关注”_locknode_”节点下序号比自己小的那个节点是否存在即可。实现如下:
1. 客户端调用create()方法创建名为“_locknode_/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL。
2. 客户端调用getChildren(“_locknode_”)方法来获取所有已经创建的子节点,注意,这里不注册任何Watcher。
3. 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点序号最小,那么就认为这个客户端获得了锁。
4. 如果在步骤3中发现自己并非所有子节点中最小的,说明自己还没有获取到锁。此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时注册事件监听。
5. 之后当这个被关注的节点被移除了,客户端会收到相应的通知。这个时候客户端需要再次调用getChildren(“_locknode_”)方法来获取所有已经创建的子节点,确保自己确实是最小的节点了,然后进入步骤3。