Zookeeper源码学习(二):客户端,会话和服务器启动

1. 客户端

客户端类关系图

Zookeeper:客户端的入口。
ClientWatchManager:客户端watcher管理器。
HostProvider:客户端地址列表管理器。
ClientCnxn:客户端连接核心类,包含SendThread和EventThread两个线程。SendThread为I/O线程,主要负责Zookeeper客户端和服务器之间的网络I/O通信;EventThread为事件线程,主要负责对服务端事件进行处理。

ZK客户端服务端交互

上图主要描述了ZK Client和Server端互动的过程:

  1. Client端把Request传递到Zookeeper类中(以Packet形式);
  2. Zookeeper类处理Request并放入OutgoingQueue中(SendThread做的);
  3. Sendthread把发出的Request移到PendingQueue;
  4. 收到回复后,SendThread从PendingQueue中取出Request,并生成Event;
  5. EventThread处理Event并触发WatchManager中的Watcher,调用CallBack。

1. 一次会话的创建过程

ZK一次会话的创建过程

ZK Client和Server端建立连接从Client来说主要分为以下三个阶段:

  1. 初始化阶段:上面介绍的几个主要功能类的实例化;
  2. 会话创建阶段:启动及创建连接;
  3. 响应处理阶段:响应及接收。

初始化阶段

  1. 初始化Zookeeper对象
  2. 设置会话默认Watcher
  3. 构造Zookeeper服务器地址列表管理器:HostProvider
  4. 创建并初始化客户端网络连接器:ClientCnxn
  5. 初始化SendThread和EventThread线程
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,  boolean canBeReadOnly) throws IOException {

    // 把传入的watcher注册到default的watcher中,(留心就可以发现getdata,exists,getchildren提供了参数为
    // boolean类型,参数名为watch的接口,调用这些接口触发的就是default的watcher)
    // 设置默认watcher,之前讲watcher的时候说过
    watchManager.defaultWatcher = watcher;

    // 负责解析配置的server地址串
    // 主要有两个功能:1.加chroot(默认prefix,之前有介绍过);2.读字符串并把多个server地址分开
    ConnectStringParser connectStringParser = new ConnectStringParser(connectString);

    // 根据之前的字符串解析hostname,ip等,并不一定会按照原来的顺序,在构造器中会将顺序打散
    HostProvider hostProvider = new StaticHostProvider(
            connectStringParser.getServerAddresses());

    // 实例化clientCnxn对象
    // ClientCnxn的构造器中有一个非常重要的参数是ClientCnxnSocket,这也是client和server建立连接的功能类
    cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
            hostProvider, sessionTimeout, this, watchManager,
            getClientCnxnSocket(), canBeReadOnly);

    // 启动sendThread和eventThread
    cnxn.start();
}

会话创建阶段
6. 启动SendThread和EventThread线程
7. 获取一个服务器地址:获取地址后,委托给ClientCnxnSocket去创建与Zookeeper服务器之间的TCP连接。
8. 创建TCP连接
9. 构造ConnectRequest请求:8只是从网络TCP层面完成了客户端与服务端之间的socket连接,但远未完成Zookeeper客户端的会话创建。SendThread根据设置,构造出一个ConnectRequest请求,包装成网络I/O层的Packet对象,放入请求发送队列outgoingQueue中去。
10. 发送请求

响应处理阶段
11. 接收服务端响应
12. 处理Response:ClientCnxnSocket将响应反序列化得到ConnectResponse对象,并获取会话sessionId。
13. 连接成功
14. 生成事件:SyncConnected-None:为了让上层应用感知到会话的成功创建,SendThread会生成一个事件,代表客户端与服务端会话创建成功,并将该事件传递给EventThread线程。
15. 查询Watcher
16. 处理事件

2. 服务器地址列表

Zookeeper客户端内部在接收到服务器地址列表后,会将其首先放入一个ConnectStringParser对象封装起来
ConnectStringParser解析器将会对传入的connectString做两个主要处理:解析chrootPath,保存服务器地址列表。

从Paxos到Zookeeper-12

在ConnectStringParser解析器中会対服各器地址做一个筒単的处理,并将服务器地址和相应的端口封装成一个InetSocketAddress对象,以ArrayList形式保存在ConnectStringParser.serverAddresses属性中。然后,经过处理的地址列表会被进一步封装到StaticHostProvider类中。
HostProvider类定义了一个客户端的服务器地址管理器。

StaticHostProvider

  • 解析服务器地址:在StaticHostProvider构造器中,把前面ConnectStringParser的Server地址会解析一遍并生成一个队列,然后就会打散。
  • 获取可用的服务器地址:在StaticHostProvider类中调用next方法会在循环队列中不断获取,特别要注意的是这个循环队列本身就已经是打乱过的。

3. ClientCnxn:网络I/O

Packet是ClientCnxn内部定义的一个对协议层的封装,作为Zookeeper中请求与相应的载体。

消息包Packet

Packet
Packet的createBB方法负责对Packet对象进行序列化,最终生成可用于底层网络传输的ByteBuffer对象。在这个过程中,只会讲里requestHeader,request,和readOnly三个属性进行序列化,其余属性都保存在客户端的上下文中,包括watcher在内的很多变量都没有序列化,这也是watcher轻量特性的保证。

outgoingqueue和pendingQueue
0utgoing队列是一个请求发送队列,专门用于存储那些需要发送到服务端的Packet集合。
Pending队列是为了存储那些已经从客户端发送到服务端的,但是需要等待服务端响应的Packet集合。

ClientCnxnSocket:底层Socket通信层

ZK底层消息传递

请求发送

  • 从outgoingQueue队列中提取出一个可发送的Packet对象,同时生成一个客户端请求序号XID并将其设置到Packet请求头中去,然后将其序列化后进行发送。
  • 请求发送完毕后,会立即将该Packet保存到pendingQueue队列中。

响应阶段
客户端获取到来自服务端的完整响应数据后,根据不同的客户端请求类型,会进行不同的处理。

  • 首先判断客户端状态是否初始化,若未初始化,那说明当前客户端与服务端之间正在进行会话创建,将接收到的ByteBuffer(incomingBuffer)反序列化为ConnectResponse。
  • 如果客户端处于正常的会话周期,且收到的响应是一个事件,那么会反序列化为WatcherEvent,并放到EventThread的等待队列中。
  • 如果是常规的请求,如GetData,Exists等,那么会从pendingQueue中取出一个Packet来处理。Zookeeper客户端首先会通过检验服务端响应包含的XID值来确保请求处理的顺序性。

SendThread

  • 维护Client和Server的会话生命周期,在一定的周期频率内向服务端发送一个PING包来实现心跳检测,实现自动重连。
  • 管理了客户端所有的请求发送和响应接收操作,其将上层客户端API操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调。
  • 将来自服务端的事件传递给EventThread去处理。

EventThread

  • EventThread是客户端ClientCnxn内部的一个事件处理线程,负责客户端的事件处理,并触发客户端注册的Watcher监听。
  • EventThread中的watingEvents队列用于临时存放那些需要被触发的Object,包括客户端注册的Watcher和异步接口中注册的回调器AsyncCallback。同时,EventThread会不断地从watingEvents中取出Object,识别具体类型(Watcher或AsyncCallback),并分别调用process和processResult接口方法来实现对事件的触发和回调。

2. 会话

会话总结

Zookeeper的连接与会话就是客户端通过实例化Zookeeper对象来实现客户端与服务器创建并保持TCP连接的过程。

1. 会话状态

会话状态迁移

2. 会话创建

Session是Zookeeper中的会话实体,代表了一个客户端会话。

从Paxos到Zookeeper-13

SessionID

Zookeeper必须保证sessionId的全局唯一性。
在SessionTracker初识化的时候,会调用initializeNextSession方法生成一个初始化sessionID。

  1. 获取当前时间的毫秒表示
  2. 左移24位:为了防止负数的出现。
  3. 右移8位:无符号右移,避免高位数值对SID的干扰。
  4. 添加机器标识:SID:SID用二进制表示后左移56位。
  5. 将步骤3与步骤4得到的两个64位表示的数值进行“|”操作

从Paxos到Zookeeper-14

SessionTracker

从Paxos到Zookeeper-15

会话创建

会话创建过程

服务端对于客户端的“会话创建”请求的处理,大致可以分为四大步骤,分别是处理ConnectRequest请求,会话创建,处理器链路处理和会话响应。

3. 会话管理

会话管理策略

  • Zookeeper的会话管理主要是通过SessionTracker来负责,其采用了分桶策略(将类似的会话放在同一区块中进行管理)进行管理,以便Zookeeper对会话进行不同区块的隔离处理以及同一区块的统一处理。
  • 所谓的分桶策略,就是按照超时时间把不同的Session放到一起统一管理,对Session超时等判断或处理也是按超时时间为单位进行操作,这样也能大大提高效率,也就是说SessionSets中,每一个SessionSet(实际上是一个SessionImpl的hashset)就是一个桶,而桶的标识就是过期时间。
  • 每一个桶的ExpireTime最终计算结果一定是ExpirationInterval的倍数,而不同的Session会根据他们激活时间的不同放到不同的桶里。

会话激活

客户端会在会话超时时间过期范围内向服务端发送PING请求来保持会话的有效性,我们俗称“心跳检测”。
在创建Session后,每次Client发起请求(PING或者读写请求),Server端都会重新激活Session,而这个过程就是Session的激活,也就是所谓的TouchSession。会话激活的过程使Server可以检测到Client的存活并让Client保持连接状态。

  1. 检验该会话是否已经被关闭
  2. 计算该会话新的超时时间ExpirationTime_New
  3. 定位该会话当前的区块
  4. 迁移会话

从Paxos到Zookeeper-16

会话超时时间检查

  • SessionTracker中有一个单独的线程专门进行会话超时检查,这里我们将其称为“超时检查线程”,其工作机制的核心思路其实非常简单:逐个依次地对会话桶中剩下的会话进行清理。
  • 我们将ExpirationInterval的倍数作为时间点来分布会话,因此,超时检查线程只要在这些指定的时间点上进行检查即可。

4. 会话清理

  1. 标记会话状态为“已关闭”:因为进行会话清理的工作相对来说较为耗时,SessionTracker把isClosing设置为了true,这样在Server收到来自客户端的请求时,发现Session处于Closing状态后便不会处理响应的请求了。
  2. 发起“会话关闭”请求:为了使对该会话的关闭操作在整个服务端集群都生效,Zookeeper使用了提交会话关闭请求的方式,并立即交付给PreRequestProcessor进行处理。
  3. 收集需要清理的临时节点:Zookeeper在内存数据库中会为每个会话都单独保存了一份由该会话维护的所有临时节点集合,因此只需要根据失效Session的id把临时节点列表取到并删除即可。
    1. 节点删除请求,且删除的节点正是上述临时节点列表中的一个。
    2. 临时节点更新(创建,修改)请求,创建目标节点正好是上述临时节点列表中的一个。
    3. 这两类请求共同点是事务处理尚未完成,还没有应用到内存数据库中。对于第一类请求,需要将所有请求对应的数据节点路径从当前临时节点列表中移出,以避免重复删除,对于第二类请求,需要将所有这些请求对应的数据节点路径添加到当前临时节点列表中,以删除这些即将被创建但是尚未保存到内存数据库中的临时节点。
  4. 添加“节点删除”事务变更:将请求放入outstandingChanges中去。
  5. 删除临时节点:FinalRequestProcessor处理器会触发内存数据库,删除该会话对应的所有临时节点。
  6. 移除会话:将会话从SessionTracker中移除。
  7. 关闭NIOServerCnxn

5. 重连

当客户端与服务端之间的网络连接断开时,Zookeeper客户端会自动进行反复的重连,直到最终成功连接上Zookeeper集群中的一台机器。此时,再次连接上服务端的客户端有可能处于以下两种状态之一。

  • Connected:在会话超时时间内连接上了集群中的任意一台Server;
  • Expired:连接上server时已经超时,服务端其实已经进行了会话清理操作,再次连接上的Session会被视为非法会话。

CONNECTION_LOSS
若客户端在setData时出现了CONNECTION_LOSS现象,此时客户端会收到None-Disconnected通知,同时会抛出异常。应用程序需要捕捉异常并且等待Zookeeper客户端自动完成重连,一旦重连成功,那么客户端会收到None-SyncConnected通知,之后就可以重试setData操作。

SESSION_EXPIRED
客户端与服务端断开连接后,重连时间耗时太长,超过了会话超时时间限制后没有成功连上服务器,服务器会进行会话清理,此时,客户端不知道会话已经失效,状态还是DISCONNECTED,如果客户端重新连上了服务器,此时状态为SESSION_EXPIRED,用于需要重新实例化Zookeeper对象,并且看应用的复杂情况,重新恢复临时数据。

SESSION_MOVED
客户端会话从一台服务器转移到另一台服务器,即客户端与服务端S1断开连接后,重连上了服务端S2,此时会话就从S1转移到了S2。当多个客户端使用相同的sessionId/sessionPasswd创建会话时,会收到SessionMovedException异常。因为一旦有第二个客户端连接上了服务端,就被认为是会话转移了。

3. 服务器启动

服务器启动总结

Zookeeper服务端的整体架构

服务器整体概览图

1. 单机版服务器启动

Zookeeper服务器的启动,大体可以分为以下五个主要步骤:配置文件解析,初始化数据管理器,初始化网络I/O管理器,数据恢复和对外服务。

单机服务器启动流程

预启动

  1. 统一由QuorumPeerMain作为启动类:无论单机或集群,在zkServer.cmd和zkServer.sh中都配置了QuorumPeerMain作为启动入口类。
  2. 解析zoo.cfg:用过ZK的同学都知道zoo.cfg是用户配置的zookeeper核心配置文件,ticktime,dataDir,dataLogDir,集群ip:port等都配置在其中。在实例化QuorumPeerMain对象后会去解析zoo.cfg文件。
  3. 创建并启动历史文件清理器DatadirCleanupManager:DatadirCleanupManager的start方法负责自动清理历史的快照和事务日志。
  4. 判断启动模式:根据地址列表判断。
  5. 再次解析zoo.cfg:这里之所以还要进行一次解析是因为这里是调用的Zookeeper Server的main方法,无法把原来解析的参数传入。而且配置文件比较小,解析并不是特别耗资源,可以接受。
  6. 创建服务器实例ZookeeperServer:Zookeeper Server是Server端的核心类,在启动时会创建Zookeeper Server的一个实例。

初始化

  1. 创建服务器统计器ServerStats:在Zookeeper Server的构造函数内实例化了Server的统计器ServerStats。

从Paxos到Zookeeper-17

  1. 创建Zookeeper数据管理器FileTxnSnapLog:FileTxnSnapLog是Zookeeper上层服务器和底层数据存储之间的对接层,提供了一系列操作数据文件的接口,如事务日志文件和快照数据文件。Zookeeper根据zoo.cfg文件中解析出的快照数据目录dataDir和事务日志目录dataLogDir来创建FileTxnSnapLog。
  2. 设置服务器tickTime和会话超时时间限制
  3. 创建ServerCnxnFactory:通过配置系统属性zookeper.serverCnxnFactory来指定使用Zookeeper自己实现的NIO还是使用Netty框架作为Zookeeper服务端网络连接工厂。
  4. 初始化ServerCnxnFactory:Zookeeper首先会初始化一个Thread,作为ServerCnxnFactory的主线程,然后再初始化NIO服务器。
  5. 启动ServerCnxnFactory主线程:虽然这里Zookeeper的NIO服务器已经对外开放端口,但是此时Zookeeper服务器是无法正常处理客户端请求的。
public void runFromConfig(ServerConfig config) throws IOException {
    FileTxnSnapLog txnLog = null;
    try {
        // 1. 创建服务器统计器ServerStats
        final ZooKeeperServer zkServer = new ZooKeeperServer();

        final CountDownLatch shutdownLatch = new CountDownLatch(1);
        zkServer.registerServerShutdownHandler(new ZooKeeperServerShutdownHandler(shutdownLatch));

        // 2. 创建数据管理器FileTxnSnapLog
        txnLog = new FileTxnSnapLog(new File(config.dataLogDir), new File(config.dataDir));
        zkServer.setTxnLogFactory(txnLog);

        // 3. 设置服务器tickTime和会话超时时间限制
        zkServer.setTickTime(config.tickTime);
        zkServer.setMinSessionTimeout(config.minSessionTimeout);
        zkServer.setMaxSessionTimeout(config.maxSessionTimeout);

        // 4. 创建ServerCnxnFactory
        cnxnFactory = ServerCnxnFactory.createFactory();
        // 初始化ServerCnxnFactory
        cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns());
        // 启动ServerCnxnFactory
        cnxnFactory.startup(zkServer);

        shutdownLatch.await();
        shutdown();

        cnxnFactory.join();
        if (zkServer.canShutdown()) {
            zkServer.shutdown(true);
        }
    } 
}
public class NIOServerCnxnFactory extends ServerCnxnFactory implements Runnable {
Thread thread;
@Override
public void configure(InetSocketAddress addr, int maxcc) throws IOException {
    configureSaslLogin();

    thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr);
    thread.setDaemon(true);
    maxClientCnxns = maxcc;
    this.ss = ServerSocketChannel.open();
    ss.socket().setReuseAddress(true);
    LOG.info("binding to port " + addr);
    ss.socket().bind(addr);
    ss.configureBlocking(false);
    ss.register(selector, SelectionKey.OP_ACCEPT);
}
  1. 恢复本地数据:每次zk启动时,都需要从本地块找数据文件和事务日志文件中进行数据恢复。
@Override
public void startup(ZooKeeperServer zks) throws IOException,
        InterruptedException {
    // 启动主线程
    start();
    // 设置server端对象
    setZooKeeperServer(zks);
    // 恢复数据等
    zks.startdata();
    zks.startup();
}
  1. 创建并启动会话管理器:所谓的会话管理器就是前面说的SessionTracker。
  2. 初始化Zookeeper的请求处理链:Zookeeper请求处理方式基于责任链模式,也就是说在Server端有多个请求处理器一次来处理一个客户端请求。在服务器启动的时候,会将这些处理器串联起来形成一个处理链。
  3. 注册JMX服务:ZK服务器的信息会以JXM的方式暴露给外部。

从Paxos到Zookeeper-18

public synchronized void startup() {
    if (sessionTracker == null) {
        // 创建session管理器
        createSessionTracker();
    }
    // 启动session管理器
    startSessionTracker();
    // 初始化zookeeper请求处理链
    setupRequestProcessors();

    // 注册JMX服务
    registerJMX();

    setState(State.RUNNING);
    notifyAll();
}
  1. 注册ZK服务器实例:

从Paxos到Zookeeper-19

2. 集群版服务器启动

集群版服务器启动流程

预启动

  1. 统一由QuorumPeerMain作为启动类
  2. 解析配置文件zoo.cfg
  3. 创建并启动历史文件清理器DatadirCleanupManager
  4. 判断当前是集群模式还是单机模式的启动

初始化

  1. 创建ServerCnxnFactory
  2. 初始化ServerCnxnFactory
  3. 创建Zookeeper数据管理器FileTxnSnapLog
  4. 创建QuorumPeer实例:Quorum是集群模式下特有的对象,是Zookeeper服务器实例(ZookeeperServer)的托管者,从集群层面看,QuorumPeer代表了Zookeeper集群中的一台机器。
  5. 创建内存数据库ZKDatabase
  6. 初始化QuorumPeer
  7. 恢复本地数据
  8. 启动ServerCnxnFactory主线程
public void runFromConfig(QuorumPeerConfig config) throws IOException {

  try {
      // 创建ServerCnxnFactory
      ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
      // 初始化ServerCnxnFactory
      cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns());

      // 创建并初始化QuorumPeer
      quorumPeer = getQuorumPeer();

      // 初始化的数据基本都是从zoo.cfg中读到的值,方法名也叫runFromConfig
      quorumPeer.setQuorumPeers(config.getServers());
      // 创建数据管理器FileTxnSnapLog
      quorumPeer.setTxnFactory(new FileTxnSnapLog(
              new File(config.getDataLogDir()),
              new File(config.getDataDir())));

      quorumPeer.initialize();

    // 恢复本地数据
    // 启动ServerCnxnFactory线程
      quorumPeer.start();
      quorumPeer.join();
  } 
}
@Override
public synchronized void start() {
    // 恢复本地数据
    loadDataBase();
    // 启动ServerCnxnFactory线程
    cnxnFactory.start();
    // 启动leader选举
    startLeaderElection();
    // 启动QuorumPeer线程
    super.start();
}

Leader选举

  1. 初始化Leader选举
  2. 注册JMX服务
  3. 检测当前服务器状态
  4. Leader选举

Leader和Follower启动期交互过程

从Paxos到Zookeeper-20

  1. 创建Leader服务器和Follower服务器
  2. Leader服务器启动Follower接收器LearnerCnxAcceptor:LearnerCnxAcceptor接收器用于负责接收所有非Leader服务器的连接请求。
  3. Learner服务器开始和Leader建立连接
  4. Leader服务器创建LearnerHandler:每个LearnerHandler实例负责Leader和Learner服务器之间几乎所有的消息通信和数据同步。
  5. 向Leader注册:将Learner基本信息发送给Leader服务器,包括当前服务器SID和服务器处理的最新ZXID。
  6. Leader解析Learner信息,计算新的epoch:如果该Learner的epoch更大的话,那么就更新Leader的epoch。等到过半的Learner已经向Leader注册后,就可以确定集群的epoch了。
  7. 发送Leader状态
  8. Learner发送ACK消息
  9. 数据同步
  10. 启动Leader和Learner服务器

Leader和Follower启动

  1. 创建并启动会话管理器
  2. 初始化Zookeeper的请求处理链
  3. 注册JMX服务

最后

大家可以关注我的微信公众号一起学习进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值