ZooKeeper源码(一)读写请求

344 阅读29分钟

背景

最近有个读者私信我,说要学习zookeeper(下文简称zk)源码,所以我就仔细研读了一下zk源码分享给大家。

从业务开发实战角度来看,zk用途很广泛,但是行业内有很多火热的替代品,比如:

1)命名服务(Nacos、Eureka、Consul)

2)配置服务(Nacos、Apollo)

3)Leader选举(一般中间件HA需要,比如Kafka3前需要zk)

4)分布式锁(Redis)

5)分布式队列(Kafka、RocketMQ、RabbitMQ、Pulsar)

从中间件角度来看,很多中间件也不再依赖zk,比如Kafka3推出KRaft。

从面试角度来看,JD上要求了解zk的也不多了,如果说面试题的话也只是个配角。

比如讲到CAP的时候问问,分布式锁用Redis和zk的区别,注册中心用Nacos和zk的区别。

又比如讲到共识算法(或者说是一致性算法?不纠结名字)的时候问问,Paxos和Raft和ZAB的区别。

但是为什么zk看起来不常用了,我还去看zk的源码呢?原因有几点

1)有读者想要了解zk;

2)笔者就职的第一家公司用了zk,包括服务注册发现、配置中心、分布式锁,所以zk也不是没人用;

3)之前看过一些zk的源码,感觉思路清奇,CP中间件值得一读;

本文主要包含以下内容

1)zk节点的线程模型:网络通讯线程、业务线程

2)zk如何处理读写请求

在阅读之前我也有一些疑问

1)zk如何保证写请求按照顺序处理?在满足写请求顺序处理的情况下,zk如何提升性能?如果像redis一样那可不行,毕竟redis单线程处理命令是内存级别的(redis都是异步复制的)。

2)zk的读请求所有节点都能处理,对一致性的保证是怎样的?

3)zk的watch是一次性的(3.6前),zk怎么保证写一定能被watch到呢?为什么说不一定能感知到每一次变更?

you cannot reliably see every change that happens to a node in ZooKeeper.

PS:文章比较干,直接看总结也是不错的选择。

线程模型

网络通讯

zk底层网络通讯有两套实现,一套基于JDK NIO,一套基于Netty,默认情况下会使用JDK NIO,即NIOServerCnxnFactory

从java doc来看:

1个accept线程,负责接收新连接然后注册channel到某个selector;

1-N个selector线程,负责监听注册到当前selector的所有channel上的io事件,当io事件发生,交给worker线程;

0-M个worker线程,负责io读写;

1个线程,负责关闭空闲连接;

NIOServerCnxnFactory#configure:selector线程数量=max(sqrt(核心数/2), 1);worker线程数量=2*核心数。

@Override
public void configure(InetSocketAddress addr, int maxcc, boolean secure) throws IOException {
    // ...
    maxClientCnxns = maxcc;
    sessionlessCnxnTimeout = Integer.getInteger(ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000); // 10s
    cnxnExpiryQueue = new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);
    // 空闲连接回收线程
    expirerThread = new ConnectionExpirerThread();
    int numCores = Runtime.getRuntime().availableProcessors();
    // selector线程
    // 32 cores sweet spot seems to be 4 selector threads
    numSelectorThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_SELECTOR_THREADS, Math.max((int) Math.sqrt((float) numCores/2), 1));
    
    // io worker线程
    numWorkerThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2 * numCores);
    workerShutdownTimeoutMS = Long.getLong(ZOOKEEPER_NIO_SHUTDOWN_TIMEOUT, 5000);
    for(int i=0; i<numSelectorThreads; ++i) {
        selectorThreads.add(new SelectorThread(i));
    }
    this.ss = ServerSocketChannel.open();
    ss.socket().setReuseAddress(true);
    ss.socket().bind(addr);
    ss.configureBlocking(false);
    // accept线程
    acceptThread = new AcceptThread(ss, addr, selectorThreads);
}

业务线程

业务线程编排,根据zk节点的状态和角色来定。

Leader角色粗略来看有2个业务线程:

1)PrepRequestProcessor线程:负责请求前置处理和发布proposal;

2)CommitProcessor线程:负责实际处理请求并响应客户端;

@Override
protected void setupRequestProcessors() {
    // 线程2 commit -> apply -> response
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(finalProcessor, getLeader());
    commitProcessor = new CommitProcessor(toBeAppliedProcessor,
            Long.toString(getServerId()), false,
            getZooKeeperServerListener());
    commitProcessor.start();
    // 线程1 请求前置处理 -> 发布proposal
    ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this,
            commitProcessor);
    proposalProcessor.initialize();
    prepRequestProcessor = new PrepRequestProcessor(this, proposalProcessor);
    prepRequestProcessor.start();
    // ZOOKEEPER-1147 3.5 local session 忽略
    firstProcessor = new LeaderRequestProcessor(this, prepRequestProcessor);
}

Follower角色粗略来看有3个业务线程:

1)SyncRequestProcessor线程:写事务日志;

2)FollowerRequestProcessor线程:follower逻辑,比如写请求转发;

3)CommitProcessor线程:commit和响应客户端;

@Override
protected void setupRequestProcessors() {
    // 线程2 commit -> response
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    commitProcessor = new CommitProcessor(finalProcessor,
            Long.toString(getServerId()), true, getZooKeeperServerListener());
    commitProcessor.start();
    // 线程1 follower定制
    firstProcessor = new FollowerRequestProcessor(this, commitProcessor);
    ((FollowerRequestProcessor) firstProcessor).start();
    // 线程0 接收leader同步请求
    syncProcessor = new SyncRequestProcessor(this, new SendAckRequestProcessor((Learner)getFollower()));
    syncProcessor.start();
}

请求处理

请求模型

ZooKeeperServer#processPacket:worker线程将数据读入ByteBuffer,封装为Request模型,提交给Leader或Follower的firstProcessor。

public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
    // We have the request, now process and setup for next
    InputStream bais = new ByteBufferInputStream(incomingBuffer);
    BinaryInputArchive bia = BinaryInputArchive.getArchive(bais);
    RequestHeader h = new RequestHeader();
    h.deserialize(bia, "header");
    incomingBuffer = incomingBuffer.slice();
    // ...

    // 封装Request模型
    Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(),
      h.getType(), incomingBuffer, cnxn.getAuthInfo());
    si.setOwner(ServerCnxn.me);

    // ...

    // 提交给firstProcessor
    submitRequest(si);
}

public void submitRequest(Request si) {
    // ...
    firstProcessor.processRequest(si);
}

Request构造时仅传入了客户端数据(final类型字段):

1)ServerCnxn cnxn:连接;

2)long sessionId:客户端session id(之后有机会讨论,可以认为一个连接对应一个session);

3)int cxid:客户端请求头中的请求id;

4)int type:客户端请求头中的请求类型,比如getChildren(ls)、create;

5)ByteBuffer request:请求内容

6)List authInfo:acl相关;

除了上述6个字段:hdr、txn都和事务相关,zxid是当前节点的全局事务id(先简单理解每个事务+1)。

public class Request {
    public Request(ServerCnxn cnxn, long sessionId, int xid, int type, ByteBuffer bb, List<Id> authInfo) {
        this.cnxn = cnxn;
        this.sessionId = sessionId;
        this.cxid = xid;
        this.type = type;
        this.request = bb;
        this.authInfo = authInfo;
    }
    // 客户端 session id
    public final long sessionId;
    // 客户端 请求id
    public final int cxid;
    // 请求类型 比如ls create
    public final int type;
    // 请求内容
    public final ByteBuffer request;
    // 连接
    public final ServerCnxn cnxn;
    // 事务头
    private TxnHeader hdr;
    // 事务数据
    private Record txn;
    public long zxid = -1;
    // acl
    public final List<Id> authInfo;
}

Leader写请求处理

我们以create为例,比如:create /hello 'world',走一遍写请求流程。

Leader的写请求从LeaderRequestProcessor开始,而LeaderRequestProcessor和3.5的local session相关,先跳过,直接进入PrepRequestProcessor。

PrepRequestProcessor(Leader)

所有继承Thread的RequestProcessor基本都是N个生产者1个消费者的形式,将请求放入队列,由单独线程消费队列中的请求,异步单线程处理不需要考虑线程安全问题,后续不再赘述。

LinkedBlockingQueue<Request> submittedRequests = new LinkedBlockingQueue<>();
public void processRequest(Request request) {
    submittedRequests.add(request);
}

@Override
public void run() {
    while (true) {
        Request request = submittedRequests.take();
        // ...
        pRequest(request);
    }
    // ...
}

PrepRequestProcessor#pRequest:对于写操作create

1)生成新的全局事务id即zxid,由于是单线程处理,所以每个写请求的对应一个唯一的全局事务id,且是按顺序递增的(zxid=zxid+1);

2)将hdr事务头和txn事务数据放入Request,后续会有很多判断hdr是否为空的逻辑,本质上就是判断当前请求是否是一个写操作;

ZooKeeperServer zks;
protected void pRequest(Request request) throws RequestProcessorException {
    request.setHdr(null);
    request.setTxn(null);
    switch (request.type) {
        case OpCode.create:
            CreateRequest create2Request = new CreateRequest();
            pRequest2Txn(request.type, zks.getNextZxid()/* 生成新的zxid */, request, create2Request, true);
            break;
        // 其他请求 ...
    }
    request.zxid = zks.getZxid();
    // 交给ProposalRequestProcessor
    nextProcessor.processRequest(request);
}
protected void pRequest2Txn(int type, long zxid, Request request,
                            Record record, boolean deserialize)
    throws KeeperException, IOException, RequestProcessorException
{
    // 设置事务头
    request.setHdr(new TxnHeader(request.sessionId, request.cxid, zxid,
            Time.currentWallTime(), type));

    switch (type) {
        case OpCode.create: {
            // 设置事务数据
            pRequest2TxnCreate(type, request, record, deserialize);
            break;
        }
        ...
    }
}
private void pRequest2TxnCreate(int type, Request request, Record record, boolean deserialize) throws IOException, KeeperException {
    // 反序列化bytebuffer到CreateRequest
    ByteBufferInputStream.byteBuffer2Record(request.request, record);
    CreateRequest createRequest = (CreateRequest)record;
    int flags = createRequest.getFlags();
    String path = createRequest.getPath();
    List<ACL>  acl = createRequest.getAcl();
    byte[] data = createRequest.getData();
    long ttl = -1;
    CreateMode createMode = CreateMode.fromFlag(flags);
    validateCreateRequest(path, createMode, request, ttl);
    String parentPath = validatePathForCreate(path, request.sessionId);

    List<ACL> listACL = fixupACL(path, request.authInfo, acl);
    // 获取父节点的ChangeRecord
    ChangeRecord parentRecord = getRecordForPath(parentPath);
    // acl
    checkACL(zks, parentRecord.acl, ZooDefs.Perms.CREATE, request.authInfo);
    int parentCVersion = parentRecord.stat.getCversion();
    // 顺序节点 path拼接
    // 这里一定不会出现重复序号
    // getRecordForPath同时考虑了已经收到的ChangeRecord,如果没有则使用db中的数据返回一个ChangeRecord
    // 当前方法也是单线程处理,ChangeRecord是顺序存储到zkserver的
    if (createMode.isSequential()) {
        path = path + String.format(Locale.ENGLISH, "%010d", parentCVersion);
    }
    // ...
    int newCversion = parentRecord.stat.getCversion()+1;
    // 设置txn
    request.setTxn(new CreateTxn(path, data, listACL, createMode.isEphemeral(),
            newCversion));
    StatPersisted s = new StatPersisted();
    // 临时节点
    if (createMode.isEphemeral()) {
        s.setEphemeralOwner(request.sessionId);
    }
    // 父节点ChangeRecord加入outstandingChanges
    parentRecord = parentRecord.duplicate(request.getHdr().getZxid());
    parentRecord.childCount++;
    parentRecord.stat.setCversion(newCversion);
    addChangeRecord(parentRecord);
    // 新节点ChangeRecord加入outstandingChanges
    addChangeRecord(new ChangeRecord(request.getHdr().getZxid(), path, s, 0, listACL));
}

private void addChangeRecord(ChangeRecord c) {
    synchronized (zks.outstandingChanges) {
        zks.outstandingChanges.add(c);
        zks.outstandingChangesForPath.put(c.path, c);
    }
}

除了zxid和事务数据,PrepRequestProcessor对即将发生变更的节点都封装为一个ChangeRecord,放入ZooKeeperServer的outstandingChanges中,outstandingChangesForPath可以理解为索引。这个outstandingChanges的作用很多,比如顺序节点。

final Deque<ChangeRecord> outstandingChanges = new ArrayDeque<>();
// this data structure must be accessed under the outstandingChanges lock
final HashMap<String, ChangeRecord> outstandingChangesForPath =
    new HashMap<String, ChangeRecord>();

ProposalRequestProcessor(Leader)

从proposal开始,对于单个写请求来说就是多线程处理了。

Step1:将请求丢给CommitProcessor异步处理;

Step2:异步发送proposal给所有follower;

Step3:异步写事务日志并给自己一个ack;

LeaderZooKeeperServer zks;
RequestProcessor nextProcessor;
SyncRequestProcessor syncProcessor;
public ProposalRequestProcessor(LeaderZooKeeperServer zks,
        RequestProcessor nextProcessor) {
    this.zks = zks;
    this.nextProcessor = nextProcessor;
    // 给leader自己一个ack
    AckRequestProcessor ackProcessor = new AckRequestProcessor(zks.getLeader());
    // 写事务日志
    syncProcessor = new SyncRequestProcessor(zks, ackProcessor);
}
public void processRequest(Request request) throws RequestProcessorException {
    nextProcessor.processRequest(request); // CommitProcessor
    // 写操作hdr不为空
    if (request.getHdr() != null) {
        // We need to sync and get consensus on any transactions
        try {
            // 1. 发送proposal给所有follower,follower写事务日志
            zks.getLeader().propose(request);
        } catch (XidRolloverException e) {
            throw new RequestProcessorException(e.getMessage(), e);
        }
        // 2. 自己异步写事务日志并ack给自己
        syncProcessor.processRequest(request);
    }
}

发布提案-Proposal

Leader将Request中的事务头和事务数据序列化封装为QuorumPacket(Leader.PROPOSAL)给到所有follower。提案Proposal=QuorumPacket+Request(hdr+txn)+QuorumVerifier。

LearnerHandler也是生产消费模型,这里将QuorumPacket投递到队列,由LearnerHandler线程异步发送给follower。

public Proposal propose(Request request) throws XidRolloverException {
    if ((request.zxid & 0xffffffffL) == 0xffffffffL) {
        // 如果事务id在当前epoch溢出,停止leader,重新选举
        String msg =
                "zxid lower 32 bits have rolled over, forcing re-election, and therefore new epoch start";
        shutdown(msg);
        throw new XidRolloverException(msg);
    }

    // 将 事务头hdr 事务数据txn 序列化
    byte[] data = SerializeUtils.serializeRequest(request);
    proposalStats.setLastBufferSize(data.length);
    // 封装为QuorumPacket
    QuorumPacket pp = new QuorumPacket(Leader.PROPOSAL, request.zxid, data, null);

    Proposal p = new Proposal();
    p.packet = pp;
    p.request = request;                
    
    synchronized(this) {
        // QuorumVerifier放入Proposal
       p.addQuorumVerifier(self.getQuorumVerifier());
        // ...
        lastProposed = p.packet.getZxid();
        outstandingProposals.put(lastProposed, p);
        sendPacket(pp); // 将Proposal发送给所有follower
    }
    return p;
}

void sendPacket(QuorumPacket qp) {
    synchronized (forwardingFollowers) {
        for (LearnerHandler f : forwardingFollowers) {
            // 请求入队,由单独线程异步发送给follower
            f.queuePacket(qp);
        }
    }
}

写事务日志-SyncRequestProcessor

对于SyncRequestProcessor,主要是将Request写入事务日志,即log.{16进制事务id}。

这里重点介绍一下SyncRequestProcessor的两个成员变量:

1)queuedRequests:请求队列,对于leader来说,就是ProposalRequestProcessor丢过来的请求;

2)toFlush:SyncRequestProcessor并不是对于每个请求都直接写磁盘,而是先写内存buffer,再刷盘,这里存放的是已经写入内存还未刷盘的数据;

public class SyncRequestProcessor extends ZooKeeperCriticalThread implements
        RequestProcessor {
    private final ZooKeeperServer zks;
    // 未处理请求
    private final LinkedBlockingQueue<Request> queuedRequests = new LinkedBlockingQueue<Request>();
    // leader -> AckRequestProcessor
    // follower -> SendAckRequestProcessor
    private final RequestProcessor nextProcessor;
    // 还未刷盘的请求
    private final LinkedList<Request> toFlush = new LinkedList<Request>();
}

SyncRequestProcessor处理请求分为三步:

1)写事务日志buffer:ZKDatabase#append;

2)按照一定几率,异步将内存中整棵树创建一个快照文件:ZooKeeperServer#takeSnapshot;

3)将事务日志buffer落盘并将请求传给下一个Processor:SyncRequestProcessor#flush->ZKDatabase#commit;

除此以外,为了减少刷盘次数,SyncRequestProcessor在请求多的情况下,会优先将请求写入buffer,当空闲时,将toFlush队列里的请求统一刷盘,并依次传递给后续Processor。

所以写请求堆积的情况下,首个客户端的写请求响应时间会变长,如果1000个请求未刷盘,强制刷一次盘,传递给下游Processor。

关于事务日志和快照文件,我们放到后续讨论,在这里我们只要知道,只有Request写磁盘完成了,才会将请求传递到下一个Processor,对于Leader来说就是AckRequestProcessor。

@Override
public void run() {
    int logCount = 0;
    int randRoll = r.nextInt(snapCount/2);
    while (true) {
        // request or flush
        // level1 无flush请求,阻塞等待新request
        // level2 有flush请求,优先处理新request
        // level3 有flush请求,没有新request,执行一次flush
        Request si = null;
        if (toFlush.isEmpty()) {
            si = queuedRequests.take(); // level1
        } else {
            si = queuedRequests.poll(); // level2
            if (si == null) { // level3
                flush(toFlush); // # 3 刷盘并传递给下一个processor
                continue;
            }
        }
        // # 1 写事务日志buffer
        if (zks.getZKDatabase().append(si)) {
            logCount++;
            // # 2 按照一定的几率,滚动事务日志(将buffer写page cache),生成snap文件
            if (logCount > (snapCount / 2 + randRoll)) {
                randRoll = r.nextInt(snapCount/2);
                // roll the log
                zks.getZKDatabase().rollLog();
                // take a snapshot
                if (snapInProcess != null && snapInProcess.isAlive()) {
                    // 已经有线程正在执行快照任务,跳过
                    LOG.warn("Too busy to snap, skipping");
                } else {
                    snapInProcess = new ZooKeeperThread("Snapshot Thread") {
                            public void run() {
                                try {
                                    // 对整棵树创建快照
                                    zks.takeSnapshot();
                                } catch(Exception e) {
                                    LOG.warn("Unexpected exception", e);
                                }
                            }
                        };
                    snapInProcess.start();
                }
                logCount = 0;
            }
        }
        toFlush.add(si);
        // 待flush请求超过1000个,强制执行一次flush
        if (toFlush.size() > 1000) {
            flush(toFlush);
        }
    }
}
private void flush(LinkedList<Request> toFlush)
        throws IOException, RequestProcessorException
{
    if (toFlush.isEmpty())
        return;
    // buffer->page cache->disk
    zks.getZKDatabase().commit();
    while (!toFlush.isEmpty()) {
        Request i = toFlush.remove();
        if (nextProcessor != null) {
            // leader -> AckRequestProcessor
            // follower -> SendAckRequestProcessor
            nextProcessor.processRequest(i);
        }
    }
    // follower -> SendAckRequestProcessor.flush
    if (nextProcessor != null && nextProcessor instanceof Flushable) {
        ((Flushable)nextProcessor).flush();
    }
}   

leader给自己ack-AckRequestProcessor

leader自己写事务日志成功后,会回复自己一个ack,传统理解就是对于这个提案给自己投一票。

class AckRequestProcessor implements RequestProcessor {
    Leader leader;
    AckRequestProcessor(Leader leader) {
        this.leader = leader;
    }
    public void processRequest(Request request) {
        QuorumPeer self = leader.self;
        if(self != null)
            leader.processAck(self.getId(), request.zxid, null);
    }
}

Leader#processAck:按照SyncRequestProcessor的处理顺序,即请求进来的顺序依次处理每个zxid代表的事务,用自己的serverid对这个提案进行一次投票,最后tryToCommit尝试提交。

假设follower还没处理完zxid对应提案,这里tryToCommit可以认为没啥影响,因为没过半票。我们先走到follower处理proposal的流程,反正最后还会回到Leader处理Follower的ack的流程,即processAck这个方法。

synchronized public void processAck(long sid, long zxid, SocketAddress followerAddr) {        
	// ...
    // 根据zxid找到Proposal(在ProposalRequestProcessor中已经调用propose方法放入这个map)
    Proposal p = outstandingProposals.get(zxid);

    // 对于这个提案,拿自己的serverid投一票
    p.addAck(sid);
    
    // 是否过半,尝试提交
    boolean hasCommitted = tryToCommit(p, zxid, followerAddr);
    // ...
}

SyncRequestProcessor+SendAckRequestProcessor(Follower)

Follower#processPacket:follower收到leader发来的提案,反序列化得到事务头hdr和事务数据txn。

protected void processPacket(QuorumPacket qp) throws Exception{
        switch (qp.getType()) {
        case Leader.PROPOSAL:           
            TxnHeader hdr = new TxnHeader();
            // 反序列化 hdr和txn
            Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);
            if (hdr.getZxid() != lastQueued + 1) {
                LOG.warn("Got zxid 0x"
                        + Long.toHexString(hdr.getZxid())
                        + " expected 0x"
                        + Long.toHexString(lastQueued + 1));
            }
            lastQueued = hdr.getZxid();
            
            if (hdr.getType() == OpCode.reconfig){
               SetDataTxn setDataTxn = (SetDataTxn) txn;       
               QuorumVerifier qv = self.configFromString(new String(setDataTxn.getData()));
               self.setLastSeenQuorumVerifier(qv, true);                               
            }
            // FollowerZooKeeperServer.logRequest
            fzk.logRequest(hdr, txn);
            break;
                // 其他类型请求...
        }
}

FollowerZooKeeperServer#logRequest:将hdr和txn封装为Request,放入一个pendingTxns阻塞队列,最终交给SyncRequestProcessor。

LinkedBlockingQueue<Request> pendingTxns = new LinkedBlockingQueue<Request>();

public void logRequest(TxnHeader hdr, Record txn) {
    Request request = new Request(hdr.getClientId(), hdr.getCxid(), hdr.getType(), hdr, txn, hdr.getZxid());
    if ((request.zxid & 0xffffffffL) != 0) {
        pendingTxns.add(request);
    }
    syncProcessor.processRequest(request);
}

SyncRequestProcessor处理逻辑基本与Leader相同,就是将事务日志刷盘,不再赘述。

最终刷盘成功后,调用后续Processor即SendAckRequestProcessor。

SendAckRequestProcessor#processRequest:封装ACK数据包(仅包含本次处理的zxid)写入buffer。

public class SendAckRequestProcessor implements RequestProcessor, Flushable {
	// Follower
    Learner learner;
    public void processRequest(Request si) {
        QuorumPacket qp = new QuorumPacket(Leader.ACK, si.getHdr().getZxid(), null,
            null);
        learner.writePacket(qp, false);
    }
}

SendAckRequestProcessor#flush:因为SyncRequestProcessor在写负载高时,会攒一批待flush的Request,SendAckRequestProcessor在processRequest阶段只是封装数据包写内存,当SyncRequestProcessor主动调用SendAckRequestProcessor#flush时,才会将一批数据写给leader。

public void flush() throws IOException {
    learner.writePacket(null, true);
}
void writePacket(QuorumPacket pp, boolean flush) throws IOException {
    synchronized (leaderOs) {
        if (pp != null) {
            // 写内存 OutputArchive.writeRecord
            leaderOs.writeRecord(pp, "packet");
        }
        if (flush) {
            // 写底层存储 BufferedOutputStream.flush
            bufferedOutput.flush();
        }
    }
}

接收ACK(Leader)

Leader#run:每个Follower与Leader建立的连接都对应一个LearnerHandler线程负责处理请求。

LearnerHandler#run:Follower的ACK最终还是由Leader#processAck处理。

// ...
while (true) {
    qp = new QuorumPacket();
    ia.readRecord(qp, "packet"); // 从BinaryInputArchive读数据到QuorumPacket

    switch (qp.getType()) {
    case Leader.ACK:
        syncLimitCheck.updateAck(qp.getZxid());
        // 处理follower在执行完sync后的ack
        leader.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress());
        break;
            // 其他请求...
    }
    //...
}

这里我们再来重点分析Leader#processAck,注意这里synchronized修饰,不用考虑多线程的情况。

synchronized public void processAck(long sid, long zxid, SocketAddress followerAddr) {        
	// ...
    // 根据zxid找到Proposal(在ProposalRequestProcessor中已经调用propose方法放入这个map)
    Proposal p = outstandingProposals.get(zxid);

    // 对于这个提案,拿自己的serverid投一票
    p.addAck(sid);
    
    // 是否过半,尝试提交
    boolean hasCommitted = tryToCommit(p, zxid, followerAddr);
    // ...
}

Proposal#addAck即父类SyncedLearnerTracker#addAck:统计提案ack。

投票

SyncedLearnerTracker,通过一个qvAcksetPairs集合存储不同集群拓扑(如果有动态集群配置)的对于一个提案的投票结果,一般来说qvAcksetPairs就一个QuorumVerifierAcksetPair元素。

SyncedLearnerTracker之后会经常见到,所有需要投票的地方,比如leader选举,都会用到他。

// static public class Proposal  extends SyncedLearnerTracker {
//    public QuorumPacket packet;
//    public Request request;
// }
public class SyncedLearnerTracker {

    protected ArrayList<QuorumVerifierAcksetPair> qvAcksetPairs = 
                new ArrayList<QuorumVerifierAcksetPair>();
	// Leader#propose发送提案前加入QuorumVerifier
    // 之所以qvAcksetPairs用集合表示,而不是单独一个QuorumVerifierAcksetPair
    // 是zk动态配置(dynamicConfigFile)的功能导致的
    // 一般情况下qvAcksetPairs就一个元素
    public void addQuorumVerifier(QuorumVerifier qv) {
        qvAcksetPairs.add(new QuorumVerifierAcksetPair(qv,
                new HashSet<Long>(qv.getVotingMembers().size())));
    }
	// 将serverid加入QuorumVerifierAcksetPair
    public boolean addAck(Long sid) {
        boolean change = false;
        for (QuorumVerifierAcksetPair qvAckset : qvAcksetPairs) {
            if (qvAckset.getQuorumVerifier().getVotingMembers().containsKey(sid)) {
                // 投票
                qvAckset.getAckset().add(sid);
                change = true;
            }
        }
        return change;
    }
    // 用QuorumVerifier决策ackset是否已经满足quorum
    public boolean hasAllQuorums() {
        for (QuorumVerifierAcksetPair qvAckset : qvAcksetPairs) {
            if (!qvAckset.getQuorumVerifier().containsQuorum(qvAckset.getAckset()))
                return false;
        }
        return true;
    }
}

QuorumVerifierAcksetPair有两个成员变量:

1)QuorumVerifier:代表一个集群拓扑关系;

2)ackset:节点serverid集合,投票箱;

public static class QuorumVerifierAcksetPair {
    // 集群拓扑
    private final QuorumVerifier qv;
    // 已经ack的serverid集合
    private final HashSet<Long> ackset;
}

QuorumVerifier集群拓扑关系:

public interface QuorumVerifier {
    // 权重
    long getWeight(long id);
    // 判断是否满足quorum
    boolean containsQuorum(Set<Long> set);
    // 关于动态集群配置 忽略
    long getVersion();
    void setVersion(long ver);
    // 所有集群节点
    Map<Long, QuorumServer> getAllMembers();
    // 可以参与投票的节点,比如Observer就不能投票
    Map<Long, QuorumServer> getVotingMembers();
    // Observer节点
    Map<Long, QuorumServer> getObservingMembers();
    boolean equals(Object o);
    String toString();
}

如果我们使用普通的集群配置,比如:

server.1=127.0.0.1:2222:2001
server.2=127.0.0.1:3333:3002
server.3=127.0.0.1:4444:4003

则实现类就是QuorumMaj,他的quorum判定条件就是常说的过半。

public QuorumMaj(Properties props) throws ConfigException {
    // 解析server.1=127.0.0.1:2222:2001
    for (Entry<Object, Object> entry : props.entrySet()) {
        String key = entry.getKey().toString();
        String value = entry.getValue().toString();

        if (key.startsWith("server.")) {
            int dot = key.indexOf('.');
            long sid = Long.parseLong(key.substring(dot + 1));
            QuorumServer qs = new QuorumServer(sid, value);
            allMembers.put(Long.valueOf(sid), qs);
            if (qs.type == LearnerType.PARTICIPANT)
                votingMembers.put(Long.valueOf(sid), qs);
            else {
                observingMembers.put(Long.valueOf(sid), qs);
            }
        } else if (key.equals("version")) {
            version = Long.parseLong(value, 16);
        }
    }
    half = votingMembers.size() / 2;
}
// quorum判定条件
 public boolean containsQuorum(Set<Long> ackSet) {
    return (ackSet.size() > half);
}

特殊的还有QuorumHierarchical,这里就不跟进了。

tryToCommit

Leader#tryToCommit:

1)判断已收到的ack是否满足quorum条件,一般就是QuorumMaj#containsQuorum是否满足过半ack;

2)提案从outstandingProposals发送Proposal队列中移除;

3)提案加入toBeApplied待apply队列;

4)发送Leader.COMMIT给所有follower(Follower走CommitProcessor);

5)发送Leader.INFORM给所有observer(忽略);

6)执行CommitProcessor#commit处理Request;

private final ConcurrentLinkedQueue<Proposal> toBeApplied = new ConcurrentLinkedQueue<Proposal>();
synchronized public boolean tryToCommit(Proposal p, long zxid, SocketAddress followerAddr) {       
    // 是否满足quorum条件(QuorumMaj过半)
    // 只有满足了才继续执行commit
    if (!p.hasAllQuorums()) {
       return false;                 
    }
    // zxid从发送proposal集合中移除
    outstandingProposals.remove(zxid);
    // 提案加入toApply队列
    toBeApplied.add(p);
    // 发送commit给所有follower Leader.COMMIT
    commit(zxid);
    // 发送给所有observer节点 Leader.INFORM
    inform(p);
    // leader自己执行commit
    zk.commitProcessor.commit(p.request);
    return  true;   
}
public void commit(long zxid) {
    synchronized(this){
        lastCommitted = zxid;
    }
    QuorumPacket qp = new QuorumPacket(Leader.COMMIT, zxid, null, null);
    sendPacket(qp);
}

CommitProcessor#commit:Leader将过半节点写事务日志成功的Request放入committedRequests队列,即代表Request已经提交。此外唤醒阻塞在CommitProcessor#wait的主线程(对于CommitProcessor来说,在leader发送proposal前就已经被调用了processRequest,后续再看)。

/**
* Requests that have been committed.
*/
protected final LinkedBlockingQueue<Request> committedRequests =
    new LinkedBlockingQueue<Request>();
public void commit(Request request) {
    if (stopped || request == null) {
        return;
    }
    committedRequests.add(request);
    // currentlyCommitting.get() != null
    if (!isProcessingCommit()) {
        // notifyAll()
        wakeup();
    }
}

FollowerZooKeeperServer#commit:对于follower,最终也是调用自己的CommitProcessor#commit,唤醒自己的CommitProcessor。

LinkedBlockingQueue<Request> pendingTxns = new LinkedBlockingQueue<Request>();
public void commit(long zxid) {
    // 在Leader.PROPOSAL处理提案时加入的Request,这里就可以拿到了
    Request request = pendingTxns.remove();
    commitProcessor.commit(request);
}

CommitProcessor(Leader)

从javadoc来看CommitProcessor需要满足三个约束:

1)每个session(理解为每个客户端连接吧)的请求必须按照顺序处理(同一个session请求不能并行);

2)写请求必须按照zxid全局事务id顺序处理(写写不能并行);

3)一个session的写必须能触发另一个session的watch,不能有竞态条件(在满足1的情况下,只要考虑不同session即可);

为了满足约束3,目前的做法是读请求和写请求不能被并行处理

总结来说,CommitProcessor就像个塞子,同session不可并行,读写不可并行,写写不可并行,读读可以并行,这也是为什么zk老是被吐槽说写操作频繁的时候有性能瓶颈的原因吧

CommitProcessor为了做流程控制,有很多成员变量:

// 这四个Request容器,都可以认为是Request所处状态

// 上游Processor丢进来的请求
// 可以理解为客户端(用户)的原始请求
protected final LinkedBlockingQueue<Request> queuedRequests = new LinkedBlockingQueue<Request>();

// 已经提交(过半写事务日志成功)的请求
// 对于leader,tryToCommitk统计到过半ack放入
// 对于follower,leader的tryToCommit统计到过半,通过Leader.COMMIT请求让follower提交放入
protected final LinkedBlockingQueue<Request> committedRequests = new LinkedBlockingQueue<Request>();

// 已经提交的请求,等待交给下一个Processor
protected final AtomicReference<Request> nextPending = new AtomicReference<Request>();
// 已经传给下一个Processor的请求
private final AtomicReference<Request> currentlyCommitting = new AtomicReference<Request>();

// 已经传给下一个Processor正在处理的请求数量
// 读读可并发,所以是个数字
protected AtomicInteger numRequestsProcessing = new AtomicInteger(0);

// 下一个Processor,基本可以认为是FinalRequestProcessor
RequestProcessor nextProcessor;

// nextProcessor处理的请求,都会异步在这个线程池中处理
protected WorkerService workerPool;

在ProposalRequestProcessor发布proposal前,Request就已经被丢到CommitProcessor了,放入queuedRequests。

protected final LinkedBlockingQueue<Request> queuedRequests =
        new LinkedBlockingQueue<Request>();
/** Request for which we are currently awaiting a commit */
protected final AtomicReference<Request> nextPending =
        new AtomicReference<Request>();
@Override
public void processRequest(Request request) {
    if (stopped) {
        return;
    }
    queuedRequests.add(request);
    // !(nextPending.get() != null)
    // nextPending.get() == null
    // 如果没有等待commit的事务,尝试唤醒主线程,执行当前request
    if (!isWaitingForCommit()) {
        // notifyAll()
        wakeup();
    }
}

CommitProcessor#run:主线程主要是做流程控制,满足那三个约束的请求放行给下一个processor。

我们以当前写请求的梳理顺序,简单走一下这个run方法。

T1:Leader的ProposalRequestProcessor将Request投递到queuedRequests,假设CommitProcessor被唤醒,此时判断committedRequests为空,因为还未收到过半ack,继续wait;

T2:过半节点写事务日志成功,leader收到过半ack,committedRequests被放入被提交的request,唤醒后这个request被放入nextPending,进入processCommitted方法;

@Override
public void run() {
    Request request;
    while (!stopped) {
        // T1:假设leader请求先到commitprocessor 且 还未过半ack
        // 由于queuedRequests.isEmpty() 且 committedRequests.isEmpty() 则 wait
        // T2:过半ack到来,commit方法被调用
        // committedRequests.isEmpty不满足 且 没有正在处理的请求numRequestsProcessing.get==0 走到下一步处理
        synchronized(this) {
            while (
                !stopped &&
                        // 没有请求 || nextPending.get() != null || currentlyCommitting.get() != null
                ((queuedRequests.isEmpty() || isWaitingForCommit() || isProcessingCommit()) &&
                        // 有已经提交的request || numRequestsProcessing.get() != 0
                 (committedRequests.isEmpty() || isProcessingRequest()))) {
                wait();
            }
        }
		// nextPending.get() == null && currentlyCommitting.get() == null
        while (!stopped && !isWaitingForCommit() &&
               !isProcessingCommit() &&
               (request = queuedRequests.poll()) != null) {
            if (needCommit(request)) {
                // 保证没有读写/写写并发的时候,这个写请求可以被处理
                // 如果是写请求,放入nextPending
                nextPending.set(request);
            } else {
                // 如果是读请求,直接放行
                sendToNextProcessor(request);
            }
        }
        // 处理写请求commit
        processCommitted();
    }
}

CommitProcessor#processCommitted:将写请求从pending移动到currentylyCommitting,放行给下一个Processor,对于Leader是ToBeAppliedRequestProcessor,对于Follower是FinalRequestProcessor。

protected void processCommitted() {
    Request request;
    if (!stopped && !isProcessingRequest() &&
            (committedRequests.peek() != null)) {
        if ( !isWaitingForCommit() && !queuedRequests.isEmpty()) {
            return;
        }
        // 对于follower 是propose过来的 没有客户端连接cnxn
        // 对于leader 是客户端的Request对象,包含客户端连接cnxn
        // 总而言之,这个请求已经记录到事务日志了
        request = committedRequests.poll();

        // 直连当前节点的客户端请求,持有客户端连接cnxn
        Request pending = nextPending.get();

        // case1 如果当前节点是客户端(我们开发)请求的节点 走pending,包含客户端连接cnxn
        if (pending != null &&
            pending.sessionId == request.sessionId &&
            pending.cxid == request.cxid) {
            pending.setHdr(request.getHdr());
            pending.setTxn(request.getTxn());
            pending.zxid = request.zxid;
            currentlyCommitting.set(pending);
            nextPending.set(null);
            // leader -> ToBeAppliedRequestProcessor
            // follower -> FinalRequestProcessor
            sendToNextProcessor(pending);
        } else {
            // case2 如果当前节点不是客户端请求的节点,代表这个请求来源于zk内部
            // 比如follower收到Leader.COMMIT后,走这
            // 这个request里没有连接cnxn
            // follower -> FinalRequestProcessor
            currentlyCommitting.set(request);
            sendToNextProcessor(request);
        }
        
        sendToNextProcessor(pending);
    }      
}

CommitProcessor#sendToNextProcessor:这个方法单独列出来是为了说明两点:

1)CommitProcessor的后一个Processor是与CommitProcessor不同的worker线程;

2)WorkerService workerPool的构造,不同于网络通讯中负责io的worker线程池,它保证了约束1,即同一个session的请求按照顺序处理;

protected WorkerService workerPool;
private void sendToNextProcessor(Request request) {
    // 处理中请求++
    numRequestsProcessing.incrementAndGet();
    // 为每个session分配不变的线程,保证同session请求按照顺序执行
    workerPool.schedule(new CommitWorkRequest(request), request.sessionId);
}
@Override
public void start() {
    int numCores = Runtime.getRuntime().availableProcessors();
    int numWorkerThreads = Integer.getInteger(
        ZOOKEEPER_COMMIT_PROC_NUM_WORKER_THREADS, numCores);
    
    if (workerPool == null) {
        // 第三个参数true
        workerPool = new WorkerService(
            "CommitProcWork", numWorkerThreads, true);
    }
    stopped = false;
    super.start();
}

CommitWorkRequest

private class CommitWorkRequest extends WorkerService.WorkRequest {
    private final Request request;

    CommitWorkRequest(Request request) {
        this.request = request;
    }
	// 同一个session的请求不会并发
    public void doWork() throws RequestProcessorException {
        try {
            nextProcessor.processRequest(request);
        } finally {
            // currentlyCommitting置空 request从CommitProcessor消失
            currentlyCommitting.compareAndSet(request, null);
            // 唤醒主线程
            if (numRequestsProcessing.decrementAndGet() == 0) {
                if (!queuedRequests.isEmpty() ||
                    !committedRequests.isEmpty()) {
                    wakeup();
                }
            }
        }
    }
}

具体WorkerService的构造就不细看了,这个需求也比较常见,比如多线程处理订单状态变更,同一个订单号要放到一个线程来保证顺序,思路就是比如创建10个线程,对订单号哈希取模,调度对应线程。

CommitProcessor(Follower)

Leader收到过半ack,调用所有Follower发送Leader.COMMIT,传入事务id。

Follower#processPacket处理Leader.COMMIT请求:

最终调用CommitProcessor#commit:

对于Follower来说,CommitProcessor也是做并发控制,比如当前有写请求在commit时,可以堵住读请求,最终放行给FinalRequestProcessor。

区别在于CommitProcessor#processCommitted方法中,follower会走case2,这个请求属于zk集群内部请求。

ToBeAppliedRequestProcessor(Leader)

ToBeAppliedRequestProcessor先将请求传给FinalRequestProcessor,在FinalRequestProcessor处理完毕后,最终将提案从apply队列中移除。

public void processRequest(Request request) throws RequestProcessorException {
    // FinalRequestProcessor
    next.processRequest(request);

    // hdr不为空,代表写请求
    // 写请求处理完毕后,再从apply队列中移除(过半ack时tryToCommit放入)
    if (request.getHdr() != null) {
        long zxid = request.getHdr().getZxid();
        Iterator<Proposal> iter = leader.toBeApplied.iterator();
        if (iter.hasNext()) {
            Proposal p = iter.next();
            if (p.request != null && p.request.zxid == zxid) {
                iter.remove();
                return;
            }
        }
    }
}

FinalRequestProcessor(Leader/Follower)

无论Leader(ToBeAppliedRequestProcessor直接透传)还是Follower,都是从CommitProcessor来到FinalRequestProcessor。

FinalRequestProcessor#processRequest:

1)ZooKeeperServer#processTxn:真正把数据应用到当前节点的内存中,并触发watch;

2)ChangeRecord处理,主要为了PreRequestProcessor正确获取path对应节点数据;

3)ZKDatabase#addCommittedProposal:写内存committedLog;

4)构造响应体(Follower不需要)

5)响应客户端(Follower不需要)

我们重点看一下ZooKeeperServer#processTxn和ZKDatabase#addCommittedProposal。

 public void processRequest(Request request) {
    ProcessTxnResult rc = null;
    // 为了保证PreRequestProcessor能正确处理ChangeRecord,这里synchronized同步
    synchronized (zks.outstandingChanges) {
        // #1 写数据(内存) 触发watch
        rc = zks.processTxn(request);
        // #2 ChangeRecord处理
        if (request.getHdr() != null) {
            TxnHeader hdr = request.getHdr();
            Record txn = request.getTxn();
            long zxid = hdr.getZxid();
            while (!zks.outstandingChanges.isEmpty()
                   && zks.outstandingChanges.peek().zxid <= zxid) {
                ChangeRecord cr = zks.outstandingChanges.remove();
                if (cr.zxid < zxid) {
                    LOG.warn("Zxid outstanding " + cr.zxid
                             + " is less than current " + zxid);
                }
                if (zks.outstandingChangesForPath.get(cr.path) == cr) {
                    zks.outstandingChangesForPath.remove(cr.path);
                }
            }
        }
        if (request.isQuorum()) {
            // #3 写内存committedLog
            zks.getZKDatabase().addCommittedProposal(request);
        }
        // 写操作,如果当前节点是follower,request对应客户端连接为空,不需要响应leader
        // 见org.apache.zookeeper.server.quorum.FollowerZooKeeperServer.logRequest
        if (request.cnxn == null) {
            return;
        }
        // #4 构造响应体
        Record rsp = null;
        switch(request.type) {
            case OpCode.create: {
                lastOp = "CREA";
                rsp = new CreateResponse(rc.path);
                err = Code.get(rc.err);
                break;
            }
            // ...
        }
        // #5 响应客户端
        long lastZxid = zks.getZKDatabase().getDataTreeLastProcessedZxid();
        ReplyHeader hdr =
            new ReplyHeader(request.cxid, lastZxid, err.intValue());
        cnxn.sendResponse(hdr, rsp, "response");
    }
}

写内存树

ZooKeeperServer#processTxn调用DataTree#processTxn:

public ProcessTxnResult processTxn(TxnHeader header, Record txn, boolean isSubTxn)
    {
    ProcessTxnResult rc = new ProcessTxnResult();

    rc.clientId = header.getClientId();
    rc.cxid = header.getCxid();
    rc.zxid = header.getZxid();
    rc.type = header.getType();
    rc.err = 0;
    rc.multiResult = null;
    switch (header.getType()) {
        case OpCode.create:
            CreateTxn createTxn = (CreateTxn) txn;
            rc.path = createTxn.getPath();
            // 创建节点
            createNode(
                    createTxn.getPath(),
                    createTxn.getData(),
                    createTxn.getAcl(),
                    createTxn.getEphemeral() ? header.getClientId() : 0,
                    createTxn.getParentCVersion(),
                    header.getZxid(), header.getTime(), null);
            break;
    }
    // 更新实际处理的最大zxid
    if (rc.zxid > lastProcessedZxid) {
        lastProcessedZxid = rc.zxid;
    }
    return rc;
}

DataTree#createNode:最终将数据写入内存map即DataTree#nodes,最后触发两种watch。

private final ConcurrentHashMap<String, DataNode> nodes =
        new ConcurrentHashMap<String, DataNode>();
public void createNode(final String path, byte data[], List<ACL> acl,
        long ephemeralOwner, int parentCVersion, long zxid, long time, Stat outputStat)
        throws KeeperException.NoNodeException,
        KeeperException.NodeExistsException {
    int lastSlash = path.lastIndexOf('/');
    String parentName = path.substring(0, lastSlash);
    String childName = path.substring(lastSlash + 1);
    StatPersisted stat = new StatPersisted();
    stat.setCtime(time);
    stat.setMtime(time);
    stat.setCzxid(zxid);
    stat.setMzxid(zxid);
    stat.setPzxid(zxid);
    stat.setVersion(0);
    stat.setAversion(0);
    stat.setEphemeralOwner(ephemeralOwner);
    DataNode parent = nodes.get(parentName);
    if (parent == null) {
        throw new KeeperException.NoNodeException();
    }
    synchronized (parent) {
        Set<String> children = parent.getChildren();
        if (children.contains(childName)) {
            throw new KeeperException.NodeExistsException();
        }
        // parent更新
        if (parentCVersion == -1) {
            parentCVersion = parent.stat.getCversion();
            parentCVersion++;
        }
        parent.stat.setCversion(parentCVersion);
        parent.stat.setPzxid(zxid);
        Long longval = aclCache.convertAcls(acl);
        DataNode child = new DataNode(data, longval, stat);
        parent.addChild(childName);
        // 【放入树】
        nodes.put(path, child);
        // ...
    }
    // ...
    // 触发watch
    dataWatches.triggerWatch(path, Event.EventType.NodeCreated);
    childWatches.triggerWatch(parentName.equals("") ? "/" : parentName,
            Event.EventType.NodeChildrenChanged);
}

写内存committedLog

这个commitedLog主要是为了崩溃恢复时follower从leader增量同步用的(有点类似于Redis的repl_backlog),如果一个follower落后不超出minCommittedLog和maxCommittedLog范围,Leader可以从内存的committedLog直接同步给follower,而不用走磁盘,这个后续再看。

public static final int commitLogCount = 500;
protected long minCommittedLog, maxCommittedLog;
protected LinkedList<Proposal> committedLog = new LinkedList<Proposal>();
public void addCommittedProposal(Request request) {
    WriteLock wl = logLock.writeLock();
    try {
        wl.lock();
        // committedLog超出阈值500,丢弃最老的
        if (committedLog.size() > commitLogCount) {
            committedLog.removeFirst();
            minCommittedLog = committedLog.getFirst().packet.getZxid();
        }
        if (committedLog.isEmpty()) {
            minCommittedLog = request.zxid;
            maxCommittedLog = request.zxid;
        }
        byte[] data = SerializeUtils.serializeRequest(request);
        QuorumPacket pp = new QuorumPacket(Leader.PROPOSAL, request.zxid, data, null);
        Proposal p = new Proposal();
        p.packet = pp;
        p.request = request;
        committedLog.add(p);
        maxCommittedLog = p.packet.getZxid();
    } finally {
        wl.unlock();
    }
}

Leader读请求处理

在看Leader读请求处理之前,先看一下单机zk用到哪些RequestProcessor,可能更好理解。

单机zk的server实现是ZooKeeperServer,只有三个Processor:

1)PrepRequest

2)Sync

3)Final

Sync和写事务日志相关,所以读的业务逻辑应该都在PrepRequest和Final里。

再回头看LeaderZookeeperServer的RequestProcessor,

Proposal只处理写请求(里面包含发布提案和写事务日志SyncProcessor),

ToBeApplied可以认为只是请求透传,

所以相对于单机zk,只是多了CommitProcessor这个塞子来处理读写/写写并发问题。

我们以getChildren(ls)来走一下读请求流程。

PreRequestProcessor#pRequest:仅检查session是否存活(暂时就理解为连接),接着就交给了ProposalRequestProcessor。

由于是个读请求,request里没被塞入事务头hdr,所以ProposalRequestProcessor将请求透传给CommitProcessor。

CommitProcessor如果当前没有写请求正在处理,就会放行请求最终进入FinalRequestProcessor,如果有写请求就等前面的写请求都处理完毕,再放行。

FinalRequestProcessor#processRequest:

最终调用DataTree#getChildren从内存中读数据返回,这个DataNode一般就是大家所说的ZNode。

Follower读请求处理

为什么先看follower读,是因为我觉得看完读再看写,会更能体会zk对于客户端一致性视图的保证。

我一直有一个问题:之前都是按照读写leader来捋的代码,如果所有客户端都连接Leader,那么写之后读能马上读到最新值,因为CommitProcessor不允许读写并发。而如果不同客户端连接不同的zk节点,是否能实现像Raft一样的线性一致读呢

这里找到了zk文档:zookeeper.apache.org/doc/r3.5.8/…

Sometimes developers mistakenly assume one other guarantee that ZooKeeper does not in fact make. This is: * Simultaneously Consistent Cross-Client Views* : ZooKeeper does not guarantee that at every instance in time, two different clients will have identical views of ZooKeeper data. Due to factors like network delays, one client may perform an update before another client gets notified of the change. Consider the scenario of two clients, A and B. If client A sets the value of a znode /a from 0 to 1, then tells client B to read /a, client B may read the old value of 0, depending on which server it is connected to. If it is important that Client A and Client B read the same value, Client B should should call the sync() method from the ZooKeeper API method before it performs its read. So, ZooKeeper by itself doesn't guarantee that changes occur synchronously across all servers, but ZooKeeper primitives can be used to construct higher level functions that provide useful client synchronization.

zk不能保证,针对所有zk节点同一时刻,两个不同的客户端能得到完全一样的zk数据。

如果有两个客户端A和B,初始情况下/a的数据为0:

T1:A更新/a为1,zk响应A成功,A通知B去读/a

T2:B读/a数据,zk可能读到旧值0,取决于B连接的是哪个zk节点

如果B要读到和A一样的最新值,B需要在读数据之前先调用sync方法。

那如果不调用sync方法,什么情况下能读到0呢?

比如A连接的是leadeer,B连接的是follower:

T1:A更新/a为1,zk响应成功,但是leader给follower的commit是异步发送的,还在路上

T2:客户端B的读请求先进入follower的CommitProcessor#queuedRequests

T3:follower收到A更新的那个proposal的Leader.COMMIT,写请求进入CommitProcessor#committedRequests,但是阻塞等待客户端B读请求先跑完

zk能保证的是针对同一节点的顺序一致,这点无论是客户端连接leader还是follower都能做到的,比如客户端B的读请求注册了watch,那么在Leader的proposal提交成功后,B能收到watch回调。

回到主题,follower怎么处理读请求。

回顾follower的processor:

FollowerRequestProcessor#run:对于读请求没有任何特殊操作,直接丢给了CommitProcessor,CommitProcessor作为塞子,防止读写、读读并发,最终还是会交给FinalRequestProcessor,最终从内存nodes中读到数据响应客户端。

public void run() {
    while (!finished) {
        Request request = queuedRequests.take();
        // We want to queue the request to be processed
        // before we submit the request to the leader
        // so that we are ready to receive the response
        nextProcessor.processRequest(request);

        // We now ship the request to the leader. As with all
        // other quorum operations, sync also follows this code
        // path, but different from others, we need to keep track
        // of the sync operations this follower has pending, so we
        // add it to pendingSyncs.
        switch (request.type) {
                // ... 没有getChildren的处理逻辑
        }
    }
}

Follower写请求处理

FollowerRequestProcessor#run:对于写请求,还是会先丢到CommitProcessor做并发控制,然后调用Follower#request方法。

Follower#request:将request封装为Leader.REQUEST请求,转发至Leader节点处理。

Leader通过LearnerHandler处理,封装为一个没有对端连接的Request,传给PrepRequestProcessor走正常leader写流程。

没有对端连接,意味着响应follower是否写成功不走FinalRequestProcessor,那走哪呢?还是leader写请求流程里的逻辑,Leader向Follower发送的两个请求:

1)Leader.PROPOSAL写事务日志

2)Leader.COMMIT让CommitProcessor主线程唤醒,放行Request,写内存nodes

总结

本文只能按照一个正常写请求的处理顺序来梳理,本身分布式中间件调试就相对困难,讲明白并发请求在分布式中间件中的运作,以文字+源码+配图的形式描述更为困难(因为他本身就无法通过从上到下的顺序来叙述),这里只是给大家一个抓手,更多的还是靠自己去梳理,开头的疑问我现在基本都能找到答案了。

线程模型

对于客户端请求:

1个accept线程,负责接收新连接然后注册channel到某个selector;

1-N个selector线程,负责监听注册到当前selector的所有channel上的io事件,当io事件发生,交给worker线程;

0-M个worker线程,负责io读写;

1个线程,负责关闭空闲连接;

n个processor线程,负责处理业务;

读写请求

整体上来说,leader和follower都能接读写请求,区别在于follower对于写请求只能转发到leader处理。

leader处理写请求

1)PrepRequestProcessor:单线程生成顺序事务zxid,创建事务头和事务数据;

2)ProposalRequestProcessor:从这里开始,为了提升zk节点的处理效率,采用多线程处理每个事务;

  • 将请求丢给CommitProcessor,并发读写需要满足三个约束;
  • 异步发送proposal给所有follower,follower走SyncRequestProcessor写事务日志,并响应leader一个ack;
  • leader的SyncRequestProcessor异步写事务日志并给自己一个ack;
  • leader统计自己+follower的ack,过半后通知CommitProcessor放行;

3)CommitProcessor在满足三个约束的情况下,放行给FinalRequestProcessor,最终写入内存的map,即DataTree#nodes,并触发watch,响应客户端;

所谓CommitProcessor的三个约束,在javadoc中:

1)每个session的请求必须按照顺序处理;

2)写请求必须按照zxid顺序处理(写写不能并行);

3)一个session的写必须能触发另一个session的watch,不能有竞态条件(实现方式是读写不能并行);

处理读请求:

1)PrepRequestProcessor:校验session还存活;

2)CommitProcessor:处理读写并发;

3)FinalRequestProcessor:从内存DataTree#nodes读数据,响应客户端;

后续

本章我们看了zk读写流程,这个过程中遇到了很多存储介质,比如:内存nodes、内存commitLog、磁盘事务日志、磁盘快照日志。这些东西到底有什么用?zk内存nodes的数据到底存储在磁盘的哪个数据文件里?

更多的比如zk崩溃恢复流程是什么样的,为什么zk在崩溃恢复阶段无法处理客户端请求?

这个放到后面的文章继续分析。

Q&A

一个session的写必须能触发另一个session的watch,不能有竞态条件

这句话怎么理解。

首先racecondition(竞态条件/竞争条件)是并发编程里尝尝会提到的一个名词,笔者第一次接触这个名词是在刚毕业的时候,阅读了"Java并发编程实战"这本书。

我的理解是:程序的运行结果会根据并发操作的时序不同,而产生预期之外的情况,就认为有racecondition。

我写个简单的kv存储,同时支持对key注册一次性监听,这个虽然不是很严谨,但是能说明问题。

public class KVStorage {
    private final Map<String, String> kv = new ConcurrentHashMap<>();
    private final Map<String, List<Listener>> k2Listeners = new ConcurrentHashMap<>();
    public String watch(String key, Listener listener) {
        String v = kv.get(key);
        if (listener != null) {
            List<Listener> listeners = 
            k2Listeners.computeIfAbsent(key, (t) -> new CopyOnWriteArrayList<>());
        listeners.add(listener);
        }
        return v;
    }
    public void put(String key, String value) {
        kv.put(key, value);
        List<Listener> listeners = k2Listeners.remove(key);
        if (listeners != null) {
            listeners.forEach(listener -> listener.onChange(key, value));
        }
    }
    public interface Listener {
        void onChange(String key, String value);
    }

    public static void main(String[] args) {
        KVStorage kvStorage = new KVStorage();
        String fooValue = kvStorage.watch("foo", new Listener() {
            @Override
            public void onChange(String key, String value) {
                kvStorage.watch("foo", this); // 再次注册
                System.out.println("key = " + key + ", value = " + value);
            }
        });

        System.out.println(fooValue); // null
        kvStorage.put("foo", "bar"); // key = foo, value = bar
    }
}

这个程序在单线程下执行是没有问题的,假设这个代码只有两个线程跑:

T1:线程A执行watch,kv.get(foo)得到null

T2:线程B执行put,kv.put设置foo为bar

T3:线程B执行k2Listeners.remove返回null

T4:线程A执行listeners.add,注册监听

这会导致线程A永远认为foo=null,这就是多线程情况下产生预期之外的事情。

这里我把zk读写客户端抽象成了两个jvm线程,但是意思是一样的。

那么如何解决这个问题呢?

你会说watch方法的注册监听和map.get换个顺序就行了。

但是实际还是不能保证的,具体时序我不理了,无论如何不用锁,都做不到原子。

但是如果直接用独占锁放在watch和put上,会造成读读互斥。

很容易想到读写锁ReadWriteLock,watch上读锁,put上写锁。

其实zk也是这个意思,但是为什么实现上没有用读写锁。

我认为有两点:

1)锁粒度

zk不是单纯的map.put那么简单,如果对watch和put方法整体加读写锁,在zk上是不合适的。

为什么不合适,我们假设读写都发生在leader节点上。

实际leader的put时序是这样的:

image.png

实际上,在1-3这个过程中,leader也可以处理读请求,只要等到过半ack之后应用到内存之后能再通知一次客户端就行了。

2)避免锁竞争,充分利用cpu

zk大量的运用了多生产单消费的线程模型,之所以是单线程,就是因为要做顺序事务。

与其让多线程去竞争锁,不如让一个线程按顺序跑。