复制
KeyDB 复制的基础是一个非常易于使用和配置的主从(master-replica)复制:它允许从属 KeyDB 实例成为主实例的精确副本。每当链接断开时,从属实例将自动重新连接到主实例,并尝试成为它的精确副本,无论主实例发生什么。
该系统通过三种主要机制工作:
- 当主实例和从属实例连接良好时,主实例通过向从属实例发送命令流来保持其更新,以复制主实例端因以下原因对数据集产生的影响:客户端写入、键过期或被驱逐,以及任何其他改变主实例数据集的操作。
- 当主从实例之间的链接因网络问题或主从实例感知到超时而断开时,从属实例会重新连接并尝试进行部分重新同步:这意味着它将尝试只获取在断开连接期间错过的命令流部分。
- 当部分重新同步不可能时,从属实例将请求进行完全重新同步。这将涉及一个更复杂的过程,其中主实例需要为其所有数据创建一个快照,将其发送给从属实例,然后在数据集更改时继续发送命令流。
KeyDB 默认使用异步复制,由于其低延迟和高性能,是绝大多数 KeyDB 用例的自然复制模式。然而,KeyDB 从属实例会定期向主实例异步确认它们收到的数据量。因此,主实例不会每次都等待命令被从属实例处理,但它知道(如果需要)哪个从属实例已经处理了哪个命令。这允许进行可选的同步复制。
客户端可以使用 WAIT
命令请求对某些数据进行同步复制。然而,WAIT
只能确保在其他 KeyDB 实例中有指定数量的已确认副本,它并不能将一组 KeyDB 实例变成一个具有强一致性的 CP 系统:在故障转移期间,已确认的写入仍可能丢失,具体取决于 KeyDB 持久化的确切配置。然而,使用 WAIT
后,在故障事件后丢失写入的概率会大大降低,仅限于某些难以触发的故障模式。
您可以查看 Sentinel 或 KeyDB Cluster 文档以获取有关高可用性和故障转移的更多信息。但是,我们鼓励从主动复制(Active-Replication)开始,因为它易于使用,并且是实现高可用性的强大工具。本文档的其余部分主要描述 KeyDB 基本复制的基本特性。
以下是关于 KeyDB 复制的一些非常重要的事实:
- KeyDB 使用异步复制,并通过从属到主的异步确认来告知已处理的数据量。
- 一个主实例可以有多个从属实例。
- 从属实例能够接受来自其他从属实例的连接。除了将多个从属实例连接到同一个主实例外,从属实例还可以以级联式结构连接到其他从属实例。所有子从属实例都将从主实例接收到完全相同的复制流。
- KeyDB 复制在主实例端是非阻塞的。这意味着当一个或多个从属实例执行初始同步或部分重新同步时,主实例将继续处理查询。
- 复制在从属实例端也基本上是非阻塞的。当从属实例正在执行初始同步时,它可以继续使用旧版本的数据集处理查询,前提是您在 keydb.conf 中配置 KeyDB 这样做。否则,您可以配置 KeyDB 从属实例在复制流中断时向客户端返回错误。但是,在初始同步之后,旧数据集必须被删除,新的数据集必须被加载。在此短暂的窗口期间(对于非常大的数据集,可能长达数秒),从属实例将阻塞传入的连接。可以配置 KeyDB 使删除旧数据集的操作在另一个线程中进行,但加载新的初始数据集仍将在主线程中进行并阻塞从属实例。
- 复制既可以用于可伸缩性,以便有多个从属实例处理只读查询(例如,可以将慢速 O(N) 操作卸载到从属实例),也可以简单地用于提高数据安全性和高可用性。
- 可以使用复制来避免主实例将完整数据集写入磁盘的成本:一种典型的技术是配置您的主实例的
keydb.conf
文件以完全避免持久化到磁盘,然后连接一个配置为不时保存或启用了 AOF 的从属实例。但是,此设置必须小心处理,因为重新启动的主实例将以空数据集开始:如果从属实例尝试与其同步,从属实例也将被清空。
#
主实例关闭持久化时的复制安全性在使用 KeyDB 复制的设置中,强烈建议在主实例和从属实例上都开启持久化。当无法做到这一点时(例如,由于磁盘非常慢导致延迟问题),实例应配置为在重启后**避免自动重启**。
为了更好地理解为什么配置为自动重启且关闭持久化的主实例是危险的,请看下面这个导致数据从主实例及其所有从属实例中被清除的故障模式:
- 我们有一个设置,节点 A 作为主实例,关闭了持久化,节点 B 和 C 从节点 A 复制。
- 节点 A 崩溃了,但它有一个自动重启系统,该系统会重启进程。然而,由于持久化已关闭,该节点重启时带有一个空的数据集。
- 节点 B 和 C 将从空的节点 A 复制,因此它们实际上会销毁自己的数据副本。
当使用 KeyDB Sentinel 实现高可用性时,关闭主实例的持久化并结合进程的自动重启也是危险的。例如,主实例可能重启得足够快,以至于 Sentinel 没有检测到故障,从而发生上述故障模式。
每当数据安全至关重要,并且在使用复制时主实例配置为无持久化,都应禁用实例的自动重启。
#
KeyDB 复制如何工作每个 KeyDB 主实例都有一个复制 ID:它是一个大的伪随机字符串,标记了数据集的特定历史。每个主实例还有一个偏移量,它会随着为发送给从属实例而产生的每个字节的复制流而增加,以便用改变数据集的新更改来更新从属实例的状态。即使没有从属实例实际连接,复制偏移量也会增加,所以基本上任何给定的配对:
标识了主实例数据集的一个确切版本。
当从属实例连接到主实例时,它们使用 PSYNC
命令来发送它们旧的主实例复制 ID 和它们到目前为止处理的偏移量。这样,主实例就可以只发送所需的增量部分。但是,如果主实例缓冲区中没有足够的积压(backlog),或者如果从属实例引用的历史(复制 ID)不再被知晓,那么就会发生完全重新同步:在这种情况下,从属实例将从头开始获得数据集的完整副本。
以下是完全同步如何工作的更详细说明:
主实例启动一个后台保存进程以生成一个 RDB 文件。同时,它开始缓冲从客户端收到的所有新的写命令。当后台保存完成后,主实例将数据库文件传输到从属实例,从属实例将其保存在磁盘上,然后加载到内存中。然后,主实例将所有缓冲的命令发送给从属实例。这是以命令流的形式完成的,其格式与 KeyDB 协议本身相同。
你可以通过 telnet 亲自尝试。在服务器正在工作时连接到 KeyDB 端口并发出 SYNC
命令。你会看到一个批量传输,然后主实例收到的每个命令都会在 telnet 会话中重新发出。实际上 SYNC
是一个旧协议,新的 KeyDB 实例不再使用,但为了向后兼容仍然存在:它不允许部分重新同步,所以现在改用 PSYNC
。
如前所述,当主从链接因某种原因断开时,从属实例能够自动重新连接。如果主实例收到多个并发的从属实例同步请求,它会执行一次后台保存以服务所有请求。
#
复制 ID 详解在上一节中,我们说过如果两个实例具有相同的复制 ID 和复制偏移量,它们的数据就完全相同。但是,理解复制 ID 究竟是什么,以及为什么实例实际上有两个复制 ID(主 ID 和次要 ID)是很有用的。
复制 ID 基本上标记了数据集的特定历史。每当一个实例从头开始作为主实例重启,或者一个从属实例被提升为主实例时,都会为该实例生成一个新的复制 ID。连接到主实例的从属实例将在握手后继承其复制 ID。因此,具有相同 ID 的两个实例因为它们持有相同的数据而相关,但可能是在不同的时间。偏移量作为逻辑时间来理解,对于给定的历史(复制 ID),谁持有最新的数据集。
例如,如果两个实例 A 和 B 具有相同的复制 ID,但一个的偏移量是 1000,另一个是 1023,这意味着第一个实例缺少应用于数据集的某些命令。这也意味着 A 只需应用少量命令,就可以达到与 B 完全相同的状态。
KeyDB 实例有两个复制 ID 的原因是因为从属实例被提升为主实例。在故障转移后,被提升的从属实例需要记住它过去的复制 ID,因为该复制 ID 是前一个主实例的 ID。这样,当其他从属实例与新主实例同步时,它们将尝试使用旧主实例的复制 ID 进行部分重新同步。这将按预期工作,因为当从属实例被提升为主实例时,它会将其次要 ID 设置为其主 ID,并记住发生此 ID 切换时的偏移量。稍后它将选择一个新的随机复制 ID,因为一个新的历史开始了。在处理新的连接从属实例时,主实例将匹配它们的 ID 和偏移量与当前 ID 和次要 ID(为安全起见,直到某个给定的偏移量)。简而言之,这意味着在故障转移后,连接到新提升的主实例的从属实例不必执行完全同步。
如果你想知道为什么一个被提升为主实例的从属实例需要在故障转移后更改其复制 ID:这可能是因为旧的主实例由于某种网络分区仍在作为主实例工作。保留相同的复制 ID 将违反“任何两个随机实例的相同 ID 和相同偏移量意味着它们具有相同数据集”的规则。
#
无盘复制通常,完全重新同步需要先在磁盘上创建一个 RDB 文件,然后再从磁盘重新加载相同的 RDB 文件,以便为从属实例提供数据。
对于磁盘速度较慢的情况,这对主实例来说可能是一个压力很大的操作。在这种设置中,子进程直接通过网络将 RDB 发送给从属实例,而不使用磁盘作为中间存储。
#
配置配置基本的 KeyDB 复制非常简单:只需在从属实例的配置文件中添加以下行:
当然,您需要将 192.168.1.1 6379 替换为您的主实例 IP 地址(或主机名)和端口。或者,您可以调用 REPLICAOF
命令,主实例将开始与从属实例进行同步。
还有一些参数用于调整主实例为执行部分重新同步而在内存中保留的复制积压(backlog)。有关更多信息,请参阅 KeyDB 发行版附带的示例 keydb.conf
文件。
可以使用 repl-diskless-sync
配置参数启用无盘复制。为了等待第一个从属实例到达后更多从属实例到来而延迟开始传输的时间,由 repl-diskless-sync-delay
参数控制。有关更多详细信息,请参阅 KeyDB 发行版中的示例 keydb.conf
文件。
#
只读从属实例从属实例支持默认启用的只读模式。此行为由 keydb.conf 文件中的 replica-read-only
选项控制,并可以在运行时使用 CONFIG SET
启用和禁用。
只读从属实例将拒绝所有写命令,这样就不可能因为失误而向从属实例写入数据。这并不意味着该功能旨在将从属实例暴露给互联网或更普遍地暴露给存在不受信任客户端的网络,因为像 DEBUG
或 CONFIG
这样的管理命令仍然是启用的。然而,可以通过在 keydb.conf 中使用 rename-command
指令禁用命令来提高只读实例的安全性。
您可能想知道为什么可以撤销只读设置,并让从属实例可以接受写操作。虽然如果从属实例和主实例重新同步或从属实例重新启动,这些写入将被丢弃,但在可写从属实例中存储临时数据有一些合法的用例。
例如,计算慢速的 Set 或 Sorted set 操作并将其存储到本地键中,是可写从属实例的一个被多次观察到的用例。
但请注意,**可写的从属实例曾经无法使设置了生存时间的键过期**。这意味着如果您使用 EXPIRE
或其他为键设置最大 TTL 的命令,该键将泄漏,虽然您在通过读命令访问它时可能不再看到它,但您会在键的数量中看到它,并且它仍会占用内存。因此,总的来说,混合使用可写(KeyDB 和 <4.0.0 Redis)从属实例和带 TTL 的键会产生问题。
现在,可写的从属实例能够像主实例一样驱逐带有 TTL 的键,但写入 DB 编号大于 63 的键除外(但默认情况下 KeyDB 实例只有 16 个数据库)。
从属实例的写入仅是本地的,不会传播到附加到该实例的子从属实例。相反,子从属实例将始终接收与顶级主实例发送给中间从属实例的复制流完全相同的流。例如,在以下设置中:
即使 B
是可写的,C 也不会看到 B
的写入,而是会拥有与主实例 A
相同的数据集。
#
设置从属实例向主实例进行身份验证如果您的主实例通过 requirepass
设置了密码,那么配置从属实例在所有同步操作中使用该密码是非常简单的。
要在正在运行的实例上执行此操作,请使用 keydb-cli
并键入:
要永久设置它,请将此行添加到您的配置文件中:
#
仅在有 N 个连接的从属实例时才允许写入可以配置 KeyDB 主实例仅在至少有 N 个从属实例当前连接到主实例时才接受写查询。
然而,由于 KeyDB 使用异步复制,无法确保从属实例实际收到了给定的写入,因此总是存在数据丢失的窗口。
该功能的工作原理如下:
- KeyDB 从属实例每秒 ping 一次主实例,确认已处理的复制流数量。
- KeyDB 主实例将记住从每个从属实例收到 ping 的最后时间。
- 用户可以配置一个最小数量的从属实例,其延迟不超过最大秒数。
如果至少有 N 个从属实例,且延迟小于 M 秒,则写入将被接受。
您可以将其视为一种尽力而为的数据安全机制,其中对于给定的写入不保证一致性,但至少数据丢失的时间窗口被限制在给定的秒数内。总的来说,有界的数据丢失比无界的好。
如果不满足条件,主实例将回复一个错误,并且写入将不被接受。
此功能有两个配置参数:
- min-replicas-to-write
<从属实例数量>
- min-replicas-max-lag
<秒数>
有关更多信息,请查看 KeyDB 源代码发行版附带的示例 keydb.conf
文件。
#
KeyDB 复制如何处理键的过期KeyDB 的过期功能允许键有一个有限的生存时间(TTL)。这样的功能依赖于实例计算时间的能力,然而 KeyDB 从属实例可以正确地复制带有过期时间的键,即使这些键是通过 Lua 脚本修改的。
为了实现这样的功能,KeyDB 不能依赖主从实例的时钟同步能力,因为这是一个无法解决的问题,会导致竞态条件和数据集分歧。因此,KeyDB 使用三种主要技术来使过期键的复制能够正常工作:
- 从属实例不会使键过期,而是等待主实例使键过期。当主实例使一个键过期(或因 LRU 将其驱逐)时,它会合成一个
DEL
命令,该命令会传输给所有的从属实例。 - 然而,由于主实例驱动的过期机制,有时从属实例可能内存中仍然存有逻辑上已经过期的键,因为主实例未能及时提供
DEL
命令。为了处理这种情况,从属实例使用其逻辑时钟来报告一个键不存在,但这**仅限于不违反数据集一致性的读操作**(因为新的命令会从主实例传来)。通过这种方式,从属实例可以避免报告逻辑上已过期的键仍然存在。实际上,使用从属实例进行扩展的 HTML 片段缓存将避免返回已经超过期望生存时间的项。 - 在 Lua 脚本执行期间,不会执行任何键过期操作。当一个 Lua 脚本运行时,概念上主实例中的时间是冻结的,因此一个给定的键在脚本运行的整个时间内要么存在要么不存在。这可以防止键在脚本执行中途过期,并且是必需的,以便以保证在数据集中产生相同效果的方式将相同的脚本发送到从属实例。
一旦一个从属实例被提升为主实例,它将开始独立地使键过期,并且不需要其旧主实例的任何帮助。
#
在 Docker 和 NAT 中配置复制当使用 Docker、其他类型的端口转发容器或网络地址转换(NAT)时,KeyDB 复制需要一些额外的注意,尤其是在使用 KeyDB Sentinel 或其他扫描主实例 INFO
或 ROLE
命令输出来发现从属实例地址的系统时。
问题在于,当在主实例上执行 ROLE
命令和 INFO
命令的复制部分时,会显示从属实例的 IP 地址是它们用来连接主实例的地址。在使用 NAT 的环境中,这个地址可能与从属实例的逻辑地址(即客户端应该用来连接从属实例的地址)不同。
类似地,列出的从属实例将使用 keydb.conf
中配置的监听端口,如果端口被重映射,这可能与转发的端口不同。
为了解决这两个问题,可以强制从属实例向主实例宣告一个任意的 IP 和端口对。要使用的两个配置指令是:
这些在最近的 KeyDB 发行版的示例 keydb.conf
中有文档说明。
#
INFO 和 ROLE 命令有两个 KeyDB 命令可以提供关于主从实例当前复制参数的大量信息。一个是 INFO
。如果该命令使用 replication
参数调用,即 INFO replication
,则只显示与复制相关的信息。另一个更便于计算机处理的命令是 ROLE
,它提供主从实例的复制状态及其复制偏移量、连接的从属实例列表等。
#
重启和故障转移后的部分重新同步当一个实例在故障转移后被提升为主实例时,它仍然能够与旧主实例的从属实例执行部分重新同步。为此,该从属实例会记住其前任主实例的旧复制 ID 和偏移量,因此即使连接的从属实例请求的是旧的复制 ID,它也可以向它们提供部分的积压数据。
但是,被提升的从属实例的新复制 ID 会不同,因为它构成了一个不同的数据集历史。例如,主实例可能会恢复可用并继续接受一段时间的写入,因此在被提升的从属实例中使用相同的复制 ID 将违反“一个复制 ID 和偏移量对只标识一个数据集”的规则。
此外,当从属实例被平稳地关闭并重新启动时,它们能够在 RDB
文件中存储所需的信息,以便与其主实例重新同步。这在升级时很有用。当需要这样做时,最好在从属实例上使用 SHUTDOWN
命令来执行“保存并退出”操作。
无法对通过 AOF 文件重新启动的从属实例进行部分重新同步。但是,可以在关闭实例之前将其持久化方式转为 RDB,然后可以重新启动,最后再重新启用 AOF。
#
从属实例上的 Maxmemory从属实例不遵守 maxmemory
,因为默认情况下从属实例会忽略此设置(除非它在故障转移后或手动被提升为主实例)。这意味着键的驱逐将完全由主实例处理,随着主实例侧的键被驱逐,DEL 命令会被发送到从属实例。
这种行为确保了主从实例保持一致,这通常是您想要的。但是,如果您的从属实例是可写的,或者您希望从属实例有不同的内存设置,并且您确定所有对从属实例执行的写入都是幂等的,那么您可以更改此默认设置(但请确保您了解自己在做什么)。
请注意,由于从属实例默认不进行驱逐,它最终可能会使用比通过 maxmemory
设置的更多的内存(因为从属实例上某些缓冲区可能更大,或者数据结构有时可能占用更多内存等等)。因此,请确保您监控您的从属实例,并确保它们有足够的内存,在主实例达到配置的 maxmemory
设置之前永远不会遇到真正的内存不足情况。
为了改变这种行为,可以允许从属实例不忽略 maxmemory。要使用的配置指令是: