Apache > ZooKeeper
 

ZooKeeper 动态重新配置

概述

在 3.5.0 版本之前,Zookeeper 的成员资格和所有其他配置参数都是静态的——在启动期间加载,在运行时不可变。运营商采用了“滚动重启”——一种手动密集且容易出错的方法来更改导致数据丢失和生产不一致的配置。

从 3.5.0 开始,不再需要“滚动重启”!ZooKeeper 完全支持自动配置更改:Zookeeper 服务器集、它们的角色(参与者/观察者)、所有端口,甚至仲裁系统都可以动态更改,而不会中断服务并保持数据一致性。重新配置会立即执行,就像 ZooKeeper 中的其他操作一样。可以使用单个重新配置命令完成多项更改。动态重新配置功能不限制操作并发性,在重新配置期间不需要停止客户端操作,对于管理员来说具有非常简单的界面,并且不会增加其他客户端操作的复杂性。

新的客户端功能允许客户端了解配置更改并更新存储在其 ZooKeeper 句柄中的连接字符串(服务器及其客户端端口列表)。概率算法用于在新的配置服务器之间重新平衡客户端,同时保持客户端迁移的程度与集成成员的变化成比例。

本文档提供重新配置的管理员手册。有关重新配置算法、性能测量等的详细描述,请参阅我们的论文:

注意:从 3.5.3 开始,动态重新配置功能默认禁用,必须通过reconfigEnabled配置选项显式打开。

配置格式的更改

指定客户端端口

服务器的客户端端口是服务器接受客户端连接请求的端口。从 3.5.0 开始,不应再使用clientPortclientPortAddress配置参数。相反,此信息现在是服务器关键字规范的一部分,如下所示:

server.<positive id> = <address1>:<port1>:<port2>[:role];[<client port address>:]<client port>**

客户端端口规范位于分号右侧。客户端端口地址是可选的,如果未指定,则默认为“0.0.0.0”。像往常一样,角色也是可选的,它可以是参与者观察者(默认为参与者)。

合法服务器声明示例:

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配置选项,以便可以完全禁用重新配置功能,并且默认情况下,通过重新配置 API 重新配置集群的任何尝试无论是否经过身份验证都会失败,除非reconfigEnabled设置为true

要将选项设置为 true,配置文件 (zoo.cfg) 应包含:

reconfigEnabled=true

动态配置文件

从 3.5.0 开始,我们区分了可以在运行时更改的动态配置参数和在服务器启动时从配置文件中读取并且在执行期间不更改的静态配置参数。目前,以下配置关键字被视为动态配置的一部分:servergroupweight

动态配置参数存储在服务器上的一个单独文件中(我们称之为动态配置文件)。该文件使用新的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

只有在将 ensemble 升级到 3.4.6 版本后,才能将正在运行的 ZooKeeper ensemble 升级到 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 Ensemble 的动态重新配置

ZooKeeper Java 和 C API 使用有助于重新配置的 getConfig 和 reconfig 命令进行了扩展。这两个命令都有一个同步(阻塞)变体和一个异步变体。我们在这里使用 Java CLI 演示这些命令,但请注意,您可以类似地使用 C CLI 或直接从程序调用命令,就像任何其他 ZooKeeper 命令一样。

API

Java 和 C 客户端都有两组 API。

安全

3.5.3之前,重新配置没有强制的安全机制,因此任何可以连接到 ZooKeeper 服务器集成的 ZooKeeper 客户端都可以通过重新配置更改 ZooKeeper 集群的状态。因此,恶意客户端有可能将受损服务器添加到集合中,例如,添加受损服务器或移除合法服务器。根据具体情况,此类情况可能是安全漏洞。

为了解决这个安全问题,我们从3.5.3开始引入了对 reconfig 的访问控制,这样只有一组特定的用户可以使用 reconfig 命令或 API,并且这些用户需要显式配置。此外,ZooKeeper 集群的设置必须启用身份验证,以便 ZooKeeper 客户端可以进行身份​​验证。

我们还为在安全环境(即公司防火墙后面)中操作 ZooKeeper 集合并与之交互的用户提供逃生舱口。对于那些想要使用重新配置功能但不希望为重新配置访问检查配置明确的授权用户列表的开销的用户,他们可以将“skipACL”设置为“yes”,这将跳过 ACL 检查并允许任何用户重新配置簇。

总体而言,ZooKeeper 为重新配置功能提供了灵活的配置选项,允许用户根据用户的安全要求进行选择。我们让用户自行决定是否采取适当的安全措施。

检索当前动态配置

动态配置存储在一个特殊的 znode ZooDefs.CONFIG_NODE = /zookeeper/config 中。新的configCLI 命令读取这个 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。第一个建立配置的版本等于第一个成功建立的leader发送的NEWLEADER消息的zxid。将配置写入动态配置文件时,版本会自动成为文件名的一部分,并且静态配置文件会更新为新动态配置文件的路径。与较早版本对应的配置文件将保留用于备份目的。

在引导期间,版本(如果存在)从文件名中提取。用户或系统管理员不得手动更改版本。系统使用它来了解哪个配置是最新的。手动操作可能会导致数据丢失和不一致。

就像get命令一样,configCLI 命令接受-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.javaReconfigRecoveryTest.javaTestReconfigServer.cc

一般的

移除服务器:可以移除任何服务器,包括leader(虽然移除leader会导致短暂的不可用,参见论文中的图6和图8)。服务器不会自动关闭。相反,它变成了“无投票权的追随者”。这有点类似于观察者,因为它的投票不计入提交操作所需的法定人数。然而,与没有投票权的追随者不同,观察者实际上并没有看到任何操作建议,也不会确认它们。因此,与观察者相比,无投票权的追随者对系统吞吐量有更显着的负面影响。非投票追随者模式只能用作临时模式,在关闭服务器之前,或者将其作为追随者或作为观察者添加到集合中。我们不会自动关闭服务器有两个主要原因。第一个原因是我们不希望所有连接到该服务器的客户端立即断开连接,导致对其他服务器的连接请求泛滥。相反,最好让每个客户端独立决定何时迁移。第二个原因是有时(很少)需要删除服务器才能将其从“观察者”更改为“参与者”(这在第附加评论)。

请注意,新配置应该有一些最少的参与者才能被认为是合法的。如果提议的更改会使集群的参与者少于 2 并且启用了独立模式(standaloneEnabled=true,请参阅 StandaloneEnabled 标志部分不会处理重新配置(BadArgumentsException)。如果禁用独立模式 (standaloneEnabled=false),则保持 1 个或多个参与者是合法的。

添加服务器:在调用重新配置之前,管理员必须确保新配置中的法定人数(大多数)参与者已经连接并与当前领导者同步。为了实现这一点,我们需要将一个新的加入服务器连接到领导者,然后才能正式成为整体的一部分。这是通过使用初始服务器列表启动加入服务器来完成的,这在技术上不是系统的合法配置,但 (a) 包含加入者,并且 (b) 向加入者提供足够的信息,以便它找到并连接给现任领导。我们列出了一些安全地执行此操作的不同选项。

  1. 加入者的初始配置由最后提交配置中的服务器和一个或多个加入者组成,其中加入者被列为观察者。例如,如果服务器 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),其中加入者不存在。发生这种情况时,会自动备份和替换加入者的配置文件。连接到当前领导者后,加入者将成为无投票权的追随者,直到重新配置系统并将他们添加到集合中(作为参与者或观察者,视情况而定)。
  2. 每个加入者的初始配置由最后提交的配置中的服务器 +加入者本身组成,列为参与者。例如,要将新服务器 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)。切勿在初始配置中将多个加入者列为参与者(请参阅下面的警告)。
  3. 无论将加入者列为观察者还是参与者,不列出所有当前配置服务器也可以,只要当前领导者在列表中即可。例如,当添加 D 时,如果 A 是当前领导者,我们可以从仅包含 (A, D) 的配置文件开始 D。然而,这更加脆弱,因为如果 A 在 D 正式加入集合之前失败,D 不认识其他任何人,因此管理员将不得不干预并使用另一个服务器列表重新启动 D。
笔记
警告

切勿在与参与者相同的初始配置中指定多个加入服务器。目前,加入服务器不知道他们正在加入现有的集合;如果多个加入者被列为参与者,他们可能会形成一个独立的法定人数,从而造成脑裂的情况,例如独立于您的主集合处理操作。可以在初始配置中将多个加入者列为观察者。

如果现有服务器的配置发生更改或在加入者成功连接并了解配置更改之前它们变得不可用,则可能需要使用更新的配置文件重新启动加入者才能连接。

最后,请注意,一旦连接到领导者,joiner 将采用最后提交的配置,其中不存在(在重写之前备份了 joiner 的初始配置)。如果 joiner 在此状态下重新启动,它将无法启动,因为它在其配置文件中不存在。为了启动它,您必须再次指定初始配置。

修改服务器参数:可以通过将服务器添加到具有不同参数的集合中来修改服务器的任何端口或其角色(参与者/观察者)。这适用于增量和批量重新配置模式。无需删除服务器然后重新添加;只需指定新参数,就好像服务器尚未在系统中一样。服务器将检测配置更改并执行必要的调整。请参阅增量模式部分中的示例和附加注释部分中此规则的例外。

也可以更改 ensemble 使用的仲裁系统(例如,即时将多数仲裁系统更改为分层仲裁系统)。但是,这仅允许使用批量(非增量)重新配置模式。通常,增量重新配置仅适用于多数仲裁系统。批量重新配置适用于分层和多数仲裁系统。

性能影响:移除追随者时实际上没有性能影响,因为它不会自动关闭(移除的影响是不再计算服务器的投票)。添加服务器时,没有领导者变化,也没有明显的性能中断。有关详细信息和图表,请参见论文中的图 6、7 和 8 。

当领导者发生变化时,最严重的中断将发生在以下情况之一:

  1. 领导者从合奏中移除。
  2. 领导者的角色从参与者变为观察者。
  3. 领导者用来向其他人发送事务的端口(仲裁端口)被修改。

在这些情况下,我们执行领导者交接,旧领导者提名新领导者。由此产生的不可用性通常比领导者崩溃时更短,因为检测领导者失败是不必要的,并且通常可以避免在交接期间选举新领导者(参见论文中的图 6 和图 8 )。

当服务器的客户端端口被修改时,它不会丢弃现有的客户端连接。到服务器的新连接必须使用新的客户端端口。

进度保证:在调用 reconfig 操作之前,需要一定数量的旧配置可用并连接,ZooKeeper 才能取得进展。调用 reconfig 后,旧配置和新配置的法定人数必须可用。最终转换发生在 (a) 新配置被激活,并且 (b) 在新配置被领导者激活之前安排的所有操作都已提交。一旦 (a) 和 (b) 发生,只需要新配置的法定人数。但是请注意,(a) 和 (b) 对客户都是不可见的。具体来说,当重新配置操作提交时,仅意味着领导者发出了激活消息。这并不一定意味着新配置的法定人数收到了此消息(激活它所必需的)或 (b) 已经发生。如果想要确保 (a) 和 (b) 都已经发生(例如,为了知道关闭被移除的旧服务器是安全的),可以简单地调用更新 (set-data, 或其他一些仲裁操作,但不是 a sync) 并等待它提交。实现这一点的另一种方法是在重新配置协议中引入另一轮(为了简单和与 Zab 的兼容性,我们决定避免使用)。

增量模式

增量模式允许在当前配置中添加和删除服务器。允许多次更改。例如:

> reconfig -remove 3 -add
server.5=125.23.63.23:1234:1235;1236

add 和 remove 选项都得到一个逗号分隔的参数列表(没有空格):

> reconfig -remove 3,4 -add
server.5=localhost:2111:2112;2113,6=localhost:2114:2115:observer;2116

服务器语句的格式与指定客户端端口一节中描述的完全相同,并且包括客户端端口。请注意,在这里,您可以只说“5=”而不是“server.5=”。在上面的例子中,如果服务器 5 已经在系统中,但是有不同的端口或者不是观察者,它会被更新并且一旦配置提交成为观察者并开始使用这些新端口。这是一种将参与者转变为观察者(反之亦然)或更改其任何端口的简单方法,而无需重新启动服务器。

ZooKeeper 支持两种类型的 Quorum 系统 - 简单的多数系统(领导者在收到大多数选民的 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,以及一个接受逗号分隔的字符串而不是列表的 API. 参见 src/java/main/org/apache/zookeeper/ZooKeeper.java。

非增量模式

重新配置的第二种模式是非增量的,即客户端提供新动态系统配置的完整规范。新配置既可以在适当的位置给出,也可以从文件中读取:

> 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}}

新配置可能使用不同的仲裁系统。例如,即使当前 ensemble 使用多数仲裁系统,您也可以指定分层仲裁系统。

批量模式 - 使用 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,一个 API 接受逗号分隔的 String 包含新成员而不是 List. 参见 src/java/main/org/apache/zookeeper/ZooKeeper.java。

有条件的重新配置

有时(尤其是在非增量模式下)新的提议配置取决于客户端“认为”的当前配置,并且应该只应用于该配置。具体来说,reconfig只有当领导者的最后一个配置具有指定的版本时,才会成功。

> reconfig -file <filename> -v <version>

在前面列出的 Java 示例中,可以指定配置版本来代替 -1 来调节重新配置。

错误条件

除了正常的 ZooKeeper 错误情况外,重新配置可能由于以下原因而失败:

  1. 当前正在进行另一个重新配置 (ReconfigInProgress)
  2. 如果启用了独立模式,则提议的更改将使集群的参与者少于 2 个,或者,如果禁用独立模式,则保留 1 个或多个参与者是合法的 (BadArgumentsException)
  3. 重新配置处理开始时,没有新配置的法定人数与领导者连接并保持最新 (NewConfigNoQuorum)
  4. -v x已指定,但y最新配置的版本不是x(BadVersionException)
  5. 请求增量重新配置,但领导者的最后一个配置使用与多数系统不同的仲裁系统 (BadArgumentsException)
  6. 语法错误 (BadArgumentsException)
  7. 从文件中读取配置时出现 I/O 异常 (BadArgumentsException)

其中大部分由ReconfigFailureCases.java中的测试用例说明。

补充评论

活跃性:为了更好地理解增量和非增量重新配置之间的区别,假设客户端 C1 将服务器 D 添加到系统中,而另一个客户端 C2 添加服务器 E。在非增量模式下,每个客户端将首先调用config以找出当前配置,然后通过添加自己的建议服务器在本地创建新的服务器列表。然后可以使用非增量提交新配置reconfig命令。在两次重新配置完成后,将仅添加 E 或 D 中的一个(不是同时添加),具体取决于哪个客户端的请求第二个到达领导者,覆盖之前的配置。其他客户端可以重复该过程,直到其更改生效。此方法保证系统范围内的进度(即,对于其中一个客户端),但不能确保每个客户端都成功。要获得更多控制权,C2 可能会请求仅在当前配置的版本未更改的情况下执行重新配置,如Conditional reconfig部分所述。这样可以避免在C1的配置先到达leader的情况下盲目覆盖C1的配置。

使用增量重新配置,这两个更改都将生效,因为它们只是由领导者一个接一个地应用于当前配置,无论是什么(假设第二个重新配置请求在它为第一个重新配置请求发送提交消息后到达领导者-- 目前,如果另一个已经挂起,领导者将拒绝提出重新配置)。由于保证两个客户端都取得进展,因此这种方法保证了更强的活性。在实践中,多个并发重新配置可能很少见。非增量重新配置是当前动态更改仲裁系统的唯一方法。增量配置目前仅允许使用多数仲裁系统。

将观察者变为追随者:显然,如果发生错误 (2),即如果剩余的参与者少于最小允许数量,则将参与投票的服务器更改为观察者可能会失败。但是,将观察者转换为参与者有时可能会因为更微妙的原因而失败:例如,假设当前配置是 (A, B, C, D),其中 A 是领导者,B 和 C 是追随者,D是观察者。另外,假设B已经崩溃。如果在 D 被称为跟随者的情况下提交了重新配置,它将失败并出现错误 (3),因为在此配置中,新配置中的大多数投票者(任何 3 个投票者)必须连接并且是最新的与领导。观察者无法确认在重新配置期间发送的历史前缀,因此它不计入这 3 个必需的服务器,并且重新配置将被中止。如果发生这种情况,客户端可以通过两个重新配置命令来完成相同的任务:首先调用重新配置以从配置中删除 D,然后调用第二个命令将其添加回参与者(跟随者)。在中间状态期间,D 是一个无投票的追随者,并且可以确认在第二个重新配置命令期间执行的状态转移。

重新平衡客户端连接

当一个 ZooKeeper 集群启动时,如果给每个客户端相同的连接字符串(服务器列表),客户端将在列表中随机选择一个服务器进行连接,这使得每个服务器的预期客户端连接数相同的服务器。我们实现了一种方法,当服务器集通过重新配置发生更改时,该方法保留了此属性。请参阅本文第 4 节和第 5.1 节。

为了使该方法起作用,所有客户端都必须订阅配置更改(通过直接或通过getConfigAPI 命令在 /zookeeper/config 上设置监视)。当手表被触发时,客户端应该通过调用读取新配置syncgetConfig如果配置确实是新的,则调用updateServerListAPI 命令。为避免同时大量客户端迁移,最好让每个客户端在调用updateServerList.

可以在以下位置找到一些示例:StaticHostProviderTest.javaTestReconfig.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;
        }
    }
}