Redis 复制
Redis 如何通过复制支持高可用性和故障转移
在 Redis 复制的基础上(不包括 Redis 集群或 Redis Sentinel 提供的高可用性功能作为附加层),存在一个领导者-跟随者(主-从)复制,它易于使用和配置。它允许复制的 Redis 实例成为主实例的精确副本。复制实例将在链接断开时自动重新连接到主实例,并会尝试无论发生什么成为主实例的精确副本,无论主实例发生了什么。
此系统使用三种主要机制
- 当主实例和副本实例连接良好时,主实例会通过向副本发送一系列命令来更新副本,以复制由于以下原因而发生在主实例侧的数据集上的影响:客户端写入、键过期或驱逐、任何其他更改主实例数据集的操作。
- 当主实例和副本实例之间的连接断开时,无论是由于网络问题还是因为主实例或副本检测到超时,副本都会重新连接并尝试进行部分重新同步:这意味着它将尝试仅获取在断开连接期间错过的命令流的部分。
- 当部分重新同步不可行时,副本将请求完全重新同步。这将涉及一个更复杂的过程,其中主实例需要创建其所有数据的快照,将其发送到副本,然后在数据集发生变化时继续发送命令流。
Redis 默认使用异步复制,它具有低延迟和高性能,是绝大多数 Redis 使用场景的自然复制模式。但是,Redis 副本会定期异步确认它们从主实例接收到的数据量。因此,主实例不会每次都等待副本处理命令,但它知道,如果需要,哪个副本已经处理了哪个命令。这允许可选的同步复制。
客户端可以使用 WAIT
命令请求某些数据的同步复制。但是,WAIT
只能确保其他 Redis 实例中有指定数量的已确认副本,它不会将一组 Redis 实例变成具有强一致性的 CP 系统:已确认的写入仍然可能在故障转移期间丢失,具体取决于 Redis 持久性的确切配置。但是,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 复制的设置中,强烈建议在主实例和副本中打开持久化。当这不可行时,例如由于磁盘速度非常慢而导致延迟问题,应配置实例以在重新启动后**避免自动重启**。
为了更好地理解为什么关闭持久化的主实例配置为自动重启很危险,请查看以下数据从主实例及其所有副本中清除的故障模式
- 我们有一个设置,节点 A 充当主实例,持久化关闭,节点 B 和 C 从节点 A 复制。
- 节点 A 崩溃,但它有一些自动重启系统,该系统会重新启动进程。但是,由于持久化已关闭,节点在重启时会使用空数据集。
- 节点 B 和 C 将从节点 A 复制,而节点 A 是空的,因此它们实际上会破坏其数据的副本。
当 Redis Sentinel 用于高可用性时,在主实例上关闭持久化,以及自动重启进程,也是危险的。例如,主实例可能重启速度足够快,以至于 Sentinel 无法检测到故障,因此上述故障模式会发生。
每当数据安全很重要,并且使用没有持久化的主实例配置的复制时,应禁用实例的自动重启。
Redis 复制的工作原理
每个 Redis 主实例都有一个复制 ID:它是一个大的伪随机字符串,它标记数据集的特定历史记录。每个主实例还会使用一个偏移量,该偏移量会为它产生的要发送到副本的每个复制流字节而递增,以使用修改数据集的新更改更新副本的状态。即使没有副本实际连接,复制偏移量也会递增,因此基本上每个给定的对
Replication ID, offset
标识主实例数据集的精确版本。
当副本连接到主实例时,它们使用 PSYNC
命令发送其旧的主实例复制 ID 和迄今为止已处理的偏移量。这样,主实例就可以只发送所需的增量部分。但是,如果主实例缓冲区中没有足够的积压,或者副本引用的是不再知道的历史记录(复制 ID),那么就会发生完全重新同步:在这种情况下,副本将从头开始获取数据集的完整副本。
以下是完全同步的更详细工作原理
主实例启动后台保存进程以生成 RDB 文件。同时,它开始缓冲从客户端接收到的所有新的写入命令。后台保存完成后,主实例将数据库文件传输到副本,副本将其保存到磁盘,然后将其加载到内存中。然后,主实例将所有缓冲的命令发送到副本。这是作为命令流完成的,并且与 Redis 协议本身的格式相同。
您可以通过 telnet 自己尝试。在服务器执行某些操作时连接到 Redis 端口,并发出 SYNC
命令。您将看到批量传输,然后主实例接收到的每个命令都将在 telnet 会话中重新发出。实际上,SYNC
是一个旧协议,不再被更新的 Redis 实例使用,但为了向后兼容,它仍然存在:它不允许部分重新同步,因此现在使用 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
命令,主实例将开始与副本同步。
在主服务器用于执行部分重新同步的内存中,还有一些用于调整复制延迟的参数。有关更多信息,请参阅 Redis 分发附带的示例 redis.conf
。
可以使用 repl-diskless-sync
配置参数启用无磁盘复制。使用 repl-diskless-sync-delay
参数控制在第一个副本到达后等待更多副本到达的传输启动延迟。有关更多详细信息,请参阅 Redis 分发中的示例 redis.conf
文件。
只读副本
从 Redis 2.6 开始,副本支持默认情况下启用的只读模式。此行为由 redis.conf
文件中的 replica-read-only
选项控制,并且可以使用 CONFIG SET
在运行时启用和禁用。
只读副本将拒绝所有写入命令,因此不可能因错误而写入副本。但这并不意味着该功能旨在将副本实例公开到互联网或更普遍地说,公开到存在不可信客户端的网络,因为管理命令(如 DEBUG
或 CONFIG
)仍然启用。有关如何保护 Redis 实例的信息,请参阅 安全 页面。
您可能想知道为什么可以恢复只读设置并拥有可以作为写入操作目标的副本实例。答案是,可写副本仅出于历史原因而存在。使用可写副本会导致主服务器和副本之间不一致,因此不建议使用可写副本。要了解这种情况可能存在问题,我们需要了解复制的工作原理。主服务器上的更改通过将常规 Redis 命令传播到副本进行复制。当主服务器上的键过期时,这会作为 DEL 命令传播。如果主服务器上存在但已删除、过期或与主服务器相比在副本上具有不同类型的键,将对从主服务器传播的 DEL、INCR 或 RPOP 等命令做出与预期不同的反应。传播的命令可能会在副本上失败或导致不同的结果。为了最大限度地降低风险(如果您坚持使用可写副本),我们建议您遵循以下建议
-
不要写入可写副本中也用于主服务器的键。(如果您无法控制写入主服务器的所有客户端,这可能难以保证。)
-
在运行系统中升级一组实例时,不要将实例配置为可写副本作为中间步骤。一般来说,如果您想保证数据一致性,不要将实例配置为可写副本,如果它可以被提升为主服务器。
从历史上看,有一些用例被认为是可写副本的合法用例。从 7.0 版本开始,这些用例现在都已过时,可以使用其他方法实现相同的效果。例如
-
计算缓慢的 Set 或 Sorted set 操作并将结果存储在使用
SUNIONSTORE
和ZINTERSTORE
等命令的临时本地键中。相反,使用返回结果但不存储结果的命令,例如SUNION
和ZINTER
。 -
使用
SORT
命令(由于可选的 STORE 选项,它不被认为是只读命令,因此不能在只读副本上使用)。相反,使用SORT_RO
,它是一个只读命令。 -
使用
EVAL
和EVALSHA
也不被认为是只读命令,因为 Lua 脚本可能会调用写入命令。相反,使用EVAL_RO
和EVALSHA_RO
,其中 Lua 脚本只能调用只读命令。
虽然如果副本和主服务器重新同步或副本重新启动,对副本的写入将被丢弃,但不能保证它们会自动同步。
在 4.0 版本之前,可写副本无法使设置了生存时间的键过期。这意味着如果您使用 EXPIRE
或其他设置键的最大 TTL 的命令,该键将泄漏,并且虽然您可能在使用读取命令访问它时不再看到它,但您会在键计数中看到它,并且它仍然会使用内存。Redis 4.0 RC3 及更高版本能够像主服务器一样逐出具有 TTL 的键,但例外情况是写入到大于 63 的 DB 编号中的键(但默认情况下,Redis 实例只有 16 个数据库)。但是请注意,即使在 4.0 版本以上,在可能存在于主服务器上的键上使用 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 使用三种主要技术来使过期键的复制能够工作
- 副本不会使键过期,而是等待主服务器使键过期。当主服务器使键过期(或因 LRU 逐出键)时,它会合成一个
DEL
命令,该命令会传输到所有副本。 - 但是,由于主服务器驱动的过期,有时副本的内存中可能仍然存在逻辑上已过期的键,因为主服务器无法及时提供
DEL
命令。为了解决这个问题,副本使用其逻辑时钟来报告键不存在,**仅针对不违反数据集一致性的读取操作**(因为来自主服务器的新命令将到达)。这样,副本避免报告逻辑上已过期的但仍然存在的键。从实际角度来看,使用副本进行扩展的 HTML 片段缓存将避免返回已过期的项目(比预期的生存时间更旧)。 - 在 Lua 脚本执行期间,不会执行任何键过期。当 Lua 脚本运行时,概念上主服务器上的时间会冻结,因此给定的键在脚本运行的整个时间内要么存在要么不存在。这可以防止键在脚本中间过期,并且需要以保证在数据集中具有相同效果的方式将相同脚本发送到副本。
一旦副本被提升为主服务器,它将开始独立使键过期,并且不需要其旧主服务器的任何帮助。
在 Docker 和 NAT 中配置复制
当使用 Docker 或其他使用端口转发或网络地址转换的容器类型时,Redis 复制需要一些额外的注意事项,尤其是在使用 Redis Sentinel 或其他扫描主服务器 INFO
或 ROLE
命令输出以发现副本地址的系统时。
问题是,当在主服务器实例中发出 ROLE
命令和 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
。如果使用 replication
参数作为 INFO replication
调用该命令,则只会显示与复制相关的信息。另一个更易于计算机理解的命令是 ROLE
,它提供了主服务器和副本的复制状态及其复制偏移量、连接的副本列表等等。
重启和故障转移后的部分同步
从 Redis 4.0 版本开始,当实例在故障转移后被提升为主服务器时,它仍然能够与旧主服务器的副本执行部分重新同步。为此,副本会记住其以前主服务器的旧复制 ID 和偏移量,因此即使连接的副本请求旧复制 ID,它也可以提供部分积压数据。
然而,提升后的副本的新的复制 ID 将不同,因为它构成了数据集的不同历史记录。例如,主节点可以返回可用状态并可以继续接受写入一段时间,因此在提升后的副本中使用相同的复制 ID 将违反复制 ID 和偏移量对仅标识单个数据集的规则。
此外,副本 - 当以柔和的方式关闭并重新启动时 - 能够在 RDB
文件中存储与主节点重新同步所需的信息。这在升级情况下非常有用。当需要这样做时,最好使用 SHUTDOWN
命令来对副本执行 save & quit
操作。
无法部分同步通过 AOF 文件重新启动的副本。但是,实例可以在关闭之前切换到 RDB 持久化,然后可以重新启动,最后可以再次启用 AOF。
副本上的 Maxmemory
默认情况下,副本将忽略 maxmemory
(除非它在故障转移后被提升为主节点或手动提升)。这意味着键的逐出将由主节点处理,并将 DEL 命令发送到副本,因为键在主节点端被逐出。
此行为确保主节点和副本保持一致,这通常是您想要的。但是,如果您的副本是可写的,或者您希望副本具有不同的内存设置,并且您确信对副本执行的所有写入都是幂等的,那么您可以更改此默认设置(但请务必了解您在做什么)。
请注意,由于副本默认情况下不会执行逐出操作,因此它最终使用的内存可能会比通过 maxmemory
设置的内存更多(因为副本上可能存在某些缓冲区,这些缓冲区可能更大,或者数据结构有时可能占用更多内存等等)。请确保您监控您的副本,并确保它们有足够的内存,永远不会在主节点达到配置的 maxmemory
设置之前遇到真正的内存不足情况。
要更改此行为,您可以允许副本不忽略 maxmemory
。要使用的配置指令是
replica-ignore-maxmemory no