客户端连接
1.创建zookeeper连接对象时,如何选择哪个服务器进行连接?
- 客户端的connectstring:localhost:2181,localhost:2182,localhost:2183
- 通过类org.apache.zookeeper.client.StaticHostProvider维护地址列表
- 通过解析connectstring后,进行随机排序,行程最终的地址列表
- 每次从形成的地址列表中选择第一个地址进行连接,如果连接不上在选择第二个地址
- 当如果当前节点时列表最后一个节点,则再重新选择第一个节点,相当于一个环
- 通过随机排序,每个zk的客户端就会随机的去连接zk服务器,分布相对均匀
- 例如 connectstring:localhost:2181,localhost:2182,localhost:2183 ,随机打乱后 connectstring:localhost:2183,localhost:2182,localhost:2181,则第一次连接时选择83这个节点,如果连接不上选择82,然后是81,如果81连接不上则开始连接第一个节点83
public StaticHostProvider(Collection<InetSocketAddress> serverAddresses)
throws UnknownHostException {
for (InetSocketAddress address : serverAddresses) {
InetAddress ia = address.getAddress();
InetAddress resolvedAddresses[] = InetAddress.getAllByName((ia!=null) ? ia.getHostAddress():
address.getHostName());
for (InetAddress resolvedAddress : resolvedAddresses) {
// If hostName is null but the address is not, we can tell that
// the hostName is an literal IP address. Then we can set the host string as the hostname
// safely to avoid reverse DNS lookup.
// As far as i know, the only way to check if the hostName is null is use toString().
// Both the two implementations of InetAddress are final class, so we can trust the return value of
// the toString() method.
if (resolvedAddress.toString().startsWith("/")
&& resolvedAddress.getAddress() != null) {
this.serverAddresses.add(
new InetSocketAddress(InetAddress.getByAddress(
address.getHostName(),
resolvedAddress.getAddress()),
address.getPort()));
} else {
this.serverAddresses.add(new InetSocketAddress(resolvedAddress.getHostAddress(), address.getPort()));
}
}
}
if (this.serverAddresses.isEmpty()) {
throw new IllegalArgumentException(
"A HostProvider may not be empty!");
}
Collections.shuffle(this.serverAddresses); // 随机排序
}
public InetSocketAddress next(long spinDelay) {
++currentIndex;
// 如果已经是最后一个节点,则从第一个节点开始
if (currentIndex == serverAddresses.size()) {
currentIndex = 0;
}
// 当地址列表只有一个地址时,再次获取之前sleep一定时间再返回,这算是一个重试间隔
if (currentIndex == lastIndex && spinDelay > 0) {
try {
Thread.sleep(spinDelay);
} catch (InterruptedException e) {
LOG.warn("Unexpected exception", e);
}
} else if (lastIndex == -1) {
// We don't want to sleep on the first ever connect attempt.
lastIndex = 0;
}
return serverAddresses.get(currentIndex);
}
2.会话
什么是会话
- 代表客户端与服务器端的一个zk连接
- 底层通信时通过tcp协议进行连接通信
- Zookeeper会在服务器端创建一个会话对象来维护这个连接的属性
- 当网络出现断网的抖动现象的时候,并不代表会话一定断开,
- 会话对象的实现是SessionImpl , 包括四个属性 1)sessionID: 唯一标识一个会话,具备全局唯一性 2)Timeout:会话超时时间,创建客户端Zookeeper对象时传入,服务器会根据最小会话时间和最大会话时间的规定来明确此值具体是什么 3)Ticktime:下次会话超时的时间,与分桶策略有关 4)isClosing:标记一个会话是否已经被关闭,当服务器检测到有会话失效时,就会把此会话标记为已关闭
会话状态
- CONNECTING
- CONNECTED
- RECONNENCTING
- RECONNECTED
- CLOSE
会话管理
1.sessionTracker
- 服务器端通过此类来管理会话,包括会话的创建,管理和清除工作
- 通过三个数据结构从三个维度来管理会话
1)sessionById属性:用于根据sessionID来查找session
2)sessionsWithTimeout属性:通过sessionID来查找此session失效时间是什么时候
3)sessionSets属性:通过某个时间查询都有那些会话在这个时间点会失效
public class SessionTrackerImpl extends Thread implements SessionTracker {
private static final Logger LOG = LoggerFactory.getLogger(SessionTrackerImpl.class);
HashMap<Long, SessionImpl> sessionsById = new HashMap<Long, SessionImpl>();
HashMap<Long, SessionSet> sessionSets = new HashMap<Long, SessionSet>();
ConcurrentHashMap<Long, Integer> sessionsWithTimeout;
2.如何高效的检测和清除失效会话
1)分桶策略
- ConcurrentHashMap<Long, Integer> sessionsWithTimeout
- 会话失效时间计算 约 定:把所有的时间按照某个单位进行等份(默认为服务器的ticktime配置)切割,此单位称呼为ExpriationInterval 公式:某次超时时间=((currentTime+sessiontimeout)/ExpirationInterval+1)*ExpriationInterval
- 举例说明:
1)由于服务器的ticktime默认是2000ms,ExpriationInterval = 2000ms
2)第一次创建会话时,currenttime=137097000000
3)创建会话时,客户端传入的超时时间时15000ms
4)则,此会话超时时间为((1370907000000+15000)/2000 +1)*2000 = 1370907016000
- 当某个会话由于有操作而导致超时时间变化,则会把会话从上一个桶移动到下一个桶
- 会话激活
1)当此会话一直有操作,则会话就不会失效
2)影响会话超时时间的因素:心跳检测,及ping命令,当客户端发现在sessionTimeout/3时间范围内还有没有任何操作命令产生,即会发送一个ping心跳请求
3)每次业务操作或者心跳检测,都会重新计算超时时间,然后在桶之间移动会话
- 会话超时检测
1)由sessionTracker中的一个线程负责检测session是否失效
2)线程检测周期也是ExpriationInterval的倍数
3)当某次检查时,如果在此次的分桶之前还有会话,就是说明这些会话都超时了,因为会话如果有业务操作或者心跳,会不断的从较小的分桶迁移到较大的分桶
4)举例 :系统启动时间时100001,此时ExpriationInterval = 2000ms,则桶的刻度为100001/2000=50 下一次检查时间为(100001/2000 +1)*2000 =102000
- 会话清理流程
1)修改会话状态为close
2)向所有的集群节点发器会话关闭请求
3)收集跟被清理的会话相关的临时节点
4)集群中的所有节点执行删除临时节点事务
5)从sessionTracker的列表中移除会话
6)关闭会话的网络连接,具体类是NIOServerCnxnFactory
- 会话重连
1)当客户端与服务端的网络断开后,客户端会不断的重新连接,当连接上后会话的状态两种情况 CONNETCTED 和 EXPIRED
2)注意 网络断开并不代表会话超时
- 三个会话异常
1)CONNECTION_LOSS : 网络闪断导致或者是客户端服务器出现问题导致,出现此问题客户端会重新找地址进行连接,直到连接上,当做某个操作过程中出现了此现象,则客户端会收到None-Disconnected(设置了watcher),同时会抛出异常KeeperExecption$ConnectionLossExecption ,当重新连接上后,客户端会收到事件通知(None-SyncConnected)
2)SESSION_EXPIRED :通常发生在CONNECTION_LOSS 期间,因为没有网络连接,就不能有操作和心跳进行,会话就会超时,由于重新连接时间较长,导致服务端关闭了会话,并清除会话,此时会话相关关联的watcher等数据都会丢失,出现这种情况 客户端要重新创建zookeeper对象,并且恢复数据,会受到异常SessionExpiredException
3)SESSION_MOVED :出现CONNECTION_LOSS时,客户端尝试重新连接下个节点 ,例如 客户端刚开始连接的是S1,由于网络中断,尝试连接S2 ,连接成功后,S2延续了会话,及会话从S1迁移到S2,当出现以下场景时,服务器端会抛出SessionMovedException异常,由于客户端的连接已经发生了变化,所以客户端收不到异常
有三台服务器 S1 S2 S3 ,开始时,客户端连接S1,此时客户但发出一个修改数据请求r1,在修改数据的请求达到S1之前,客户端重新连接上了S2服务器,此时出现会话转移,连接S2后,客户端又发起一次数据修改请求r2,r1被S1服务器处理,r2被S2处理(比r1处理的要早),这样对于客户端来说,请求被处理两次,并且r2被r1处理的结果覆盖了,因此,服务器通过检查会话的所有者来判断此次会话是否合法,不合法就抛出moved异常
3.数据与存储
1.ZKDatabase
- 负责管理zk的所有会话 datatree存储和事务日志
- 会定时向磁盘快照内存数据
- 当节点启动后,会通过磁盘上的事务日志和快照文件恢复完整的内存数据
2.DataTree
- 整个zk的数据就靠datatree维护,包括数据,目录,权限
- 数据的领域模型,不包括对外的连接
3.DataNode
- 树形结构中的每个节点
- 引用到父节点
- 也以用到子节点列表
4.事务日志
- 存储于datalog或者datalogDir配置的目录
- 对应目录下的version-2代表的是日志格式版本号
- 日志文件命名 :文件大小都是64M ,后缀都是16进制格式数字,逐渐增大,其本质是本日志的第一条zxid
5.日志格式
- zk提供了解析日志的工具类LogFormatter
- 日志信息格式如下: