上一篇说道了server端有链式调用来处理客户端请求,链式调用主要有三个处理器,分别是preRequestProcessor、syncRequestProcessor和filnalRequestProcessor,下面来看这三个处理器的内容。
首先是preRequestProcessor,可以看到根据请求头的请求类型的不同,进入不同的CASE,这意味着命令是可定制化的。再讲述server之前,我们知道客户端已经和服务端建链,客户端此时的状态时是一直在sendThread的线程循环中,等待可发送的包来消费,并且每隔一段时间会发送ping-pong请求,来保持服务端和客户端链路正常。
protected void pRequest(Request request) throws RequestProcessorException {
LOG.info("Prep>>> cxid = " + request.cxid + " type = " +
request.type + " id = 0x" + Long.toHexString(request.sessionId));
request.setHdr(null);
request.setTxn(null);
try {
//识别命令
switch (request.type) {
case OpCode.createContainer:
case OpCode.create:
case OpCode.create2:
CreateRequest create2Request = new CreateRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, create2Request, true);
break;
case OpCode.createTTL:
CreateTTLRequest createTtlRequest = new CreateTTLRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, createTtlRequest, true);
break;
case OpCode.deleteContainer:
case OpCode.delete:
DeleteRequest deleteRequest = new DeleteRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, deleteRequest, true);
break;
case OpCode.setData:
SetDataRequest setDataRequest = new SetDataRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, setDataRequest, true);
break;
case OpCode.reconfig:
ReconfigRequest reconfigRequest = new ReconfigRequest();
ByteBufferInputStream.byteBuffer2Record(request.request, reconfigRequest);
pRequest2Txn(request.type, zks.getNextZxid(), request, reconfigRequest, true);
break;
case OpCode.setACL:
SetACLRequest setAclRequest = new SetACLRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, setAclRequest, true);
break;
case OpCode.check:
CheckVersionRequest checkRequest = new CheckVersionRequest();
pRequest2Txn(request.type, zks.getNextZxid(), request, checkRequest, true);
break;
case OpCode.multi:
.
//All the rest don't need to create a Txn - just verify session
case OpCode.sync:
case OpCode.exists:
case OpCode.getData:
case OpCode.getACL:
case OpCode.getChildren:
case OpCode.getAllChildrenNumber:
case OpCode.getChildren2:
case OpCode.ping:
case OpCode.setWatches:
case OpCode.setWatches2:
case OpCode.checkWatches:
case OpCode.removeWatches:
case OpCode.getEphemerals:
case OpCode.multiRead:
case OpCode.addWatch:
....
nextProcessor.processRequest(request);
}
为了更有条理的梳理请求,我们先从简答的客户端增删查改节点的路径开始,从下面增加节点值开始。 三个参数,第一个zk.setData是设置根节点的值,三个参数分别是路径、节点值和版本号。形成请求后提交到客户端的发送队列。
public Stat setData(final String path, byte[] data, int version) throws KeeperException, InterruptedException {
final String clientPath = path;
PathUtils.validatePath(clientPath);
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.setData);
SetDataRequest request = new SetDataRequest();
request.setPath(serverPath);
request.setData(data);
request.setVersion(version);
SetDataResponse response = new SetDataResponse();
ReplyHeader r = cnxn.submitRequest(h, request, response, null);
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code.get(r.getErr()), clientPath);
}
return response.getStat();
}
synchronized (state) { //锁住state状态,相当于是队列是同步的,保证包的顺序性
if (!state.isAlive() || closing) {
conLossPacket(packet);
} else {
// If the client is asking to close the session then
// mark as closing
if (h.getType() == OpCode.closeSession) {
closing = true;
}
outgoingQueue.add(packet);
}
}
sendThread.getClientCnxnSocket().packetAdded(); //唤醒selector
看完客户端处理,看看服务端是如何处理set 命令的请求的。首先进入前置处理器,PrerequestProcessor,首先初始化请求,然后进入请求处理函数pRequest2Txn。
LOG.info("Prep>>> cxid = " + request.cxid + " type = " +
request.type + " id = 0x" + Long.toHexString(request.sessionId));
request.setHdr(null); //初始化请求
request.setTxn(null);
...
case OpCode.setData:
SetDataRequest setDataRequest = new SetDataRequest();//构造数据对象
//自增zkserver hzxid,改参数是一个 AtomicLong 类型,线程安全
pRequest2Txn(request.type, zks.getNextZxid(), request, setDataRequest, true);
break;
这个函数初始化一个txnheader,记录请求的sessionid,serverid和zxid(hzxid),以及时间和请求类型,方便后续请求写入日志和数据入snapshot。然后进入到setData case分支。该分支主要处理记录要变更的节点数据、zxid、版本以及权限信息,并放入处理队列,以及更新部分请求信息。
protected void pRequest2Txn(int type, long zxid, Request request, Record record, boolean deserialize) throws KeeperException, IOException, RequestProcessorException {
if (request.getHdr() == null) {
request.setHdr(new TxnHeader(request.sessionId, request.cxid, zxid,
Time.currentWallTime(), type)); //sessionID在请求过程初始随机化了,cxid指sid,zxid数据计数器
}
...
case OpCode.setData:
zks.sessionTracker.checkSession(request.sessionId, request.getOwner());//服务端也会检查客户端的session是否超时,要求进行重连
SetDataRequest setDataRequest = (SetDataRequest) record;
if (deserialize) {
ByteBufferInputStream.byteBuffer2Record(request.request, setDataRequest);//复制请求数据buffer到setDataRequest
}
path = setDataRequest.getPath();
validatePath(path, request.sessionId); //验证节点路径是否有效
nodeRecord = getRecordForPath(path); //加队列锁从outstandingChanges队列中读取上一次节点修改信息以及加node锁从zkdatabase获取该路径的节点信息
zks.checkACL(request.cnxn, nodeRecord.acl, ZooDefs.Perms.WRITE, request.authInfo, path, null); //验证节点acl,验证不通过抛异常
int newVersion = checkAndIncVersion(nodeRecord.stat.getVersion(), setDataRequest.getVersion(), path); //验证修改的节点最新的版本和当前的版本是否一致,若不一直抛出异常,-1的话不验证版本
request.setTxn(new SetDataTxn(path, setDataRequest.getData(), newVersion)); //设置入数据库的节点名称和值以及版本号
nodeRecord = nodeRecord.duplicate(request.getHdr().getZxid());
nodeRecord.stat.setVersion(newVersion); // version+1
nodeRecord.stat.setMtime(request.getHdr().getTime());//记录操作时间
nodeRecord.stat.setMzxid(zxid);//zid+1
nodeRecord.data = setDataRequest.getData();
nodeRecord.precalculatedDigest = precalculateDigest(
DigestOpCode.UPDATE, path, nodeRecord.data, nodeRecord.stat);
setTxnDigest(request, nodeRecord.precalculatedDigest);
addChangeRecord(nodeRecord); //将NodeRecoord记录到zkserver的队列
break;
....
前置处理器部分处理完后,进入同步处理器,该处理器也是在一个线程中进行阻塞消费队列,该处理器在源码中标记了集群中不同角色的作用, 如果角色是Leader,则同步请求并落盘,给你发ack,若是follwer,则同样数据落盘,但是通过SendAckRequestProcessor发送给leader,若是Observer,则单纯数据落盘。
因为这个是单机的zk,所以这个会进行数据落盘,然后直接进入最终处理器。下面是该处理器处理过程。先借鉴一下别人的分析,帮助我们理事物日志txnlog和数据存储文件snapshot,正好找到这段分析就是zk数据读写的源码分析总结,太巧了~~~
ZK的数据存储在磁盘上分为快照和事务日志两种:
1.对于客户端的写请求zk会将其写入到transaction log中
2.如果事务日志达到一定数量,则产生一个新日志,并启动一个线程进行进行takeSnapshot,把当前的内存DataTree和会话信息写入到snapshot file中,snapshot的文件名以datatree的最后处理的transactionId为结尾
3.ZK在启动的时候首先将snapshot从磁盘加载到内存的DataTree中,然后根据dataTree.lastProcessedZxid+1找到需要处理的transaction log记录,一一附加到datatree上。
Zookeeper高效原因之一就是对请求的处理主要是操作内存结构. 然而对于每个写请求都需要写事务日志,为了提高append日志效率,zk使用了group commit机制,也就是并非每条日志都直接flush到磁盘上,而是等到多个请求产生的日志缓存起来,到达指定条数后,一次性flush到磁盘上。
此外zk为了在运行过程中迅速地向事务日志文件中append记录,为每个txn log文件预先分配了指定大小的磁盘块(通过preAllocSize来设置),默认是64M,接近这个值的时候(少于4k)便会再次分配64M.这样做的原因是如果没有预先分配,那么写完一个块的话就要重新分配,会带来额外的磁盘寻道。
下面是这个同步请求落盘的同步处理器代码,基本上和上面的分析能对上,现在我们明白,zk是通过事物日志和zxid来更新内存数据。
@Override
public void run() {
try {
// we do this in an attempt to ensure that not all of the servers
// in the ensemble take a snapshot at the same time
resetSnapshotStats();
lastFlushTime = Time.currentElapsedTime();
while (true) {
ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUE_SIZE.add(queuedRequests.size());
long pollTime = Math.min(zks.getMaxWriteQueuePollTime(), getRemainingDelay());
Request si = queuedRequests.poll(pollTime, TimeUnit.MILLISECONDS);//先处理第队列的剩余元素
if (si == null) {
/* We timed out looking for more writes to batch, go ahead and flush immediately */
flush();
si = queuedRequests.take();
}
if (si == REQUEST_OF_DEATH) {
break;
}
LOG.info("*^^*进入中间处理器{}", si);
long startProcessTime = Time.currentElapsedTime();
ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUE_TIME.add(startProcessTime - si.syncQueueStartTime);
// track the number of records written to the log
if (zks.getZKDatabase().append(si)) { //将此次请求请写入txn事物日志 新建Log.1,并且会预留磁盘通过preAllocSize来设置),默认是64M
if (shouldSnapshot()) { //根据日志数量和日志大小判断是否数据入库
resetSnapshotStats();
// roll the log
zks.getZKDatabase().rollLog();
// take a snapshot
if (!snapThreadMutex.tryAcquire()) { //等待上一个snapshot写完
LOG.warn("Too busy to snap, skipping");
} else {
new ZooKeeperThread("Snapshot Thread") {
public void run() {
try {
zks.takeSnapshot();//保存data session到新的snap中,zkserver启动的时候会重新刷一次
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
snapThreadMutex.release();
}
}
}.start();
}
}
} else if (toFlush.isEmpty()) { //没有可写的包,处理本次请求
// optimization for read heavy workloads
// iff this is a read, and there are no pending
// flushes (writes), then just pass this to the next
// processor
if (nextProcessor != null) {
nextProcessor.processRequest(si);//进入后置处理
if (nextProcessor instanceof Flushable) {
((Flushable) nextProcessor).flush();
}
}
continue;
}
toFlush.add(si);
if (shouldFlush()) { //设置批量写入日志,批量更新
flush();
}
ServerMetrics.getMetrics().SYNC_PROCESS_TIME.add(Time.currentElapsedTime() - startProcessTime);
}
} catch (Throwable t) {
handleException(this.getName(), t);
}
LOG.info("SyncRequestProcessor exited!");
}
结束完上面的处理,可以进入最后一个处理器,finalprocessprocessor ,后置处理器逻辑主要是更新内存中的zktree,更新zxid cversion等,然后返回结果给客户端。主要逻辑集中在这个函数。可以看到最后面如果是个分布式请求,会将结果发送给一个ack队列中,然后给follwer同步,后续看zk集群一些源码也会有涉及。
// entry point for FinalRequestProcessor.java
public ProcessTxnResult processTxn(Request request) {
TxnHeader hdr = request.getHdr();
processTxnForSessionEvents(request, hdr, request.getTxn()); //处理创建和关闭session,对于某些特定请求
final boolean writeRequest = (hdr != null); //写请求,带有Hdr
final boolean quorumRequest = request.isQuorum(); //分布式请求,带有修改的请求
// return fast w/o synchronization when we get a read
if (!writeRequest && !quorumRequest) { //读请求
return new ProcessTxnResult();
}
synchronized (outstandingChanges) { //用来更新队列的块以及更新zxdb的数据
//对比权限和更改内存数据 以及更新server的zxid cversion
ProcessTxnResult rc = processTxnInDB(hdr, request.getTxn(), request.getTxnDigest());
// request.hdr is set for write requests, which are the only ones
// that add to outstandingChanges.
if (writeRequest) {
long zxid = hdr.getZxid();
while (!outstandingChanges.isEmpty()
&& outstandingChanges.peek().zxid <= zxid) { //已经更新过的请求队列可以出列了
ChangeRecord cr = outstandingChanges.remove();
ServerMetrics.getMetrics().OUTSTANDING_CHANGES_REMOVED.add(1);
if (cr.zxid < zxid) {
LOG.warn(
"Zxid outstanding 0x{} is less than current 0x{}",
Long.toHexString(cr.zxid),
Long.toHexString(zxid));
}
if (outstandingChangesForPath.get(cr.path) == cr) {
outstandingChangesForPath.remove(cr.path);
}
}
}
// do not add non quorum packets to the queue.
if (quorumRequest) {
getZKDatabase().addCommittedProposal(request); //加入commit队列相当于ack了,发送到follower
}
return rc;
}
}
客户端会阻塞等到主线程,直达结果又返回,然后获取结果。
public ReplyHeader submitRequest(
RequestHeader h,
Record request,
Record response,
WatchRegistration watchRegistration,
WatchDeregistration watchDeregistration) throws InterruptedException {
ReplyHeader r = new ReplyHeader();
Packet packet = queuePacket(
h,
r,
request,
response,
null,
null,
null,
null,
watchRegistration,
watchDeregistration);
synchronized (packet) { //加锁同步请求获取
if (requestTimeout > 0) {
// Wait for request completion with timeout
waitForPacketFinish(r, packet); //加入requestTimeout超时等待
} else {
// Wait for request completion infinitely
while (!packet.finished) {
packet.wait();//释放锁,等待,加同步
}
}
}
if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {
sendThread.cleanAndNotifyState();
}
return r;
}
看完整个客户端set操作流程,了解了zk客户端与服务端通信过程,能够更加了解zk的客户端和服务端网络模型,zk数据结构,数据的读写与保存,以及在分布式集群中,不同角色的处理逻辑。对整个zk的有了一个整体的概念。例如这时候让你想一下zk watch的原理,其实比较简单了,就是客户端创建一个监听事件,会在服务端保存一个cnxserver实例,该实例实际上是维持客户端与服务端的连接对象,然后zkserver会维持一个watchmanger来管理watch,当节点发生改变时,就会触发watch,最终利用cnxserver实例来通知客户端,让客户端知道节点的发生了什么事件。
这个是zk的单机模式,有了这些基础,后面取了解zk的集群模式,并且了解选主算法,会有很大的帮助。后续也会探究zk集群模式下的一些源码。