ZooKeeper Sessions
本文主要内容为 zookeeper 官方文档的翻译
ZooKeeper客户端使用某种语言创建一个 handle 来建立与ZooKeeper服务的会话。 一旦创建, handle 处于 CONNECTING 状态,客户端库尝试连接到组成 ZooKeeper 服务的其中一个服务器,此时 handle 切换到 CONNECTED 状态。 在正常操作期间, handle 将处于这两种状态之一。 如果发生不可恢复的错误,例如会话过期或身份验证失败,或者如果应用程序显式关闭 handle,则 handle 将移动到 CLOSED 状态。 ZooKeeper客户端可能的状态转换如下图所示:
要创建一个客户端会话,应用程序代码必须提供一个连接字符串,其中包含以逗号分隔的主机:端口列表,每个端口对对应于一个ZooKeeper服务器(例如: “127.0.0.1:3001,127.0.0.1:3002”)。 ZooKeeper客户端库将选择一个任意服务器并尝试连接到它。 如果此连接失败,或者客户端由于任何原因断开与服务器的连接,客户端将自动尝试列表中的下一个服务器,直到连接(重新)建立。
3.2.0 新增:在服务器主机端口后,可选添加一个 chroot 路径。例如:“127.0.0.1:4545/app/”,此时,客户端所有的操作都会添加此路径前缀,包括 getting/setting等,例如在 “/foo/bar"的操作会执行在”/app/foo/bar"上。这个特性可应用于多租户环境,使得每个租户都有不同的根路径。
当客户端获得一个ZooKeeper服务的 handle 时,ZooKeeper创建一个ZooKeeper Session,并分配给客户端一个 64 位的 session id。如果客户端连接到不同的ZooKeeper服务器,它将发送 session id 作为连接握手的一部分。作为一种安全措施,服务器为 session id 创建一个密码,而任一服务器都可以对其进行验证。当客户端建立会话时,密码被发送到带有 session id 的客户端。每当客户机与新服务器重新建立会话时,它就会发送 session id 和密码。
ZooKeeper客户端创建会话时,会有一个超时参数,the session timeout。客户端发送一个请求,获取该超时参数,服务器返回合适的超时参数作为响应。当前实现要求超时参数至少是tickTime的2倍(在服务器配置中设置),最多是tickTime的20倍。ZooKeeper客户端API可以协商的超时时间。
接着,handle 将开始搜索在会话创建期间指定的服务器列表。最终,当客户端和至少一个服务器之间的连接重新建立时,会话将再次转换到“连接”状态(如果在 the session timeout 内重新连接),或者转换到“过期”状态(如果在 the session timeout 后重新连接)。不建议创建一个新的会话对象来断开连接。ZK客户端库将为您处理重新连接。特别是,我们在客户端库中内置了启发式方法来处理诸如“羊群效应”之类的事情……只有在通知会话过期时才创建新会话(强制)。
会话过期由ZooKeeper集群本身管理,而不是由客户端管理。当ZK客户端与集群建立会话时,它会提供上面详细介绍的“超时”值。集群使用这个值来确定客户机的会话何时到期。当集群在指定的会话超时时间(即没有心跳)内没有收到客户端消息时,就会发生过期。在会话到期时,集群将删除该会话拥有的所有临时节点,并立即通知所有 连接的客户端(任何监视这些znode的人)更改。此时,会话过期的客户端仍然与集群断开连接,直到/除非它能够重新建立到集群的连接,否则它将不会收到会话过期的通知。客户机将保持断开状态,直到与集群重新建立TCP连接,此时过期会话的监视者将收到“会话过期”通知。
ZooKeeper会话建立调用的另一个参数是默认的监控:the default watcher。当客户机中发生任何状态更改时,监视程序将得到通知。例如,如果客户端失去与服务器的连接或者客户端会话过期等,客户端将被通知。该监视程序应该将初始状态设定为断开连接状态(即在任何状态更改事件被客户端库发送给监视程序之前)。对于新连接,发送给监视程序的第一个事件通常是会话连接事件。
会话通过客户端发送的请求保持活跃。如果会话空闲了一段时间,将使会话超时,客户端将发送一个PING请求以保持会话活动。这个PING请求不仅允许ZooKeeper服务器知道客户端仍然是活动的,而且允许客户端验证它与ZooKeeper服务器的连接仍然是活动的。PING的时间是足够保守的,以确保合理的时间来检测死连接和重新连接到一个新的服务器。
一旦成功建立到服务器的连接(连接),基本上有两种情况下客户端库会产生connection loss :
- 应用程序在不再活动/有效的会话上调用操作
- ZooKeeper客户端断开与服务器的连接,当有对该服务器的挂起操作时,即有一个挂起的异步调用。
在3.2.0中添加了——SessionMovedException。有一个内部异常称为 SessionMovedException,通常不会被客户端看到。发生此异常是因为在另一个服务器上重新建立的会话连接上收到了一个请求。导致此错误的正常原因是客户端向服务器发送请求,但网络数据包被延迟,因此客户端超时并连接到一个新的服务器。当延迟的数据包到达第一个服务器时,旧的服务器检测到会话已经移动,并关闭客户端连接。客户端通常不会看到这个错误,因为他们没有从那些旧的连接中读取。(旧的联系通常是关闭的。)出现这种情况的一种情况是,两个客户机试图使用保存的会话id和密码重新建立相同的连接。其中一个客户端将重新建立连接,而另一个客户端将断开连接(导致这对客户端尝试无限期地重新建立连接/会话)。
更新服务器列表。我们允许客户端通过提供一个新的以逗号分隔的主机:端口对列表来更新连接字符串,每个主机:端口对对应于一个ZooKeeper服务器。该功能调用一个概率负载平衡算法,该算法可能导致客户端断开与当前主机的连接,目的是在新列表中实现每个服务器期望的统一连接数。如果客户端连接的当前主机不在新列表中,此调用将始终导致连接被丢弃。否则,决定取决于服务器的数量是增加了还是减少了,以及减少了多少。
例如,如果之前的连接字符串包含3台主机,而现在列表中包含这3台主机和另外2台主机,那么连接到这3台主机中的每台主机的40%的客户端将移动到其中一台新主机上,以平衡负载。该算法将导致客户端断开其与当前主机的连接(概率为0.4),并在这种情况下导致客户端连接到随机选择的2个新主机中的一个。
另一个例子,假设我们有5现在主机和主机的更新列表,删除2,剩下的客户端连接到3主机将保持联系,而所有客户机连接到2删除主机需要搬到一个3主机,随机选取的。如果断开连接,客户端将移动到一种特殊模式,使用概率算法(而不仅仅是轮询)选择要连接的新服务器。
在第一个例子中,每个客户端决定以0.4的概率断开连接,但一旦做出决定,它将尝试连接到一个随机的新服务器,只有当它不能连接到任何新服务器时,它才会尝试连接到旧的服务器。在找到一个服务器或尝试新列表中的所有服务器但连接失败后,客户端返回到正常操作模式,从connect String中选择一个任意服务器并尝试连接它。如果失败,它将继续轮流尝试不同的随机服务器。(参见上面用于最初选择服务器的算法)
Local session (3.5.0 新增)
背景:在ZooKeeper中创建和关闭会话是很昂贵的,因为它们需要quorum确认,当需要处理数千个客户端连接时,它们成为ZooKeeper集群的瓶颈。所以在3.5.0之后,我们引入了一种新的会话类型:本地会话,它没有普通(全局)会话的全部功能,这个特性可以通过开启 localSessionsEnabled 来实现。
当 localSessionsUpgradingEnabled 是 disable 时:
-
本地会话不能创建临时节点
-
一旦本地会话丢失,用户就不能使用会话id/密码重新建立它,会话及其监视就永远消失了。注意:丢失tcp连接并不一定意味着会话丢失。如果在会话超时之前可以与相同的zk服务器重新建立连接,那么客户端可以继续(它只是不能移动到另一个服务器)。
-
当一个本地会话连接时,会话信息只维护在它所连接的zookeeper服务器上。leader不知道这样一个会话的创建,也没有写入磁盘的状态。
-
ping、过期和其他会话状态维护由当前会话所连接的服务器来处理。
当 localSessionsUpgradingEnabled 为 enable 时:
-
本地会话可以自动升级为全局会话。
-
当创建一个新会话时,它将本地保存在包装好的 LocalSessionTracker 中。它随后可以根据需要升级为全局会话(例如创建临时节点)。如果请求升级,会话将从本地集合中删除,同时保持相同的会话ID。
-
目前只有创建临时节点的操作需要从本地升级到全局。原因是临时节点的创建严重依赖于全局会话。如果本地会话可以创建临时节点而不升级为全局会话,将会导致不同节点之间的数据不一致。leader还需要知道会话的生命周期,以便在关闭/到期时清除临时节点。这需要一个全局会话,因为本地会话绑定到其特定的服务器。
-
升级期间的会话可以是本地会话和全局会话,但是两个线程不能同时调用升级操作。
-
ZooKeeperServer(Standalone)使用SessionTrackerImpl;LeaderZookeeper使用LeaderSessionTracker,其包含 SessionTrackerImpl (global) 和 LocalSessionTracker(如果启用);FollowerZooKeeperServer和ObserverZooKeeperServer使用LocalSessionTracker,其包含LearnerSessionTracker。关于会话的类的UML图:
±---------------+ ±-------------------+ ±--------------------+ | | --> | | ----> | LocalSessionTracker | | SessionTracker | | SessionTrackerImpl | ±--------------------+ | | | | ±----------------------+ | | | | ±------------------------> | LeaderSessionTracker | ±---------------+ ±-------------------+ | ±----------------------+ | | | | | || ±--------------------------+ ±--------> | | | UpgradeableSessionTracker | | | | | ------------------------+ ±--------------------------+ | | | v ±----------------------+ | LearnerSessionTracker | ±----------------------+
Q & A
使用config选项禁用本地会话升级的原因是什么?
- 在一个需要处理大量客户端的大型部署中,我们知道客户端通过观察者连接,而观察者只是本地会话。因此,这更像是一种保护措施,防止有人意外地创建大量临时节点和全局会话。
会话何时创建?
- 在当前的实现中,当处理ConnectRequest和createSession请求到达FinalRequestProcessor时,它将尝试创建一个本地会话。
如果创建会话在服务器A发送,客户端断开与其他服务器B的连接,服务器B再次发送,然后断开并连接回服务器A,会发生什么?
- 当客户端重新连接到B时,它的sessionId在B的本地会话跟踪程序中不存在。因此B将发送验证数据包。如果A发出的createssession在验证包到达之前被提交,客户端将能够连接。否则,客户端将获得会话过期,因为仲裁还不知道这个会话。如果客户端还试图再次连接回A,则会话已经从本地会话跟踪程序中删除。所以A需要发送一个验证数据包给leader。根据请求的时间,结果应该与B相同。
Watches
zk 所有的读操作(getData()、getChildren()、exists())都可以添加一个监听器(watch),当节点发生变化时,便会触发监听器。监听器有以下特点:
- 一次性:所有的监听器都是一次性的,当监听被触发之后,再次改变节点或数据就不会再触发,因此需要重复使用时必须在触发后重新注册
- 异步发送:zk 客户端注册完 watch 后,通过异步的方式发送给 zk 服务端集群。因此,可能出现在监听器到达服务端集群之前,所监控的节点发生了变化。对此情况,zk 做了顺序保证,当客户端发送监听器后,zk 保证在监听器真正被注册到服务器前,当前节点的所有变化对客户端都是不可见的
- 监控方式:由于每个 Znode 节点都具有与自身关联的 data 数据以及子节点,因此,注册的监听器可简单分为是对数据的监听还是对子节点的监听。而客户端对于不同的读操作,监听不同的内容
- getData() 和 exists():监听当前节点是 data 数据,当 data 数据发生变化时,触发该监听器
- getChildren() :监听的是子节点,当子节点的数量发生变化时,触发该监听器
触发监听的事件类型
- Created:触发 exist
- Deleted:触发 exists,getData 和 getChildren
- Changed:触发 exists 和 getData.
- Child:触发 getChildren
3.6.0 后新增永久性 watch,永久性 watch 在触发之后不会被删除
永久性 watch 触发事件类型有:
- NodeCreated
- NodeDeleted
- NodeDataChanged
另外,永久性的 watch 会递归触发,及注册节点的字节点变化也会触发当前节点的 watch。可以通过 addWatch 方法添加永久性 watch,removeWatches 方法移除。