ZooKeeper启动源码分析

  • 1. 客户端启动
  • 1. getClientCnxnSocket()
  • 2. 创建ClientCnxn实例
  • 2.1. SendThread
  • 2.2. EventThread
  • 3. 启动服务


从github上拉下来的zk源码会附带很多单元测试,咱们就源码中的ZooKeeperTestClient#create_get_stat_test()方法来分析 :

public class ZooKeeperTestClient extends ZKTestCase implements Watcher {
    protected String hostPort = "127.0.0.1:22801";
    protected static final String dirOnZK = "/test_dir";
    protected String testDirOnZK = dirOnZK + "/" + Time.currentElapsedTime();


    private void create_get_stat_test() throws IOException, InterruptedException, KeeperException {
        ZooKeeper zk = new ZooKeeper(hostPort, 10000, this);
        String parentName = testDirOnZK;
        String nodeName = parentName + "/create_with_stat_tmp";
        deleteNodeIfExists(zk, nodeName);
        deleteNodeIfExists(zk, nodeName + "_2");
        Stat stat = new Stat();
        zk.create(nodeName, null, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, stat);
        assertNotNull(stat);
        assertTrue(stat.getCzxid() > 0);
        assertTrue(stat.getCtime() > 0);
        zk.close();
    }


    public synchronized void process(WatchedEvent event) {
        try {
            System.out.println("Got an event " + event.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

1. 客户端启动

zk在创建实例的时候就启动了两个线程去处理不同的业务,一个是负责客户端和服务端的通信,另一个是负责在客户端回调注册的Watchers进行通知处理。
我们来看下它是怎么实现的:

ZooKeeper zk = new ZooKeeper(hostPort, 10000, this);

请注意这里传了一个watcher即this

我们将Zookeeper的构造函数一层层点进去看下:

public ZooKeeper(
        String connectString,
        int sessionTimeout,
        Watcher watcher,
        boolean canBeReadOnly,
        HostProvider aHostProvider) throws IOException {
        this(connectString, sessionTimeout, watcher, canBeReadOnly, aHostProvider, null);
    }

    public ZooKeeper(
        String connectString,
        int sessionTimeout,
        Watcher watcher,
        boolean canBeReadOnly,
        HostProvider hostProvider,
        ZKClientConfig clientConfig
    ) throws IOException {
        LOG.info(
            "Initiating client connection, connectString={} sessionTimeout={} watcher={}",
            connectString,
            sessionTimeout,
            watcher);

        this.clientConfig = clientConfig != null ? clientConfig : new ZKClientConfig();
        //保存了之前new ZooKeeper时传入的主机地址(InetSocketAddress实例)
        this.hostProvider = hostProvider;
        //解析连接地址
        ConnectStringParser connectStringParser = new ConnectStringParser(connectString);

        cnxn = createConnection(
            connectStringParser.getChrootPath(),
            hostProvider,
            sessionTimeout,
            this.clientConfig,
            watcher,
            getClientCnxnSocket(),
            canBeReadOnly);
        cnxn.start();
    }

这里有三个重要的逻辑:

1. getClientCnxnSocket()

看下这个方法的实现

private ClientCnxnSocket getClientCnxnSocket() throws IOException {
        String clientCnxnSocketName = getClientConfig().getProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
        if (clientCnxnSocketName == null) {
            clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
        }
        try {
            Constructor<?> clientCxnConstructor = Class.forName(clientCnxnSocketName)
                                                       .getDeclaredConstructor(ZKClientConfig.class);
            ClientCnxnSocket clientCxnSocket = (ClientCnxnSocket) clientCxnConstructor.newInstance(getClientConfig());
            return clientCxnSocket;
        } catch (Exception e) {
            throw new IOException("Couldn't instantiate " + clientCnxnSocketName, e);
        }
    }

这个会创建一个ClientCnxnSocket实例,主要负责处理客户端的socket请求,后面的文章会分析到它,他的实现有两种,一个是基于Netty的实现,另一个是基于NIO的实现,可以在配置文件中配置使用哪一个。假设我们在配置文件中没有指定。那默认使用的是基于NIO实现的ClientCnxnSocketNIO

win zookeeper 打不开 zookeeper启动不成功_中间件

2. 创建ClientCnxn实例

ClientCnxn是为客户端管理套接字IO的类,看一下它的构造方法和成员变量:

public class ClientCnxn {

    private static final Logger LOG = LoggerFactory.getLogger(ClientCnxn.class);

    /* ZOOKEEPER-706: If a session has a large number of watches set then
     * attempting to re-establish those watches after a connection loss may
     * fail due to the SetWatches request exceeding the server's configured
     * jute.maxBuffer value. To avoid this we instead split the watch
     * re-establishement across multiple SetWatches calls. This constant
     * controls the size of each call. It is set to 128kB to be conservative
     * with respect to the server's 1MB default for jute.maxBuffer.
     */
    private static final int SET_WATCHES_MAX_LENGTH = 128 * 1024;

    /* predefined xid's values recognized as special by the server */
    // -1 means notification(WATCHER_EVENT)
    public static final int NOTIFICATION_XID = -1;
    // -2 is the xid for pings
    public static final int PING_XID = -2;
    // -4 is the xid for AuthPacket
    public static final int AUTHPACKET_XID = -4;
    // -8 is the xid for setWatch
    public static final int SET_WATCHES_XID = -8;
    /**
     * These are the packets that have been sent and are waiting for a response.
     */
    private final Queue<Packet> pendingQueue = new ArrayDeque<>();
    /**
     * These are the packets that need to be sent.
     * 待发送的消息会先暂存到这个队列中
     */
    private final LinkedBlockingDeque<Packet> outgoingQueue = new LinkedBlockingDeque<Packet>();

    final SendThread sendThread;

    final EventThread eventThread;

        public ClientCnxn(
        String chrootPath,
        HostProvider hostProvider,
        int sessionTimeout,
        ZKClientConfig clientConfig,
        Watcher defaultWatcher,
        ClientCnxnSocket clientCnxnSocket,
        long sessionId,
        byte[] sessionPasswd,
        boolean canBeReadOnly
    ) throws IOException {
        this.chrootPath = chrootPath;
        this.hostProvider = hostProvider;
        this.sessionTimeout = sessionTimeout;
        this.clientConfig = clientConfig;
        this.sessionId = sessionId;
        this.sessionPasswd = sessionPasswd;
        this.readOnly = canBeReadOnly;

        this.watchManager = new ZKWatchManager(
                clientConfig.getBoolean(ZKClientConfig.DISABLE_AUTO_WATCH_RESET),
                defaultWatcher);

        this.connectTimeout = sessionTimeout / hostProvider.size();
        this.readTimeout = sessionTimeout * 2 / 3;
			
        this.sendThread = new SendThread(clientCnxnSocket);
        this.eventThread = new EventThread();
        initRequestTimeout();
    }

可以看到在构造方法里面创建了两个线程,这两个线程都是ClientCnxn内部类

2.1. SendThread

这个线程主要负责与服务端通信,是个典型的生产者-消费者模型。zk产生的消息会先放到outgoingQueue阻塞队列中,然后由sendThread去消费,后面会分析它的sendThread方法。传进去的参数是上面第一步讲到的ClientCnxnSocket实例

2.2. EventThread

该线程维护了一个waitingEvents队列

class EventThread extends ZooKeeperThread {
        private final LinkedBlockingQueue<Object> waitingEvents = new LinkedBlockingQueue<Object>();
}

sendThread中产生的事件会放到EventThread的这个队列中,然后由它去消费这同样是一个生产者-消费者的体现

3. 启动服务

启动服务就只是开启了sendThread和eventThread

cnxn.start();

        public void start() {
        //负责客户端和服务端的通信
        sendThread.start();
        //主要负责在客户端回调注册的Watchers进行通知处理
        eventThread.start();
    }

关于ZooKeeper的客户端启动就分析到这儿,下一篇会分析上面提到的SendThreadWorkThread
Zk客户端(ClientCnxn)中的EventThread和SendThread