ZooKeeper 动态重新配置
概述
在 3.5.0 版本发布之前,Zookeeper 的成员资格和其他所有配置参数都是静态的 - 在启动期间加载,在运行时不可变。操作员诉诸于“滚动重启” - 一种手动密集且容易出错的更改配置方法,导致生产中数据丢失和不一致。
从 3.5.0 开始,“滚动重启”不再需要!ZooKeeper 全面支持自动配置更改:Zookeeper 服务器的集合、它们的角色(参与者/观察者)、所有端口,甚至法定人数系统都可以动态更改,而不会中断服务,并且在维护数据一致性的同时。重新配置立即执行,就像 ZooKeeper 中的其他操作一样。可以使用单个重新配置命令进行多个更改。动态重新配置功能不限制操作并发性,不要求在重新配置期间停止客户端操作,对管理员来说具有非常简单的界面,并且不会给其他客户端操作增加复杂性。
新的客户端功能允许客户端了解配置更改,并更新存储在 ZooKeeper 句柄中的连接字符串(服务器列表及其客户端端口)。使用概率算法在新的配置服务器之间重新平衡客户端,同时保持客户端迁移范围与集成成员资格变更成正比。
本文档提供了重新配置的管理员手册。有关重新配置算法、性能测量等的详细说明,请参阅我们的论文
- Shraer, A., Reed, B., Malkhi, D., Junqueira, F. 主/备集群的动态重新配置。在 USENIX 年度技术会议 (ATC)(2012) 中,425-437:链接:论文 (pdf),幻灯片 (pdf),视频,hadoop 峰会幻灯片
注意:从 3.5.3 开始,动态重新配置功能默认禁用,必须通过 reconfigEnabled 配置选项显式启用。
配置格式更改
指定客户端端口
服务器的客户端端口是服务器接受客户端连接请求的端口。从 3.5.0 开始,不再使用 clientPort 和 clientPortAddress 配置参数。相反,此信息现在是服务器关键字规范的一部分,如下所示
server.<positive id> = <address1>:<port1>:<port2>[:role];[<client port address>:]<client port>**
客户端端口规范位于分号的右侧。客户端端口地址是可选的,如果未指定,则默认为“0.0.0.0”。与往常一样,角色也是可选的,它可以是 participant 或 observer(默认情况下为 participant)。
合法服务器语句的示例
server.5 = 125.23.63.23:1234:1235;1236
server.5 = 125.23.63.23:1234:1235:participant;1236
server.5 = 125.23.63.23:1234:1235:observer;1236
server.5 = 125.23.63.23:1234:1235;125.23.63.24:1236
server.5 = 125.23.63.23:1234:1235:participant;125.23.63.23:1236
指定多个服务器地址
从 ZooKeeper 3.6.0 开始,可以为每个 ZooKeeper 服务器指定多个地址(请参阅 ZOOKEEPER-3188)。这有助于提高可用性,并为 ZooKeeper 增加网络级弹性。当服务器使用多个物理网络接口时,ZooKeeper 能够绑定到所有接口,并在发生网络错误时运行时切换到工作接口。可以使用管道 ('|') 字符在配置中指定不同的地址。
使用多个地址的有效配置示例
server.2=zoo2-net1:2888:3888|zoo2-net2:2889:3889;2188
server.2=zoo2-net1:2888:3888|zoo2-net2:2889:3889|zoo2-net3:2890:3890;2188
server.2=zoo2-net1:2888:3888|zoo2-net2:2889:3889;zoo2-net1:2188
server.2=zoo2-net1:2888:3888:observer|zoo2-net2:2889:3889:observer;2188
standaloneEnabled 标志
在 3.5.0 之前,可以在独立模式或分布式模式下运行 ZooKeeper。这些是独立的实现堆栈,在运行时无法在它们之间切换。默认情况下(为了向后兼容),standaloneEnabled 设置为 true。使用此默认设置的后果是,如果从单个服务器启动,则不允许集群增长,如果从多个服务器启动,则不允许它收缩到包含少于两个参与者。
将标志设置为 false 会指示系统即使集群中只有一个参与者,也要运行分布式软件堆栈。要实现这一点,(静态)配置文件应包含
standaloneEnabled=false**
使用此设置,可以启动包含单个参与者的 ZooKeeper 集群,并通过添加更多服务器动态地增长它。同样,可以通过删除服务器来缩小集群,以便只保留单个参与者。
由于运行分布式模式允许更大的灵活性,我们建议将标志设置为 false。我们预计传统的独立模式将在未来被弃用。
reconfigEnabled 标志
从 3.5.0 开始到 3.5.3 之前,没有办法禁用动态重新配置功能。我们希望提供禁用重新配置功能的选项,因为在启用重新配置的情况下,我们担心恶意行为者可以对 ZooKeeper 集群的配置进行任意更改,包括向集群添加受损服务器。我们希望由用户自行决定是否启用它,并确保适当的安全措施到位。因此,在 3.5.3 中引入了 reconfigEnabled 配置选项,以便可以完全禁用重新配置功能,并且除非将 reconfigEnabled 设置为 true,否则任何尝试通过重新配置 API(无论是否经过身份验证)重新配置集群的尝试都将默认失败。
要将选项设置为 true,配置文件 (zoo.cfg) 应包含
reconfigEnabled=true
动态配置文件
从 3.5.0 开始,我们区分了可以在运行时更改的动态配置参数和在服务器启动时从配置文件中读取且在其执行期间不会更改的静态配置参数。目前,以下配置关键字被视为动态配置的一部分:server、group 和 weight。
动态配置参数存储在服务器上的一个单独文件中(我们称之为动态配置文件)。此文件使用新的dynamicConfigFile关键字从静态配置文件链接。
示例
zoo_replicated1.cfg
tickTime=2000
dataDir=/zookeeper/data/zookeeper1
initLimit=5
syncLimit=2
dynamicConfigFile=/zookeeper/conf/zoo_replicated1.cfg.dynamic
zoo_replicated1.cfg.dynamic
server.1=125.23.63.23:2780:2783:participant;2791
server.2=125.23.63.24:2781:2784:participant;2792
server.3=125.23.63.25:2782:2785:participant;2793
当整体配置更改时,静态配置参数保持不变。动态参数由 ZooKeeper 推送,并覆盖所有服务器上的动态配置文件。因此,不同服务器上的动态配置文件通常是相同的(它们只能在重新配置进行中或新配置尚未传播到某些服务器时暂时不同)。一旦创建,就不应手动更改动态配置文件。更改只能通过下面概述的新重新配置命令进行。请注意,更改脱机集群的配置可能会导致与存储在 ZooKeeper 日志(以及从日志填充的特殊配置 znode)中的配置信息不一致,因此强烈不建议这样做。
示例 2
用户可能更愿意最初指定一个配置文件。因此,以下内容也是合法的
zoo_replicated1.cfg
tickTime=2000
dataDir=/zookeeper/data/zookeeper1
initLimit=5
syncLimit=2
clientPort=
如果每个服务器上的配置文件尚未采用此格式,它们将自动拆分为动态文件和静态文件。因此,上面的配置文件将自动转换为示例 1 中的两个文件。请注意,如果 clientPort 和 clientPortAddress 行(如果已指定)是冗余的(如上面的示例所示),它们将在此过程中自动删除。原始静态配置文件已备份(在 .bak 文件中)。
向后兼容性
我们仍支持旧的配置格式。例如,以下配置文件是可接受的(但不推荐)
zoo_replicated1.cfg
tickTime=2000
dataDir=/zookeeper/data/zookeeper1
initLimit=5
syncLimit=2
clientPort=2791
server.1=125.23.63.23:2780:2783:participant
server.2=125.23.63.24:2781:2784:participant
server.3=125.23.63.25:2782:2785:participant
在启动期间,将创建一个动态配置文件,其中包含前面解释的配置的动态部分。然而,在这种情况下,“clientPort=2791”行将保留在服务器 1 的静态配置文件中,因为它不是冗余的——它不是使用配置格式更改部分中解释的格式作为“server.1=...”的一部分指定的。如果调用重新配置来设置服务器 1 的客户端端口,我们将从静态配置文件中删除“clientPort=2791”(动态文件现在将此信息作为服务器 1 规范的一部分包含在内)。
升级到 3.5.0
只有在将整体升级到 3.4.6 版本后,才应将正在运行的 ZooKeeper 整体升级到 3.5.0。请注意,这仅适用于滚动升级(如果您愿意完全关闭系统,则不必经历 3.4.6)。如果您尝试在不经过 3.4.6 的情况下进行滚动升级(例如,从 3.4.5),您可能会收到以下错误
2013-01-30 11:32:10,663 [myid:2] - INFO [localhost/127.0.0.1:2784:QuorumCnxManager$Listener@498] - Received connection request /127.0.0.1:60876
2013-01-30 11:32:10,663 [myid:2] - WARN [localhost/127.0.0.1:2784:QuorumCnxManager@349] - Invalid server id: -65536
在滚动升级期间,每个服务器将依次关闭并使用新的 3.5.0 二进制文件重新启动。在使用 3.5.0 二进制文件启动服务器之前,我们强烈建议更新配置文件,以便所有服务器语句“server.x=...”都包含客户端端口(请参阅部分 指定客户端端口)。如前所述,您可以将配置保留在单个文件中,并保留 clientPort/clientPortAddress 语句(尽管如果您以新格式指定客户端端口,则这些语句现在是多余的)。
ZooKeeper 集群的动态重新配置
ZooKeeper Java 和 C API 已扩展,包含 getConfig 和 reconfig 命令,以方便重新配置。这两个命令都有同步(阻塞)变体和异步变体。我们在此使用 Java CLI 演示这些命令,但请注意,您也可以使用 C CLI 或直接从程序调用命令,就像任何其他 ZooKeeper 命令一样。
API
对于 Java 和 C 客户端,有两组 API。
-
重新配置 API:重新配置 API 用于重新配置 ZooKeeper 集群。从 3.5.3 开始,重新配置 Java API 从 ZooKeeper 类移到 ZooKeeperAdmin 类,并且使用此 API 需要设置 ACL 和用户身份验证(有关更多信息,请参阅 安全性)。
-
获取配置 API:获取配置 API 用于检索存储在 /zookeeper/config znode 中的 ZooKeeper 集群配置信息。使用此 API 不需要特定的设置或身份验证,因为任何用户都可以读取 /zookeeper/config。
安全
在 3.5.3 之前,没有对重新配置强制执行安全机制,因此任何可以连接到 ZooKeeper 服务器集的 ZooKeeper 客户端都可以通过重新配置更改 ZooKeeper 集群的状态。因此,恶意客户端有可能向集中添加受损服务器,例如,添加受损服务器或删除合法服务器。在个案中,此类情况可能是安全漏洞。
为了解决此安全问题,我们从 3.5.3 开始引入了对重新配置的访问控制,以便只有特定的一组用户可以使用重新配置命令或 API,并且需要明确配置这些用户。此外,ZooKeeper 集群的设置必须启用身份验证,以便可以对 ZooKeeper 客户端进行身份验证。
我们还为在安全环境(即公司防火墙后面)中操作和与 ZooKeeper 集群交互的用户提供了一个逃生舱口。对于那些想要使用重新配置功能但不想为重新配置访问检查配置授权用户显式列表的开销的用户,他们可以将 "skipACL" 设置为 "yes",这将跳过 ACL 检查并允许任何用户重新配置集群。
总体而言,ZooKeeper 为重新配置功能提供了灵活的配置选项,允许用户根据其安全要求进行选择。我们由用户自行决定适当的安全措施是否到位。
-
访问控制:动态配置存储在特殊 znode ZooDefs.CONFIG_NODE = /zookeeper/config 中。默认情况下,此节点对除超级用户和明确配置为具有写访问权限的用户之外的所有用户都是只读的。需要使用重新配置命令或重新配置 API 的客户端应配置为具有对 CONFIG_NODE 的写访问权限的用户。默认情况下,只有超级用户具有完全控制权,包括对 CONFIG_NODE 的写访问权限。可以通过设置具有与指定用户关联的写权限的 ACL,由超级用户授予其他用户写访问权限。有关如何使用身份验证设置 ACL 和使用重新配置 API 的一些示例,请参阅 ReconfigExceptionTest.java 和 TestReconfigServer.cc。
-
身份验证:用户的身份验证与访问控制无关,并且委托给 ZooKeeper 的可插入身份验证方案支持的现有身份验证机制。有关此主题的更多详细信息,请参阅 ZooKeeper 和 SASL。
-
禁用 ACL 检查:如果 skipACL 设置为“yes”,ZooKeeper 支持 "skipACL" 选项,以便完全跳过 ACL 检查。在这种情况下,任何未经身份验证的用户都可以使用重新配置 API。
检索当前动态配置
动态配置存储在特殊 znode ZooDefs.CONFIG_NODE = /zookeeper/config 中。新的 config
CLI 命令读取此 znode(目前它只是 get /zookeeper/config
的包装器)。与普通读取一样,要检索最新提交的值,你应首先执行 sync
。
[zk: 127.0.0.1:2791(CONNECTED) 3] config
server.1=localhost:2780:2783:participant;localhost:2791
server.2=localhost:2781:2784:participant;localhost:2792
server.3=localhost:2782:2785:participant;localhost:2793
注意输出的最后一行。这是配置版本。版本等于创建此配置的重新配置命令的 zxid。第一个已建立配置的版本等于第一个成功建立的领导者发送的 NEWLEADER 消息的 zxid。当配置写入动态配置文件时,版本会自动成为文件名的一部分,并且静态配置文件会使用新动态配置文件的路径进行更新。与早期版本相对应的配置文件会保留以用于备份目的。
在启动期间,版本(如果存在)将从文件名中提取。版本绝不应由用户或系统管理员手动更改。系统使用它来了解哪个配置是最新的。手动操作它会导致数据丢失和不一致。
与 get
命令一样,config
CLI 命令接受 -w 标志以设置对 znode 的监视,并接受 -s 标志以显示 znode 的统计信息。它还接受一个新标志 -c,该标志仅输出与当前配置相对应的版本和客户端连接字符串。例如,对于上述配置,我们将获得
[zk: 127.0.0.1:2791(CONNECTED) 17] config -c
400000003 localhost:2791,localhost:2793,localhost:2792
请注意,当直接使用 API 时,此命令称为 getConfig
。
作为任何读取命令,它返回您的客户端连接到的跟随者已知的配置,该配置可能略有过期。可以使用 sync
命令获得更强有力的保证。例如,使用 Java API
zk.sync(ZooDefs.CONFIG_NODE, void_callback, context);
zk.getConfig(watcher, callback, context);
注意:在 3.5.0 中,将哪个路径传递给 sync()
命令并不重要,因为服务器的所有状态都已更新为与领导者一致(因此可以使用不同的路径而不是 ZooDefs.CONFIG_NODE)。但是,这可能会在将来发生变化。
修改当前动态配置
修改配置是通过 reconfig
命令完成的。有两种重新配置模式:增量和非增量(批量)。非增量模式仅指定系统的新的动态配置。增量模式指定对当前配置的更改。reconfig
命令返回新配置。
一些示例在:ReconfigTest.java、ReconfigRecoveryTest.java 和 TestReconfigServer.cc 中。
常规
删除服务器:可以删除任何服务器,包括领导者(尽管删除领导者会导致短暂的不可用,请参阅 论文 中的图 6 和 8)。服务器不会自动关闭。相反,它成为“无投票权的跟随者”。这有点类似于观察者,因为它的投票不计入提交操作所需的投票法定人数。但是,与无投票权的跟随者不同,观察者实际上看不到任何操作建议,也不会确认它们。因此,与观察者相比,无投票权的跟随者对系统吞吐量有更显着的负面影响。无投票权的跟随者模式仅应在关闭服务器或将其作为跟随者或观察者添加到集合之前用作临时模式。我们不自动关闭服务器主要有两个原因。第一个原因是我们不希望连接到此服务器的所有客户端立即断开连接,从而导致大量连接请求涌向其他服务器。相反,如果每个客户端独立决定何时迁移会更好。第二个原因是,有时(很少)可能需要删除服务器才能将其从“观察者”更改为“参与者”(这在 其他评论 一节中进行了说明)。
请注意,新配置应具有一定数量的参与者,才能被视为合法。如果提议的更改将使集群的参与者少于 2 个,并且已启用独立模式 (standaloneEnabled=true,请参阅部分 standaloneEnabled 标志),则不会处理重新配置 (BadArgumentsException)。如果已禁用独立模式 (standaloneEnabled=false),则保留 1 个或更多参与者是合法的。
添加服务器:在调用重新配置之前,管理员必须确保新配置中大多数参与者的法定人数已连接并与当前领导者同步。为此,我们需要在加入服务器正式成为集成的一部分之前将其连接到领导者。这是通过使用服务器的初始列表启动加入服务器来完成的,从技术上讲,该列表不是系统的合法配置,但 (a) 包含加入者,并且 (b) 向加入者提供足够的信息,以便其查找并连接到当前领导者。我们列出了一些安全执行此操作的不同选项。
- 加入者的初始配置由上次提交的配置中的服务器和一个或多个加入者组成,其中 加入者列为观察者。例如,如果服务器 D 和 E 同时添加到 (A, B, C) 中,并且服务器 C 正在被移除,则 D 的初始配置可以是 (A, B, C, D) 或 (A, B, C, D, E),其中 D 和 E 列为观察者。同样,E 的配置可以是 (A, B, C, E) 或 (A, B, C, D, E),其中 D 和 E 列为观察者。请注意,将加入者列为观察者实际上不会使他们成为观察者 - 它只会阻止他们意外地与其他加入者形成法定人数。相反,他们将联系当前配置中的服务器并采用上次提交的配置 (A, B, C),其中不存在加入者。加入者的配置文件会在发生这种情况时自动备份和替换。在连接到当前领导者后,加入者将成为没有投票权的跟随者,直到重新配置系统并将它们添加到集成中(作为参与者或观察者,视情况而定)。
- 每个加入者的初始配置由上次提交的配置中的服务器 + 加入者本身组成,列为参与者。例如,要将新服务器 D 添加到由服务器 (A, B, C) 组成的配置中,管理员可以使用由服务器 (A, B, C, D) 组成的初始配置文件启动 D。如果 D 和 E 同时添加到 (A, B, C) 中,则 D 的初始配置可以是 (A, B, C, D),而 E 的配置可以是 (A, B, C, E)。同样,如果同时添加 D 并移除 C,则 D 的初始配置可以是 (A, B, C, D)。切勿在初始配置中将多个加入者列为参与者(请参阅下面的警告)。
- 无论将参与者列为观察者还是参与者,只要当前领导者在列表中,不列出所有当前配置服务器也是可以的。例如,当添加 D 时,如果 A 是当前领导者,我们可以用仅包含 (A, D) 的配置文件启动 D。但是,这样做比较脆弱,因为如果 A 在 D 正式加入集群之前发生故障,D 将不认识其他人,因此管理员必须干预并使用另一个服务器列表重新启动 D。
注意
警告
切勿在同一初始配置中将多个加入服务器指定为参与者。当前,加入服务器不知道它们正在加入现有集群;如果将多个加入者列为参与者,它们可能会形成一个独立的法定人数,从而创建脑裂情况,例如独立于主集群处理操作。在初始配置中将多个加入者列为观察者是可以的。
如果现有服务器的配置发生更改或在加入者成功连接并了解配置更改之前变得不可用,则可能需要使用更新的配置文件重新启动加入者才能连接。
最后,请注意,一旦连接到领导者,加入者就会采用最后提交的配置,其中它不存在(加入者的初始配置在被重写之前会得到备份)。如果加入者在此状态下重新启动,它将无法启动,因为它不存在于其配置文件中。为了启动它,您将再次需要指定初始配置。
修改服务器参数:可以通过使用不同的参数将其添加到集群来修改服务器的任何端口或其角色(参与者/观察者)。这在增量和批量重新配置模式下都适用。没有必要删除服务器然后重新添加它;只需指定新参数,就像服务器尚未在系统中一样。服务器将检测到配置更改并执行必要的调整。请参阅增量模式部分中的示例,以及其他注释部分中对此规则的例外。
还可以更改集群使用的法定人数系统(例如,动态地将多数法定人数系统更改为分层法定人数系统)。但是,这仅允许使用批量(非增量)重新配置模式。通常,增量重新配置仅适用于多数法定人数系统。批量重新配置适用于分层和多数法定人数系统。
性能影响:删除跟随者时几乎没有性能影响,因为它不会自动关闭(删除的效果是服务器的投票不再被计算)。添加服务器时,没有领导者变更,也没有明显的性能中断。有关详细信息和图表,请参阅论文中的图 6、7 和 8。
最重大的中断将在以下情况之一导致领导者变更时发生
- 领导者从集群中移除。
- 领导者的角色已从参与者变为观察者。
- 领导者用于向其他人发送事务的端口(仲裁端口)已修改。
在这些情况下,我们会执行领导者交接,旧领导者会提名一位新领导者。由此产生的不可用时间通常比领导者崩溃时更短,因为无需检测领导者故障,并且在交接期间通常可以避免选举新领导者(请参阅 论文 中的图 6 和 8)。
当服务器的客户端端口被修改时,它不会断开现有的客户端连接。与服务器的新连接必须使用新的客户端端口。
进度保证:为了能够进行重新配置操作,需要有旧配置的仲裁组可用并连接到 ZooKeeper。一旦调用重新配置,则旧配置和新配置的仲裁组都必须可用。一旦(a)新配置被激活,并且(b)领导者在激活新配置之前安排的所有操作都被提交,最终转换就会发生。一旦(a)和(b)发生,则只需要新配置的仲裁组。但是,请注意,客户端既看不到(a),也看不到(b)。具体来说,当重新配置操作提交时,它只表示领导者已发出激活消息。它并不一定意味着新配置的仲裁组收到了此消息(这是激活它的必要条件)或(b)已发生。如果希望确保(a)和(b)都已发生(例如,为了知道关闭已删除的旧服务器是安全的),则可以简单地调用更新(set-data
或其他一些仲裁操作,但不是 sync
)并等待它提交。实现此目的的另一种方法是向重新配置协议引入另一轮(出于简单性和与 Zab 的兼容性,我们决定避免这样做)。
增量模式
增量模式允许向当前配置添加和删除服务器。允许进行多个更改。例如
> reconfig -remove 3 -add
server.5=125.23.63.23:1234:1235;1236
添加和删除选项都获取一个逗号分隔的参数列表(无空格)
> reconfig -remove 3,4 -add
server.5=localhost:2111:2112;2113,6=localhost:2114:2115:observer;2116
服务器语句的格式与 指定客户端端口 部分中描述的格式完全相同,并且包括客户端端口。请注意,在这里,您可以直接说“5=”,而不是“server.5=”。在上面的示例中,如果服务器 5 已在系统中,但具有不同的端口或不是观察者,则它将被更新,并且一旦配置提交,它将成为观察者并开始使用这些新端口。这是一种将参与者变成观察者,反之亦然,或更改其任何端口的简单方法,而无需重新启动服务器。
ZooKeeper 支持两种类型的仲裁系统——简单的多数系统(领导者在收到大多数选民的 ACK 后提交操作)和更复杂的层次系统,其中不同服务器的选票具有不同的权重,并且服务器被分为投票组。目前,仅当领导者已知的最后一个提议配置使用多数仲裁系统时,才允许进行增量重新配置(否则会抛出 BadArgumentsException)。
增量模式 - 使用 Java API 的示例
List<String> leavingServers = new ArrayList<String>();
leavingServers.add("1");
leavingServers.add("2");
byte[] config = zk.reconfig(null, leavingServers, null, -1, new Stat());
List<String> leavingServers = new ArrayList<String>();
List<String> joiningServers = new ArrayList<String>();
leavingServers.add("1");
joiningServers.add("server.4=localhost:1234:1235;1236");
byte[] config = zk.reconfig(joiningServers, leavingServers, null, -1, new Stat());
String configStr = new String(config);
System.out.println(configStr);
还提供了一个异步 API,以及一个接受用逗号分隔的字符串(而不是 List
非增量模式
重新配置的第二种模式是非增量的,在这种模式下,客户端会提供新动态系统配置的完整规范。新配置既可以在原处给出,也可以从文件中读取
> reconfig -file newconfig.cfg
//newconfig.cfg 是一个动态配置文件,请参阅 动态配置文件
> reconfig -members
server.1=125.23.63.23:2780:2783:participant;2791,server.2=125.23.63.24:2781:2784:participant;2792,server.3=125.23.63.25:2782:2785:participant;2793}}
新配置可以使用不同的仲裁系统。例如,即使当前集合使用多数仲裁系统,您也可以指定分层仲裁系统。
批量模式 - 使用 Java API 的示例
List<String> newMembers = new ArrayList<String>();
newMembers.add("server.1=1111:1234:1235;1236");
newMembers.add("server.2=1112:1237:1238;1239");
newMembers.add("server.3=1114:1240:1241:observer;1242");
byte[] config = zk.reconfig(null, null, newMembers, -1, new Stat());
String configStr = new String(config);
System.out.println(configStr);
还提供了一个异步 API,以及一个接受包含新成员的用逗号分隔的字符串(而不是 List
条件重新配置
有时(尤其是在非增量模式下),新提议的配置取决于客户端“认为”的当前配置,并且应该仅应用于该配置。具体来说,只有当领导者的最后一个配置具有指定版本时,reconfig
才会成功。
> reconfig -file <filename> -v <version>
在前面列出的 Java 示例中,可以指定配置版本以限定重新配置,而不是 -1。
错误条件
除了正常的 ZooKeeper 错误条件外,重新配置还可能由于以下原因而失败
- 目前正在进行另一个重新配置(ReconfigInProgress)
- 提议的更改将使集群的参与者少于 2 个,如果启用了独立模式,或者,如果禁用了独立模式,则保持 1 个或更多参与者是合法的(BadArgumentsException)
- 重新配置处理开始时,新配置的仲裁组未连接或未与领导者保持最新(NewConfigNoQuorum)
- 指定了
-v x
,但最新配置的版本y
不是x
(BadVersionException) - 请求了增量重新配置,但领导者的最后一个配置使用与多数系统不同的仲裁系统(BadArgumentsException)
- 语法错误(BadArgumentsException)
- 从文件中读取配置时出现 I/O 异常(BadArgumentsException)
ReconfigFailureCases.java 中的测试用例演示了其中的大部分异常。
其他注释
活性:为了更好地理解增量和非增量重新配置之间的区别,假设客户端 C1 向系统添加服务器 D,而另一个客户端 C2 添加服务器 E。在非增量模式下,每个客户端首先会调用 config
来找出当前配置,然后通过添加其自己的建议服务器在本地创建一个新的服务器列表。然后可以使用非增量 reconfig
命令提交新配置。在两个重新配置完成后,只有 E 或 D 之一会被添加(而不是两者),具体取决于哪个客户端的请求先到达领导者,并覆盖之前的配置。另一个客户端可以重复此过程,直到其更改生效。此方法保证了系统范围的进度(即,对于其中一个客户端),但不能确保每个客户端都成功。为了有更多控制权,C2 可能会请求仅在当前配置的版本未更改的情况下执行重新配置,如 条件重新配置 部分中所述。通过这种方式,如果 C1 的配置先到达领导者,它可以避免盲目覆盖 C1 的配置。
使用增量重新配置,两个更改都将生效,因为它们只是由领导者一个接一个地应用于当前配置,无论该配置是什么(假设第二个重新配置请求在领导者发送第一个重新配置请求的提交消息后到达领导者——目前,如果另一个重新配置已经处于挂起状态,领导者将拒绝提出重新配置)。由于保证两个客户端都会取得进展,因此此方法保证了更强的活性。在实践中,多个并发重新配置可能很少见。非增量重新配置目前是动态更改仲裁系统唯一的方法。增量配置目前仅允许与多数仲裁系统一起使用。
将观察者更改为跟随者:显然,如果发生错误 (2),即参与投票的服务器更改为观察者可能会失败,即,如果剩余的参与者数量少于允许的最小数量。但是,将观察者转换为参与者有时可能会因更微妙的原因而失败:例如,假设当前配置为 (A, B, C, D),其中 A 是领导者,B 和 C 是跟随者,而 D 是观察者。此外,假设 B 已崩溃。如果提交了一个重新配置,其中 D 被称为跟随者,则它将失败并出现错误 (3),因为在这个配置中,新配置中的大多数选民(任何 3 个选民)必须连接并与领导者保持最新状态。观察者无法确认重新配置期间发送的历史记录前缀,因此它不计入这 3 个必需的服务器,并且重新配置将被中止。如果发生这种情况,客户端可以通过两个重新配置命令来完成相同任务:首先调用重新配置以从配置中删除 D,然后调用第二个命令以将其作为参与者(跟随者)添加回来。在中间状态下,D 是一个无投票权的跟随者,并且可以确认在第二个重新配置命令期间执行的状态传输。
重新平衡客户端连接
当启动 ZooKeeper 集群时,如果每个客户端都给出了相同的连接字符串(服务器列表),客户端将随机选择列表中的一个服务器进行连接,这使得每个服务器的预期客户端连接数相同。我们实现了一种方法,当服务器集通过重新配置发生变化时,该方法会保留此属性。请参阅 论文 中的第 4 节和第 5.1 节。
为了使该方法起作用,所有客户端都必须订阅配置更改(通过直接或通过 getConfig
API 命令在 /zookeeper/config 上设置监视)。当监视触发时,客户端应通过调用 sync
和 getConfig
读取新配置,如果配置确实为新配置,则调用 updateServerList
API 命令。为了避免同时进行大规模客户端迁移,最好让每个客户端在调用 updateServerList
之前随机休眠一小段时间。
可以在以下位置找到一些示例:StaticHostProviderTest.java 和 TestReconfig.cc
示例(这不是一个配方,而是一个简化的示例,只是为了解释一般思想)
public void process(WatchedEvent event) {
synchronized (this) {
if (event.getType() == EventType.None) {
connected = (event.getState() == KeeperState.SyncConnected);
notifyAll();
} else if (event.getPath()!=null && event.getPath().equals(ZooDefs.CONFIG_NODE)) {
// in prod code never block the event thread!
zk.sync(ZooDefs.CONFIG_NODE, this, null);
zk.getConfig(this, this, null);
}
}
}
public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
if (path!=null && path.equals(ZooDefs.CONFIG_NODE)) {
String config[] = ConfigUtils.getClientConfigStr(new String(data)).split(" "); // similar to config -c
long version = Long.parseLong(config[0], 16);
if (this.configVersion == null){
this.configVersion = version;
} else if (version > this.configVersion) {
hostList = config[1];
try {
// the following command is not blocking but may cause the client to close the socket and
// migrate to a different server. In practice it's better to wait a short period of time, chosen
// randomly, so that different clients migrate at different times
zk.updateServerList(hostList);
} catch (IOException e) {
System.err.println("Error updating server list");
e.printStackTrace();
}
this.configVersion = version;
}
}
}