ZooKeeper 源码分析 session建立及超时机制 (基于3.4.6)

1. ZooKeeper session建立及超时机制 概述

首先说一下, 为什么要写下这篇, 原因也很简单, 因为session的建立及超时机制特别

1. ZooKeeper 集群的所有 sessionImpl 都在 Leader端, 而Follower端只将 sessionId 与 timeout 存储到 HashMap里面
2. 在 Leader 端 每个 LearnerHandler 会定期的向Follower/Observer发送给ping 包, Follower/Observer在接受到之后, 将会将对应的要检查超时的 sessionId 发给 Leader, 统一让Leader进行检查
3. Leader用SessionTrackerImpl线程来检查Session是否超时, 而 session 将放在一个以 expirationTime为Key的HashMap里面, 定时的获取并检查, 超时的话就进行删除, 不超时的话将session移到下一个将要超时的 Bucket 里面(见touchSession)

接下来就直接上代码(我们这里从Follower的角度出发)

1. Client 连接 Follower

当Client连接Follower时, 会调用 FollowerZooKeeperServer.processPacket 来进行处理(这里不涉及Zookeeper自己的NIO/NettyNIO处理部分), 最后会直接调用 LeaderZooKeeperServer.submitRequest方法将对应的Request进行提交, 到这里有必要说一下 Follower的RequestProcessor处理链

/**
 * Follower 的 RequestProcessor 处理链 (2条)
 * 第一条 链
 * FollowerRequestProcessor: 区分处理 Request, 将 Request 交由下个 RequestProcessor, 而若涉及事务的操作, 则 交由 Follower 提交给 leader (zks.getFollower().request())
 * CommitProcessor: 这条链决定这着 Request 能否提交, 里面主要有两条链 , queuedRequests : 存储着 等待 ACK 过半确认的 Request, committedRequests :存储着 已经经过 ACK 过半确认的 Request
 * FinalRequestProcessor: 前面的 Request 只是在经过 SynRequestProcessor 持久化到 txnLog 里面, 而 这里做的就是真正的将数据改变到 ZKDataBase 里面(作为  Follower 一定会在 FollowerZooKeeperServer.logRequest 进行同步Request 数据到磁盘里面后再到 FinalRequestProcessor)
 *
 * 第二条 链
 * SynRequestProcessor: 主要是将 Request 持久化到 TxnLog 里面, 其中涉及到 TxnLog 的滚动, 及 Snapshot 文件的生成
 * AckRequestProcessor: 主要完成 针对 Request 的 ACK 回复, 对 在Leader中就是完成 leader 自己提交 Request, 自己回复 ACK
 *
 * 1. FollowerRequestProcessor --> CommitProcessor --> FinalRequestProcessor
 * 2. SyncRequestProcessor --> SendAckRequestProcessor
 */
@Override
protected void setupRequestProcessors() {
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    commitProcessor = new CommitProcessor(finalProcessor, Long.toString(getServerId()), true);
    commitProcessor.start();
    firstProcessor = new FollowerRequestProcessor(this, commitProcessor);
    ((FollowerRequestProcessor) firstProcessor).start();
    syncProcessor = new SyncRequestProcessor(this, new SendAckRequestProcessor((Learner)getFollower()));
    syncProcessor.start();
}

Leader 的RequestProcessor处理链

/**
 * Leader 的 RequestProcessor 处理链
 *
 * 第一条 RequestProcessor 链
 * PreRequestProcessor : 创建和修改 TxnRequest
 * ProposalRequestProcessor : 提交  Proposal 给各个 Follower 包括 Leader自己 (Leader自己是在 ProposalRequestProcessor 里面通过 syncProcessor.processRequest(request) 直接提交 Proposal)
 * CommitProcessor : 将 经过集群中的过半 Proposal 提交(提交的操作直接在 Leader.processAck -> zk.commitProcessor.commit(p.request))
 * ToBeAppliedRequestProcessor: 这个处理链其实是 Request 处理时经过的最后一个 RequestProcessor, 其中最令人困惑的是 toBeApplied, 而 toBeApplied 中其实维护的是 集群中经过 过半 ACK 同意的 proposal, 只有经过 FinalRequestProcessor 处理过的 Request 才会在 toBeApplied 中进行删除
 * FinalRequestProcessor: 前面的 Request 只是在经过 SynRequestProcessor 持久化到 txnLog 里面, 而 这里做的就是真正的将数据改变到 ZKDataBase 里面
 *
 * 第二条 RequestProcessor 链
 * 在 leader 中, SynRequestProcessor, AckRequestProcessor 的创建其实是在 ProposalRequestProcessor 中完成的
 * SynRequestProcessor: 主要是将 Request 持久化到 TxnLog 里面, 其中涉及到 TxnLog 的滚动, 及 Snapshot 文件的生成
 * AckRequestProcessor: 主要完成 针对 Request 的 ACK 回复, 对 在Leader中就是完成 leader 自己提交 Request, 自己回复 ACK
 *
 * PrepRequestProcessor --> ProposalRequestProcessor --> CommitProcessor --> ToBeAppliedRequestProcessor --> FinalRequestProcessor
 *                                                    \
 *                                                     SynRequestProcessor --> AckRequestProcessor (这条分支是在 ProposalRequestProcessor 里面进行构建的)
 */
@Override
protected void setupRequestProcessors() {
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(
            finalProcessor, getLeader().toBeApplied);
    // 投票确认处理器
    commitProcessor = new CommitProcessor(toBeAppliedProcessor,
            Long.toString(getServerId()), false);
    commitProcessor.start();
    // 投票发起处理器
    ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this,
            commitProcessor);
    proposalProcessor.initialize();
    // 预处理器
    firstProcessor = new PrepRequestProcessor(this, proposalProcessor);
    ((PrepRequestProcessor)firstProcessor).start();
}

下面先来张总体的流程图:

session检查.png

上面这张图片有点大, 建议在 百度云 里面进行下载预览, 接下来我们会一步一步进行下去PS: 吐槽一下简书的图片系统, 图片一旦大了就预览出问题(不清晰)

整个流程涉及好几个过程, 下面一一分析:

2. FollowerZooKeeperServer createSession
// 创建 session
long createSession(ServerCnxn cnxn, byte passwd[], int timeout) {
    long sessionId = sessionTracker.createSession(timeout);     // 1. 创建 会话 Session, 生成 SessionImpl 放入对应的 sessionsById, sessionsWithTimeout, sessionSets 里面, 返回 sessionid
    Random r = new Random(sessionId ^ superSecret);
    r.nextBytes(passwd);                                        // 2. 生成一个随机的 byte[] passwd
    ByteBuffer to = ByteBuffer.allocate(4);
    to.putInt(timeout);
    cnxn.setSessionId(sessionId);                               // 3. 提交 Request 到RequestProcessor 处理链
    submitRequest(cnxn, sessionId, OpCode.createSession, 0, to, null);
    return sessionId;                                           // 4. 返回此回话对应的 sessionId
}
3. FinalRequestProcessor 处理请求
switch (request.type) {
case OpCode.sync:                                // 2. 处理同步数据
    zks.pendingSyncs.add(request);
    zks.getFollower().request(request);
    break;
case OpCode.create:                              // 3. 从这里 看出 path 创建/删除/设置数据/设置访问权限/创建,关闭session, 多个操作 -> 都 是 Follower 交给 leader 进行处理
case OpCode.delete:
case OpCode.setData:
case OpCode.setACL:
case OpCode.createSession:
case OpCode.closeSession:
case OpCode.multi:
    zks.getFollower().request(request);          // 4. 将事务类的请求都交给 Leader 处理
    break;
}

follower提交 Request 到Leader

/**
 * send a request packet to the leader
 *
 * @param request
 *                the request from the client
 * @throws IOException
 */
void request(Request request) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();       // 1. 将要发送给 Leader 的数据包序列化
    DataOutputStream oa = new DataOutputStream(baos);
    oa.writeLong(request.sessionId);
    oa.writeInt(request.cxid);
    oa.writeInt(request.type);
    if (request.request != null) {
        request.request.rewind();
        int len = request.request.remaining();
        byte b[] = new byte[len];
        request.request.get(b);
        request.request.rewind();
        oa.write(b);
    }
    oa.close();                                                     // 2. 封装请求数据包
    QuorumPacket qp = new QuorumPacket(Leader.REQUEST, -1, baos.toByteArray(), request.authInfo);

    writePacket(qp, true);                                         // 3. 将 事务请求 request 发送给 Leader
}

FinalRequestProcessor处理了Request后一方面是follower提交给Leader, 另一方面是提交给 CommitProcessor

4. CommitProcessor 处理请求
@Override
public void run() {
    try {
        Request nextPending = null;            
        while (!finished) {                                            // while loop
            int len = toProcess.size();
            for (int i = 0; i < len; i++) {
                Request request = toProcess.get(i);
                LOG.info("request:"+ request);                         // 1. Follower 里面就是 丢给 FinalRequestProcessor 处理
                nextProcessor.processRequest(request);                 // 2. 将 ack 过半的 Request 丢给 ToBeAppliedRequestProcessor 来进行处理 (Leader 中是这样处理)
            }
            toProcess.clear();
            synchronized (this) {
                if ((queuedRequests.size() == 0 || nextPending != null)// 3. 如果没有 Commit 的请求, 则进行wait, 直到 commit 请求的到来
                        && committedRequests.size() == 0) {
                    wait();
                    continue;
                }
                // First check and see if the commit came in for the pending
                // request
                if ((queuedRequests.size() == 0 || nextPending != null)// 4. 当 Leader 通过了 过半ACK确认后, 则会将这个 Request 丢给 Follower 来处理, Follower 会直接将 Request 丢到 committedRequests 里面, 进而处理
                        && committedRequests.size() > 0) {
                    Request r = committedRequests.remove();
                    /*
                     * We match with nextPending so that we can move to the
                     * next request when it is committed. We also want to
                     * use nextPending because it has the cnxn member set
                     * properly.
                     */
                    if (nextPending != null                            // 5. 这里其实就是比较 nextPending 与 committedRequests 中的 request 请求
                            && nextPending.sessionId == r.sessionId    // 6. 而 nextPending 又是从 queuedRequests 里面拿出来的, 若相同, 则直接用 committedRequests 里面的 消息头, 消息体, zxid
                            && nextPending.cxid == r.cxid) {
                        // we want to send our version of the request.
                        // the pointer to the connection in the request
                        nextPending.hdr = r.hdr;
                        nextPending.txn = r.txn;
                        nextPending.zxid = r.zxid;
                        toProcess.add(nextPending);                    // 7. 将 请求 直接加入 toProcess, 直到下次 loop 被 nextProcessor 处理
                        nextPending = null;
                    } else {                                           // 8. Leader 直接 调用 commit 方法提交的 请求, 直接加入 toProcess, 直到下次 loop 被 nextProcessor 处理 (这个 IF 判断中是 Leader 中处理的)
                        // this request came from someone else so just
                        // send the commit packet
                        toProcess.add(r);
                    }
                }
            }

            // We haven't matched the pending requests, so go back to
            // waiting
            if (nextPending != null) {
                continue;
            }

            synchronized (this) {
                // Process the next requests in the queuedRequests
                while (nextPending == null && queuedRequests.size() > 0) {
                    Request request = queuedRequests.remove();
                    switch (request.type) {
                    case OpCode.create:
                    case OpCode.delete:
                    case OpCode.setData:
                    case OpCode.multi:
                    case OpCode.setACL:
                    case OpCode.createSession:
                    case OpCode.closeSession:
                        nextPending = request;                          // 9. 若请求是事务请求, 则将 follower 自己提交的 request 赋值给 nextPending
                        break;
                    case OpCode.sync:
                        if (matchSyncs) {
                            nextPending = request;
                        } else {
                            toProcess.add(request);
                        }
                        break;
                    default:                                            // 10.这里直接加入到 队列 toProcess 中的其实是 非 事务的请求 (比如getData), 丢到 toProcess 里面的请求会丢到下个 RequestProcessor
                        toProcess.add(request);
                    }
                }
            }
        }
    } catch (InterruptedException e) {
        LOG.warn("Interrupted exception while waiting", e);
    } catch (Throwable e) {
        LOG.error("Unexpected exception causing CommitProcessor to exit", e);
    }
    LOG.info("CommitProcessor exited loop!");
}

在CommitProcessor 里面有几个特别的队列

/**
 * Requests that we are holding until the commit comes in.
 */
// 等待 ACK 确认的 Request
LinkedList<Request> queuedRequests = new LinkedList<Request>();

/**
 * Requests that have been committed.
 */
// 已经 Proposal ACK 过半确认过的 Request, 一般的要么是 Leader 自己 commit, 要么就是 Follower 接收到 Leader 的 commit 消息
LinkedList<Request> committedRequests = new LinkedList<Request>();

// 等待被 nextProcessor 处理的队列, 其里面的数据是从 committedRequests, queuedRequests 里面获取来的
ArrayList<Request> toProcess = new ArrayList<Request>();

5. LearnerHandler 处理Request请求

此时LearnerHandler在while loop里面处理对应的Request请求

while (true) {
    qp = new QuorumPacket();
    ia.readRecord(qp, "packet");                         // 47. 这里其实就是不断的从数据流(来源于 Follower 的) 读取数据
    LOG.info("qp:" + qp);

    long traceMask = ZooTrace.SERVER_PACKET_TRACE_MASK;
    if (qp.getType() == Leader.PING) {
        traceMask = ZooTrace.SERVER_PING_TRACE_MASK;
    }
    if (LOG.isTraceEnabled()) {
        ZooTrace.logQuorumPacket(LOG, traceMask, 'i', qp);
    }
    tickOfNextAckDeadline = leader.self.tick + leader.self.syncLimit;
    LOG.info("tickOfNextAckDeadline :" + tickOfNextAckDeadline);

    ByteBuffer bb;
    long sessionId;
    int cxid;
    int type;

    LOG.info("qp.getType() : " + qp);
    switch (qp.getType()) {
    case Leader.ACK:                                   // 48. 处理 Follower 回复给 Leader 的ACK 包看看之前的投票是否结束 ( 这里是 Follower 在处理 UPTODATE 后恢复 ACK)
        if (this.learnerType == LearnerType.OBSERVER) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Received ACK from Observer  " + this.sid);
            }
        }
        LOG.info("syncLimitCheck.updateAck(qp.getZxid()):"  + qp.getZxid());
        syncLimitCheck.updateAck(qp.getZxid());
        LOG.info("this.sid:" + this.sid + ", qp.getZxid():" + qp.getZxid() + ", sock.getLocalSocketAddress():" + sock.getLocalSocketAddress());
                                                      // 49. ack 包处理成功, 如果 follower 数据同步成功, 则将它添加到 NEWLEADER 这个投票的结果中
        leader.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress());        
        break;
    case Leader.PING:                                 // 50. ping 数据包, 更新 session 的超时时间
        // Process the touches
        ByteArrayInputStream bis = new ByteArrayInputStream(qp.getData());
        DataInputStream dis = new DataInputStream(bis);
        while (dis.available() > 0) {
            long sess = dis.readLong();
            int to = dis.readInt();
            LOG.info("leader.zk.touch: sess" + sess + ", to:"+to);
            leader.zk.touch(sess, to);
        }
        break;
    case Leader.REVALIDATE:                          // 51. 检查 session 是否还存活
        bis = new ByteArrayInputStream(qp.getData());
        dis = new DataInputStream(bis);
        long id = dis.readLong();
        int to = dis.readInt();
        LOG.info("id:"+id + ", to:" + to);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        dos.writeLong(id);
        boolean valid = leader.zk.touch(id, to);
        LOG.info("id:" + id + ", to:" + to + ", valid:" + valid);
        if (valid) {
            try {
                //set the session owner
                // as the follower that
                // owns the session
                leader.zk.setOwner(id, this);
            } catch (SessionExpiredException e) {
                LOG.error("Somehow session " + Long.toHexString(id) + " expired right after being renewed! (impossible)", e);
            }
        }
        if (LOG.isTraceEnabled()) {
            ZooTrace.logTraceMessage(LOG,
                                     ZooTrace.SESSION_TRACE_MASK,
                                     "Session 0x" + Long.toHexString(id)
                                     + " is valid: "+ valid);
        }
        dos.writeBoolean(valid);
        qp.setData(bos.toByteArray());
        queuedPackets.add(qp);                         // 52. 将数据包返回给对应的 follower
        break;
    case Leader.REQUEST:                               // 53. REQUEST 数据包, follower 会将事务请求转发给 leader 进行处理
        bb = ByteBuffer.wrap(qp.getData());
        sessionId = bb.getLong();
        cxid = bb.getInt();
        type = bb.getInt();
        bb = bb.slice();                               // 54. 读取事务信息
        Request si;
        LOG.info(" sessionId:" + sessionId + ", cxid:" + cxid + ", type:" + type);
        if(type == OpCode.sync){
            si = new LearnerSyncRequest(this, sessionId, cxid, type, bb, qp.getAuthinfo());
        } else {
            si = new Request(null, sessionId, cxid, type, bb, qp.getAuthinfo());
        }
        si.setOwner(this);
        LOG.info("si:" + si);
        leader.zk.submitRequest(si);                   // 55. 将事务请求的信息交由 Leader 的 RequestProcessor 处理
        break;
    default:
    }
}

LearnerHandler调用Leader.zk.submitRequest(Request request) 到RequestProcessor处理链里面;

6. PrepRequestProcessor 处理请求
case OpCode.createSession:                                  // 创建 session
    request.request.rewind();
    int to = request.request.getInt();
    request.txn = new CreateSessionTxn(to);                 // 组装事务体, 事务头在最前面已经弄好了
    request.request.rewind();
    zks.sessionTracker.addSession(request.sessionId, to);   // 调用 sessionTracker.addSession() 将follower里的session加入到Leader的sessionsWithTimeout里面
    zks.setOwner(request.sessionId, request.getOwner());
    break;

这里的操作就是将session加入到Leader的sessionsById里面

7. ProposalRequestProcessor 处理请求
    nextProcessor.processRequest(request);         // 1. 这里的 nextProcessor 其实就是 CommitProcessor
if (request.hdr != null) {                         // 2. 若是 事务请求
    // We need to sync and get consensus on any transactions
    try {
        zks.getLeader().propose(request);          // 3. Leader 进行 Request 的投票 (Proposal) 将 request 发送给 Follower
    } catch (XidRolloverException e) {
        throw new RequestProcessorException(e.getMessage(), e);
    }
    syncProcessor.processRequest(request);         // 4. 将 request 交给 syncProcessor 进行落磁盘
}

这里就这几步:

1. 提交请求到CommitProcessor.queuedRequests里面
2. 通过zks.getLeader().propose(request) 向各个Follower提交 Leader.PROPOSAL
3. 本机的 syncProcessor处理请求(持久化, 接下来就是本机的 AckRequestProcessor回复ack给 Leader.processAck 阻塞这里, ACK过半了就不会阻塞)
8. Follower.processPacket 处理请求

接着就是 Follower处理Leader提出的Proposal

case Leader.PROPOSAL:                                             // 1. 处理 Leader 发来的 Proposal 包, 投票处理
    TxnHeader hdr = new TxnHeader();
    Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);// 2. 反序列化出 Request
    if (hdr.getZxid() != lastQueued + 1) {                        // 3. 这里说明什么呢, 说明 Follower 可能少掉了 Proposal
        LOG.warn("Got zxid 0x"
                + Long.toHexString(hdr.getZxid())
                + " expected 0x"
                + Long.toHexString(lastQueued + 1));
    }
    lastQueued = hdr.getZxid();
    fzk.logRequest(hdr, txn);                                     // 4. 将 Request 交给 FollowerZooKeeperServer 来进行处理

fzk.logRequest 提交Request到syncProcessor里面, 而后就是通过SendAckRequestProcessor向Leader发送刚才Proposal对应的ack

9. Leader.processAck 处理Follower发来的ack
/**
 * 参考资料
 * http://blog.csdn.net/vinowan/article/details/22196707
 *
 * Keep a count of acks that are received by the leader for a particular
 * proposal
 * 
 * @param zxid the zxid of the proposal sent out
 */
synchronized public void processAck(long sid, long zxid, SocketAddress followerAddr) {
    LOG.info("sid:" + sid + ", zxid:" + zxid + ", followerAddr:" + followerAddr);
    if (LOG.isTraceEnabled()) {
        LOG.trace("Ack zxid: 0x{}", Long.toHexString(zxid));
        for (Proposal p : outstandingProposals.values()) {
            long packetZxid = p.packet.getZxid();
            LOG.trace("outstanding proposal: 0x{}",
                    Long.toHexString(packetZxid));
        }
        LOG.trace("outstanding proposals all");
    }

    LOG.info("(zxid & 0xffffffffL) == 0 :" + ((zxid & 0xffffffffL) == 0));
    if ((zxid & 0xffffffffL) == 0) {                              // 1. zxid 全是 0
        /*
         * We no longer process NEWLEADER ack by this method. However,
         * the learner sends ack back to the leader after it gets UPTODATE
         * so we just ignore the message.
         */
        return;
    }

    LOG.info("outstandingProposals :" + outstandingProposals);
    if (outstandingProposals.size() == 0) {                       // 2. 没有要回应 ack 的 Proposal 存在
        if (LOG.isDebugEnabled()) {
            LOG.debug("outstanding is 0");
        }
        return;
    }
    LOG.info("lastCommitted :" + lastCommitted + ", zxid:" + zxid);
    if (lastCommitted >= zxid) {                                  // 3. Leader 端处理的 lastCommited >= zxid, 说明 zxid 对应的 proposal 已经处理过了
        if (LOG.isDebugEnabled()) {
            LOG.debug("proposal has already been committed, pzxid: 0x{} zxid: 0x{}", Long.toHexString(lastCommitted), Long.toHexString(zxid));
        }
        // The proposal has already been committed
        return;
    }
    Proposal p = outstandingProposals.get(zxid);                  // 4. 从投票箱 outstandingProposals 获取 zxid 对应的 Proposal
    LOG.info("p:" + p);
    if (p == null) {
        LOG.warn("Trying to commit future proposal: zxid 0x{} from {}", Long.toHexString(zxid), followerAddr);
        return;
    }
    LOG.info("p:" + p + ", sid:" + sid);

    p.ackSet.add(sid);                                            // 5. 将 follower 的 myid 加入结果列表
    if (LOG.isDebugEnabled()) {
        LOG.info("Count for zxid: 0x{} is {}", Long.toHexString(zxid), p.ackSet.size());
    }

    LOG.info("self.getQuorumVerifier().containsQuorum(p.ackSet):" + self.getQuorumVerifier().containsQuorum(p.ackSet));
    if (self.getQuorumVerifier().containsQuorum(p.ackSet)){       // 6. 判断是否票数够了, 则启动  leader 的 CommitProcessor 来进行处理

        LOG.info("zxid:" + zxid + ", lastCommitted:" + lastCommitted);
        if (zxid != lastCommitted+1) {
            LOG.warn("Commiting zxid 0x{} from {} not first!", Long.toHexString(zxid), followerAddr);
            LOG.warn("First is 0x{}", Long.toHexString(lastCommitted + 1));
        }

        LOG.info("outstandingProposals:" + outstandingProposals);
        outstandingProposals.remove(zxid);                        // 7. 从 outstandingProposals 里面删除那个可以提交的 Proposal
        if (p.request != null) {
            toBeApplied.add(p);                                   // 8. 加入到 toBeApplied 队列里面, 这里的 toBeApplied 是 ToBeAppliedRequestProcessor, Leader 共用的队列, 在经过 CommitProcessor 处理过后, 就到 ToBeAppliedRequestProcessor 里面进行处理
            LOG.info("toBeApplied:" + toBeApplied);               // 9. toBeApplied 对应的删除操作在 ToBeAppliedRequestProcessor 里面, 在进行删除时, 其实已经经过 FinalRequestProcessor 处理过的
        }

        if (p.request == null) {
            LOG.warn("Going to commmit null request for proposal: {}", p);
        }
        commit(zxid);                                             // 10. 向 集群中的 Followers 发送 commit 消息, 来通知大家, zxid 对应的 Proposal 可以 commit 了
        inform(p);                                                // 11. 向 集群中的 Observers 发送 commit 消息, 来通知大家, zxid 对应的 Proposal 可以 commit 了
        zk.commitProcessor.commit(p.request);                     // 12. 自己进行 proposal 的提交 (直接调用 commitProcessor 进行提交 )
                                                                  // 13. 其实这里隐藏一个细节, 就是有可能 有些 Proposal 在 Follower 上进行了 commit, 而 Leader 上还没来得及提交, 就有可能与集群间的其他节点断开连接
        LOG.info("pendingSyncs :" + pendingSyncs);
        if(pendingSyncs.containsKey(zxid)){
            for(LearnerSyncRequest r: pendingSyncs.remove(zxid)) {
                sendSync(r);
            }
        }
    }
}

这里处理ACK时, 若已经收到集群中过半的ack则就可以向集群中的其他节点发送commit, 或inform其他Observer节点, 然后 zk.commitProcessor.commit(p.request) 提交request到Leader的commitProcessor.committedRequests里面, 最后就是 先在FinalRequestProcessor处理一下, 再在ToBeAppliedRequestProcessor.toBeApplied删除request

10. FollowerZooKeeperServer.commit(long zxid) 提交Proposal
/**
 * When a COMMIT message is received, eventually this method is called, 
 * which matches up the zxid from the COMMIT with (hopefully) the head of
 * the pendingTxns queue and hands it to the commitProcessor to commit.
 * @param zxid - must correspond to the head of pendingTxns if it exists
 */
public void commit(long zxid) {
    if (pendingTxns.size() == 0) {
        LOG.warn("Committing " + Long.toHexString(zxid)
                + " without seeing txn");
        return;
    }
    long firstElementZxid = pendingTxns.element().zxid;         // 1. http://blog.csdn.net/fei33423/article/details/53749138
    if (firstElementZxid != zxid) {                             // 2. 这里就有经典问题, 在 Leader 端提交了 3 个 Proposal 的信息(comit 1, comit 2, comit 3), 但 follower 在接收到 comit 1 后就接收到 comit 3
        LOG.error("Committing zxid 0x" + Long.toHexString(zxid) // 3. 则就会打印这里的日志, 并且进行退出
                + " but next pending txn 0x"
                + Long.toHexString(firstElementZxid));
        System.exit(12);
    }
    Request request = pendingTxns.remove();
    commitProcessor.commit(request);                            // 4. 提交到 commitProcessor.committedRequests 里面
}

而后就是Follower.FinalRequestProcessor进行最终的响应客户端处理

11. Session超时机制 Leader.ping()

在Leader上有个while loop会遍历 LearnerHandler 然后发送 ping请求给 Follower/Observer

/**
 * ping calls from the leader to the peers
 */
// 这里其实是 Leader 向 Follower 发送 ping 请求
// 在向 Learner 发送ping消息之前, 首先会通过 syncLimitCheck 来检查
public void ping() {
    long id;
    if (syncLimitCheck.check(System.nanoTime())) {
        synchronized(leader) {
            id = leader.lastProposed;
        }
        QuorumPacket ping = new QuorumPacket(Leader.PING, id, null, null);
        queuePacket(ping);
    } else {
        LOG.warn("Closing connection to peer due to transaction timeout.");
        shutdown();
    }
}
12. Follower处理Leader发来的ping请求

Follower在接到Leader的ping请求后会将sessionId及timeout发送给Leader, 进行超时机制检查

// Follower 将自己的 sessionId 及超时时间发送给 Leader, 让 Leader 进行 touch 操作, 校验是否 session 超时
protected void ping(QuorumPacket qp) throws IOException {
    // Send back the ping with our session data
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    DataOutputStream dos = new DataOutputStream(bos);
    HashMap<Long, Integer> touchTable = zk   // 1. 获取 Follower/Observer 的 touchTable(sessionId <-> sessionTimeout) 发给 Leader 进行session超时的检测
            .getTouchSnapshot();
    for (Entry<Long, Integer> entry : touchTable.entrySet()) {
        dos.writeLong(entry.getKey());
        dos.writeInt(entry.getValue());
    }
    qp.setData(bos.toByteArray());          // 2. 转化成字节数组, 进行数据的写入
    writePacket(qp, true);                  // 3. 发送数据包
}
13. Leader处理Follower发来的sessionId及timeout

Leader在接收到Follower发来的sessionId及timeout, 将会调用SessionTrackerImpl.touchSession(long sessionId, int timeout)来进行校验

// 更新 session 的过期时间
synchronized public boolean touchSession(long sessionId, int timeout) {
    ZooTrace.logTraceMessage(LOG,
            ZooTrace.CLIENT_PING_TRACE_MASK,
            "SessionTrackerImpl --- Touch session: 0x"
                    + Long.toHexString(sessionId) + " with timeout " + timeout);

    SessionImpl s = sessionsById.get(sessionId);    // 1. 从 sessionsById 获取 session, sessionsById 是一个 SessionId <-> SessionImpl 的 map
    // Return false, if the session doesn't exists or marked as closing
    if (s == null || s.isClosing()) {
        return false;
    }                                               // 2. 计算过期时间
    long expireTime = roundToInterval(System.currentTimeMillis() + timeout); 
    if (s.tickTime >= expireTime) {
        // Nothing needs to be done
        return true;
    }
    SessionSet set = sessionSets.get(s.tickTime);   // 3. 这里的 SessionSet 就是一个 timeout 对应额 Bucket, 将有一个线程, 在超时时间点检查这个 SessionSet
    if (set != null) {
        set.sessions.remove(s);
    }
    s.tickTime = expireTime;                        // 4. 下面的步骤就是将 session 以 tickTime 为单位放入 sessionSets 中
    set = sessionSets.get(s.tickTime);
    if (set == null) {
        set = new SessionSet();
        sessionSets.put(expireTime, set);
    }
    set.sessions.add(s);                            // 5. 将 SessionImpl 放入对应的 SessionSets 里面
    return true;
}
总结

zookeeper的session机制只适用于有少量client连接Server的场景(zookeeper的默认maxClientCnxns 是60, 超过的话就会socket主动关闭), 当有百万连接时, 用这种session集中, 用一条线程检测超时的机制可能在性能上出现问题, 当zookeeper还是给出了一种很好的思考方向! 在理解了session创建机制后, 对应的create/setData/delete就很好理解了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 本文将从系统模型、序列化与协议、客户端工作原理、会话、服务端工作原理以及数据存储等方面来揭示ZooKeeper的技...
    端木轩阅读 3,729评论 0 42
  • 一个真正的写数据流程是怎么样的?一个真正的读数据流程是怎么样的?一个真正的同步数据流程是怎么样的?从哪里到哪里?什...
    时待吾阅读 3,967评论 0 14
  • ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于...
    rthsfjhtrj阅读 512评论 0 1
  • 在阅读了Zab的论文<<Zab:High-performance broadcast for primary-ba...
    AlstonWilliams阅读 4,105评论 1 8
  • ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于...
    caryr阅读 355评论 0 0