本文是Zookeeper官方文档翻译的系列文章之一,关于历史的文档,可参考如下链接:
Zookeeper概览-官方文档翻译
Zookeeper开发者指南——Zookeeper数据模型
Zookeeper会话(ZooKeeper Sessions)
Zookeeper客户端通过使用语言绑定创建句柄与Zookeeper服务建立一个session;创建之后,句柄以 CONNECTING 状态开始工作,客户端库尝试连接到组成 ZooKeeper 服务的其中一台服务器,此时它(句柄)切换到 CONNECTED 状态。在正常操作期间,客户端的句柄将处于CONNECTING或者CONNECTED的状态;如果出现了不可恢复的错误,例如会话过期,授权失败,或者应用显示地关闭了句柄, 这些场景下,句柄将会变成COLSED状态;下图显示了客户端状态的转换:
客户端状态转换图
为了创建客户端的会话,应用代码中必须提供可以连接到服务端的连接字符串, 连接字符以逗号分隔的host:port对(例如:"127.0.0.1:4545"或者"127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002"),一对host:port表示了Zookeeper的一台服务;Zookeeper客户端库选择任意一台服务器进行连接,如果连接失败,或者客户端无法连接到服务端,则客户端会自动地尝试下一个host:port对,直到和服务端成功建立连接;
3.2版本新增内容: “chroot”后缀也可以添加到连接字符串的后面,这个可选的;当解析与此root相关的路径时,将会执行客户端的命令(这个与unix的chroot命令类似);如果使用这个后缀,则连接字符串如下所示:"127.0.0.1:4545/app/a" 或者 "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a",客户端将会把服务端的“app/a”目录作为根目录,之后所有的路径将会是基于“app/a”这个目录, 例如:getting/setting "/foo/bar",从服务端的角度来看是基于根目录,实际上是操作"/app/a/foo/bar";在多租户场景下,这个特性非常有用,在这种环境中,特定Zookeeper服务的每个用户可能对应到不同的root目录;这种特性能够让客户端共用一些功能,客户端的每个操作看着是在根目录下进行的,但是实际上是应用在部署时指定的一个目录下
当客户端获取到了连接到Zookeeper服务的句柄, Zookeeper会创建一个用64位的数字表示的会话,并返回给客户端;如果客户端连接到了另外一个服务器,客户端会将session ID作为连接请求的一部分发送给服务端;出于安全考虑,Zookeeper服务端将会为会话ID(任何服务器都可以验证这个会话ID)创建一个密码;在客户端和服务器建立连接之后,服务器会把会话ID、密码一同返回给客户端;当客户端需要连接到新的服务器时,客户端就可以把会话ID以及密码一同发给服务端
Zookeeper客户端库创建会话的其中一个参数是会话timeout(毫秒为单位);客户端发送一个要求的timeout, 服务端器则回服务端的timeout;当前的具体实现是要求timeout至少是ticktime的2倍(在服务端配置),最多是ticktime的20倍;Zookeeper 客户端 API 允许访问协商的timeout;
当一个客户端(会话)与ZK服务集群的连接断开,它将开始搜索在会话创建期间指定的服务器列表(host:port对字符串), 以求重新建立连接。最终,当客户端和至少一个服务器重新建立连接时,如果在会话超时值内重新连接成功,则会话将再次转换到“CONNECTED”状态,或者如果在会话超时后才重新连接成功,则会转换到“EXPIRED”状态。如果客户端和服务器之间断开了连接,不建议马上创建一个新的会话对象。ZK客户端库将自动为开发者重新连接服务端。特别是,我们在客户端库中内置了方法来处理“羊群效应”等问题。只有当客户端被强制通知会话过期时才创建一个新的会话。
会话过期由ZooKeeper集群管理,而不是客户端。当ZK客户端与集群建立一个会话时,它会提供一个“timeout”值。集群使用这个值来确定客户端会话何时到期。当集群在指定的会话超时时间内没有收到来自客户端的消息时(即没有心跳),就会发生过期。在会话过期时,集群将删除该会话所拥有的任何/所有临时节点,并立即通知任何/所有连接的客户端 (任何监听这些znode的客户端/服务器)。此时,会话过期的客户端仍然与集群断开连接,在客户端能够重新与集群建立连接之前,客户端是不会收到会话过期的通知,一旦客户端与集群重新建立了TCP连接,此时过期会话的监听者将收到“会话过期”通知。
会话过期的监听器看到的过期会话的状态转换示例如下:
- 'connected' : 客户端与服务器建立会话,并且客户端与服务器正在正常通讯中
- .... 客户端与集群断开连接
- 'disconnected' : 客户端与集群已经断开连接
- .... 随着时间的推移,当集群超时后,客户端不能再访问服务端,因为它与集群断开了连接
- .... 随着时间的推移,客户端恢复与集群的网络连接
- 'expired' : 最终,客户端重新连接到集群,然后集群通知客户端会话过期
ZooKeeper创建会话使用的另一个参数是默认的watcher。当客户端的状态发生任何更改时,会通知监听器(watcher)。例如,如果客户端失去与服务器的连接,则会通知客户端,或者如果客户端会话过期等等。这个监听器应该认为初始状态是断开的(即在任何状态变更的事件被客户端库发送给监听器之前)。在创建新连接的情况下,发送到监听器的第一个事件通常是会话连接事件。
客户端通过发送请求来保证会话是始终有效的;在会话即将超时的时候,客户端将发送一个PING请求以保持会话状态是有效的。PING请求不仅可以让ZooKeeper服务器知道客户端仍然是active的,还可以让客户端验证与ZooKeeper服务器的连接是否仍然是active的。 发送PING的时机非常保守,目的是让服务端有合理的时间来检测已经断开的连接并让客户端重新连接到新服务器。
一旦客户端与服务器成功建立连接,基本上有两种情况,客户端库在执行同步或者异步操作时会产生connectionloss,并且会出现以下两种情况中的一种:
- 应用程序在不再活动/有效的会话中执行操作
- 当服务器有挂起的操作时(即有一个挂起的异步调用),ZooKeeper 客户端与服务器断开连接。
3.2.0版本中新增的内容- SessionMovedException
SessionMovedException 是内部异常,客户端通常不可见。 发生此异常的原因是服务器接收到了已经与新服务器建立连接的会话在之前发出的旧请求。这种错误一般是因为客户端向服务器发送请求,但网络数据包延迟,导致客户端超时并连接到新服务器。此时,当延迟的数据包到达原来的旧服务器时,旧服务器检测到会话已经转移了,然后关闭客户端连接。 客户端通常不会看到此错误,因为它们不会从那些旧连接中读取数据,而且旧连接通常也已经被关闭了。客户端可以看到这种情况的其中一种场景是当两个客户端尝试使用保存的会话 ID 和密码重新建立相同的连接时。其中一个客户端将重新建立连接,而第二个客户端将断开连接(导致这对客户端无限期地尝试重新建立其连接/会话)。
更新服务器的列表
Zookeeper允许客户端通过提供一个新的逗号分隔的host:port对列表来更新连接字符串,每个host:port对对应一个 ZooKeeper 服务器。这个功能触发一个负载均衡算法,这个算法可能会导致客户端与其当前主机断开连接,目标是在新列表中实现每个服务器的预期统一连接数,即每个服务器的连接数比较均衡。如果客户端连接的当前主机不在新列表中,则此调用将始终导致连接断开。否则,将根据服务器数量是增加还是减少以及增加多少来决定如何进行分配。
例如,如果之前的连接字符串包含 3 台主机,而新的连接字符串包含这 3 台主机和另外 2 台主机,则连接到 3 台主机中的每台主机的 40% 的客户端将移动到其中一台新主机以保证每台服务器的负载比较平衡。该算法将导致客户端以 0.4 的概率断开与当前主机的连接,在这种情况下,导致客户端连接到随机选择的 2 个新主机之一。
另一个例子——假设我们有 5 台主机,现在删除 2 台主机,连接到剩余 3 台主机的客户端将保持连接,而连接到 2 台已删除主机的所有客户端都需要随机移动到剩余3台主机的其中一个。如果连接断开,客户端将进入特殊模式,在该模式下,客户端使用概率算法选择新的服务器进行连接,而不仅仅是轮询。
在第一个示例中,每个客户端决定以 0.4 的概率断开连接,但一旦做出决定,客户端将尝试连接到随机的新服务器,并且只有当客户端无法连接到任何新服务器时才会尝试连接到旧服务器。在找到服务器或尝试新列表中的所有服务器但连接失败后,客户端返回正常模式,从连接字符串(host:port对)中选择任意服务器并尝试连接到它。如果失败,它将继续循环尝试随机选择不同的服务器。(见上文用于最初选择服务器的算法)
Local session(3.5.0版本新增的内容)
ZooKeeper 中会话的创建和关闭成本很高,因为它们需要仲裁确认,当需要处理数千个客户端连接时,会话的创建和关闭成为了 ZooKeeper 集群的瓶颈。所以在 3.5.0 之后,Zookeeper引入了一种新的会话类型:本地会话,它不具备普通(全局)会话的全部功能,这个功能需要开启 localSessionsEnabled 才能使用。
当 localSessionsUpgradingEnabled 被禁用时,LocalSession不能执行下操作:
- 本地会不能创建临时节点
- 一旦本地会话丢失,用户将无法使用会话ID/密码重新建立本地会话,会话及其监听器将永远消失。 注意:丢失 tcp 连接并不一定意味着会话丢失。 如果可以在会话超时之前与同一个 zk 服务器重新建立连接,则客户端可以继续与服务端进行通讯(客户端也只是无法移动到另一台服务器)
- 当一个本地会话连接服务端时,会话信息只维护在它所连接的zookeeper服务器上。leader不知道有这样一个会话,也没有把会话状态写入磁盘
- ping、过期和其他会话状态的维护是由当前会话连接的服务器处理的
当启用localSessionsUpgradingEnabled时,本地会话具备以下功能:
- 本地会话可以自动地升级为全局会话
- 当创建一个新的会话时,它被保存在本地包装的LocalSessionTracker中。它随后可以根据需要升级为全局会话(例如创建临时节点)。如果请求升级session,则保持原来的会话ID,从本地集合中删除原来的会话
- 目前只有创建临时节点的操作需要从本地会话升级到全局会话。原因是临时节点的创建严重依赖于全局会话。如果本地会话不升级到全局会话就可以创建临时节点,则会导致不同节点之间的数据不一致。leader还需要知道会话的生命周期,以便在session关闭/到期时清理临时节点。这需要一个全局会话,因为本地会话被绑定到它的特定服务器上
- 会话升级时,这个会话可以是本地会话,也可以是全局会话,但是升级操作不是线程安全的,不能被两个线程并发调用
Q&A
问题:配置选项禁用本地会话升级的原因是什么
答案:在一个大型的部署中,需要处理大量的客户端,我们知道客户端通过观察者连接,这应该是本地会话。因此,这更像是一种保护措施,防止客户端意外创建大量临时节点和全局会话。
问题:什么时候创建session;
答案:在当前的实现中,当处理ConnectRequest时,或者createSession请求到达FinalRequestProcessor时,客户端将尝试创建一个本地会话
问题:如果在服务器A发送创建会话的请求,而客户端断开与A的连接,并与其他服务器B连接,最终再次发送,然后断开连接,重新连接到服务器A,会发生什么?
答案:当客户端重新连接到B时,它的sessionId将不存在于B的本地会话跟踪器中。B会发送验证包给客户端。如果由A发出的CreateSession在验证包到达之前提交,客户端将能够连接A。否则,客户端将得到会话过期的通知(A同时也删除了会话),因为quorum还不知道这个会话。如果客户端也试图再次连接回A,会话已经从本地会话跟踪器中删除。所以A需要发送一个验证包给leader。结果应该与B的情形一致,依赖于请求的时间
版权声明:内容来源于互联网和用户投稿 如有侵权请联系删除