一人一首成名曲,深入研究一套源码非常有意义,尤其是基础组件,会让我们深层次的了解系统架构设计的原理,同时大大提高自己的编码能力;欢迎大家一块探讨zookeeper源码!
众所周知,互联网从一开始就伴随着高并发的问题,高并发的解决方案除了应用分而治之的技术手段,还有就是在内存、硬盘、DB层面的折中处理,大家都知道访问数据的优先级:内存 > 磁盘 > 数据库;由此也催生了众多的缓存系统,例如memcache、redis等等;今天我们要说的zookeeper也是如此,有自己的缓存数据;
zookeeper的各个节点的分工不同,大致如下:
leader节点-处理事务与非事务请求;
follower节点-参与决议,转发事务请求;
observer节点-主要是分担系统负载的功能,不参与决议;
那么这些节点是如何进行数据同步的呢,也就是说,zookeeper在启动期间如何快速的完成节点之间的数据同步,来提供对外服务的呢;
zkdatabase.java 中有一个committedLog,它存放的已经提交的事务消息记录,最大存放500个这样的记录;
zkdatabase.java
protected LinkedList<Proposal> committedLog = new LinkedList<Proposal>();
系统加载期间:
zkdatabase.java
public long restore(DataTree dt, Map<Long, Integer> sessions,
PlayBackListener listener) throws IOException {
snapLog.deserialize(dt, sessions);
return fastForwardFromEdits(dt, sessions, listener);
}
这里先从snapshot文件中反序列化出数据,然后再从增量事务日志中获取大于snapshot文件最大zxid的一些数据进行加载; 注意这里传入了一个PlayBackListener,它的含义是数据加载完后的监听处理工作;也就是说我们在初始化期间committedLog里面存放的事务消息,一定是在增量事务日志中的,而不存在snapshot文件中;我们看到在加载期间循环调用了onTxnLoaded
zkdatabase.java
public long fastForwardFromEdits(DataTree dt, Map<Long, Integer> sessions,
PlayBackListener listener) throws IOException {
FileTxnLog txnLog = new FileTxnLog(dataDir);
TxnIterator itr = txnLog.read(dt.lastProcessedZxid+1);
long highestZxid = dt.lastProcessedZxid;
TxnHeader hdr;
try {
while (true) {
// iterator points to
// the first valid txn when initialized
hdr = itr.getHeader();
if (hdr == null) {
//empty logs
return dt.lastProcessedZxid;
}
if (hdr.getZxid() < highestZxid && highestZxid != 0) {
LOG.error("{}(higestZxid) > {}(next log) for type {}",
new Object[] { highestZxid, hdr.getZxid(),
hdr.getType() });
} else {
highestZxid = hdr.getZxid();
}
try {
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();
}
}
return highestZxid;
}
而onTxnLoaded的实例化函数如下:
public void onTxnLoaded(TxnHeader hdr, Record txn){
addCommittedProposal(hdr, txn);
}
其实就是调用了addCommittedProposal;
zkdatabase.java
private void addCommittedProposal(TxnHeader hdr, Record txn) {
Request r = new Request(null, 0, hdr.getCxid(), hdr.getType(), null, null);
r.txn = txn;
r.hdr = hdr;
r.zxid = hdr.getZxid();
addCommittedProposal(r);
}
public void addCommittedProposal(Request request) {
WriteLock wl = logLock.writeLock();
try {
wl.lock();
if (committedLog.size() > commitLogCount) {
committedLog.removeFirst();
minCommittedLog = committedLog.getFirst().packet.getZxid();
}
if (committedLog.size() == 0) {
minCommittedLog = request.zxid;
maxCommittedLog = request.zxid;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
try {
request.hdr.serialize(boa, "hdr");
if (request.txn != null) {
request.txn.serialize(boa, "txn");
}
baos.close();
} catch (IOException e) {
LOG.error("This really should be impossible", e);
}
QuorumPacket pp = new QuorumPacket(Leader.PROPOSAL, request.zxid,
baos.toByteArray(), null);
Proposal p = new Proposal();
p.packet = pp;
p.request = request;
committedLog.add(p);
maxCommittedLog = p.packet.getZxid();
} finally {
wl.unlock();
}
}
上述的本质是将事务消息封装为一个Request消息,然后存放到committedLog,然后更新了minCommittedLog和maxCommittedLog;committedLog 存放最大消息数量为500;
public static final int commitLogCount = 500;
加载完之后,就可以进行数据节点之间的选举流程和同步流程;
系统运行期间:
zookeeper的责任链的最后一个环节是FinalRequestProcessor,它处理完后,会去判断是否是事务请求,如果是事务请求则会将该消息添加到committedLog;
FinalRequestProcessor.java
if (Request.isQuorum(request.type)) {
zks.getZKDatabase().addCommittedProposal(request);
}
说白了,就是在运行期间,事务消息请求也会存放到committedLog中,那么这个committedLog在运行期间呢,就起到了一个非常关键的作用,就是数据同步;下节再讲一下初始化期间的同步规则。
····················
欢迎关注课程:
《Zookeeper源码分析》
《透视HashMap让你Get到源码大师的编程内功和编程思想》(限时一元福利)