我是靠谱客的博主 缥缈泥猴桃,最近开发中收集的这篇文章主要介绍Zookeeper底层技术实现细节系统模型会话管理数据和存储参考,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 系统模型
    • 数据模型
    • 节点类型
    • 节点状态信息
    • 节点版本
    • Watcher 监听数据变更
    • ACL保障数据安全
      • 权限模式 Scheme
      • 授权对象 ID
      • 权限 Permission
  • 会话管理
    • 分桶策略
    • 会话激活
    • 会话超时检查
    • 会话清理
  • 数据和存储
    • 内存数据
    • 事务日志
    • 数据快照 snapshot
    • zk启动数据加载
  • 参考

系统模型

数据模型

zk引入了ZNode,类似于Unix文件系统的树结构。ZNode是ZooKeeper中数据的最小但愿,每个ZNode上都可以保存数据,同时还可以挂载子节点。

节点类型

根据是否具备持久和顺序的特性,zk节点分为四种节点类型,持久代表一经在zk服务器创建,则不会自动删除,顺序表示每次创建节点都会在节点名后面加一个递增数字后缀来标志顺序,同时顺序节点下不允许有子节点

在Zookeeper中的定义如下:

public enum CreateMode {
/**
* 数据节点一旦被创建,会一直存在于zk服务器,直到有删除操作来主动清除
*/
PERSISTENT (0, false, false),
/**
* 具备持久节点的特性,同时zk会在顺序节点的父节点中维护一份顺序表,在顺序节点创建是,自动为节点添加一个单调递增顺序后缀,用来标志创建顺序
*/
PERSISTENT_SEQUENTIAL (2, false, true),
/**
* 当创建节点的机器断开与zk的连接时,节点会被移除
*/
EPHEMERAL (1, true, false),
/**
* 具备临时节点的特性,同时zk会在顺序节点的父节点中维护一份顺序表,在顺序节点创建是,自动为节点添加一个单调递增顺序后缀,用来标志创建顺序
*/
EPHEMERAL_SEQUENTIAL (3, true, true);
private boolean ephemeral;
private boolean sequential;
private int flag;
CreateMode(int flag, boolean ephemeral, boolean sequential) {
this.flag = flag;
this.ephemeral = ephemeral;
this.sequential = sequential;
}
}

节点状态信息

对于zk的每个数据节点,除了数据内容外,还有一些额外的状态信息,可参照zk的Stat类:

ublic class Stat implements Record {
// created zxid,表示该数据节点被创建时的事务ID
private long czxid;
// modified zxid,表示该节点最后一次被更新时的事务ID
private long mzxid;
// created time,表示节点被创建的时间
private long ctime;
// modified time,表示该节点最后一次被更新的时间
private long mtime;
// 数据节点的版本号,每次变更数据节点信息都会生成一个新的版本号,用来标志这次改动
private int version;
// 子节点的版本号,标志当前节点作为父节点变更子节点的次数
private int cversion;
// 节点的ACL版本号
private int aversion;
// 创建该临时节点的会话的sessionID,如果该节点是持久节点,属性值为0
private long ephemeralOwner;
// 数据内容的长度
private int dataLength;
// 子节点个数
private int numChildren;
// 表示该节点的子节点列表最后一次被修改的事务ID,只有子节点列表变更才会变更pzxid,子节点内容变更不会。
private long pzxid;
}

节点版本

zk引入数据节点版本的概念,来保证分布式数据的原子操作,版本号可以表示对应版本操作的变更次数,实际内容值可能不会发生变化。可以基于版本号而非数据内容来实现乐观锁,并且避免ABA问题的出现。在每次修改前,都先获取最新的版本号,以确认修改操作的数据是最新的,在实际提交修改时附带版本号,如果发现附带的版本号不是最新的,说明存在并发修改,需要重新获取最新版本号数据再进行修改。zk底层的修改操作源码如下:

case OpCode.setData:
zks.sessionTracker.checkSession(request.sessionId, request.getOwner());
SetDataRequest setDataRequest = (SetDataRequest)record;
if(deserialize)
ByteBufferInputStream.byteBuffer2Record(request.request, setDataRequest);
path = setDataRequest.getPath();
validatePath(path, request.sessionId);
nodeRecord = getRecordForPath(path);
checkACL(zks, nodeRecord.acl, ZooDefs.Perms.WRITE,
request.authInfo);
// 获取版本号
version = setDataRequest.getVersion();
int currentVersion = nodeRecord.stat.getVersion();
// 版本号比对
if (version != -1 && version != currentVersion) {
throw new KeeperException.BadVersionException(path);
}
// 更新版本号,+1
version = currentVersion + 1;
request.txn = new SetDataTxn(path, setDataRequest.getData(), version);
nodeRecord = nodeRecord.duplicate(request.hdr.getZxid());
nodeRecord.stat.setVersion(version);
addChangeRecord(nodeRecord);
break;

Watcher 监听数据变更

zk支持客户端以推拉模式订阅zk数据,一方面客户端可以直接从服务端主动拉取数据,也可以向zk注册自己感兴趣的节点的监听器,zk在相关节点变更后,会将变更信息推送给客户端进行处理。

在zk的Watcher接口下,提供了KeeperState和EventType两个枚举类,分别代表通知状态和时间类型。两个枚举类的枚举成员定义如下:

public enum KeeperState {
/** Unused, this state is never generated by the server */
@Deprecated
Unknown (-1),
/** The client is in the disconnected state - it is not connected
* to any server in the ensemble. */
Disconnected (0),
/** Unused, this state is never generated by the server */
@Deprecated
NoSyncConnected (1),
/** The client is in the connected state - it is connected
* to a server in the ensemble (one of the servers specified
* in the host connection parameter during ZooKeeper client
* creation). */
SyncConnected (3),
/**
* Auth failed state
*/
AuthFailed (4),
/**
* The client is connected to a read-only server, that is the
* server which is not currently connected to the majority.
* The only operations allowed after receiving this state is
* read operations.
* This state is generated for read-only clients only since
* read/write clients aren't allowed to connect to r/o servers.
*/
ConnectedReadOnly (5),
/**
* SaslAuthenticated: used to notify clients that they are SASL-authenticated,
* so that they can perform Zookeeper actions with their SASL-authorized permissions.
*/
SaslAuthenticated(6),
/** The serving cluster has expired this session. The ZooKeeper
* client connection (the session) is no longer valid. You must
* create a new client connection (instantiate a new ZooKeeper
* instance) if you with to access the ensemble. */
Expired (-112);
}
public enum EventType {
None (-1),
NodeCreated (1),
NodeDeleted (2),
NodeDataChanged (3),
NodeChildrenChanged (4);
}

他们的对应关系如下所示:

Watcher具有以下特性:

  1. 一次性,一旦一个Watcher被触发,zk会从相应的存储中移除,以减轻服务端压力,但开发人员需要反复注册。
  2. 客户端串行执行,客户端Watcher回调是一个串行同步过程,这保持了顺序
  3. 轻量,WatchedEvent是zk整个Watcher机制的最小通知但愿,数据包含三部分内容:通知状态、时间类型和节点路径。Watcher通知非常简单,只会告知客户端发生了时间,而不说明具体内容,需要客户端去主动获取。

ACL保障数据安全

ACL(Access Control List), 访问控制列表,可以针对任意用户和组进行细粒度的权限控制。

在zk中,ACL机制有三方面内容:权限模式(Scheme),授权对象(ID)和权限(Permission),通常使用"scheme?permission"来标识一个有效的ACL信息。

权限模式 Scheme

权限模式是用来确定权限验证过程中使用的检验策略,常用有几下几种:

  1. ip:控制ip访问,可以配置ip望断,如192.168.*,表示对该ip实施相应权限控制策略
  2. Digest:类似于"username:password"方式,实际传输会进行加密。
  3. World:标示数据节点的访问权限对所有用户开放
  4. Super:在Super模式下,超级用户可以对任意zk的数据节点进行任何操作

授权对象 ID

基于特定权限模式的权限内容,如IP模式先是具体的ip值或网段,Digest下是username:password等

权限 Permission

通过Scheme:ID校验后可以被执行的操作,主要分为5类:

  1. create:子节点的创建权限,表示可以在该节点下创建子节点
  2. delete:子节点的删除权限,表示可以删除该节点下的子节点
  3. read:数据节点的读取权限,可以读取数据节点内容极其子节点列表
  4. write:数据节点的更新权限,可以对数据节点进行更新操作
  5. admin:数据节点的管理权限,可以对数据节点进行ACL相关的设置操作

会话管理

客户端在和zk服务端完成连接创建后,就建立了一个会话,在一个会话的生命周期内,存在多种状态,每个会话都会在这些状态间相互切换,具体的状态包括:CONNECTING,CONNECTED,RECONNECTING,RECONNECTED,CLOSE

在zk中,每个会话通过Session实体标志,包括以下4个基本属性

  1. sessionID:用来唯一标志一个会话,是一个64位长整型,高8为为机器id,后56位位当前时间的毫秒表示
  2. TimeOut:会话超时时间
  3. TickTime:下次会话超时时间点,zk会为每个会话标记一个下次会话超时时间点,来高效低耗得实现会话的超时检查和清理
  4. isClosing:用来标记一个会话是否已经被关闭,在服务端检测到会话超时失效的时候标记

Session主要由zk的会话管理器SessionTracker来维护,负责会话的创建、管理和清理等工作,内部的核心数据结构定义如下:

public class SessionTrackerImpl extends ZooKeeperCriticalThread implements SessionTracker {
// 根据sessionID来管理Session实体
HashMap<Long, SessionImpl> sessionsById = new HashMap<Long, SessionImpl>();
// 根据sessionID管理会话的超时时间,会被定期持久化到快照文件
HashMap<Long, SessionSet> sessionSets = new HashMap<Long, SessionSet>();
// 用于根据下次会话超时时间点来管理会话和进行超时检查
ConcurrentHashMap<Long, Integer> sessionsWithTimeout;
// 记录即将创建的新会话的sessionID
long nextSessionId = 0;
// 记录下一个会话超时时间点
long nextExpirationTime;
// 记录会话超时间隔
int expirationInterval;
}

分桶策略

zk的会话管理主要由SessionTracker负责,基于分桶策略进行管理,即将相似的会话放在一个桶区块,zk对会话进行不同区块的隔离处理以及同一区块的统一处理。

桶分配的原则是每个会话的“下次超时时间点”。ExpirationTime = CurrentTime + SessionTimeout。在实际实现中,zk的leader服务器在运行期间会定时地进行超时检查,时间间隔为ExpirationInterval,默认是ticktime值。完整的超时时间点计算方式为:

ExpirationTime = ((CurrentTime + SessionTimeout) / ExpirationInterval + 1) * ExpirationInterval,即每个桶的划分单位总是ExpirationInterval的整数倍。

会话激活

为了保持客户端会话的有效性,客户端会在每个会话超时时间点ExpirationTime前向服务端发送PING请求作为心跳检测。具体激活流程如下:

  1. 检查该会话是否已关闭,如果已关闭,则停止激活流程
  2. 计算该会话新的超时时间ExpirationTime_New
  3. 定位该会话所属桶区块,根据会话老的超时时间ExpirationTime_Old来定位
  4. 迁移会话,将该会话从老的区块取出,放入ExpirationTime_New对应的新区块

在以下时机会触发会话激活:

  1. 只要客户端向服务端发送请求,就会触发一次会话激活
  2. 如果客户端发现在sessionTimeout / 3时间内尚未和服务器进行通信,会发起一个PING请求来激活会话

会话超时检查

在zk的SessionTracker中有一个单独的线程专门进行会话超时检查,每次会等待到会话超时时间点,将超时时间点内没有激活的会话进行清理,而后继续等待到下一个会话超时时间点。具体实现源码如下所示:

@Override
synchronized public void run() {
try {
while (running) {
currentTime = System.currentTimeMillis();
// 等待到下一超时时间点
if (nextExpirationTime > currentTime) {
this.wait(nextExpirationTime - currentTime);
continue;
}
SessionSet set;
// 获取已超时的会话集
set = sessionSets.remove(nextExpirationTime);
if (set != null) {
// 遍历会话
for (SessionImpl s : set.sessions) {
// 关闭会话
setSessionClosing(s.sessionId);
// 过期处理
expirer.expire(s);
}
}
// 更新下一会话超时时间点
nextExpirationTime += expirationInterval;
}
} catch (InterruptedException e) {
LOG.error("Unexpected interruption", e);
}
LOG.info("SessionTrackerImpl exited loop!");
}

会话清理

在定位到过期会话后,开始进行清理工作:

  1. 标记会话状态为“已关闭”,即isClosing=true,清理会话需要一段时间,确保这个时间内不会处理来自该客户端的新请求。
  2. 发起“会话关闭”请求,确保该会话的关闭操作在整个服务端集群中都生效
  3. 收集需要清理的临时节点,会话失效后,与该会话关联的所有临时节点都需要清除,如果在处理过程中,恰好有临时节点的删除或创建请求,需要额外处理,如果有正在删除需要避免重复删除,如果有正在创建,需要确保删除。
  4. 节点删除,完成收集后,Zookeeper会逐个转换成请求,而后创建节点删除请求触发删除所有会话关联的临时节点
  5. 移除会话,从SessionTracker移除,主要包括sessionsById、sessionWithTimeout,sessionSets这三个数据结构。
  6. 关闭连接请求

数据和存储

内存数据

zk有多个核心数据结构以将数据缓存在内存中,具体包括:

  1. DataTree:代表内存的一份完整树形结构数据以及节点监听器等,内部定义了一个nodes,类型是ConcurrentHashMap<String, DataNode>,存储了路径和节点的映射关系
  2. DataNode:是数据存储的最小但愿,包括具体的数据内容,acl,stat信息,Set孩子节点路径等。
  3. ZKDatabase:是zk的内存数据库,负责管理zookeeper所有会话、DataTree存储和事务日志,ZKDatabase会定时向磁盘dump快照数据,并在zk启动的时候,通过磁盘的事务日志和快照数据文件恢复成一个完整的内存数据库

事务日志

zk的事务日志存储在dataLogDir中,文件名如log.2c01631713,后缀为写入该事务日志的第一条事务记录ZXID,因此可以通过事务日志名快速定位事务日志。

在事务日志中,存储了客户端sessionID、CXID、ZXID、操作类型、节点数据、节点路径、节点数据内容、节点ACL,是否为临时节点,父节点的子节点版本号等信息。

具体的事务日志写入过程步骤:

  1. 确认是否有事务日志文件可写,如果是第一次写入事务日志或刚写满日志文件,此时没有日志文件与zk服务器关联,则zk会与该事务操作关联的ZXID为后缀创建一个事务日志文件,同时构建事务日志文件头信息(包含魔数、事务日志格式版本和dbid)
  2. 确定事务日志文件是否需要扩容(预分配),zk的事务日志文件采取磁盘空间与分配的策略,如果检测到当前事务日志文件剩余不足4KB,会对事务日志文件扩容64M,内容使用"0"填充。而之所以要进行与分配,是因为每次事务操作都需要写入事务日志,所以事务日志写入性能决定zk服务器对事务请求的响应,事务写入近似磁盘IO过程,文件的不断追加写入操作会触发底层磁盘IO为文件开辟新的磁盘块(磁盘seek),为了降低开辟频率,提高IO效率,通过预分配来减少Seek开销。
  3. 事务序列化,对事务内容进行序列化,最终生成一个字节数组
  4. 生成checksum,为了保证事务日志文件的完整性和数据的准确性,zk在将事务日志写入文件前会对序列化产生的字节数组计算checksum,以用于校验
  5. 写入事务日志文件流,将序列化后的事务头、事务体及checksum写入文件流中,这里可能存在缓存。
  6. 事务日志刷入磁盘,将缓存中的数据强制刷入缓存,可以通过配置控制是否要执行这一步操作

数据快照 snapshot

在每次事务操作后,zk会将数据存储在内存中,在进行若干次事务日志记录后,zk会将内存数据库的数据全量dump到本地文件,作为数据快照,可以用snapCount配置每次数据快照之间的事务操作次数。

可以通过dataDir配置指定快照数据的存储路径,类似于事务日志,文件名后缀同样以zxid结尾,表示该日志文件的第一个数据,和事务日志不同的是,数据快照没有采用预分配机制,该文件的大小可以反映zk内存的全量数据。

内存数据快照的写入流程如下:

  1. 确定是否许哟啊进行数据快照。在每次进行事务日志记录后,zk会检测是否需要进行数据快照。在实际运行中,为了避免集群所有机器同一时刻今进行数据快照,zk会采取"过半随机"策略,当已经记录的事务日志大于(snapCount / 2 + randRoll)(randRoll是[1,snapCount/2]区间的随机值)时,就会触发数据快照操作
  2. 切换事务日志文件,用下一个要写入的数据zxid来创建一个新的数据快照文件
  3. 获取全量数据(DataTree)和会话信息保存到本地磁盘中。
  4. 数据序列化,首先会序列化一个头信息,包含魔术、版本号和dbid信息,而后将会话信息和dataTree序列化,同时生成一个checksum,一并写入侉子好数据文件。

zk启动数据加载

zk启动会将存储在磁盘的快照数据加载到内存中。具体操作流程:

  1. 初始化FileTxnSnapLog,FileTxnSnapLog是zk事务日志和快照数据访问层,用于衔接上层业务和底层数据存储,初始化工作包括FileTxnLog(事务日志管理器)和FileSnap(快照数据管理器)的初始化
  2. 初始化ZKDatabase,完成了zk服务器和底层数据存储的对接,开始构建zk服务器层的内存数据库。
  3. 创建PlayBackListener监听器,主要用于接收事务应用过程中的回调,如在zk数据恢复后期,用来进行数据订正
  4. 处理快照文件,加载获取租金的100个快照文件,并逐个解析,校验checksum,拿到第一个最新的有效的快照文件,而后停止解析。
  5. 获取最新的ZXID:zxid_for_snap,代表zk开始进行数据快照的时刻
  6. 处理事务日志,根据zxid_for_snap获取该zxid后的事务日志,用来完成数据订正,恢复在快照之后处理的事务
  7. 获取最新的zxid,并解析出epoch,和磁盘的currentEpoch与acceptedEpoch对比校验。

参考

《从Paxos到Zookeeper》

最后

以上就是缥缈泥猴桃为你收集整理的Zookeeper底层技术实现细节系统模型会话管理数据和存储参考的全部内容,希望文章能够帮你解决Zookeeper底层技术实现细节系统模型会话管理数据和存储参考所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(51)

评论列表共有 0 条评论

立即
投稿
返回
顶部