zookeeper源码解析-单机服务端

∥☆過路亽.° 提交于 2019-12-29 19:22:51

1.引入

zk单机服务端虽然在实际生产使用中不可能碰到,但是用于接下来分析zk集群服务端有很大用处,我们秉承循序渐进的方式,先介绍一下单机服务端的启动和处理流程

2.单机服务器启动

在单机启动过程中,会创建服务端连接对象,用于处理客户端的请求。其处理过程,可以从以下代码开始介绍。

ZooKeeperServerMain.java
-------------------------

   public void runFromConfig(ServerConfig config)
            throws IOException, AdminServerException {
        .....
            if (config.getClientPortAddress() != null) {
                cnxnFactory = ServerCnxnFactory.createFactory();
                cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false);
                cnxnFactory.startup(zkServer);
                needStartZKServer = false;
            }
      ......

上面代码中的cnxnFactory.startup就是触发单机服务端启动的入口,它的实现有NIO和Netty两种方式,这里我们直接介绍NIO也就是默认的实现方式,它主要包含一下几个步骤:

  1. 启动SelectorThread(处理客户端请求线程),accepThread(处理接收连接进行事件),ExpirerThread(处理过期连接)
  2. 加载内存数据库
  3. 定时清理session,设置请求处理器(当前的处理器链为: PrepRequestProcessor -> SyncRequestProcessor -> FinalRequestProcessor,在处理客户端请求时会依次介绍这几个处理器)
NIOServerCnxnFactory.java
-------------------------

    public void startup(ZooKeeperServer zks, boolean startServer)
            throws IOException, InterruptedException {
        // 启动相关线程
        start();
        setZooKeeperServer(zks);
        // zk server启动()
        if (startServer) {
            // 加载数据到zkDataBase,清除无效的session,将zk数据和session序列化后保存到文件中等
            zks.startdata();
            // 启动定时清除session的管理器,注册jmx,添加请求处理器
            zks.startup();
        }
    }

SelectorThread是专门用于处理客户端请求的线程,它的主要就是监听NIO读写事件并将请求封装成IOWorkRequest对象并存储到调度池中。

NIOServerCnxnFactory.java
-------------------------

 public void run() {
            try {
                while (!stopped) {
                    try {
                    // 处理读写请求,select方法中最主要的是handleIO方法(即处理读写)
                        select();
                       // 接收连接(忽略) processAcceptedConnections();
                      // 设置NIO监听事件(忽略) processInterestOpsUpdateRequests();
                    } catch (RuntimeException e) {
                        LOG.warn("Ignoring unexpected runtime exception", e);
                    } catch (Exception e) {
                        LOG.warn("Ignoring unexpected exception", e);
                    }
                }

     // 处理读写
        private void handleIO(SelectionKey key) {
            IOWorkRequest workRequest = new IOWorkRequest(this, key);
            NIOServerCnxn cnxn = (NIOServerCnxn) key.attachment();

            cnxn.disableSelectable();
            key.interestOps(0);
            // 防止连接过期
            touchCnxn(cnxn);
            // 将请求放入调度池
            workerPool.schedule(workRequest);
        }

调度池的实现方式很简单就是使用多个线程池并行处理多个任务,最终会调用workRequest的doWork方法,所以看看这个方法的实现:

NOIServerCnxnFactory.IOWorkRequest
----------------------------

        public void doWork() throws InterruptedException {
            if (!key.isValid()) {
                selectorThread.cleanupSelectionKey(key);
                return;
            }

            if (key.isReadable() || key.isWritable()) {
               // 最终调用连接对象(NOIServerCnxn)的doIO方法
                cnxn.doIO(key);

                if (stopped) {
                    cnxn.close();
                    return;
                }
                if (!key.isValid()) {
                    selectorThread.cleanupSelectionKey(key);
                    return;
                }
                touchCnxn(cnxn);
            }

处理过程其实就是调用NIOServerCnxn的doIO方法(这里只介绍核心逻辑),当需要处理客户端的请求是会调用readPayload方法,如果响应客户端会调用handleWrite方法。

NIOServerCnxn.java
---------------------


    void doIO(SelectionKey k) throws InterruptedException {
     if (k.isReadable()) {
             ......
              // 处理客户端的请求
              readPayload();
             ...... 
            }
            if (k.isWritable()) {
                // 处理发送数据到客户端(响应)
                handleWrite(k);
              .....
            }
  • 服务端响应流程

    由于handleWrite方法比较简单,我们直接从这个方法说起,从下面的代码片段可以看出,这个方法其实就是将outgoingBuffers队列中的数据发送到客户端,即如果需要响应客户端,只需要将响应数据添加到outgoingBuffers队列中即可。
NIOServerCnxn.java
------------------

  void handleWrite(SelectionKey k) throws IOException, CloseRequestException {
        if (outgoingBuffers.isEmpty()) {
            return;
        }

        ByteBuffer directBuffer = NIOServerCnxnFactory.getDirectBuffer();
        if (directBuffer == null) {
            ByteBuffer[] bufferList = new ByteBuffer[outgoingBuffers.size()];
     
            sock.write(outgoingBuffers.toArray(bufferList));
            ......
            }
         } else {
			 ......
             for (ByteBuffer b : outgoingBuffers) {
                if (directBuffer.remaining() < b.remaining()) {
                    b = (ByteBuffer) b.slice().limit(
                            directBuffer.remaining());
                }
                int p = b.position();
                directBuffer.put(b);
                b.position(p);
                if (directBuffer.remaining() == 0) {
                    break;
                }
            }
			......
        }
    }

  • 服务端读流程(处理请求流程)
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eHka0Bh-1577608488262)(C58DB0F3D39047B1A85159384E6EDE79)]
    通过上面的时序图,可以清楚得看到zk处理客户端请求的主要流程,在submitRequest之前只是将请求数据进行二次封装和校验,最终还是得通过 PrepRequestProcessor , SyncRequestProcessor , FinalRequestProcessor依次处理,所以这里直接从PrepRequestProcessor开始介绍。
  1. PrepRequestProcessor

它的processRequest方法很简单,就是将请求对象添加到submittedRequests,而Processor是一个线程,在启动之后会执行run方法,这个run方法就是取submittedRequests队列中的请求进行处理。

PreRequestProcessor.java
-------------------------

    public void run() {
        try {
            while (true) {
                Request request = submittedRequests.take();
                ......
                // 是否是结束请求
                if (Request.requestOfDeath == request) {
                    break;
                }
                // 处理请求
                pRequest(request);
            }
            ......

其pRequest方法虽然比较长,但是细看这个方法会发现就是将请求对象分成多种请求类型(比如:create,setData,getData等),对于客户端写处理转成对应事务对象并设置到request中,如果是读请求只是会进行校验,比如:path,session等校验,然后调用方法

 nextProcessor.processRequest(request)

交给SyncRequestProcessor处理。

2.SyncRequestProcessor


和PrepRequestProcessor一样,processRequest只是用于添加队列元素,在run方法中才是处理逻辑。

SyncRequestProcessor.java
--------------------------
 public void run() {
        try {
            int logCount = 0;

            int randRoll = r.nextInt(snapCount/2);
            while (true) {
                Request si = null;
                // 从请求处理队列中,拿出一个请求
                if (toFlush.isEmpty()) {
                    si = queuedRequests.take();
                } else {
                    si = queuedRequests.poll();
                    // 如果没有请求,就写入磁盘,并且交给下一个处理器处理
                    if (si == null) {
                        flush(toFlush);
                        continue;
                    }
                }
                if (si == requestOfDeath) {
                    break;
                }
                if (si != null) {
                    // track the number of records written to the log
                    if (zks.getZKDatabase().append(si)) {
                        logCount++;
                        // 控制大于某个随机数才写入事务数据和快照,是为了防止多个节点同时同步数据
                        if (logCount > (snapCount / 2 + randRoll)) {
                            randRoll = r.nextInt(snapCount/2);
                            // 准备创建一个快照文件,所以需要将事务日志刷新,并将日志流置为null,保证再次使用的时候会重新创建一个新的事务日志文件
                            zks.getZKDatabase().rollLog();
                            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;
                        }
                    } else if (toFlush.isEmpty()) {
                        if (nextProcessor != null) {
                            nextProcessor.processRequest(si);
                            if (nextProcessor instanceof Flushable) {
                                ((Flushable)nextProcessor).flush();
                            }
                        }
                        continue;
                    }
                    toFlush.add(si);
                    // 如果请求数达到1000保存事务日志。
                    if (toFlush.size() > 1000) {
                        flush(toFlush);
                    }
                }
            }
        } catch (Throwable t) {
            handleException(this.getName(), t);
        } finally{
            running = false;
        }
        LOG.info("SyncRequestProcessor exited!");
    }
            ......

代码看起来比较复杂,所以在这里总结一下执行流程:

  • 循环取queuedRequests队列中的请求,如果请求不存在就直接刷新事务日志并交给下一个处理器(FinalRequestProcessor)处理。
  • 为了保证所有的服务器尽可能不在同一时刻创建快照,增加了一个随机数控制当前的日志数是不是符合条件,如果符合条件,就将原来的事务日志文件刷新等待创建新的事务日志文件,并通过异步的方式保存当前的快照。
  • 如果处理的日志请求数已经达到1000条,不管queuedRequests队列中是不是还有请求,直接刷新到磁盘并且交给一下处理器处理。
  1. FinalRequestProcessor

在SyncRequestProcessor达到条件并且刷新到磁盘中,就会调用FinalRequestProcessor的processRequest方法处理(注意:这个处理器和前面两个不一样,它不是线程,所以处理逻辑就是在processRequest方法中实现了)
这个类主要是做两件事:

  • 修改zk的内存数据库ZkDataBase
  • 根据客户端请求,创建对应的响应对象,并发送到客户端。

修改内存数据库

FinalRequestProcessor.java
-------------------------

 public void processRequest(Request request) {
        ProcessTxnResult rc = null;
        synchronized (zks.outstandingChanges) {
            // Need to process local session requests
            // 处理事务和会话
            rc = zks.processTxn(request);
            ......
            // 事务性请求存放到committedLog
            if (request.isQuorum()) {
                zks.getZKDatabase().addCommittedProposal(request);
            }
       }

创建对应的响应对象

FinalRequestProcessor.java
-------------------------


   ServerCnxn cnxn = request.cnxn;

        String lastOp = "NA";
        zks.decInProcess();
        Code err = Code.OK;
        Record rsp = null;
            // 处理不同的请求类型
            switch (request.type) {
            case OpCode.ping: {
                // 更新延迟
                zks.serverStats().updateLatency(request.createTime);

                lastOp = "PING";

                cnxn.updateStatsForResponse(request.cxid, request.zxid, lastOp,
                        request.createTime, Time.currentElapsedTime());

                cnxn.sendResponse(new ReplyHeader(-2,
                        zks.getZKDatabase().getDataTreeLastProcessedZxid(), 0), null, "response");
                return;
            }
            ......
            case OpCode.create: {
                lastOp = "CREA";
                rsp = new CreateResponse(rc.path);
                err = Code.get(rc.err);
                break;
            }
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!