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"。与往常一样,角色也是可选的,可以是participantobserver(默认为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配置选项,默认情况下可以完全禁用重新配置功能,任何通过reconfig 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的静态配置文件中,因为它不是冗余配置——该参数并未按照Changes to Configuration Format章节说明的格式作为"server.1=..."的一部分进行指定。如果执行了重新配置并设置了服务器1的客户端端口,我们会从静态配置文件中移除"clientPort=2791"(此时动态文件已将该信息作为服务器1规范的一部分包含在内)。

升级到3.5.0版本

将正在运行的ZooKeeper集群升级到3.5.0版本前,必须先将集群升级到3.4.6版本。请注意,这仅适用于滚动升级场景(如果您可以接受完全关闭系统,则无需经过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=..."服务器声明都包含客户端端口(参见章节Specifying the client port)。如前所述,您可以保留单文件配置形式,同时保留clientPort/clientPortAddress声明(不过如果您采用新格式指定客户端端口,这些声明现在就是冗余的)。

ZooKeeper集群的动态重新配置

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

API

Java和C客户端都有两套API接口。

安全性

3.5.3版本之前,reconfig操作没有强制性的安全机制,因此任何能够连接到ZooKeeper服务器集群的客户端都有能力通过reconfig来改变ZooKeeper集群的状态。这意味着恶意客户端有可能向集群中添加被入侵的服务器,例如添加被攻陷的服务器,或移除合法的服务器。这类情况根据具体情况可能构成安全漏洞。

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

我们还为在安全环境(如公司防火墙内)中操作和与ZooKeeper集群交互的用户提供了一个应急方案。对于那些希望使用重新配置功能但不想为重新配置访问检查配置显式授权用户列表的用户,可以将"skipACL"设置为"yes",这将跳过ACL检查并允许任何用户重新配置集群。

总体而言,ZooKeeper为重新配置功能提供了灵活的配置选项,允许用户根据自身安全需求进行选择。我们将采取适当安全措施的决策权留给用户自行判断。

获取当前的动态配置

动态配置存储在一个特殊的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.javaReconfigRecoveryTest.javaTestReconfigServer.cc 中。

通用设置

移除服务器:任何服务器都可以被移除,包括领导者(尽管移除领导者会导致短暂不可用,详见论文中的图6和图8)。服务器不会自动关闭,而是会转变为"无投票权跟随者"。这种状态与观察者类似,因为其投票不计入提交操作所需的法定票数。但与无投票权跟随者不同,观察者实际上不会收到任何操作提案,也不会进行ACK确认。因此相比观察者,无投票权跟随者对系统吞吐量的负面影响更为显著。无投票权跟随者模式应仅作为临时状态使用,之后需要关闭该服务器,或将其重新添加为集群中的跟随者/观察者。我们不自动关闭服务器主要有两个原因:首先是为了避免连接到该服务器的所有客户端立即断开,导致其他服务器遭遇连接请求洪流;其次是因为有时(虽然罕见)可能需要通过移除服务器来将其从"观察者"转为"参与者"(详见补充说明章节)。

请注意,新配置必须包含最低数量的参与者才能被视为合法。如果提议的变更会导致集群参与者少于2个且启用了独立模式(standaloneEnabled=true,请参阅章节The standaloneEnabled flag),则不会处理该重新配置请求(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. 每个新加入节点的初始配置由最近提交配置中的服务器组成,同时将该新节点本身列为参与者。例如,要向由服务器(A, B, C)组成的配置中添加新服务器D,管理员可以使用包含服务器(A, B, C, D)的初始配置文件启动D。如果同时向(A, B, C)添加D和E两个节点,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。
注意
警告

切勿在同一个初始配置中将多个加入服务器指定为参与者。当前,加入服务器并不知道它们正在加入一个现有的集群;如果将多个加入服务器列为参与者,它们可能会形成一个独立的仲裁组,导致脑裂情况,例如与主集群独立处理操作。在初始配置中将多个加入服务器列为观察者是可以的。

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

最后需要注意的是,一旦连接到领导者,加入者将采用最后一个已提交的配置,而该配置中并不包含该加入者(加入者的初始配置在被重写前已备份)。如果加入者在这种状态下重启,由于配置文件中不存在该节点,它将无法启动。要使其重新启动,您必须再次指定初始配置。

修改服务器参数: 您可以通过将服务器以不同参数添加到集群中来修改其任意端口或角色(参与者/观察者)。此操作在增量模式和批量重新配置模式下均适用。无需先移除服务器再重新添加;只需像该服务器尚未加入系统一样指定新参数即可。服务器将检测配置变更并执行必要的调整。示例请参阅增量模式章节,例外情况请参见补充说明章节。

也可以动态更改集群使用的仲裁系统(例如,将多数仲裁系统更改为分层仲裁系统)。然而,这仅允许使用批量(非增量)重新配置模式。一般来说,增量重新配置仅适用于多数仲裁系统。批量重新配置同时适用于分层和多数仲裁系统。

性能影响: 移除一个follower实际上不会对性能产生影响,因为它不会被自动关闭(移除的效果是该服务器的投票不再被计入)。添加服务器时,不会发生leader变更,也不会造成明显的性能中断。详情和图表请参阅论文中的图6、7和8。

当发生领导者变更时,将造成最严重的中断,通常由以下情况之一引发:

  1. Leader 从集群中被移除。
  2. Leader的角色从参与者变更为观察者。
  3. 领导者用于向其他节点发送事务的端口(仲裁端口)已被修改。

在这些情况下,我们会执行领导者交接流程,由原领导者提名新领导者。由此产生的不可用时间通常比领导者崩溃时更短,因为在交接过程中无需检测领导者故障,且通常可以避免选举新领导者的环节(详见论文中的图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

服务器声明的格式与Specifying the client port章节中描述的完全一致,包含客户端端口。注意这里可以用"5="替代"server.5="。在上例中,如果服务器5已存在于系统中但使用不同端口或非观察者状态,配置提交后将被更新为观察者并开始使用新端口。这种方式无需重启服务器即可实现参与者与观察者角色切换或端口变更。

ZooKeeper支持两种类型的仲裁系统——简单多数系统(领导者收到大多数投票者的确认后提交操作)和更复杂的分层系统(不同服务器的投票具有不同权重,服务器被划分为投票组)。目前,仅当领导者已知的最新提议配置使用多数仲裁系统时,才允许增量重新配置(否则会抛出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的API。具体请参阅src/java/main/org/apache/zookeeper/ZooKeeper.java。

非增量模式

第二种重新配置模式是非增量式的,即客户端提供新的动态系统配置的完整规范。新配置可以直接指定或从文件中读取:

> reconfig -file newconfig.cfg

//newconfig.cfg 是一个动态配置文件,详见 Dynamic configuration file

> 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)作为新成员参数的API。具体参见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可以要求仅在当前配置版本未变更时执行重新配置。通过这种方式,若C1的配置先到达领导者节点,C2就能避免盲目覆盖C1的配置。

通过增量重新配置,这两项变更都将生效,因为领导者会依次将它们应用到当前配置中,无论当前配置是什么(假设第二个重新配置请求在领导者发送第一个重新配置请求的提交消息后到达——目前如果已有另一个重新配置待处理,领导者将拒绝提议重新配置)。由于保证两个客户端都能取得进展,这种方法提供了更强的活跃性保证。实际上,多个并发重新配置的情况可能较为罕见。目前非增量重新配置是动态更改仲裁系统的唯一方式。增量配置目前仅允许在多数仲裁系统中使用。

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

重新平衡客户端连接

当ZooKeeper集群启动时,如果为每个客户端提供相同的连接字符串(服务器列表),客户端会随机选择列表中的服务器进行连接,这使得每台服务器预期的客户端连接数量保持均衡。我们实现了一种方法,在通过重新配置更改服务器集合时仍能保持这一特性。详见论文中的第4节和第5.1节。

为了使该方法生效,所有客户端必须订阅配置变更(通过在/zookeeper/config上直接设置监视或通过getConfig API命令)。当监视被触发时,客户端应通过调用syncgetConfig来读取新配置,如果确实是新配置,则调用updateServerList API命令。为避免大量客户端同时迁移,最好让每个客户端在调用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;
        }
    }
}