ZooKeeper客户端源码分析
1、线程模型
ZK客户端启动时会启动两个线程,一个叫SendThread,另一个叫EventThread。SendThread和服务器打交道负责发包收包。EventThread负责处理事件,比如执行异步请求的回调函数,处理服务器主动推送的通知包(在服务器端注册了watcher就能收到这种通知包)。
ZooKeeper客户端对于用户来说,大部分方法都是线程安全的,也就是说多个线程能同时掉用一个zookeeper客户端实例。在发送请求时,线程会阻塞在它的请求包上,直到收到响应包线程才会被唤醒。
异步请求的回调函数是在EventThread中调用的,Watcher的process方法也是在这个线程中掉用,所以在开发回调函数和process方法时要注意不要阻塞这条线程。
2、状态
状态的定义是org.apache.zookeeper.ZooKeeper.States,这是一个枚举,其值有:CONNECTING, ASSOCIATING, CONNECTED, CONNECTEDREADONLY, CLOSED, AUTH_FAILED, NOT_CONNECTED。ZK的状态变量实际是由ClientCnxn持有,名为state,它是一个volatile变量,这说明它会被多个线程读取。状态的切换工作主要是由SendThread来做。状态的转移图如下:

3、会话
客户端连接上服务器后会收到一个session ID,这个SID和服务器上的watch和临时节点绑定,一旦过期,它绑定的watcher和临时节点会被清除。客户端会不断地发送ping包,确保会话不会过期。
3.1、如何获得SID,为什么要获取SID
ZooKeeper类有个getSessionId方法可以获取SID,至于为什么要获取SID,那是因为应用想用这个SID重连其他服务器,比如这个构造函数就是要用到一个指定的SID:
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd)
3.2、会话如果过期,会产生什么后果?
会话过期会导致EventThread关闭,进而导致ZooKeeper对象将不再可用,如需继续使用ZK服务,则必须要创建一个新的ZooKeeper对象。会话超时是不可恢复错误。
3.3、假设会话超时时间为60秒,服务器一直没有发送任何包给客户端,那么客户端会在经过多少秒后判定服务器失败?
40秒,也就是会话超时时间的2/3。客户端需要留下1/3的时间来切换服务器。
4、ClientCnxn
这个类为客户端管理socket i/o。ClientCnxn管理一个可用服务器列表并且在需要时“透明地”切换服务器。
4.1、pendingQueue和outgoingQueue
ClientCnxn拥有这两条队列,pendingQueue中的包均已经发送给服务器,正在等待响应;outgoingQueue的包还没有发送给服务器,还在等待被发送。
4.2、readOnly变量
此变量的值是在创建ZooKeeper对象时传入的,默认值为false。ZooKeeper类的初始化函数的文档有关于此值的描述:
(added in 3.4) whether the created client is allowed to go to read-only mode in case of partitioning. Read-only mode basically means that if the client can't find any majority servers but there's partitioned server it could reach, it connects to one in read-only mode, i.e. read requests are allowed while write requests are not. It continues seeking for majority in the background.
这个值是在3.4版本引入的,用于设置客户端是否能够在发生网络分区时切换入只读模式。只读模式意味着如果客户端只找到少数(n/2)个服务器时,它会用只读模式连接其中的一个服务器;此时,它的读请求会被接受但写请求则不会。客户端处于只读模式下,为了回到读写模式,会在后台继续寻找大多数服务器。如果此值为false,客户端是不允许连接上只读服务器的。只读服务器的定义如下:
Read Only Mode Server: (disabled by default). ROM allows clients sessions which requested ROM support to connect to the server even when the server might be partitioned from the quorum. In this mode ROM clients can still read values from the ZK service, but will be unable to write values and see changes from other clients. See ZOOKEEPER-784 for more details.
4.3、seenRwServerBefore变量
此值在以下情况会被设置为true:
- 连接时提供session ID(服务器切换),成功后此值被设置为true。
- 连接成功后发现是一个R/W服务器,则设置为true。
- 一旦设置为true,后面就无法修改成为false了。
此值唯一的作用是在建立连接时选择是传送之前分配的Session ID还是0,如果为true,则将之前分配的Session ID传给服务端,否则会发送一个为0的Session ID。
4.4、xid和zxid
xid的作用是保证服务器的响应包顺序和客户的请求包的顺序一致,客户端在发送请求时会递增这个值并赋予请求包;每次收到响应包时都会检查请求队列头部的请求包的xid是否和响应包的xid一致,如果不一致的话那么就断开连接。当xid为负值时,比如-2、-4、-1时,他们分别表示当前包为ping响应包、认证响应包、通知包,这些包不需要匹配请求队列里请求包,换句话说那就是它们对应的请求包并没有加入请求队列中。
zxid由服务器提供,表示服务器端状态的版本。客户端收到响应包后会从包中取出此值并更新本地的lastZxid变量。当客户端遇到网络故障并重新寻找一个可用的服务器时,它会询问服务器的zxid是多少,如果这个zxid小于lastZxid,客户端会跳过此服务器尝试和下一个建立连接。
4.5、变量negotiatedSessionTimeout
客户端和服务器端协商的会话超时时间,单位是ms. 这个时间才是真正的时间, 客户端发出请求中的超时时间并不是最后协商好的时间,因为服务端可能会根据业务在客户端指定的超时时间基础上增加或减少。比方说客户端发送一个10ms的超时时间,这显然不现实,服务器可能会用5秒替换掉这个10毫秒。当服务器发回的negotiatedSessionTimeout小于或等于0,这表示这个会话已经超时了,重连服务失败。
5、事件线程(EventThread)
5.1、事件线程可以被关掉
会话过期后,事件线程就被要求关闭。事件线程一旦关闭就不能再启动,如果还想继续使用ZK服务,应用需要再次创建一个新的ZooKeeper对象,以此来启动事件线程。
6、发送线程(SendThread)
6.1、SendThread的主循环如下:
while (zk客户端还活着) {
if(zk没有连接){
优先尝试连接已经检测到的RW服务器,如果前面一轮迭代没有探测出RW服务器,则尝试连接host provider提供的下一个服务器。
}
if(zk已经连接){
1、检查SASL认证是否成功,如果失败则将状态设置为AUTH_FAILED表示客户端已经死了,后面无法正常使用了。这种情况会退出循环。
2、判断是否读超时?(服务器端一直没有响应)
} else {
判断是否连接超时(一直连不上服务端)
}
如果超时(不管是读超时还是连接超时),就报SessionTimeoutException异常,但是这不会退出循环,因为通过切换服务器,客户端还有机会继续活着。
if(zk已经连接)
尝试发送ping包
if(状态是States.CONNECTEDREADONLY)
通过ping服务器探测RW服务器,这个操作是同步的,它阻塞线程的最长时间为1秒。
发包收包
}
6.2、SendThread的关闭
当客户端状态变成CLOSED或者AUTH_FAILED时,SendThread会退出主循环。主动关闭客户端或者会话超时时客户端状态才会变成CLOSED。
6.3、SendThread主循环发生异常
pengdingQueue和outgoingQueue将被清空,队列里面的包会被触发并让用户会收到异常:当客户端状态为AUTH_FAILED,用户会收到AuthFailedException;当客户端状态为CLOSED,用户会收到SessionExpiredException;其他状态下均报ConnectionLossException。
7、Watcher
7.1、Watcher的原理
Watcher分为客户端Watcher和服务器端Watcher。客户端watcher需要实现接口org.apache.zookeeper.Watcher,并将实现的对象传给在ZooKeeper的一些方法中,比如:
public List<String> getChildren(final String path, Watcher watcher, Stat stat) {}
上面的代码如果被执行,客户端向服务器端发送请求里有个叫watch的字段会被设置成true,这样服务端就会分配一个Watcher(服务端Watcher)来侦听“DataTree”中某个节点的事件。而客户端也会将传入的watcher对象用Watcher管理器管理(ZKWatchManager)起来,需要注意的是只有方法调用成功才会把watch交给ZKWatchManager管理,这一操作的发生在ClientCnxn的finishPacket方法中。
当侦听的事件发生时,比如说子节点变少了,服务器端Watcher会向客户端发送通知,告诉它子节点列表有更新,同时也会删除服务端的Watcher。
客户端收到通知后,会将本地的Watcher管理器的和path关联watcher取出同时移除出管理器,然后并触发它们工作。下面这个方法就是完成这个任务的核心方法:
public Set<Watcher> materialize(Watcher.Event.KeeperState state, Watcher.Event.EventType type, String clientPath)
综上可以看出,不管是客户端还是服务端,Watcher的生命周期都是很短暂的(除了默认Wather),事件一旦发生,它们在两端都会被删除,如果想继续监听节点的事件,客户端必须再次调用上面说到的getChildren,再次再两端分配Watcher才能侦听到接下来的事件。
7.2、哪条线程在使用ZKWatchManager?
尽管ZKWatchManager是在ZooKeeper中定义的,但是使用它的线程却主要是SendThread。

浙公网安备 33010602011771号