Zookeeper服务端对于会话创建的处理,大体可以分为请求接收,会话创建,预处理,事务处理,事务应用和会话响应6大环节。
请求接收
会话创建
预处理
事务处理
Sync流程
所谓Sync流程,其核心就是使用SyncRequestProcessor处理器记录事务日志的过程。
完成事务日志记录后,每个Follower服务器都会向Leader服务器发送ACK消息,表明自身完成了事务日志的记录。
Proposal流程
每一个事务请求都需要集群中过半机器投票认可才能被真正应用到Zookeeper的内存数据库中去,这个投票与统计的过程被称为“Proposal流程”。
Commit流程
事务应用
会话响应
大体分为四个步骤,分别是请求的预处理,事务处理,事务应用和请求响应。
预处理
事务处理
对于事务请求,ZooKeeper服务端都会发起事务处理流程。无论对于会话创建请求还是SetData请求,或是其他事务请求,事务处理流程都是一致的,都是由ProposalRequestProcessor处埋器发起,通过Sync、Proposal 和Commit三个子流程相互协作完成的。
事务应用
请求响应
大体可以分为3个步骤,分别是请求的预处理,非事务请求和请求响应。
预处理
由于GetData请求是非事务请求,因此省去了许多事务预处理逻辑,包括创建请求事务头、ChangeRecord和事务体等,以及对数据节点版本的检查。
非事务处理
请求响应
在Zookeeper中,数据存储分为两部分:内存数据存储于磁盘数据存储。
private final ConcurrentHashMap nodes = new ConcurrentHashMap();
private final Map> ephemerals = new ConcurrentHashMap>();
事务日志文件名后缀其实是一个事务ID:zxid,并且是写入该日志文件第一条事务记录的zxid
zxid本身由两部分组成,高32位代表当前Leader周期(epoch),低32位则是真正的操作序列号。
日志写入
public synchronized boolean append(TxnHeader hdr, Record txn)
throws IOException {
// 根据事务id来判断目前最大的zxid,为了判断是否是和上一个可写的事务日志有关联
if (hdr.getZxid() <= lastZxidSeen) {
LOG.warn("Current zxid " + hdr.getZxid() + " is <= " + lastZxidSeen + " for “ + hdr.getType());
} else {
lastZxidSeen = hdr.getZxid();
}
// logStream即日志流为空,用来存序列化数据
if (logStream == null) {
// 根据zxid创建新的文件
logFileWrite = new File(logDir, Util.makeLogName(hdr.getZxid()));
// 两个流
fos = new FileOutputStream(logFileWrite);
logStream = new BufferedOutputStream(fos);
oa = BinaryOutputArchive.getArchive(logStream);
FileHeader fhdr = new FileHeader(TXNLOG_MAGIC, VERSION, dbId);
// 序列化
fhdr.serialize(oa, "fileheader");
// 提取文件流,刷新到磁盘
logStream.flush();
// 当前通道的大小
currentSize = fos.getChannel().position();
streamsToFlush.add(fos);
}
// 这一步判断剩余空间不足4k时填充文件至64M,为了效率
// 文件的不断追加写入操作会触发底层磁盘I/O为文件开辟新的磁盘块即磁盘seek
// 避免随着每次事务的写入过程中文件大小增长带来的seek开销,直至创建新的事务日志
currentSize = padFile(fos.getChannel());
// 把事务头和事务体序列化
byte[] buf = Util.marshallTxnEntry(hdr, txn);
if (buf == null || buf.length == 0) {}
// 生成校验值,用了Adler32算法
Checksum crc = makeChecksumAlgorithm();
crc.update(buf, 0, buf.length);
// 写入buffer流中
oa.writeLong(crc.getValue(), "txnEntryCRC");
// 将序列化的事务记录写入OutputArchive
Util.writeTxnBytes(oa, buf);
// 但是这时候还没有写入文件!!!只在buffer流中。真正写入文件是在commit方法中
return true;
}
日志截断
public boolean truncate(long zxid) throws IOException {
FileTxnIterator itr = null;
try {
// 获取迭代器
itr = new FileTxnIterator(this.logDir, zxid);
PositionInputStream input = itr.inputStream;
// 从当前位置开始清空
long pos = input.getPosition();
RandomAccessFile raf = new RandomAccessFile(itr.logFile, "rw");
raf.setLength(pos);
raf.close();
// 存在下一个log文件
while (itr.goToNextLog()) {
// 删除
if (!itr.logFile.delete()) {
LOG.warn("Unable to truncate {}", itr.logFile);
}
}
} finally {
// 关闭迭代器
close(itr);
}
return true;
}
数据快照用来记录Zookeeper服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中。
快照数据文件也是使用zxid的十六进制表示来作为文件名后缀,该后缀标识了本次数据快照开始时刻的服务器最新zxid。
将内存数据库写入快照数据文件中其实是一个序列化过程。
Zookeeper会在进行若干次事务日志记录之后,将内存数据库的全量数据Dump到本地文件中,这个过程就是数据快照。
数据快照
public synchronized void serialize(DataTree dt, Map sessions, File snapShot)
throws IOException {
if (!close) {
// 输出流
OutputStream sessOS = new BufferedOutputStream(new FileOutputStream(snapShot));
CheckedOutputStream crcOut = new CheckedOutputStream(sessOS, new Adler32());
//CheckedOutputStream cout = new CheckedOutputStream()
OutputArchive oa = BinaryOutputArchive.getArchive(crcOut);
// 新生文件头
FileHeader header = new FileHeader(SNAP_MAGIC, VERSION, dbId);
// 序列化dt、sessions、header
serialize(dt, sessions, oa, header);
// 获取验证的值
long val = crcOut.getChecksum().getValue();
// 写入值
oa.writeLong(val, "val");
// 写入"/"
oa.writeString("/", "path");
// 强制刷新
sessOS.flush();
crcOut.close();
sessOS.close();
}
}
数据的初始化工作,其实就是从磁盘中加载数据的过程,主要包括了从快照文件中加载快照数据和根据事务日志记录进行数据订正两个过程。
public long restore(DataTree dt, Map sessions,
PlayBackListener listener) throws IOException {
// 根据snap文件反序列化dt和sessions
snapLog.deserialize(dt, sessions);
return fastForwardFromEdits(dt, sessions, listener);
}
public long fastForwardFromEdits(DataTree dt, Map sessions, PlayBackListener listener) throws IOException {
FileTxnLog txnLog = new FileTxnLog(dataDir);
// 获取比最后处理的zxid+1大的log文件的迭代器
TxnIterator itr = txnLog.read(dt.lastProcessedZxid + 1);
// 最大的zxid
long highestZxid = dt.lastProcessedZxid;
TxnHeader hdr;
try {
while (true) {
// itr在read函数调用后就已经指向第一个合法的事务
// 获取事务头
hdr = itr.getHeader();
// 事务头为空
if (hdr == null) {
//empty logs
// 表示日志文件为空
return dt.lastProcessedZxid;
}
// 事务头的zxid小于snapshot中的最大zxid并且其不为0,则会报错
if (hdr.getZxid() < highestZxid && highestZxid != 0) {
LOG.error("{}(higestZxid) > {}(next log) for type {}",
new Object[]{highestZxid, hdr.getZxid(),
hdr.getType()});
} else { // 重新赋值highestZxid
highestZxid = hdr.getZxid();
}
try {
// 在datatree上处理事务
processTransaction(hdr, dt, sessions, itr.getTxn());
} catch (KeeperException.NoNodeException e) {
throw new IOException("Failed to process transaction type: " +
hdr.getType() + " error: " + e.getMessage(), e);
}
// 每处理完一个事务都会进行回调
listener.onTxnLoaded(hdr, itr.getTxn());
if (!itr.next())
break;
}
} finally {
if (itr != null) {
itr.close();
}
}
// 返回最高的zxid
return highestZxid;
}
当Learner服务器向Leader完成注册后,就进入数据同步环节。
直接差异化同步(DIFF同步)
场景:peerLastZxid介于minCommittedLog和maxCommittedLog之间
全量同步(SNAP同步)
场景1:peerLastZxid小于minCommittedLog
场景2:Leader服务器上没有提议缓存队列
仅回滚同步(TRUNC同步)
当Leader服务器发现某个Learner包含了一条自己没有的事务记录,那么久需要让该Learner进行事务回滚。
大家可以关注我的微信公众号一起学习进步。