本文为书籍《从Paxos到Zookeeper 分布式一致性原理与实践》倪超著_北京:电子工业出版社的读书笔记,这本书还是蛮值得推荐的。
ZK的连接与会话就是客户端通过实例化ZooKeeper对象来实现客户端与服务器创建并保持TCP连接的过程。
一、会话状态
会话状态有:CONNECTING、CONNECTED、RECONNECTING、RECONNECTED和CLOSE
一旦客户端开始创建ZooKeeper对象,那么客户端状态就会变成CONNECTING,同时客户端开始从配置的服务器地址列表中逐个选取IP地址来尝试进行网络连接,直到成功连接上服务器,然后将客户端状态变成为CONNECTED。
如果会话出现超时、权限检查失败、或是客户端主动退出程序等情况,那么客户端的状态就会直接变成CLOSE
二、会话创建
1.客户端一次会话的创建过程
客户端主要分为3个阶段
1.初始化阶段
2.会话创建阶段
3.响应处理阶段
2.1.1初始化阶段
1.初始化Zookeeper对象
通过调用Zookeeper的构造方法来实例化一个Zookeeper对象,在初始化过程中会创建一个客户端的Watcher管理器:ClientWatchManager
2.设置会话默认Watcher
如果在构造方法中传入了Watcher对象,那么客户端会将这个对象作为默认Watcher保存在ClientWatchManager中
3.构造Zookeeper服务器地址列表管理器:HostProvider
对于构造方法中传入的服务器地址,客户端会将其存放在服务器地址列表管理器HostProvider中
4.创建并初始化客户端网络连接器:ClientCnxn
Zookeeper客户端首先会创建一个网络连接器ClientCnxn,用来管理客户端与服务器的网络交互。
另外,客户端在创建ClientCnxn的同时,还会初始化客户端的两个核心队列outgoingQueue和pendingQueue,分别作为客户端的请求发送队列和服务端响应的等待队列。
在这一步客户端还会创建ClientCnxnSocket处理器,它是ClientCnxn连接器的底层I/O处理器。
5.初始化SendThread和EventThread
客户端会创建两个核心网络线程SendThread和EventThread,前者用于管理客户端和服务端之间的所有网络I/O,后者则用于进行客户端的事件处理。
同时,客户端还会将ClientCnxnSocket分配给SendThread作为底层网络I/O处理器,并初始化EventThread的待处理事件队列waitingEvents用于存放所有等待被客户端处理的事件。
2.1.2会话创建阶段
6.启动SendThread和EventThread
SendThread会先判断当前客户端的状态,进行一系列清理工作,为客户端发送“会话创建”请求做准备。
7.获取一个服务器地址
在开始创建TCP连接之前,SendThread首先需要获取一个Zookeeper服务器的目标地址,这通常是从HostProvider中随机选一个地址,然后委托给ClientCnxnSocket去创建与Zookeeper服务器之间的TCP连接。
8.创建TCP连接
获取到一个服务器地址后,ClientCnxnSocket负责和服务器创建一个TCP长连接。
9.构造ConnectRequest请求
在TCP连接创建完毕后,SendThread会负责根据当前客户端的实际设置,构造出一个ConnectRequest请求,该请求代表了客户端试图与服务器创建一个会话。
同时,Zookeeper客户端还会进一步将该请求包装成网络I/O层的Packet对象,放入请求发送队列outgoingQueue中去。
10.请求发送
当客户端请求准备完毕后,就开始可以向服务器发送请求了。
ClientCnxnSocket负责从outgoingQueue中取出一个待发送的Packet对象,将其序列化成ByteBuffer后,向服务器发送。
2.1.3相应处理阶段
11. 接受服务端响应
ClientCnxnSocket接收到服务端的响应后,会首先判断当前的客户端状态是否“已初始化”,如果尚未完成初始化,那么就认为响应一定是会话创建请求的响应,直接交由readConnectResult方法来处理该响应。
12. 处理Response
ClientCnxnSocket会对接收到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到Zookeeper服务端分配的会话sessionId
13. 连接成功
连接成功后,一方面需要通知SendThread线程,进一步对客户端进行会话参数的设置,包括readTimeout和connectTimeout等,并更新客户端状态。
另一方面,需要通知地址管理器HostProvider当前成功连接的服务器地址。
14 生成事件:SyncConnected-None
为了能够让上层应用感知到会话的成功创建,SendThread会生成一个事件SyncConnected-None,代表客户端与服务器会话创建成功,并将该事件传递给EventThread线程。
15. 查询Watcher
EventThread线程收到事件后,会从ClientWatcherManager管理器中查询出对应的Watcher,针对SyncConnected-None事件,那么就直接找出步骤2中存储的默认Watcher,然后将其放到EventThread的waitingEvents队列中去。
16. 处理事件
EventThread不断的从waitingEvents队列中取出待处理的Watcher对象,然后直接调用该对象的process接口方法,以达到触发Watcher的目的。
2.服务端会话创建的工作原理
2.2.1 Session属性
1.sessionID:会话ID
用来唯一标识一个会话,每次客户端创建新会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionID。
2.TimeOut:会话超时时间
客户端在构造ZooKeeper实例的时候,会配置一个sessionTimeout参数用于指定会话的超时时间。
ZooKeeper客户端向服务端发送这个超时时间后,服务器会根据自己的超时时间限制最终确定会话的超时时间。
3.TickTime:下一次会话超时时间点
为了便于ZooKeeper对会话实行“分桶策略”管理,同时也是为了高效低耗地实现会话的超时检查与清理,ZooKeeper会为每一个会话标记下一个下次会好超时时间点。
TickTime是一个13位的long型数据,其值的计算方式会在会话管理中写到。
4.isClosing:该属性用于标记一个会话是否已经被关闭
当服务端检测到一个会话已经超时失效的时候,会将该会话的isClosing属性标记为“已关闭”,这样就是能确保不会再处理来自该会话的新请求了。
2.2.2 SessionTracker
SessionTracker是ZooKeeper服务端的会话管理器,负责会话的创建、管理、清理等工作。
每一个会话在SessionTracker内部都保留了三分,具体如下:
sessionsById:这是一个hashmap<Long,SessionImpl>类型的数据结构,用于根据sessionID来管理session实体
sessionsWithTimeout:这是一个concurrentHashMap<Long,Integer>类型的数据结构,用于根据sessionID来管理会话的超时时间。该数据结构和ZooKeeper内存数据库相连通,会被定期持久化到快照文件中去。
sessionSets:这是一个hashmap<Long,SessionSet>类型的数据结构,用于根据下次会话超时时间点来归档会话,便于进行会话管理和超时检查。
2.2.3 创建连接
另外整理
三、会话管理
1.分桶策略
ZooKeeper的会话管理主要是由SessionTracker负责。
分桶策略是指将类似的会话放在同一区块中进行管理,以便ZooKeeper对会话进行不同区块的隔离处理以及同一区块的统一处理。
图中ExpirationTime是指该会话最近一次可能超时的时间点,对于一个新创建的会话而言,其会话创建完毕后,ZooKeeper就会为其计算ExpirationTime.
ZooKeeper的leader服务器在运行期间会定时地进行会话超时检查,其时间间隔是ExpirationInterval,单位是毫秒,默认值是tickTime的值,即默认情况下,每个2000毫秒进行一次会话超时检查。
ExpirationTime的计算公式:
ExpirationTime_ = CurrentTime + SessionTimeout
ExpirationTime = ( ExpirationTime_ / ExpirationInterval + 1 ) * ExpirationInterval
也就是说,上图中横坐标ExpirationTime的值始终是ExpirationInterval的整数倍。
2.会话激活
客户端会在会话超时时间过期范围内向服务器发送PING请求来保持会话的有效性。
有2中方式触发请求
1.客户端向服务端发送请求,包括读或者写请求,那么就会触发一次会话激活。
2.如果客户端发现在 sessionTimeout / 3 时间内没有向服务端发送过任何请求,那么就会主动发起一次PING请求来激活会话。
3.会话超时检查
SessionTracker中有一个单独的线程专门进行会话超时检擦,它逐个依次地对会话桶中剩下的会话进行清理。
如果一个会话被激活,那么ZooKeeper会将其从上一个会话桶迁移到下一个会话桶中,例如图中session.n这个会话由于触发了会话激活,被从expirationTime 1桶迁移到了 expirationTime n 桶中去了。于实,expirationTime 1中留下的所有会话都是尚未激活的。超时检查线程的任务就是定时检查出这个会话桶中所有剩下的未被迁移的会话。
四、会话清理
当SessionTracker会话超时检查线程整理出一些已经过期的会话后,就要开始进行会话清理了。
会话清理步骤如下:
1.标识会话状态为”已关闭“
2.发起”会话关闭“请求
3.收集需要清理的临时节点
4.添加”节点删除“事务变更
5.删除临时节点
6.移除会话
7.关闭NIOServerCnxn
五、重连
当客户端和服务器之间的网络连接断开时,ZooKeeper客户端会自动进行反复的重连,直到最终成功连接上ZooKeeper集群中的一台机器。在这种情况下,再次连接上服务端的客户端有可能会处于以下两种状态之一:
CONNECTED:如果在会话超时时间内重新连上了ZooKeeper集群中的一台机器,那么被认为重连成功。
EXPIRED:如果在会话超时时间以外重新连上了ZooKeeper集群中的一台机器,那么服务端其实已经对该会话进行了会话清理操作,因此在此连接上的会话会被是为非法会话
当客户端与服务端之间的连接断开后,用户在客户端可能会看到异常类型以及该怎么正确的处理这些异常:
CONNECTION_LOSS(连接断开)
在这种情况下,客户端会自从地址列表种重新逐个选取新的地址并尝试进行重新连接,直到最终成功连接上服务器。
举个例子:假设某应用在使用ZooKeeper客户端进行setData操作的时候,正好出现了CONNECTION_LOSS现象,那么客户端会立即收到事件None-Disconnected通知,同时会抛出异常org.apache.zookeeper.KeeperException$ConnectionLossException.
在这种情况下,我们的应用需要做的事情就是捕获住ConnectionLossException,然后等待客户端自动完成重连。
一旦客户端重连成功了,那么客户端就会收到事件None-SyncConnected通知,之后就可以重试刚刚出错的setData操作
SESSION_EXPIRED (会话过期)
这种情况下用户需要重新实例化一个ZooKeeper对象,并且看应用的复杂情况,重新恢复临时数据。
SESSION_MOVED(会话转移)
因为会话过期重连,导致会话从ZK服务器1转移到了ZK服务器2上。
ZK服务器从3.2.0版本之后提出了会话转移的概念。在处理客户端请求的时候,会首先检查会话的所有者(Owner),如果客户端请求的会话Owner不是当前服务器的话,那么就会直接抛出SessionMovedException异常。