Redis 复制

Redis 如何通过复制支持高可用性和故障转移

Redis Open Source

在 Redis 复制的基础(不包括 Redis Cluster 或 Redis Sentinel 作为附加层提供的高可用性功能)之上,存在一种简单易用和配置的**主从**(master-replica)复制。它允许副本 Redis 实例成为主实例的精确副本。副本会在每次链接断开时自动重新连接到主实例,并且**无论**主实例发生什么,都会尝试成为其精确副本。

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

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

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

客户端可以使用 [`WAIT`](/commands/wait) 命令请求特定数据的同步复制。然而,[`WAIT`](/commands/wait) 只能确保在其他 Redis 实例中有指定数量的已确认副本,它不会将一组 Redis 实例转变为具有强一致性的 CP 系统:根据 Redis 持久化的具体配置,在故障转移期间,已确认的写入仍可能丢失。然而,[`WAIT`](/docs/latest/commands/wait/) 显著降低了在故障事件后丢失写入的可能性,除非发生特定的难以触发的故障模式。

有关高可用性和故障转移的更多信息,请查阅 Redis Sentinel 或 Redis Cluster 文档。本文档的其余部分主要描述 Redis 基本复制的基本特征。

关于 Redis 复制的重要事实

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

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

在使用 Redis 复制的设置中,强烈建议在主实例和副本中都开启持久化。当这不可能时(例如由于磁盘非常慢导致的延迟问题),应该配置实例以避免在重启后自动重启。

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

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

当使用 Redis Sentinel 进行高可用性时,在主实例上关闭持久化并伴随进程的自动重启也是危险的。例如,主实例可以足够快地重启,以至于 Sentinel 检测不到故障,从而发生上述故障模式。

每次数据安全至关重要,并且主实例配置为不进行持久化的情况下使用复制时,应禁用实例的自动重启。

Redis 复制的工作原理

每个 Redis 主实例都有一个复制 ID:它是一个大的伪随机字符串,标记给定数据集的历史记录。每个主实例还有一个偏移量,每生成一个字节的复制流(用于发送给副本,以修改数据集的新更改更新副本的状态)就递增。即使没有实际连接的副本,复制偏移量也会递增,因此基本上每对给定的

Replication ID, offset

(复制 ID 和偏移量)识别主实例数据集的精确版本。

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

以下是完整同步工作的更详细流程

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

你可以通过 telnet 亲自尝试。在服务器正在工作时连接到 Redis 端口,并发出 [`SYNC`](/commands/sync) 命令。你将看到批量传输,然后在 telnet 会话中重新发送主实例接收到的每个命令。实际上,[`SYNC`](/commands/sync) 是一个旧协议,较新的 Redis 实例已不再使用,但为了向后兼容仍然存在:它不允许部分同步,因此现在使用 [`PSYNC`](/commands/psync) 代替。

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

复制 ID 解释

在上一节中,我们说过如果两个实例具有相同的复制 ID 和复制偏移量,则它们具有完全相同的数据。然而,了解复制 ID 到底是什么,以及为什么实例实际上有两个复制 ID(主 ID 和辅助 ID)是很有用的。

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

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

Redis 实例有两个复制 ID 的原因是副本被提升为主实例。故障转移后,被提升的副本需要仍然记住它过去的复制 ID,因为该复制 ID 是旧主实例的 ID。这样,当其他副本与新主实例同步时,它们将尝试使用旧主实例复制 ID 进行部分同步。这将按预期工作,因为当副本被提升为主实例时,它将其辅助 ID 设置为其主 ID,记住此 ID 切换发生时的偏移量。之后它将选择一个新的随机复制 ID,因为新的历史记录开始了。处理新连接的副本时,主实例将使用当前 ID 和辅助 ID(出于安全考虑,最多到给定偏移量)来匹配它们的 ID 和偏移量。简而言之,这意味着故障转移后,连接到新提升的主实例的副本无需执行完整同步。

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

无盘复制

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

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

配置

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

replicaof 192.168.1.1 6379

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

还有一些参数用于调整主实例在内存中占用的复制积压,以执行部分同步。有关更多信息,请参阅 Redis 发行版随附的示例 `redis.conf`。

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

只读副本

自 Redis 2.6 起,副本支持只读模式,默认启用。此行为由 redis.conf 文件中的 `replica-read-only` 选项控制,可以使用 [`CONFIG SET`](/commands/config-set) 在运行时启用和禁用。

只读副本将拒绝所有写入命令,这样就不可能因为错误而写入副本。这并不意味着此功能旨在将副本实例暴露给互联网或更广泛地暴露给存在不受信任客户端的网络,因为诸如 [`DEBUG`](/commands/debug) 或 [`CONFIG`](/commands/config) 之类的管理命令仍然启用。[`安全`](/docs/latest/operate/oss_and_stack/management/security/) 页面描述了如何保护 Redis 实例。

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

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

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

从历史上看,曾有一些被认为可写副本的合理用例。截至版本 7.0,这些用例现已全部过时,并且可以通过其他方式实现相同功能。例如

  • 使用 [`SUNIONSTORE`](/commands/sunionstore) 和 [`ZINTERSTORE`](/commands/zinterstore) 等命令计算慢速 Set 或 Sorted set 操作,并将结果存储在临时本地键中。相反,使用不存储结果而返回结果的命令,例如 [`SUNION`](/commands/sunion) 和 [`ZINTER`](/commands/zinter) 。

  • 使用 [`SORT`](/commands/sort) 命令(由于可选的 STORE 选项,此命令不被视为只读命令,因此不能在只读副本上使用)。相反,使用 [`SORT_RO`](/commands/sort_ro) ,这是一个只读命令。

  • 使用 [`EVAL`](/commands/eval) 和 [`EVALSHA`](/commands/evalsha) 也不被视为只读命令,因为 Lua 脚本可能调用写入命令。相反,使用 [`EVAL_RO`](/commands/eval_ro) 和 [`EVALSHA_RO`](/commands/evalsha_ro) ,其中 Lua 脚本只能调用只读命令。

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

在版本 4.0 之前,可写副本无法使设置了生存时间 (TTL) 的键过期。这意味着如果你使用 [`EXPIRE`](/commands/expire) 或其他为键设置最大 TTL 的命令,键会泄漏,尽管你使用读取命令可能不再看到它,但你会在键计数中看到它,并且它仍会占用内存。Redis 4.0 RC3 及更高版本能够像主实例一样驱逐设置了 TTL 的键,除了写入数据库编号大于 63 的键(但默认情况下 Redis 实例只有 16 个数据库)。但请注意,即使在 4.0 以上版本中,对可能存在于主实例上的键使用 [`EXPIRE`](/commands/expire) 也可能导致副本和主实例之间的数据不一致。

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

A ---> B ---> C

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

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

如果你的主实例通过 `requirepass` 设置了密码,配置副本在所有同步操作中使用该密码非常简单。

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

config set masterauth <password>

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

masterauth <password>

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

从 Redis 2.8 开始,你可以配置 Redis 主实例仅在当前至少有 N 个副本连接到主实例时接受写入查询。

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

此功能的工作原理如下

  • Redis 副本每秒 ping 主实例,确认已处理的复制流数量。
  • Redis 主实例会记住上次接收到每个副本 ping 的时间。
  • 用户可以配置最小副本数,这些副本的延迟不超过指定的最大秒数。

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

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

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

此功能有两个配置参数

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

有关更多信息,请查阅 Redis 源代码发行版随附的示例 `redis.conf` 文件。

Redis 复制如何处理键的过期

Redis 过期功能允许键具有有限的生存时间 (TTL)。此功能依赖于实例计时能力,然而,Redis 副本即使在 Lua 脚本修改键的情况下也能正确复制带有过期时间的键。

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

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

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

在 Docker 和 NAT 中配置复制

使用 Docker 或其他使用端口转发或网络地址转换的容器时,Redis 复制需要额外注意,尤其是在使用 Redis Sentinel 或其他扫描主实例 [`INFO`](/commands/info) 或 [`ROLE`](/commands/role) 命令输出来发现副本地址的系统时。

问题在于,在主实例中发出 [`ROLE`](/commands/role) 命令以及 [`INFO`](/commands/info) 输出的复制部分时,会显示副本使用连接到主实例的 IP 地址,在使用 NAT 的环境中,这可能与副本实例的逻辑地址(客户端应使用此地址连接到副本)不同。

类似地,副本将列出配置在 redis.conf 中的监听端口,如果端口被重新映射,此端口可能与转发端口不同。

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

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

并在近期 Redis 发行版的示例 `redis.conf` 中有文档说明。

INFO 和 ROLE 命令

有两个 Redis 命令提供了大量关于主实例和副本实例当前复制参数的信息。一个是 [`INFO`](/commands/info) 。如果命令以 `replication` 参数调用(即 `INFO replication`),则仅显示与复制相关的信息。另一个更适合计算机处理的命令是 [`ROLE`](/commands/role) ,它提供主实例和副本的复制状态以及它们的复制偏移量、连接的副本列表等信息。

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

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

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

此外,副本在优雅关机并重启时,能够在 RDB 文件中存储与主实例重新同步所需的信息。这在升级情况下很有用。当需要这样做时,最好使用 [`SHUTDOWN`](/commands/shutdown) 命令在副本上执行 `save & quit` 操作。

通过 AOF 文件重启的副本不可能进行部分同步。但是,可以在关机之前将实例切换到 RDB 持久化,然后重启,最后再次启用 AOF。

副本上的 Maxmemory

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

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

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

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

replica-ignore-maxmemory no
评价此页面
回到顶部 ↑