1. Zookeeper技术内幕
1.1. 会话
1.1.1. sessionID生成
4个基本属性:
sessionlD:会话ID,用来唯一标识一个会话,每次客户端创建新会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionID。
TimeOut:会话超时间。客户端在构造ZooKeeper实例的时候,会配置一个 sessionTimeout参数用于指定会话的超时时间。ZooKeeper客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定会话的超时时间。
TickTime:下次会话超时时间点。为了便于ZooKeeper对会话实行“分桶策略”管理,同时也是为了高效低耗地实现会话的超时检测与清理,ZooKeeper会为每个会话标记一个下次会话超时时间点。TickTime是一个13位的long型数据,其值接近于当前时间加上TimeOut,但不完全相等。
isClosing:该属性用于标记一个会话是否已经被关闭。通常当服务端检测到一个会话巳经超时失效的时候,会将该会话的isClosing属性标记为“已关闭”,这样就能确保不再处理来自该会话的新请求了。
SessionTrackerImpl初始化的时候,会调用initializeNextSession方法来生成一个初始化的sessionid。
public static long initializeNextSession(long id) {
long nextSid;
nextSid = (Time.currentElapsedTime() << 24) >>> 8;
nextSid = nextSid | (id <<56);
return nextSid;
}
上面这个方法就是zookeeper初始化sessionid的算法,答题分为以下5个步骤:
1. 获取当前时间的毫秒标识。
我们假设System.currentTimeMillis()取出的值是1380895182327,其64位二进制表示是:
0000000000000000000000010100000110000011110001000100110111110111
其中明影部分表示高24位,下划线部分表示低40位。
2. 左移24位
将步骤1中的数值左移24位,得到如下二进制表示的数值:
0100000110000011110001000100110111110111000000000000000000000000
从上面这个数值中,我们可以看到之前的髙24位已经被移出,同时低24位全部使用0进行了补齐。
3. 无符号右移8位
在将步骤2中的数值无符号右移8位,得到如下二进制表示的数值:
0000000001000001100000111100010001001101111101110000000000000000
从上面这个数值中,我们可以看到,髙位添加了 8个0。
4. 添加机器标识: SID
在initializeNextSession方法中,出现了一个id变量,该变量就是当前 ZooKeeper服务器的S1D值。该值通常是一个整数,例如1、2或3,这里我们为了便于表述,假设该值为2。整数2的64位二进制表示如下:
0000000000000000000000000000000000000000000000000000000000000010
可以发现其高56位都是0,将其左移56位后,可以得到如下二进制表示的数值:
0000001000000000000000000000000000000000000000000000000000000000
5. 将步骤3和步骤4得到的两个64位表示的数值进行“丨”操作
0000000001000001100000111100010001001101111101110000000000000000
|
0000001000000000000000000000000000000000000000000000000000000000
可以得到如下数值:
0000001001000001100000111100010001001101111101110000000000000000
通过以上的步骤,就完成一个sessionid的初始化,因为ID是一个机器编号,比如1、2或3,因此经过上述算法计算之后,我们就可以得到一个单机唯一的序列号。高位标识所在的机器,后56位使用当前时间的毫秒数标识进行随机。
接下来,我们从几个算法细节上再来看看sessionid的初始化算法。
为什么是左移24位?
以上述步骤1中使用的当前时间为例:
0000000000000000000000010100000110000011110001000100110111110111
左移24位后是:
0100000110000011110001000100110111110111000000000000000000000000
我们发现左移24位后,将高位的1移除了,剩下的最高位是0,这样做的目的就是为了防止负数的出现。如果是左移23位之后,是一个负数,再次基础上即时进行了右移操作,期数值最高位依然是1,因此之后就无法清晰地从sessionid中分辨出SID的值。
1.1.2. 分桶策略
Zookeeper采用了一种特殊的会话管理方式,称之为“分桶策略”,是指将类似的会话放在同一区块中进行管理,以便于zookeeper对会话进行不同区块的格里处理以及同一区块的统一处理,
在上图中,我们可以看到,ZooKeeper将所有的会话都分配在了不同的区块之中,分配的原则是每个会话的“下次超时时间点”(ExpirationTime)。ExpirationTime是指该会话最近一次可能超时的时间点,对于一个新创建的会话而言,其会话创建完毕后, ZooKeeper就会为其计算ExpirationTime,计算方式如下:
ExpirationTime = CurrentTime + SessionTimeout
其中CurrentTime指当前时间,单位足毫秒:SessionTimeout指该会话设置的超时时间,单位也是亳秒。那么上图中坐标所标识的时间,是否就是通过上述公式计算出来的呢?答案是否定的,在ZooKeeper的实际实现中,还做了一个处理。ZooKeeper的Leader服务器在运行期间会定时地进行会话超时检査,其时间间隔是ExpirationInterval,单位是毫秒,默认值是tickTime的值,即默认情况下,每隔2000毫秒进行一次会话超时检查。为了方便对多个会话同时进行超时检査,完整的ExpirationTime的计算方式如下:
ExpirationTime_ = CurrentTime + SessionTimeout
ExpirationTime = (ExpirationTime_ /Expirationlnterval +1)*Expirationlnterval
也就垃说,上图中横坐标的ExpirationTime值总是ExpirationInterval的整数倍数。举个实际例子,假设当前时间的毫秒表示是1370907000000,客户端会话设置的超时时间是15000毫秒,ZooKecper服务器设置的tickTime为2000毫秒,那么ExpirationInterval 的值同样为为2000亳秒,于是我们可以计算该会话的ExpirationTime值为1370907016000, 计算过程如下:
ExpirationTime_ = 1370907000000+15000 = 1370907015000
ExpirationTime =( 1370907015000/2000 + 1) x 2000 = 137090701600
会话激活
为了保持客户端会话的有效性,在ZooKeeper的运行过程中,客广端会在会话超时时间过期范围内向服务端发送PING请求来保持会话的存效性,我们俗称“心跳检测”。同时,服务端需要不断地接收来自客户端的这个心跳检测,并且需要重新激活对应的客户端会话,我们将这个重新激活的过程称为TouchSession。会话激活的过程,不仅能够使服务端检测到对应客户端的存活性,同时也能让客户端自己保持连接状态。其主要流程如下图所示。
重新计算超时时间之后,将改会话从老的区块中取出,放到新的超时时间对应的新区块中,
通过以上的步骤,就基本完成会话激活的过程。在上面的会话激活过程中,我们可以看到,只要客户端发来心跳检测,那么服务端就会进行一次会话激活。心跳检测由客户端主动发起,以PING请求的形式向服务端发送。但实际上,在ZooKeeper服务端的设计中,只要客户端有请求发送到服务端,那么就会触发一次会话激活。因此,总的来讲,大体会出现以下两种情况下的会话激活。
•只要客户端向服务端发送请求,包括读或写请求,那么就会触发一次会话激活。
•如果客户端发现在sessionTimeout/3时间内尚未和服务器进行过任何通信,即没有向服务端发送任何请求,那么就会主动发起一个PING请求,服务端收到该请求 后,就会触发上述第一种情况下的会话激活。
会话超时检查
在ZooKeeper中,会话超时检査由SessionTracker负责的。SessionTracker中有一个单独的线程进行会话超时检査,这里我们将其称为“超时检査线程”,其工作机制的核心思路其实非常简单:逐个依次地对会话桶中剩下的会话进行清理。
在上图中,我们可以看到,如果一个会话被激活,那么ZooKeeper会将其从上一个会话桶迁移到下一个会话桶中,例如图中的session这个会话,由于触发了会话激活,因此ZooKeeper会将其从expirationTimel捕迁移到expirationTime n桶中去。于是, expirationTime 1中留下的所有会话都是尚未被激活的。因此,超时检査线程的任务就是定时检查出这个会话桶中所有剩下的未被迁移的会话。
那么超时检查线程是如何做到定时检查的呢?这里就和ZooKeeper会话的分桶策略紧密联系起来了。在会话分桶策略中,我们将Expirationlnterval的倍数作为时间点来分布会话,因此,超时检测线程只要在这些指定的时间点上进行检査即可,这样既提髙了会话检査的效率,而且由于是批量清理,因此性能非常好——这也是为什么ZooKeeper要通过分桶策略来管理客户端会话的主要的原因。因为在实际生产环境中,一个ZooKeeper集群的客户端会话数能会非常多,逐个依次检杳会话的方式会非常耗费时间。