文档:复制

在 Valkey 复制的基础(不包括 Valkey 集群或 Valkey Sentinel 作为附加层提供的高可用性功能)上,存在一种易于使用和配置的主从(primary-replica)复制。它允许 Valkey 副本实例成为主实例的精确副本。无论主实例发生什么情况,只要连接中断,副本就会自动重新连接到主实例,并尝试成为其精确副本。

该系统通过三种主要机制工作

  1. 当主实例和副本实例连接良好时,主实例通过向副本发送命令流来保持副本更新,以复制由于客户端写入、键过期或逐出、以及任何其他改变主实例数据集的操作而在主实例端发生的数据集变化。
  2. 当主实例和副本实例之间的链接由于网络问题或主实例或副本实例检测到超时而中断时,副本实例会重新连接并尝试进行部分重同步:这意味着它将尝试仅获取在断开连接期间丢失的命令流部分。
  3. 当无法进行部分重同步时,副本实例将请求完全重同步。这将涉及一个更复杂的过程,其中主实例需要创建其所有数据的快照,将其发送给副本实例,然后继续发送命令流,因为数据集会发生变化。

Valkey 默认使用异步复制,其低延迟和高性能使其成为绝大多数 Valkey 用例的自然复制模式。然而,Valkey 副本会定期异步向主实例确认其接收到的数据量。因此,主实例不会每次都等待命令被副本处理,但如果需要,它知道哪个副本已经处理了哪个命令。这允许有可选的同步复制。

客户端可以使用 WAIT 命令请求某些数据的同步复制。然而,WAIT 只能确保在其他 Valkey 实例中有指定数量的已确认副本,它不会将一组 Valkey 实例变成一个具有强一致性的 CP 系统:根据 Valkey 持久化的具体配置,已确认的写入在故障转移期间仍然可能丢失。但是,通过 WAIT,在故障事件后丢失写入的可能性大大降低到某些难以触发的故障模式。

您可以查看 Valkey Sentinel 或 Valkey 集群文档,了解有关高可用性和故障转移的更多信息。本文档的其余部分主要描述 Valkey 基本复制的基本特性。

关于 Valkey 复制的重要事实

  • Valkey 使用异步复制,副本异步向主实例确认已处理的数据量。
  • 一个主实例可以有多个副本。
  • 副本能够接受来自其他副本的连接。除了将多个副本连接到同一个主实例外,副本还可以以级联结构连接到其他副本。所有子副本将从主实例接收到完全相同的复制流。
  • Valkey 复制在主实例端是非阻塞的。这意味着当一个或多个副本执行初始同步或部分重同步时,主实例将继续处理查询。
  • 复制在副本端也主要是非阻塞的。当副本执行初始同步时,如果您的 valkey.conf 配置了 Valkey 这样做,它可以处理使用旧版本数据集的查询。否则,您可以配置 Valkey 副本在复制流中断时向客户端返回错误。然而,在初始同步之后,必须删除旧数据集并加载新数据集。在此短暂的时间窗口内(对于非常大的数据集可能长达数秒),副本将阻塞传入连接。您可以配置 Valkey,使旧数据集的删除在另一个线程中进行,但是加载新的初始数据集仍将在主线程中进行并阻塞副本。
  • 复制既可以用于可伸缩性,为只读查询(例如,可以将缓慢的 O(N) 操作卸载到副本)提供多个副本,也可以仅仅用于提高数据安全性和高可用性。
  • 您可以使用复制来避免主实例将完整数据集写入磁盘的开销:一种典型技术是配置主实例的 valkey.conf 以完全避免持久化到磁盘,然后连接一个配置为不时保存或启用 AOF 的副本。然而,必须谨慎处理此设置,因为重新启动的主实例将以空数据集开始:如果副本尝试与其同步,副本也将被清空。

主实例关闭持久化时的复制安全性

在使用 Valkey 复制的设置中,强烈建议在主实例和副本实例上都启用持久化。当无法实现这一点时,例如由于磁盘速度过慢导致的延迟问题,应将实例配置为避免自动重新启动

为了更好地理解为什么关闭持久化并配置为自动重新启动的主实例是危险的,请查看以下故障模式,其中主实例及其所有副本中的数据都将被清除

  1. 我们有一个设置,节点 A 作为主实例,关闭了持久化,节点 B 和 C 从节点 A 复制。
  2. 节点 A 崩溃,但它有一个自动重启系统,会重新启动进程。然而,由于持久化已关闭,节点会以空数据集重新启动。
  3. 节点 B 和 C 将从空的节点 A 复制,因此它们将有效地销毁其数据副本。

当使用 Valkey Sentinel 实现高可用性时,关闭主实例上的持久化并同时自动重启进程也是危险的。例如,主实例重启速度可能快到 Sentinel 未检测到故障,从而发生上述故障模式。

每当数据安全至关重要,并且复制与未配置持久化的主实例一起使用时,应禁用实例的自动重启。

Valkey 复制的工作原理

每个 Valkey 主实例都有一个复制 ID:它是一个大的伪随机字符串,标记数据集的给定历史。每个主实例还会获取一个偏移量,该偏移量会随着其生成并发送给副本的复制流的每个字节而增加,以用修改数据集的新更改来更新副本的状态。即使没有副本实际连接,复制偏移量也会增加,因此基本上每对给定的

Replication ID, offset

标识主实例数据集的精确版本。

当副本连接到主实例时,它们使用 PSYNC 命令发送其旧的主实例复制 ID 和它们迄今为止处理的偏移量。这样,主实例就可以只发送所需的增量部分。但是,如果主实例缓冲区中没有足够的积压日志,或者副本引用的历史(复制 ID)不再已知,则会发生完全重同步:在这种情况下,副本将从头开始获取数据集的完整副本。

以下是完全同步的详细工作方式

主实例启动一个后台保存进程以生成 RDB 文件。同时,它开始缓冲从客户端接收到的所有新写入命令。当后台保存完成后,主实例将数据库文件传输给副本,副本将其保存到磁盘,然后加载到内存中。然后,主实例将所有缓冲的命令发送给副本。这是以命令流的形式完成的,其格式与 Valkey 协议本身相同。

您可以通过 telnet 亲自尝试。在服务器工作时连接到 Valkey 端口并发出 SYNC 命令。您将看到一次批量传输,然后主实例接收到的每个命令都将在 telnet 会话中重新发出。实际上,SYNC 是一个较旧的协议,不再被较新的 Valkey 实例使用,但为了向后兼容性仍然存在:它不允许部分重同步,所以现在改用 PSYNC

如前所述,当主从链接因某种原因中断时,副本能够自动重新连接。如果主实例接收到多个并发的副本同步请求,它会执行一次后台保存以服务所有这些请求。

复制 ID 解释

在上一节中,我们提到如果两个实例具有相同的复制 ID 和复制偏移量,它们就拥有完全相同的数据。然而,了解复制 ID 究竟是什么,以及为什么实例实际上有两个复制 ID:主 ID 和辅助 ID,这很有用。

复制 ID 基本上标记了数据集的给定历史。每次实例从头开始作为主实例重新启动,或者副本被提升为主实例时,都会为该实例生成一个新的复制 ID。连接到主实例的副本在握手后将继承其复制 ID。因此,两个具有相同 ID 的实例通过它们拥有相同数据的事实相关联,但可能在不同的时间点。偏移量充当逻辑时间,用于在给定历史(复制 ID)中理解谁拥有最新的数据集。

例如,如果两个实例 A 和 B 具有相同的复制 ID,但一个的偏移量为 1000,另一个为 1023,这意味着前者缺少应用于数据集的某些命令。这也意味着 A 通过应用少量命令就可以达到与 B 完全相同的状态。

Valkey 实例拥有两个复制 ID 的原因是副本被提升为主实例。故障转移后,被提升的副本需要仍然记住其过去的复制 ID,因为该复制 ID 是前一个主实例的复制 ID。通过这种方式,当其他副本与新的主实例同步时,它们将尝试使用旧的主实例复制 ID 执行部分重同步。这将按预期工作,因为当副本被提升为主实例时,它会将其辅助 ID 设置为其主 ID,记住此 ID 切换发生时的偏移量。稍后它将选择一个新的随机复制 ID,因为新的历史开始了。在处理新连接的副本时,主实例将它们的 ID 和偏移量与当前 ID 和辅助 ID(为了安全,最高可达给定偏移量)进行匹配。简而言之,这意味着在故障转移后,连接到新晋主实例的副本不必执行完全同步。

如果您想知道为什么副本被提升为主实例后需要更改其复制 ID:可能是由于某些网络分区,旧的主实例仍然作为主实例工作:保留相同的复制 ID 将违反任何两个随机实例具有相同 ID 和相同偏移量意味着它们具有相同数据集的事实。

无盘复制

通常,完全重同步需要先在磁盘上创建 RDB 文件,然后从磁盘重新加载相同的 RDB 文件以向副本提供数据。

对于慢速磁盘,这可能是主实例的一项非常耗费资源的操作。Valkey 支持无盘复制。在这种设置中,子进程直接通过网络将 RDB 发送到副本,而无需使用磁盘作为中间存储。

配置

配置基本的 Valkey 复制非常简单:只需在副本配置文件中添加以下行

replicaof 192.168.1.1 6379

当然,您需要将 192.168.1.1 6379 替换为您的主实例 IP 地址(或主机名)和端口。或者,您可以调用 REPLICAOF 命令,主实例将开始与副本同步。

还有一些参数用于调整主实例为执行部分重同步而在内存中占用的复制积压日志。有关更多信息,请参阅 Valkey 发行版中附带的示例 valkey.conf

可以使用 repl-diskless-sync 配置参数启用无盘复制。开始传输以等待更多副本在第一个副本到达后到达的延迟由 repl-diskless-sync-delay 参数控制。有关更多详细信息,请参阅 Valkey 发行版中的示例 valkey.conf 文件。

只读副本

副本默认是只读的。此行为由 valkey.conf 文件中的 replica-read-only 选项控制,并可以在运行时使用 CONFIG SET 启用和禁用。

只读副本将拒绝所有写入命令,从而避免因错误而写入副本。这并不意味着该功能旨在将副本实例暴露给互联网或更普遍地暴露给存在不受信任客户端的网络,因为像 DEBUGCONFIG 这样的管理命令仍然启用。有关如何保护 Valkey 实例的信息,请参阅安全页面。

您可能想知道为什么可以恢复只读设置并拥有可以被写入操作作为目标的副本实例。答案是可写副本的存在仅出于历史原因。使用可写副本可能导致主实例和副本之间的数据不一致,因此不建议使用可写副本。为了理解在哪些情况下这可能是一个问题,我们需要理解复制的工作原理。主实例上的更改通过将常规 Valkey 命令传播到副本进行复制。当主实例上的键过期时,这会作为 DEL 命令传播。如果一个键存在于主实例上,但在副本上被删除、过期或与主实例相比具有不同的类型,它将对从主实例传播的 DEL、INCR 或 RPOP 等命令做出与预期不同的反应。传播的命令可能在副本上失败或导致不同的结果。为了最小化风险(如果您坚持使用可写副本),我们建议您遵循以下建议:

  • 不要向可写副本中写入主实例也在使用的键。(如果您无法控制所有向主实例写入的客户端,这可能很难保证。)

  • 在运行系统中升级一组实例时,不要将实例配置为可写副本作为中间步骤。通常,如果您想保证数据一致性,则不要将实例配置为可写副本,如果它可能被提升为主实例的话。

从历史上看,可写副本曾有一些被认为是合法的用例。从 7.0 版本开始,这些用例现在都已过时,并且可以通过其他方式实现相同的功能。例如:

  • 计算慢速的 Set 或 Sorted set 操作并将结果存储在临时本地键中,使用 SUNIONSTOREZINTERSTORE 等命令。相反,请使用直接返回结果而不存储的命令,例如 SUNIONZINTER

  • 使用 SORT 命令(由于可选的 STORE 选项,该命令不被视为只读命令,因此不能在只读副本上使用)。相反,请使用 SORT_RO,它是一个只读命令。

  • 使用 EVALEVALSHA 也不被视为只读命令,因为 Lua 脚本可能调用写入命令。相反,请使用 EVAL_ROEVALSHA_RO,其中 Lua 脚本只能调用只读命令。

虽然如果副本与主实例重新同步或副本重新启动,对副本的写入将被丢弃,但不能保证它们会自动同步。

在 4.0 版本之前,可写副本无法使设置了生存时间(TTL)的键过期。这意味着如果您使用 EXPIRE 或其他为键设置最大 TTL 的命令,该键将泄漏,并且虽然您在使用读取命令访问时可能不再看到它,但您会在键计数中看到它,并且它仍将占用内存。Valkey 能够像主实例一样逐出具有 TTL 的键,但 DB 编号大于 63 的键除外(但默认情况下 Valkey 实例只有 16 个数据库)。请注意,即使在 4.0 以上版本中,对可能存在于主实例上的键使用 EXPIRE 也会导致副本和主实例之间的数据不一致。

另请注意,副本写入仅限于本地,并且不会传播到附加到该实例的子副本。相反,子副本将始终接收与顶级主实例发送给中间副本的复制流相同的复制流。因此,例如在以下设置中

A ---> B ---> C

即使 B 是可写的,C 也不会看到 B 的写入,而是会拥有与主实例 A 相同的数据集。

设置副本向主实例进行身份验证

如果您的主实例通过 requirepass 设置了密码,则配置副本在所有同步操作中使用该密码是微不足道的。

要在运行中的实例上执行此操作,请使用 valkey-cli 并输入

config set primaryauth <password>

要永久设置它,请将其添加到您的配置文件中

primaryauth <password>

仅允许在连接 N 个副本时写入

您可以配置 Valkey 主实例,使其仅在至少有 N 个副本当前连接到主实例时才接受写入查询。

然而,由于 Valkey 使用异步复制,无法确保副本实际接收到给定写入,因此总会存在数据丢失的窗口。

该功能的工作原理如下

  • Valkey 副本每秒向主实例发送 ping,确认已处理的复制流数量。
  • Valkey 主实例将记住上次从每个副本接收到 ping 的时间。
  • 用户可以配置最小副本数量,使其延迟不超过最大秒数。

如果至少有 N 个副本,且延迟小于 M 秒,则写入将被接受。

您可以将其视为一种尽力而为的数据安全机制,其中不保证给定写入的一致性,但至少将数据丢失的时间窗口限制在给定秒数内。通常,有限的数据丢失优于无限的数据丢失。

如果条件不满足,主实例将回复错误,并且写入将不被接受。

此功能有两个配置参数

  • min-replicas-to-write <副本数量>
  • min-replicas-max-lag <秒数>

有关更多信息,请查阅 Valkey 源代码发行版中附带的示例 valkey.conf 文件。

Valkey 复制如何处理键的过期

Valkey 过期功能允许键具有有限的生存时间 (TTL)。此功能依赖于实例计时能力,但 Valkey 副本正确复制带过期的键,即使这些键使用 Lua 脚本进行了更改。

为了实现此功能,Valkey 不能依赖主实例和副本实例拥有同步时钟的能力,因为这是一个无法解决的问题,会导致竞态条件和数据不一致,因此 Valkey 使用三种主要技术来使过期键的复制能够正常工作:

  1. 副本不会使键过期,而是等待主实例使键过期。当主实例使键过期(或因 LRU 逐出)时,它会生成一个 DEL 命令,并将其传输给所有副本。
  2. 然而,由于主实例驱动的过期机制,有时副本内存中可能仍存在逻辑上已过期的键,因为主实例未能及时提供 DEL 命令。为了解决这个问题,副本使用其逻辑时钟报告键不存在,仅限于不会违反数据集一致性的读取操作(因为主实例的新命令将到达)。通过这种方式,副本避免报告仍然存在的逻辑上已过期的键。实际操作中,一个使用副本进行扩展的 HTML 片段缓存将避免返回已超出所需生存时间的项目。
  3. 在 Lua 脚本执行期间,不会执行键过期。当 Lua 脚本运行时,主实例中的时间在概念上是冻结的,因此给定键在脚本运行的整个过程中要么存在,要么不存在。这可以防止键在脚本执行期间过期,并且需要以一种保证在数据集上具有相同效果的方式将相同的脚本发送到副本。

一旦副本被提升为主实例,它将开始独立地使键过期,并且不再需要其旧主实例的任何帮助。

在 Docker 和 NAT 中配置复制

当使用 Docker 或其他使用端口转发的容器类型,或网络地址转换(NAT)时,Valkey 复制需要额外注意,尤其是在使用 Valkey Sentinel 或其他系统(其中扫描主实例 INFOROLE 命令输出以发现副本地址)时。

问题在于,当在主实例中发出 ROLE 命令和 INFO 输出的复制部分时,它们会显示副本使用连接到主实例的 IP 地址,而在使用 NAT 的环境中,该地址可能与副本实例的逻辑地址(客户端应连接副本所使用的地址)不同。

同样,副本将列出 valkey.conf 中配置的监听端口,如果端口被重新映射,该端口可能与转发端口不同。

为了解决这两个问题,可以强制副本向主实例通告任意 IP 和端口对。要使用的两个配置指令是:

replica-announce-ip 5.5.5.5
replica-announce-port 1234

并记录在最近 Valkey 发行版中的示例 valkey.conf 文件中。

INFO 和 ROLE 命令

有两个 Valkey 命令提供了关于主实例和副本实例当前复制参数的大量信息。一个是 INFO。如果该命令以 replication 参数调用,即 INFO replication,则只显示与复制相关的信息。另一个更易于计算机解析的命令是 ROLE,它提供主实例和副本的复制状态以及它们的复制偏移量、连接的副本列表等等。

重启和故障转移后的部分同步

当实例在故障转移后被提升为主实例时,它仍能与旧主实例的副本执行部分重同步。为此,副本会记住其前任主实例的旧复制 ID 和偏移量,因此即使连接的副本请求旧的复制 ID,它也能提供部分积压日志。

然而,被提升副本的新复制 ID 将会不同,因为它构成了一个不同的数据集历史。例如,主实例可能会恢复可用并继续接受写入一段时间,因此在被提升的副本中使用相同的复制 ID 将违反“复制 ID 和偏移量对仅标识单个数据集”的规则。

此外,副本在正常关闭并重新启动时,能够将与主实例重新同步所需的信息存储在 RDB 文件中。这在升级情况下很有用。当需要此功能时,最好使用 SHUTDOWN 命令在副本上执行 save & quit 操作。

无法通过 AOF 文件部分同步已重新启动的副本。但是,实例可以在关闭之前转换为 RDB 持久化,然后重新启动,最后可以再次启用 AOF。

副本上的 Maxmemory

默认情况下,副本会忽略 maxmemory(除非它在故障转移后或手动被提升为主实例)。这意味着键的逐出将由主实例处理,当键在主实例侧逐出时,主实例会将 DEL 命令发送到副本。

此行为可确保主实例和副本保持一致,这通常是您想要的。但是,如果您的副本是可写的,或者您希望副本具有不同的内存设置,并且您确定对副本执行的所有写入都是幂等的,那么您可以更改此默认设置(但请务必理解您正在做什么)。

请注意,由于副本默认不进行逐出,它最终可能会使用比通过 maxmemory 设置的内存更多的内存(因为副本上可能存在某些更大的缓冲区,或者数据结构有时可能占用更多内存等等)。请务必监控您的副本,并确保它们有足够的内存,以免在主实例达到配置的 maxmemory 设置之前发生真实的内存不足情况。

要更改此行为,您可以允许副本不忽略 maxmemory。要使用的配置指令是:

replica-ignore-maxmemory no