zk技术内幕

一、系统模型

1、数据模型
  • zk结构视图与unix的文件系统有点类似,但是没有目录和文件的相关概念。而是使用特有的数据节点的概念,称为zNode节点,zNode节点是zk中数据最小的单元,每个zNode都可以保存数据,通是还可以挂载子节点,因此构成了一个层次化的空间命名,称之为数。
  • zk中的zNode的节点路径标识方式和unix文件系统路径类似,都是一些列的斜杠(/)进行分割的路径标识,可以在节点中写入数据,也可以再节点下面创建子节点。


    zk技术内幕_第1张图片
    zk数据模型
  • 事务ID:事务是对物理和抽象的应用状态上的操作集合。在zk中事务是指能够改变zk服务器状态的操作,我们称之为事务操作或者更新操作,一般包括数据节点创建于删除,数据节点内容更新和客户端会话创建与失效等操作。对于zk的一个事物请求,zk都会为其分配一个全局唯一的事务id,用zxid来表示,通常是一个64位的数字。
2、节点特征

(1) 节点类型:在zk中每一个节点都是有什么周期的,具体生命周期的长度取决于数据节点的节点类型。zk中节点类型分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)三类,并通过组合可以组成下面四种类型:

1、持久节点:所谓持久节点就是该数据节点被创建后,就会一直存在于zk服务器中,直到有删除操作来主动清除这个节点。
2、持久顺序节点:在zk中,每一个父节点都会为它的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序,基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建子节点过程中,zk会自动为给定节点加上一个数字后缀,作为一个新的、完整的节点名。(数字上限为整形最大值)
3、临时节点:临时节点的生命周和客户端的会话绑定在一起,也就是说这个客户端失效,那么这个节点就会被自动清理掉。同时规定,临时节点不能创建子节点,临时节点只能作为叶节点存在。
4、临时顺序节点:就是在临时节点的基础之上,添加了顺序的特性。

(2)状态信息:zk数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息。可以通过get来查看一个数据节点的内容,如图所示,第一行是数据节点的数据内容,第二行开始就是数据节点的状态信息,其实就是数据节点的stat对象的格式化输出

zk技术内幕_第2张图片
数据节点信息和内容

  • Stat类包含了zk上的一个数据节点的所有状态信息,包括事务id、版本信息和子节点个数。
属性 说明
czxid 表示数据节点被创建时候的事务id
mzid 节点最后一次被更新的事务id
ctime 节点被创建的时间
mtime 节点最后一次被更新的时间
version 数据节点的版本号
cversion 子节点的版本号
aversion 节点acl的版本号
ephemeralOwner 创建临时节点的会话id
dataLength 数据内容的长度
pzxid 该节点的子节点列表最后被修改的事物id
numChildren 当前子节点的个数
3、版本

(1)悲观锁和乐观锁:

悲观锁:称为悲观并发控制(PCC),具有强烈的独占性和排他性,能够有效的额避免不同事务对同一个数据并发更新而造成的数据一致性问题。一般认为,在实际生产中,悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景。
乐观锁:称乐观并发控制(OCC),悲观锁假定事务之间一定会出现互相干扰,而乐观锁则认为多个事务之间在处理过程中不会彼此影响(不总是会影响),因此在事务处理的绝大部分时间里不需要进行加锁处理。但是有并发一定存在者更新数据的冲突。(乐观锁机制就是更新请求提交之前,每一个事务都会首先检查当前事务读取数据后,是否还有其他事务对该数据进行了修改。如果其他事务有更新的话,那么正在提交的事务就需要回滚。)乐观锁通常用在数据并发竞争不大,事务冲突较少的场景中。

(2)乐观锁详解
一般乐观锁对事务的控制分为三阶段:数据读取、写入效验、数据写入,其中写入效验是整个乐观锁关键所在。再写入效验阶段,事务会检查数据在读取节点后是否还有其他事务对数据进行过更新,以确保数据更新的一致性。(实现乐观锁可以基于CAS原理和版本控制)

CAS原理:对于值V,每一次更新前都会对比其值是否是预期值A,只有符合预期值,才会将V原子化的更新为新值B。

(3)zk中版本控制实现乐观锁

  • 在zk中,每一个节点都有三种不同的版本信息(version、cversion、aversion),对数据节点的任何操作都会引起版本号的变化。以version为例说明。当一个数据节点被创建后,version为0,(表示数据节点被创建后,被更新过0次),如果对该节点的数据内容进行了更新操作,那么version的值就会变为1,这里的变更强调的是变更次数,即时变更内容不变,version的值还是会发生变化的。
  • zk中version参数正是CAS原理演化过来的,具体实现过程为:

加入一个客户端试图进行更新操作,它会携带上次获取到的version值进行更新,如果在这个时间内,zk服务器上该节点的数据恰好已经被其他客户端更新了,那么其数据版本一定也发生了变化,因此肯定与客户端携带的version无法匹配,也就无法更新成功,这样可以有效的避免了一些分布式更新问题。而version这个参数就充当了CAS中的“预期值”

  • 原理
//获取值V
 version = setDataRequest.getVersion();
//获取zk版本号预期值A
            currentVersion = nodeRecord.stat.getVersion();
//如果A是-1,说明没有并不要求使用乐观锁;如果不为-1,那么进行A和V对比
//不匹配就抛出异常
            if (version != -1 && version != currentVersion) {
                throw new BadVersionException(path);
            }
            version = currentVersion + 1;
4、zk数据节点小结
  • 如下图所示,zk的数据节点有以下特征:

1、每个子目录项如NameService都被称作znode,这个znode是被它所在的路径唯一标识,如Server1这个znode的标识为/NameService/Server1
2、znode可以有子节点目录,并且每个znode可以存储数据,注意EPHEMERAL类型的目录节点不能有子节点目录
3、znode是有版本的,每个znode中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据
4、znode可以是临时节点,一旦创建这个znode的客户端与服务器失去联系,这个znode也将自动删除,ZooKeeper的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态成为session,如果znode是临时节点,这个session失效,znode也就被删除.
5、znode可以被监控,包括这个目录节点中存储的数据被修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是ZooKeeper的核心特性.

zk技术内幕_第3张图片
zk数据结构模型

二、Watch机制

  • zk中引入了watcher机制来实现分布式的发布、订阅功能。zk允许客户端向服务器注册一个watcher监听,当服务器的一些指定事件触发了这个watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。
  • 原理:zk中watcher机制主要包括客户端线程、客户端watchManager和zk服务器三个部位。具体工作流程中,客户端向zk服务器注册watcher的同时,会将watcher对象存储在客户端的watchManager中;当zk服务器触发watcher事件后,会向客户端发送通知,客户端线程从watchermanager中取出对应的watcher对象来执行回调逻辑。

(1)watcher接口

public interface Watcher {
//事件的回调方法,当zk向客户端发送一个watcher事件通知时候,客户端就会
//对相应的process方法进行回调,从而实现对事件的处理
    void process(WatchedEvent var1);
//事件通知类型
    public interface Event {
        public static enum EventType {
            None(-1),
            NodeCreated(1),
            NodeDeleted(2),
            NodeDataChanged(3),
            NodeChildrenChanged(4);
....
        }
//事件通知状态
        public static enum KeeperState {
      ·    //客户端与服务器断开连接(此时处于断开连接状态)
            Disconnected(0),
 //此时处于连接状态(成功连接时候,触发条件创建、删除、节点数据改变、子节点列表发生变化)
            SyncConnected(3),
            AuthFailed(4),
            ConnectedReadOnly(5),
            SaslAuthenticated(6),
          //会话超时
            Expired(-112);
.....

(2)WatchedEvent和WatcherEvent

  • zk中使用watchedEvent对象来封装服务端事件并传递给watcher,从而方便回调方法process对服务端事件进行处理。
  • WatchedEvent是一个逻辑事件,用于服务端和客户端程序执行过程中所需要的逻辑对象,而WatcherEvent可以用于网络传送。服务端在生成WatchedEvent事件之后,会调用getWrapper方法将自己包装成WatcherEvent,传送给客户端,客户端在接受到服务端这个事件对象后,还原为WatchedEvent事件,并传递给process方进行回处理。
public class WatchedEvent {
//通知状态
    private final KeeperState keeperState;
//事件类型
    private final EventType eventType;
//节点路径
    private String path;
...
//通过该方法将watchedEvent包装为watcherEvent
    public WatcherEvent getWrapper() {
        return new WatcherEvent(this.eventType.getIntValue(), 
this.keeperState.getIntValue(), this.path);
    }
}
//实现了序列化,可以在网络上传送
public class WatcherEvent implements Record {
    private int type;
    private int state;
    private String path;
...
}

(3)工作机制


zk技术内幕_第4张图片
watch工作机制图
  • 总体来说watcher工作机制分为客户端注册Watcher、客户端注册Watcher、客户端注册Watcher三部曲。

客户端注册:

在创建ZK客户端实例的时候,可以向构造方法中传入一个默认的Watcher,代表注册watcher(或者getData,getChildren,Exist三个方法进行注册)。注册好了之后,会对该请求request进行标记,标记为:使用了Watcher监听。然后把Watcher的注册信息封装为WatcherRegistration对象。然后再封装为packet对象,packet可以看做是最小的通信协议单元,用于进行网络传输。随后,客户端就像服务器端发送这个请求,同时等待请求的返回。

服务端处理Watcher:

ServerCnxn存储:我们知道ServerCnxn是服务端与客户端进行网络交互的一个接口,代表了客户端与服务端的连接。其底层采用netty实现。所以,在接受到注册请求之后,服务端会将ServerCnxn对象和数据阶段路径保存到WatchManager的watchTable和watch2Paths中。
Watcher触发当watcher监听的对应的额数据节点的数据内容发生变更时候。通过调用WatchManager的triggerWatch方法触发相关的事件。其通过将节点信息和事件类型进行封装成为watchedEvent,并查找到到对应节点的注册的watcher,然后分别调用watcher的回调函数process。而在process函数中其实就是通过封装的ServerCnxn。但本质上并不是客户端Watcher的真正业务逻辑,而是借助当前客户端连接的ServerCnxn对象来实现对客户端的WatchedEvent传递,真正的客户端回调与业务逻辑是在客户端也就是说服务端在完成watchedEvent封装后,会通过网络传送给客户端进行处理

客户端回调Watcher:

对于服务器端的响应,客户端都是由SendThread中的readResponse方法来统一处理的。(反序列化、处理chrootPath、还原为WatchedEvent、回调Watcher)

(4)小结

  • watcher的特性总结:

1、一次性:无论是服务器还是客户端,一旦一个Watcher被触发,就会从相应的存储中移除,这样的设计有利用缓解服务端的压力。但是开发人员要记住的一点就是反复注册!
2、客户端串行执行:客户端回调过程是一个串行的过程,因为要保证顺序。
3、轻量:WatchedEvent是ZK整个通知机制中的最小通知单元,我们说过它只包含三个部分,通知状态、事件类型、节点路径。因此它非常的轻量。但是你必要要记住这一点,它只会告诉客户端发生了事件,不会说明具体内容,你一定要自己重新去主动获取数据。另外我们说过,客户端注册Watcher的时候并不会把真实的Watcher对象传递到服务器,同时服务器端也仅仅是保存了当前连接的ServerCnxn对象。因此真的是非常轻量,网络开销和服务器内存开销都非常廉价。

(5)curator客户端使用watcher

  • curator引入cache来实现对zk服务端事件的监听。Cache是curator中对事件监听的包装,其对事件的监听可以看成是一个本地缓存视图与远程zk视图的对比进程。同时curator能够自动为开发人员处理反复注册监听问题,curator分为两类节点的监听:节点监听和子节点监听。

NodeCache:用于监听指定zk数据节点本身的变化,定义了事件处理的回调接口NodeCacheListener,只要数据节点发生变化(这里的变化不包括删除后的变化),就会回调该方法。
PathChildrenCache:用于处理zk数据节点的子节点变化情况,但是无法触发二级子节点的改变。

三、会话

1、会话状态
  • zk客户端与服务端成功连接后,就建立起了一个会话。zk会话在整个运行期间的生命周期中,会在不同的会话状态之间进行切换,这些状态一般可以分为connecting、connected、reconneting、reconnected、close等。
2、会话创建

session实体:session实体包含sessionID(会话id,用于标识唯一会话)、timeout(会话超时时间)、ticktime(下次会话超时时间)、isclosing(标记一个会话是否已经关闭)四个属性
sessionID:会话id,需要保证其全局唯一性。

//id为服务器id(myid文件中的值)
  public static long initializeNextSession(long id) {
//该算法核心是高8位确定所在机器,后56位使用当前时间的毫秒表示进行随机
        long nextSid = 0L;
        nextSid = System.currentTimeMillis() << 24 >>> 8;
        nextSid |= id << 56;
        return nextSid;
    }

sessionTracker:是zk的服务端的会话管理器,负责会话的创建、管理和清理工作。

public class SessionTrackerImpl extends ZooKeeperCriticalThread implements SessionTracker {
  //根据sessionId管理session实体
    HashMap sessionsById = new HashMap();
//用于根据当前的会话超时时间点来归档会话,以便进行会话管理和超时检验
    HashMap sessionSets = new HashMap();
//根据sessionId来管理会话的超时时间
    ConcurrentHashMap sessionsWithTimeout;
...

创建会话:服务端对于客户端的请求大致分为四个步骤,connectionRequest请求、会话创建、处理器链路处理、会话响应。在zk中由NIOServerCnxn来负责接收来自客户端的会话创建请求,并反序列化connetRequest请求,然后根据zk服务端的配置来完成会话超时时间的协商。随后sessionTracker将会为该会话分配一个sessionId,并将其注册到sessionsById和sessionWithTimeout中,同时进行会话的激活。之后,该会话请求会在zk服务端的各个请求处理器之间进行顺序流转,最后完成会话的创建。

3、会话管理

分桶策略:zk中会话管理由sessionTracker负责,采用“分桶管理策略”。就是说将类似的会话放在同一区块中进行管理,以便于zk对会话进行不同区块的隔离处理以及同一区块的统一处理。
分桶策略的分配原则是每个会话的下次超时时间点ExpirationTime(指的是最近一次可能超时的事件点,会话创建完成ExpirationTime=currenTime+sessionTimout),zk的leader服务器会在运行期间定时的进行会话超时检查,其时间间隔是EXpiratiionInterval(默认是2000毫秒,实际是通过公司计算出来的)
会话激活:为了保证客户端的会话有效性,在zk运行过程中,客户端会在会话超时时间内向服务器发送PING请求来保持会话的有效性,称为心跳检查。同时服务端不断受到客户端的心跳检测,并且需要重新激活对应的客户端会话,这个过程称为TouchSession。会话激活过程不仅能够使服务端检测到对应客户端的存活性,还可以让客户端自己保持连接状态。
会话激活四个过程(检测会话是否关闭、计算该会话的新的超时时间、定位该会话当前区块、迁移会话)。
会话超时检查:sessionTracker中有一个专门的线程用于超时检查,其工作机制就是:逐个依次的对会话桶中剩余的会话进行清理
会话清理:sessionTracker的会话超时检查线程整理出一些已经过期的会话后,就要开始进行会话清理了。

4、会话重连

会出现重连的情况:连接断开、会话过期、会话转移时候

四、zk中各服务器角色以及Leader选举

1、zk各个服务器角色

leader:是整个zk集群工作机制中的核心,主要功能(1)事务请求的唯一调度和处理者,保证集群事务处理的顺序性(2)集群内部各个服务的调度者
follower:是zk集群状态的跟随着,主要功能(1)处理客户端非事物请求,转发事物请求给leader服务器(2)参与事物请求proposal的投票(3)参与leader选举
Observer:与follower唯一区别是,observer不参加任何形式的投票,包括leader选举和proposal的投票。Observer的存在就是在减少投票和选举中性能影响,而达到集群快速扩容的目的。

2、leader选举过程

  • leader选举是zk中最重要技术之一,也是保证分布式数据一致性的关键所在
    服务启动时期的选举:

1、每一个server发出一个投票
2、接收来自各个服务器的投票
3、处理投票(zxid大优先,myid大的优先)
4、统计投票
5、改变服务器状态

运行期间的leader选举(leader挂了后)

1、变更状态(oberver变更为LOOKING)
2、每一个server发出一个投票
3、接收来自各个服务器的投票
4、处理投票(zxid大优先,myid大的优先)
5、统计投票
6、改变服务器状态(原子广播)

3、选举算法

略(后续研究)

五、数据与存储

  • zk中数据存储分为两个部分:内存数据存储和磁盘数据存储
1、内存数据存储
  • zk的数据模型是一棵树,而这颗树存储在内存中,包括所有的节点路径、节点数据以及ACL信息等,zk会定时的将这些数据存储到磁盘上。
    DataTree:DataTree是zk内存数据存储的核心,是一个数的数据结构,代表了内存中一份完成的数据。
    DataNode:DataNode是数据存储的最小单元,内部出了保存节点的数据内容、acl列表和节点状态外,还记录了基本数据结构中的树的描述
    ZKDatabase:是zk的内存数据库,负责管理zk的所有会话、DataTree存储和事物日志,会定期的向磁盘dump快照数据,同时zk启动的时候,会通过磁盘上的事物日志和快照文件恢复成一个完成的内存数据库。
2、事物日志
  • 配置文件中的dataDir是用于存储事物日志文件的目录。也可以配置dataLogDir为事物日志单独分配一个文件存储目录。文件存储时候都是以ZXID事务id来命名的。
3、snapshot-数据快照
  • 数据快照是zk数据存储的另一个核心机制,数据快照用来记录zk服务器上某一个时刻的全量内存数据内容,并将其写入到指定的磁盘文件中。

六、客户端

客户端核心组件有:zk实例、ClientWatchManager(watch管理器)、HostProvider(客户端地址列表管理器)和ClientCnxn(客户端核心线程,主要由SendThread和EventThread构成,前者负责网络通信,后者负责事件处理)。客户端启动过程:

1、设置默认的watcher
2、设置zk服务器地址列表
3、创建clientCnxn

七、服务端

集群版zk服务端启动过程:

1、预启动(加载配置文件等)
2、初始化(恢复本地数据等)
3、leader选举
4、leader和follow的交互
5、leader和follow的启动

八、ACL

  • zk中提供了一套完成的ACL(Access Control List)权限机制来保证数据的安全。
  • ACL是访问控制列表,是一种相对来说比较新颖且更加细粒度的权限管理方式,可以针对任意用户和组件进行细粒度的权限控制。通常要理解ACL,要从权限模式scheme、授权对象ID和权限permission来入手,通常使用scheme:id:permission来标识一个有效的ACL。

参考:《从paxos到zookeeper分布式一致性原理与实践》

你可能感兴趣的:(zk技术内幕)