会话创建过程中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,用于存放所有等待被客户端处理的事件。
会话创建阶段
6.启动SendThread和EventThread
SendThread首先会判断当前客户端的状态,进行一系列清理工作,为客户端发送“会话创建”请求做准备。
7.获取一个服务器地址
在开始创建TCP连接之前,SendThread首先需要获取一个ZooKeeper服务器的目标地址,这通常是从HostProvider中随机获取出一个地址,然后委托给ClientCnxnSocket去创建与ZooKeeper服务器之间的TCP连接。
8.创建TCP连接
获取到一个服务器地址后,ClientCnxnSocket负责和服务器创建一个TCP长连接。
9.构造ConnectRequest请求
SendThread负责根据当前客户端的实际设置,构造出一个ConnectRequest请求,该请求代表了客户端试图与服务器创建一个会话。同时,ZooKeeper客户端还会进一步将该请求包装成网络I/O层的Packet对象,放入请求发送队列outgoingQueue中去。
10.发送请求
当客户端请求准备完毕后,就可以开始向服务端发送请求了。ClientCnxnSocket负责从outgoingQueue中取出一个待发送的Packet对象,将其序列化成ByteBuffer后,向服务端进行发送。
11.接收服务端响应
ClientCnxnSocket接收到服务端的响应后,会首先判断当前的客户端状态是否是“已初始化”,如果尚未完成初始化,那么就认为该响应一定是会话创建请求的享元,直接交由readConnectResult方法来处理该响应。
12.处理Response
ClientCnxnSocket会对接收到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到ZooKeeper服务端分配的会话sessionId。
13.连接成功
连接成功后,一方面需要通知SendThread线程,进一步对客户端进行会话参数的设置,包括readTimeout和connectTimeout等,并更新客户端状态。另一方面,需要通知地址管理器HostProvider当前成功连接的服务器地址。
14.生成事件:SyncConnected-None
为了能够让上层应用感知到会话的成功创建,SendThread会生成一个事件SyncConnected-None,代表客户端与服务器会话创建成功,并将该事件传递给EventThread线程。
15.查询Watcher
EventThread线程收到事件后,会从ClientWatchManager管理器中查询出对应的Watcher,针对SyncConnected-None事件,那么就直接找出步骤2中存储的默认Watcher,然后将其放到EventThread的waitingEvents队列中去。
16.处理事件
EventThread不断地从waitingEvents队列中取出待处理的Watcher对象,然后直接调用该对象的process接口方法,以达到触发Watcher的目的。
会话创建过程中ZooKeeper服务端的工作原理
Session
Session是ZooKeeper中的会话实体,代表了一个客户端会话。包含以下4个基本属性。会话ID,用来唯一标识一个会话,每次客户端创建新会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionID
属性 | 说明 |
---|---|
sessionID | 会话ID,用来唯一标识一个会话 |
TimeOut | 会话超时时间 |
TickTime | 下次会话超时时间点 |
isClosing | 用于标记一个会话是否已经被关闭 |
SessionID
sessionID用来唯一标识一个会话,因此ZooKeeper必须保证sessionID的全局唯一性。接下来看看sessionID是如何生成的。
在SessionTracker初始化的时候,会调用initializeNextSession方法来生成一个初始化的sessionID,之后在ZooKeeper的正常运行过程中,会在该sessionID的基础上为每个会话进行分配,其初始化算法如下:
public static long initializeNextSession(long id){
long nextSid = 0;
nextSid = (System.currentTimeMillis() << 24) >>》 8;
nextSid = nextSid | (id << 56);
return nextSid;
}
从上面的代码可以看出sessionID的生成大致可以分为下面5个步骤。
1.获取当前时间的毫秒表示。
2.将获取到的当前时间的毫秒值左移24位
3.再将其进行无符号右移8位
4.添加机器标识SID。参数id就是当前zookeeper服务器的SID值,也就是配置在myid文件中的值,其高位56位都是0,因而需要右移56位。
5.将步骤3和步骤4的数值进行“|”操作
可以将这个算法概况为:高8位确定了所在机器,后56位使用当前时间的毫秒表示进行随机。
SessionTracker
SessionTracker是ZooKeeper服务端的会话管理器,负责会话的创建、管理和清理等工作。每一个会话在SessionTracker内部都保留了三份,如下表所示:
名称 | 说明 |
---|---|
sessionsById | 用于根据sessionID来管理Session实体,一个HashMap<Long, SessionImpl>类型的数据结构 |
sessionsWithTimeout | 用于根据sessionID来管理会话的超时时间,一个ConcurrentHashMap<Long, Integer>类型的数据结构 |
sessionSets | 用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时检查,一个HashMap<Long, SessionSet>类型的数据结构 |
创建连接
服务器对于客户端的“会话创建”请求的处理,可以分为四个步骤:处理ConnectRequest请求、会话创建、处理器链路处理、会话响应。
1.由NIOServerCnxn来负责接收来自客户端的“会话创建”请求,并反序列化出ConnectRequest请求,然后根据ZooKeeper服务端的配置完成会话超时时间的协商。
2.SessionTracker会为该会话分配一个sessionID,并将其注册到sessionsById和sessionsWithTimeout中去,同时进行会话的激活。
3.该“会话请求”还会在ZooKeeper服务端的各个请求处理器之间进行顺序流转,最终完成会话的创建。
分桶策略
会话管理主要由SessionTracker负责,采用分桶策略来管理。分桶策略就是指将类似的会话放在同一区块中进行管理,以便于ZooKeeper对会话进行不同区块的隔离处理以及同一区块的统一处理。如下图所示。
ZooKeeper将所有的会话都分配在了不同的区块中,分配的原则是每个会话的“下次超时时间点”(ExpirationTime),指该会话最近一次可能超时的时间点,其计算方式如下:
ExpirationTime = CurrentTime + SessionTimeout
在ZooKeeper的实际实现中,还做了一个处理。ZooKeeper的Leader服务器在运行期间会定时地进行会话超时检查,其时间间隔是ExpirationInterval,单位是毫秒,默认值是tickTime的值,即默认情况下,每隔2000毫秒会进行一次会话超时检查。完整的ExpirationTime的计算方式如下:
ExpirationTime_ = CurrentTime + SessionTimeout
ExpirationTime = (ExpirationTime_/ExpirationInterval+1) × ExpirationInterval