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
这样的管理命令仍然启用。Security页面描述了如何保护Redis实例。
您可能会想知道为什么可以撤销只读设置,并拥有可以成为写操作目标的副本实例。答案是可写副本仅出于历史原因存在。使用可写副本可能导致主服务器和副本之间的不一致,因此不建议使用可写副本。要理解在哪些情况下这可能成为问题,我们需要了解复制的工作原理。主服务器上的更改通过将常规Redis命令传播到副本来进行复制。当主服务器上的键过期时,这会作为DEL命令传播。如果主服务器上存在但在副本上被删除、过期或类型不同的键,对从主服务器传播的DEL、INCR或RPOP等命令的反应将与预期不同。传播的命令可能在副本上失败或导致不同的结果。为了最小化风险(如果您坚持使用可写副本),我们建议您遵循以下建议:
-
不要向可写副本中的键写入数据,这些键在主服务器上也被使用。 (如果你无法控制所有向主服务器写入数据的客户端,这可能很难保证。)
-
在运行系统中升级一组实例时,不要将实例配置为可写副本作为中间步骤。 一般来说,如果您想保证数据一致性,不要将实例配置为可写副本,如果它有可能被提升为主实例。
历史上,有一些用例被认为是可写副本的合法用途。 截至7.0版本,这些用例现在都已过时,可以通过其他方式实现相同的效果。 例如:
-
使用像
SUNIONSTORE
和ZINTERSTORE
这样的命令来计算慢速的Set或Sorted set操作,并将结果存储在临时的本地键中。 相反,使用不存储结果而直接返回结果的命令,例如SUNION
和ZINTER
。 -
使用
SORT
命令(由于可选的STORE选项,该命令不被视为只读命令,因此不能在只读副本上使用)。 相反,使用SORT_RO
,这是一个只读命令。 -
使用
EVAL
和EVALSHA
也不被视为只读命令,因为 Lua 脚本可能会调用写命令。 相反,使用EVAL_RO
和EVALSHA_RO
,其中 Lua 脚本只能调用只读命令。
如果副本和主服务器重新同步或副本重新启动,写入副本的数据将被丢弃,但不能保证它们会自动同步。
在4.0版本之前,可写副本无法使设置了生存时间的键过期。这意味着如果你使用EXPIRE
或其他为键设置最大TTL的命令,键将会泄漏,尽管在使用读取命令访问时你可能不再看到它,但你会在键的计数中看到它,并且它仍然会占用内存。Redis 4.0 RC3及更高版本能够像主节点一样驱逐带有TTL的键,除了写入数据库编号大于63的键(但默认情况下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秒,则写入将被接受。
你可以将其视为一种尽力而为的数据安全机制,其中不保证给定写入的一致性,但至少将数据丢失的时间窗口限制在给定的秒数内。一般来说,有界的数据丢失比无界的要好。
如果条件不满足,主节点将回复错误,并且写入不会被接受。
此功能有两个配置参数:
- 最小写入副本数
- 最小副本最大延迟
<秒数>
欲了解更多信息,请查看随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
命令来执行副本的保存并退出
操作。
无法通过AOF文件部分同步重新启动的副本。但是,在关闭实例之前,可以将其转换为RDB持久化,然后可以重新启动,最后可以再次启用AOF。
副本上的Maxmemory
默认情况下,副本会忽略maxmemory
(除非在故障转移后或手动提升为主节点)。这意味着键的驱逐将由主节点处理,当主节点上的键被驱逐时,会向副本发送DEL命令。
这种行为确保了主节点和副本保持一致,这通常是您想要的。 但是,如果您的副本是可写的,或者您希望副本具有不同的内存设置,并且您确定对副本执行的所有写入都是幂等的,那么您可以更改此默认设置(但请确保您理解自己在做什么)。
请注意,由于副本默认不会驱逐数据,它最终可能会使用比通过maxmemory
设置的更多的内存(因为某些缓冲区在副本上可能更大,或者数据结构有时可能占用更多内存等)。
确保您监控您的副本,并确保它们有足够的内存,在主节点达到配置的maxmemory
设置之前,永远不会遇到真正的内存不足情况。
要更改此行为,您可以允许副本不忽略maxmemory
。使用的配置指令是:
replica-ignore-maxmemory no