聊聊Zookeeper技术内幕之客户端与SetData请求处理

发布时间 2023-07-10 21:26:13作者: 又见阿郎

从客户端会话创建到网络连接、请求处理,简单的叙述下流程与逻辑

客户端

客户端是开发人员使用ZooKeeper最主要的途径,ZooKeeper的客户端主要由以下几个核心组件组成。

  • ZooKeeper实例:客户端的入口。
  • ClientWatchManager:客户端Watcher管理器。
  • HostProvider:客户端地址列表管理器(管理服务器地址信息)。
  • ClientCnxn:客户端核心线程,其内部又包含两个线程,即SendThread和EventThread。前者是一个I/O线程,主要负责ZooKeeper客户端和服务端之间的网络I/O通信;后者是一个事件线程,主要负责对服务端事件进行处理。

对如上的组件,需要有一个初始化的过程,而这就是Zookeeper构造函数执行的结果。

在初始化阶段中,会做如下的事情:

  1. 初始化Zookeeper对象。
    通过调用ZooKeeper的构造方法来实例化一个ZooKeeper对象,在初始化过程中,会创建一个客户端的Watcher管理器:ClientWatchManager。

  2. 设置会话默认Watcher
    如果在构造方法中传入了一个Watcher对象,那么客户端会将这个对象作为默认Watcher保存在ClientWatchManager中。

  3. 构造Zookeeper服务器地址列表管理器:HostProvider。
    对于构造方法中传入的服务器地址,客户端会将其存放在服务器地址列表管理器HostProvider中。

  4. 创建并初始化客户端网络连接器:ClientCnxn。
    ZooKeeper客户端首先会创建一个网络连接器ClientCnxn,用来管理客户端与服务器的网络交互。另外,客户端在创建ClientCnxn的同时,还会初始化客户端两个核心队列outgoingQueue和pendingQueue,分别作为客户端的请求发送队列和服务端响应的等待队列。

  5. 初始化SendThread和EventThread。
    客户端会创建两个核心网络线程SendThread和EventThread,前者用于管理客户端和服务端之间的所有网络I/O,后者则用于进行客户端的事件处理。同时,客户端还会将ClientCnxnSocket分配给SendThread作为底层网络I/O处理器,并初始化EventThread的待处理事件队列waitingEvents,用于存放所有等待被客户端处理的事件。

Zookeeper的构造函数有蛮多,但最后都是调用此构造函数方法,其参数列表也正是对应着上述的功能模块的初始化。


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();
        this.hostProvider = hostProvider;
        ConnectStringParser connectStringParser = new ConnectStringParser(connectString);

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

初始化过程的泳道图如下

image

请求处理(SetData请求)

在Zookeeper中,更新类的操作都属于事务操作,即这个操作需要事务保障的。

服务端对于SetData请求的处理,大体可以分为4大步骤,分别是请求的预处理、事务处理、事务应用和请求响应

流程逻辑大概如下所示:

image

预处理

  1. I/O层接收来自客户端的请求。

  2. 判断是否是客户端“会话创建”请求。ZooKeeper对于每一个客户端请求,都会检查是否是“会话创建”请求。对于SetData请求,因为此时已经完成了会话创建,因此按照正常的事务请求进行处理。

  3. 将请求交给ZooKeeper的PrepRequestProcessor处理器进行处理。

  4. 创建请求事务头。

  5. 会话检查。客户端会话检查是指检查该会话是否有效,即是否已经超时。如果该会话已经超时,那么服务端就会向客户端抛出SessionExpiredException异常。

  6. 反序列化请求,并创建ChangeRecord记录。面对客户端请求,ZooKeeper首先会将其进行反序列化并生成特定的SetDataRequest请求。SetDataRequest请求中通常包含了数据节点路径path、更新的数据内容data和期望的数据节点版本version。同时,根据请求中对应的path,ZooKeeper会生成一个ChangeRecord记录,并放入outstandingChanges队列中。outstandingChanges队列中存放了当前ZooKeeper服务器正在进行处理的事务请求,以便ZooKeeper在处理后续请求的过程中需要针对之前的客户端请求的相关处理,例如对于“会话关闭”请求来说,其需要根据当前正在处理的事务请求来收集需要清理的临时节点。

  7. ACL检查。由于当前请求是数据更新请求,因此ZooKeeper需要检查该客户端是否具有数据更新的权限。如果没有权限,那么会抛出NoAuthException异常。

  8. 数据版本检查。

  9. 创建请求事务体SetDataTxn。

  10. 保存事务操作到outstandingChanges队列中去。

事务处理

对于事务请求,ZooKeeper服务端都会发起事务处理流程。无论对于会话创建请求还是SetData请求,或是其他事务请求,事务处理流程都是一致的,都是由ProposalRequestProcessor处理器发起,通过Sync、Proposal和Commit三个子流程相互协作完成的。

事务应用

  1. 交付给FinalRequestProcessor处理器。

  2. 事务应用。ZooKeeper会将请求事务头和事务体直接交给内存数据库ZKDatabase进行事务应用,同时返回ProcessTxnResult对象,包含了数据节点内容更新后的stat。

  3. 将事务请求放入队列:commitProposal。

请求响应

  1. 统计处理。

  2. 创建响应体SetDataResponse。SetDataResponse是一个数据更新成功后的响应,主要包含了当前数据节点的最新状态stat。

  3. 创建响应头。

  4. 序列化响应。

  5. I/O层发送响应给客户端。

参考

《从Paxos到Zookeeper:分布式一致性原理与实践 》