Zookeeper内存结构

Zookeeper是怎么存储数据的,什么机制保证集群中数据是一致性,在网络异常,当机以及停电等异常情况下恢复数据的,我们知道数据库给我们提供了这些功能,其实zookeeper也实现了类似数据库的功能。

1.      Zookeeper内存结构

Zookeeper数据在内存中的结构类似于linux的目录结构

DataTree代表这个目录结构, DataNode代表一个节点
DataTree:
默认初始化三目录
)""
) "/zookeeper"
) "/zookeeper/quota"
DataNode
表示一个节点
) 存储了父节点的引用
) 节点的权限信息
) 子节点路径集合

Snapshot

Snapshot是datatree在内存中某一时刻的影像,zookeeper有一定的机制会定时生成datatree的snapshot。FileSnap实现了SnapShot接口负责将数据写入文件中,下面我们来看看snap相关内容。

2.1 snapshot文件格式

   Snapshot是以二进制形式存在在文件的,我们用ue打开一个新的snapshot文件

Snapshot文件的中数据大体可以分为两部分header和body。

Header数据格式:

FileHeader{
int magic //魔数 常量ZKSN 代表zookeeper snapshot文件
int version //版本 常量 2
long dbid //dbid 常量 -1
}

这里很奇怪 version和dbid都是常量,那还有什么意思,也许是保留字段为后续版本使用。

由头部字段可以计算出头部信息占用 4 + 4 + 8 =16bit的固定长度

5A 4B 53 4E 就是魔术ZKSN

00 00 00 02 就是dbid号2

FF FF FF FF FF FF FF FF就是十六进制的-1

public voidserialize(OutputArchive oa, String tag) throws IOException {
scount = ;
serializeList(longKeyMap, oa);
serializeNode(oa, newStringBuilder(""));
if (root != null) {
oa.writeString("/","path");
}
} 2.1)序列化longKeyMap是存储在datatree中的acl权限集合
readInt("map") //acl的映射个数?
while (map > ) {
readLong("long") //这个long值longKeyMap的key,作用?是这一组acl的key
readInt("acls")
while (acls) {
readInt("perms")
readString("scheme")
readString("id")
}
}
2.2)存储datatree中数据节点
readString("path")//第一个datanode ""
while(!path.equals("/")) { // "/"代表路径结束
readRecord(node, "node")包括:
readBuffer("data")
readLong("acl")
deserialize(archive,"statpersisted") 状态存储包括:
readLong("czxid") //createNode时事务号
readLong("mzxid") //createNode时与Czxid同,setData时的事务号
readLong("ctime") // 创建节点时间
readLong("mtime") //createNode时与ctime相同,setData时间
readInt("version") //createNode版本为0,setData的数据版本号
readInt("cversion") //createNode版本为0,增加/删除子节点时父节点+1
readInt("aversion") //createNode版本为0,setACL时节点的版本号
readLong("ephemeralOwner") // 临时节点表示sessionid,非临时节点这个值为0
readLong("pzxid") //createNode时与Czxid同,增加/删除子节点时为子节点事务号
readString("path") //读取下一个路径
}
) 文件尾部校验数据
2F snapshot文件结尾5位数据用来校验snapshot文件是否有效
01一个int的数值就是数字1,代表后面1一个字符数据
2F 就是snapshot的结束符/

Body数据格式:

Snapshot文件中头部信息之后,紧接着就是body部分的信息,body数据大小是动态不是固定。

1) Map<Long, Integer> sessionWithTimeoutbody信息前面部分存储的是内存中活着的session以及session的超时时间

oa.writeInt(sessSnap.size(),"count");

for (Entry<Long, Integer> entry :sessSnap.entrySet()) {

oa.writeLong(entry.getKey().longValue(), "id");

oa.writeInt(entry.getValue().intValue(),"timeout");

}

由上面序列到文件代码可以看出先写入一个int类型字段用来存储sessionWithTimeout的个数,然后在遍历集合以一个long一个int的形式写入

2) 紧接着就是对datatree序列化到文件了

我们看下datatree的序列化方法

4)Snapshot序列化

5)Snapshot反序列化

 5)TxnLog事务日志

事务日志文件用来记录事物操作,每一个事务操作如添加,删除节点等等,都会在事务日志中记录一条记录,用来在zookeeper异常情况下,通过txnlog和snapshot文件来恢复数据,下面我们来看下txnLog事务日志文件的格式

打开一个事务日志文件看看


一个日志文件LogFile: FileHeader TxnList ZeroPad三部分组成

1)   日志文件头FileHeader: {

magic   4bytes (ZKLG) //常量代表

version 4bytes        //常量2

dbid     8bytes        //这个没啥用,就是默认值0

}

头文件是固定长度 16  = 4 + 4 + 8数据,它的值也固定

2)   TxnList代表记录记录集合,txn代表一条记录

Txn:checksum Txnlen TxnHeaderRecord 0x42由顺序的五部分组成

//序列化TxnHeader Record记录到byte[]

byte[] buf = Util.marshallTxnEntry(hdr, txn);

Checksum crc = makeChecksumAlgorithm();

//根据指定数组更新校验值

crc.update(buf, 0, buf.length);

//将校验吗写入输出流

oa.writeLong(crc.getValue(), "txnEntryCRC");

//将TxnHeader Record数据写入到输出流

//1.先计算buf数据长度写入

//2.写入buf数组数据

//3.记录尾部以’B’字符结尾,写入0x42

Util.writeTxnBytes(oa, buf);

2.1) checksum校验位计算,是由Adler32校验算法计算TxnHeader Record序列化后的字节码(跟文档说明有出入,文档说是由Txnlen TxnHeaderRecord 0x42计算出来的, 可是看代码不是,难道我理解错了????????)

2.2) TxnLen:记录数据长度包括记录头TxnHeader和记录Record

2.3)TxnHeader: {

sessionid 8bytes

cxid 4bytes  // 与客户端交互的xid

zxid 8bytes  // 服务器端生成的事务id

time 8bytes  // 时间

type 4bytes  // 事务操作的类型

}

2.4)Record:事务记录的内容,由jute规范定义了序列化反序列化流程,各个事务操作都实现了Record接口,下面看下创建的事务操作记录

public class CreateTxn implements Record {

privateString path;  //创建路径

privatebyte[] data;  //节点数据内容

privatejava.util.List<org.apache.zookeeper.data.ACL> acl; //节点权限

privateboolean ephemeral; //是否临时节点

privateint parentCVersion; //父节点的版本号

//下面过程就是序列化过程,反序列化类似

publicvoid serialize(OutputArchive a_, String tag) throws Java.io.IOException {

a_.startRecord(this,tag);

a_.writeString(path,"path");

a_.writeBuffer(data,"data");

{

a_.startVector(acl,"acl");

if(acl!= null) {          int len1 =acl.size();

for(int vidx1 = 0; vidx1<len1;vidx1++) {

org.apache.zookeeper.data.ACL e1 = (org.apache.zookeeper.data.ACL)acl.get(vidx1);

a_.writeRecord(e1,"e1");

}

}

a_.endVector(acl,"acl");

}

a_.writeBool(ephemeral,"ephemeral");

a_.writeInt(parentCVersion,"parentCVersion");

a_.endRecord(this,tag);

}

2.5)0x42:每条事务记录尾部以’B’字符结尾就是0x42

3) 每个文件尾部都用一个字符 0 填充, 工具Util.padLogFile扩充文件的时候在尾部填写上

6)FileTxnLog& FileTxnSnapLog工具

FileTxnLog类用来操作事务记录文件下面我们来看看这个类主要实现方法

4.1)append方法: 用来向文件尾部添加一条记录

4.1.1)判断当前输入流logStream是否已清空(在同步处理器SyncRequestProcessor中据一定算法得出一个count,记录大于count就要rollLog,开启一个新的文件,算法是: 100000/2 + random.nextInt(100000/2), 这个十万是一个默认值可配置),清空开启一个新的文件写入

4.1.2)padFile()判断是否要扩充文件容量预分配buffer,当buffer容量小于等于4k的时候预分配,每次扩充预分配64k容量

4.1.3)数据序列化,写入输入流缓存

4.2)read(zxid): 读取事务日志, 这个方法在服务当机恢复的时候,用来遍历事务日志来恢复数据。Zxid是事物日志号递增生成,在FileTxnLog中会将大于zxid的所有日志文件组合成一个FileTxnIterator用来遍历从zxid开始的所有日志

4.2.1)FileTxnIterator的构造器中调用init方法,init方法中过滤出所有需要读的日志文件,并利用goToNextLog()方法打开第一个日志日志文件的输入流

4.2.2)FileTxnIterator的next方法用来从日志文件中读取一条记录,校验并反序列化出来,读取成功返回true,如果读到了文件末尾调goToNextLog()读下一个文件,以此递归直到最后

4.2.3)FileTxnIterator的getTxn()方法,返回next()方法中读取的记录

4.3)commit()方法,将流中数据刷到硬盘SyncRequestProcessor中任务会定时调用异步刷盘

4.4)truncate(zxid)方法,用来删除日志,主要是在恢复数据的时候,利用leader的最后有效zxid,来删除learner的无效多余的事务记录。类似于数据库中的truncate操作概念,它并不是一条一条删除记录,大于zxid的文件直接将文件删除掉,zxid所在文件直接修改文件的长度,将文件长度设置到zxid所在的位置


4.5)序列化事务记录

4.6)反序列化事务记录

5FileTxnSnapLog

这是一个工具类主要用来操作TxnLog和Snapshot文件,我们主要关注一下restore方法

7)ZKDatabase

ZKDatabase在内存中维护了zookeeper的sessions, datatree和commit logs集合。 当zookeeper server启动的时候会将txnlogs和snapshots从磁盘读取到内存中

6.1)loadDatabase: 跟数据库的启动类似zookeeper服务启动结合txnlogs和snapshot, snapshot是内存数据的某个点一份影像,takeSnapshot操作还是很耗时,为了性能根据某算法(在同步处理器SyncRequestProcessor中据一定算法得出一个count,记录大于count就要takeSnapshot,算法是: 100000/2 + random.nextInt(100000/2),这个十万是一个默认值可配置)计算出一个点来异步做一次takeSnapShot操作,这个跟数据库实现原理上很类似, 但是这样在非正常关机情况下,最新有效的那个snapshot并不是内存中最新的数据,所以需要利用txnLogs来把没有生成snapshot的操作在内存重新执行一边来恢复到非正常关闭服务那一刻内存情况。

下面我们来看一下loadDatabase的流程:

6.1.1) 构建一个PlayBackListener对象

6.1.2) snapshot的反序列,倒叙排目录下的snapshot文件,遍历查找出最新的那个有效snapshot文件进行反序列化到内存(具体流程查看snapshot那部分介绍),snapshot的反序列后我们会知道snapshot最新的zxid叫做lastProcessedZxid, 这个lastProcessedZxid之前的事务操作,都成功执行并序列到snapshot中可恢复到内存,lastProcessedZxid之后的操作只有事务日志,不能直接通过snapshot恢复。

6.1.2) lastProcessedZxid+1从事务日志文件txnLog读取事务操作

FileTxnLog txnLog = newFileTxnLog(dataDir);

TxnIterator itr =txnLog.read(dt.lastProcessedZxid+1);

遍历TxnIterator,执行processTransaction方法,就是把事务操作在内存中在执行一边把丢失的操作补回来

同时将事务操作通过PlayBackListener添加到commitedLog集合,commitedLog的事务操作在服务恢复的时候会同步到其他leaner server, 因为很有可能其他leaner server也没有及时的takesnapshot

返回最后的事务日志zxid给database,作为ZKDatabase的最新事物id

6.1.3) 在zookeeperServer成功loadDatabase后,会及时主动的做一次takesnapshot操作来得到一份最新的内存影像

8)数据存储小结

Zookeeper数据是以文件形式存储在硬盘上的,以snapshot为主,txnlog为辅。因为当对内存数据进行变更的时候,会保证将事务操作记入log日志,而snapshot只是内存某一个时刻影像,为了性能takeSnapshot生成snapshot并不是实时的,而是由后台线程根据一定规则处理的

来看看snapshot和txnlog在磁盘上的文件

文件名是以log.或者snapshot.加上一串long的16进制数字组成,这个long值就是zxid服务器端事务id

Snapshot文件名生成, FileTxnSnapLog.save方法中

long lastZxid = dataTree.lastProcessedZxid;

FilesnapshotFile = new File(snapDir, Util.makeSnapshotName(lastZxid));

如上代码创建一个新的snapshot文件,工具Util用来用来创建文件名

public static String makeSnapshotName(long zxid) {

return "snapshot." +Long.toHexString(zxid);

}

日志Log文件生成,在FileTxnLog.apend方法中,如果被执行了rollLog方法,那么文件输入流会被清空,这里会创建一个新的文件

if (logStream==null) {

logFileWrite = new File(logDir,("log." + Long.toHexString(hdr.getZxid())));

fos = newFileOutputStream(logFileWrite);

………

}

如上代码可以看出文件名是最新请求的zxid,这里snapshot和log文件都和zxid有关,那么下面我们来看看zxid。

Zxid

当客户端一个事务请求操作是leader的PrepRequestProcessor处理器会对请求进行预处理包括生成zxid设置到请求中去,zxid的生成是通过调用ZookeeperServer.getNextZxid生成

protected long hzxid = 0;

synchronized long getNextZxid() {

return ++hzxid;

}

它是hzxid一个自增的long值,有没有奇怪这个变量取名叫做hzixd多了一个h, h我的理解是high的缩写代表64位long的高32位。Zxid的分为两部分高32位用来存储每次选举的时代epoch,低32位用来存储事务请求的自增序列。所谓选举时代就是一个数值,标记代表一次选举,跟年份一样是自增的。每次服务器启动或者zookeeper异常导致重新选举都会在原来epoch值加一代表一个新的时代,工具类ZxidUtils用来操作前32或者后32位

public class ZxidUtils {

static public long getEpochFromZxid(long zxid) {

return zxid >> 32L;

}

static public long getCounterFromZxid(long zxid) {

return zxid & 0xffffffffL;

}

static public long makeZxid(long epoch,long counter) {

return (epoch << 32L) | (counter & 0xffffffffL);

}

static public String zxidToString(long zxid) {

return Long.toHexString(zxid);

}

}

比如现在epoch=4代表经历了4次选举,如果重新选举后epoch值为5,通过工具类的zxid=hzxid=ZxidUtils.makeZxid(5,0)= 21474836480,此时低32重新开始值为0, 如果这时来了新的请求值为zxid=21474836481=21474836480+ 1 = ZxidUtils.makeZxid(5, 1)

zookeeper原理解析-数据存储的更多相关文章

  1. 1.zookeeper原理解析-数据存储之Zookeeper内存结构

    Zookeeper是怎么存储数据的,什么机制保证集群中数据是一致性,在网络异常,当机以及停电等异常情况下恢复数据的,我们知道数据库给我们提供了这些功能,其实zookeeper也实现了类似数据库的功能. ...

  2. 解析数据存储MySQL

    为了适应不同项目对不同感兴趣属性的解析存储,数据存储结构采用纵向的属性列表方式,即一个url页面多个属性存储多条记录方式,并且按照text,html, data,num几大典型类型分别对应存储. 创建 ...

  3. zookeeper原理解析-序列化

    1)底层通信数据封装与操作           BinaryInputArchive& BinaryOutputArchive底层通信数据封装与操作     BinaryInputArchiv ...

  4. zookeeper原理解析-选举

    1)QuorumPeerMain加载 Zookeeper集群启动的入口类是QuorumPeerMain来加载配置启动QuorumPeer线程.首先我们来看下QuorumPeer, 谷歌翻译quorum ...

  5. Zookeeper原理分析之存储结构Snapshot

    Zookeeper内存结构 Zookeeper数据在内存中的结构类似于linux的目录结构,DataTree代表这个目录结构, DataNode代表一个节点.DataTree默认初始化三个目录:&qu ...

  6. ZooKeeper学习之路 (八)ZooKeeper原理解析

    ZooKeeper中的各种角色 ZooKeeper与客户端 每个Server在工作过程中有三种状态: LOOKING:当前Server不知道leader是谁,正在搜寻 LEADING:当前Server ...

  7. ZooKeeper原理解析

    目录 ZooKeeper中的各种角色 ZooKeeper与客户端 Zookeeper节点数据操作流程 Paxos 算法概述(ZAB 协议) ZooKeeper 的选主机制 选择机制中的概念 选举消息内 ...

  8. zookeeper原理解析-客户端与服务器端交互

    Zookeeper集群中server数量总是确定的,所以集群中的server交互采用比较可靠的bio长连接模型:不同于集群中sever间交互zookeeper客户端其实数量是未知的,为了提高zooke ...

  9. zookeeper原理解析-服务器端处理流程

    1)处理器链 这部分内容我们主要讲解zookeeper请求在zookeeper server端的处理流程,对于不同角色的zookeeper具有不同的处理流程, ZookeepeerServer的sta ...

随机推荐

  1. Atitit mac os 版本 新特性 attilax大总结

    Atitit mac os 版本 新特性 attilax大总结 1. Macos概述1 2. 早期2 2.1. Macintosh OS (系统 1.0)  1984年2 2.2. Mac OS 7. ...

  2. linux定制

    http://cc.bingj.com/cache.aspx?q=OpenEmbedded+clfs&d=4706495287069596&mkt=zh-CN&setlang= ...

  3. Android中的AlertDialog使用示例五(自定义对话框)

    在Android开发中,我们经常会需要在Android界面上弹出一些对话框,比如询问用户或者让用户选择.这些功能我们叫它Android Dialog对话框,AlertDialog实现方法为建造者模式. ...

  4. AEAI CRM_v1.5.2升级说明,开源客户关系管理系统

    1.升级说明 本次AEAI CRM升级内容主要是针对数通畅联推出AEAI ECP企业云联平台而升级的,其中对AEAI CRM的各模块进行扩展,同时增加了移动门户版功能及为AEAI ECP提供数据服务接 ...

  5. [Erlang 0115] 2014值得期待的Erlang两本新书

    在2014年的开头就有这样一个令人振奋的好消息,Erlang有一本新书即将出版 <The Erlang Runtime System>,其作者happi在2013年3月份公布了这本书的写作 ...

  6. HTML---用记事本写html

    <DOCTYPE HTML> <--DOCTYPE 文档类型,浏览器按照该类型解析--> <html> <head> <title>这个是h ...

  7. ASP.NET MVC 5 03 - 安装MVC5并创建第一个应用程序

    不知不觉 又逢年底, 穷的钞票 所剩无几. 朋友圈里 各种装逼, 抹抹眼泪 MVC 继续走起.. 本系列纯属学习笔记,如果哪里有错误或遗漏的地方,希望大家高调指出,当然,我肯定不会低调改正的.(开个小 ...

  8. linux shell字符串内置的常用操作(获取长度、查找、替换)

    在编写shell程序时,经常会涉及到字符串相关操作.有许多命令语句,如awk,sed都能够做字符串各种操作. 事实上shell内置一系列操作符号,能够达到相似效果,使用内部操作符会省略启动外部程序等时 ...

  9. GDB 完全教程

    一.简介 GDB是一个功能强大的调试器,它是一个自由软件,能够用在许多UNIX平台上.它同时也是Linux系统中的默认调试器.GDB已被移植到许多其他的计算机平台上,并且能够用于调试嵌入式实时系统.一 ...

  10. JAVA中整型的存储和左右移位运算

    byte,1个字节8位, -128 ~ 127之间,首位表示正负,0为正,1为负,0111,1111表示127,-127为127取反1000,0000,再加1,即1000,0001为-127,-127 ...