Zookeeper 底层线程通信模型

一、前言

    zookeeper version 3.6

    在读本文之前相信你已经了解过网络编程的相关知识,如果没有的话你需要先去了解下 NIO 相关知识,否则读起来可能会有点吃力。

二、Java 中的通信模型

    一)BIO 模型

        相信大部分情况下你最早接触的通信模型就是 BIO ,这种模型的有点就是使用起来非常简单,并且在并发量小的情况下能实现较高的性能,在早期互联网没有这么发达的时候,这种模型使用是非常普遍的,但随着互联网的发展,对于网络平台的并发要求也就越来越高,BIO 模型越来越难以适应这种状况了,因为要求一个 Socket 对应一个 Thread 进行处理,比如 2000qps 的场景下即使机器可以处理这么多请求,线程上下文切换的成本也是很高的。以下图摘自 Doug Lee 的 Scalable IO in Java http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf 。

    二)Reactor 模型

        NIO 模型 Doug Lee 介绍了三种,分别是 single thread、mutithread、main-sub reactor。

        1、single thread

            这种线程模型相当于对 BIO 的优化,它把处理 socket 连接事件与处理业务请求做了解耦,如果没有连接事件则当前线程不会阻塞等待直到有客户端连接,对如下语句做了优化:

serverSocket.accept();

            单这种处理其实不难看出也有它的问题。这种处理方式虽然让等待连接不会阻塞,但是处理业务的线程是 io 线程,如果说针对某些请求处理时间长的场景,也是就是图中 decode-compute-encode 处理时间过长,会拖慢整个业务请求的处理速度。

        2、multiThread

            reactor 的多线程模型解决的单线程模型存在的问题,也就是对处理 io 事件的线程与处理业务逻辑的线程做了解耦,io 线程不会处理业务逻辑,而处理业务逻辑可以针对不同的场景进行对应线程池的调整,这样不会因为某些业务逻辑处理时间过长占用 io 线程时间过长导致最后没办法处理 io 请求。

            其实做了这么多已经能很好的解决 BIO 中很多的缺陷了,但是 multiThread 其实还有一点不足,就是针对长连接的场景,大部分时间其实都在处理读写事件,这样的话 selector 线程就没必要扫描注册了 register 事件的 socket,而建立连接也不需要扫描哪些注册了 read 或 write 事件的 socket。

        3、main-sub reactor

            这种方式也是大多数框架在使用的,像 netty 还有我们本次介绍的 zookeeper 也都是使用了这种线程模型,它解决了  multiThread 中出现的问题,对连接事件与读写事件做了解耦。

三、Zookeeper 中的线程模型

    一)Server 端

        主启动类在 org.apache.zookeeper.server.quorum.QuorumPeerMain#main 类中,本次探究线程模型没有涉及这么多,看直接看下线程模型相关的代码 org.apache.zookeeper.server.quorum.QuorumPeerMain#runFromConfig

public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException {
    //省略部分代码。。。

    if (config.getClientPortAddress() != null) {
        cnxnFactory = ServerCnxnFactory.createFactory();
        cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), false);
    }
    
    //省略部分代码。。。

    quorumPeer.start();
}


public synchronized void start() {
    //省略部分代码。。。

    startServerCnxnFactory();
}

        这部分做的事情很简单,ServerCnxnFactory#createFactory 就是创建一个 ServerCnxnFactory ,重点在下面的 configure 方法,quorumPeer#start 方法也很简单,就是启动在 configure 配置的线程,下面看下 ServerCnxnFactory#configure

public void configure(InetSocketAddress addr, int maxcc, int backlog, boolean secure) throws IOException {
    //省略部分代码。。。

    numSelectorThreads = Integer.getInteger(
            ZOOKEEPER_NIO_NUM_SELECTOR_THREADS,
            Math.max((int) Math.sqrt((float) numCores / 2), 1));

    //省略部分代码。。。

    for (int i = 0; i < numSelectorThreads; ++i) {
        selectorThreads.add(new SelectorThread(i));
    }

    
    //省略部分代码。。。

    this.ss = ServerSocketChannel.open();
    ss.socket().setReuseAddress(true);
    LOG.info("binding to port {}", addr);
    if (listenBacklog == -1) {
        ss.socket().bind(addr);
    } else {
        ss.socket().bind(addr, listenBacklog);
    }
    ss.configureBlocking(false);
    acceptThread = new AcceptThread(ss, addr, selectorThreads);
}

        根据我们上面谈到的 Reactor 模型,并且我们知道 Zookeeper 是基于 main-sub reactor 模型很容易看得懂这段代码,简单的画图描述一下是这样的

        AcceptThread 主要的工作是处理连接就绪信息,获取就绪的连接放到就绪队列里,由 SelectorThread 进行注册读事件,读取完成之后将请求扔给线程池处理,线程池处理完的响应扔到 updateQueue 中,最后写回。下面看下 AcceptThread 实现细节

public AcceptThread(ServerSocketChannel ss, InetSocketAddress addr, Set<SelectorThread> selectorThreads) throws IOException {
            super("NIOServerCxnFactory.AcceptThread:" + addr);
            this.acceptSocket = ss;
            //注册事件
            this.acceptKey = acceptSocket.register(selector, SelectionKey.OP_ACCEPT);
            this.selectorThreads = Collections.unmodifiableList(new ArrayList<SelectorThread>(selectorThreads));
            selectorIterator = this.selectorThreads.iterator();
        }


//org.apache.zookeeper.server.NIOServerCnxnFactory.AcceptThread#select
private void select() {
    // 省略部分代码。。。
    if (key.isAcceptable()) {
        if (!doAccept()) {
            pauseAccept(10);
        }
    }
}

//org.apache.zookeeper.server.NIOServerCnxnFactory.AcceptThread#doAccept
private boolean doAccept() {
    //省略部分代码
    if (!selectorIterator.hasNext()) {
        selectorIterator = selectorThreads.iterator();
    }
    SelectorThread selectorThread = selectorIterator.next();
    if (!selectorThread.addAcceptedConnection(sc)) {
            throw new IOException("Unable to add connection to selector queue"+ (stopped ? "(shutdown in progress)" : ""));
    }
}

//org.apache.zookeeper.server.NIOServerCnxnFactory.SelectorThread#addAcceptedConnection
public boolean addAcceptedConnection(SocketChannel accepted) {
    if (stopped || !acceptedQueue.offer(accepted)) {
        return false;
    }
    wakeupSelector();
    return true;
}

        逻辑还是很容易看懂的,就是上图中展示的注册连接事件然后等到事件就绪之后轮询 SelectorThread ,并将对应的 Socket 扔到 selectorThread 的队列中,整个 AcceptThread 流程结束,下面看下 SelectorThread

    public void run() {
        while (!stopped) {
            try {
                select();
                processAcceptedConnections();
                processInterestOpsUpdateRequests();
            } catch (RuntimeException e) {
                LOG.warn("Ignoring unexpected runtime exception", e);
            } catch (Exception e) {
                LOG.warn("Ignoring unexpected exception", e);
            }
        }
    }

        整个 selectorThread 包含三部分,为方便理解我们按照  processAcceptedConnections -> processInterestOpsUpdateRequests -> select 的顺序进行分析

    private void processAcceptedConnections() {
        SocketChannel accepted;
        while (!stopped && (accepted = acceptedQueue.poll()) != null) {
            SelectionKey key = null;
            try {
                key = accepted.register(selector, SelectionKey.OP_READ);
                NIOServerCnxn cnxn = createConnection(accepted, key, this);
                key.attach(cnxn);
                addCnxn(cnxn);
            } catch (IOException e) {
                // register, createConnection
                cleanupSelectionKey(key);
                fastCloseSock(accepted);
            }
        }
    }

    private void processInterestOpsUpdateRequests() {
        SelectionKey key;
        while (!stopped && (key = updateQueue.poll()) != null) {
            if (!key.isValid()) {
                cleanupSelectionKey(key);
            }
            NIOServerCnxn cnxn = (NIOServerCnxn) key.attachment();
            if (cnxn.isSelectable()) {
                key.interestOps(cnxn.getInterestOps());
            }
        }
    }

    private void select() {
    if (key.isReadable() || key.isWritable()) {
        handleIO(key);
    } else {
        LOG.warn("Unexpected ops in select {}", key.readyOps());
    }

        这两个方法主要作用是注册事件,processAcceptedConnections 将 read 事件注册到了selector 中并绑定了一个 NIOServerCnxn 对象,这个对象与 Socket 是一一对应的,底层数据的读写都在这个类中完成,select 主要逻辑在 handleIO中。

    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);
    }

        看下 handleIO 中一共干了这么几件事:①首先把当前 selectorThread 与 key 包装成一个对象。②取消 SelectionKey 感兴趣的事件,从这步可以看出 zk 处理请求的方式是逐条处理,接收到请求后会等待处理完成之后再处理下一条请求。③ 对 session 续期,后面分析会提到这里的逻辑。④将任务丢给线程池。

   二)Client 端

        

        Client 端也有两个线程 SendThread 和 EventThread

        SendThread 主要处理与 Server 端的请求跟响应,每次请求前,判断请求类型,如果不是 ping,权限验证请求则会将请求放在 pendingQueue 中,Server 端处理完成后判断是否是 Watcher 回调,如果是添加到 eventQueue 队列中,会将请求从请求队列中拉去到并将响应结果写入到请求对象中一起返回到上层

        EventThread 主要处理 Watcher 的回调

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值