文章目录
安全
Redis 的安全模型依赖于可信的客户端和环境。其设计初衷是使 Redis 只被可信客户端访问,通常不应该直接暴露 Redis 实例到互联网或不受信任的环境中。在安全方面,Redis 提供了多层防护措施,主要包括:
- 访问控制
Redis 提供访问控制机制,推荐使用 Redis 6 及以上版本的访问控制列表(ACL),以创建具备细粒度权限的命名用户。传统的认证方式是通过 requirepass
设置在 redis.conf
文件中定义密码,客户端必须通过 AUTH
命令认证。
- 网络安全
为保证网络安全,应确保 Redis 端口仅对可信客户端开放,建议使用防火墙阻止外部访问。可以在 redis.conf
文件中指定绑定本地接口,示例如下:
bind 127.0.0.1
避免暴露 Redis 端口以防止如 FLUSHALL
命令导致的数据丢失。
- 受保护模式
Redis 从 3.2.0 版本开始引入了受保护模式,默认情况下,如果没有设置绑定接口或密码,Redis 将仅对本地请求作出响应,而拒绝外部访问。这一模式旨在减少暴露 Redis 实例所带来的安全隐患。
- TLS 支持
Redis 还支持 TLS,可以对所有通信通道进行加密,包括客户端连接、复制链接以及 Redis 集群总线协议,确保传输过程中的数据安全。
- 禁止特定命令
可以通过 redis.conf
文件中的 rename-command
指令,禁止或重命名 Redis 命令,防止未授权客户端执行敏感操作。例如,以下命令将 CONFIG
命令重命名为不可猜测的字符串:
rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
- 恶意输入攻击防护
Redis 实现了基于随机种子的哈希函数来防止恶意输入攻击。此外,Redis 使用了 qsort
算法进行排序,但由于该算法未随机化,恶意用户可能通过精心设计的输入触发最坏情况的行为,导致服务性能下降。
- 代码安全
在经典的 Redis 配置中,客户端可以访问所有命令,但 Redis 本身采用了多种安全编码实践,防止如缓冲区溢出等问题。建议运行 Redis 时不要使用 root 权限,而是以一个低权限的 redis
用户运行。
ACL
见ACL
TLS
见TLS
配置
Redis的确可以在没有配置文件的情况下启动,使用默认配置,但这种方式只适合于测试和开发阶段。在生产环境中,推荐通过redis.conf
配置文件来配置Redis。
redis.conf`配置文件
redis.conf
文件中的配置指令格式非常简单:
关键字 参数1 参数2 ... 参数N
例如:
replicaof 127.0.0.1 6380
如果参数包含空格,可以使用双引号或单引号将其包裹,例如:
requirepass "hello world"
单引号内的字符串可以包含反斜杠转义的字符,双引号内的字符串还可以使用带有反斜杠的十六进制表示法,例如\xff
。
Redis不同版本的redis.conf
文件都有详尽的描述文档,包含指令的详细解释:
通过命令行传递参数
除了redis.conf
,还可以通过命令行直接传递Redis的配置参数,这种方式常用于测试。例如:
./redis-server --port 6380 --replicaof 127.0.0.1 6379
命令行传递参数的格式与redis.conf
文件类似,唯一的区别是关键字前需要加上--
。实际上,这种方式会在内存中生成一个临时的配置文件。
动态修改Redis配置
可以使用CONFIG SET
和CONFIG GET
命令在Redis运行时动态修改或查询配置。需要注意的是,修改后的配置不会保存到redis.conf
文件中,下次重启时仍会使用旧配置。如果希望永久保存动态修改的配置,可以手动修改redis.conf
文件,或者使用CONFIG REWRITE
命令,自动更新配置文件中与当前运行配置不一致的字段。
Redis作为缓存使用
如果打算将Redis用作缓存,可以使用以下配置:
maxmemory 2mb
maxmemory-policy allkeys-lru
在这种配置下,Redis会在内存达到2MB时,使用LRU算法自动淘汰旧的键值对,类似于Memcached的工作方式。
Redis Sentinel
Redis Sentinel 提供了高可用性解决方案,适用于不使用 Redis Cluster 时的 Redis 部署。Sentinel 能够自动监控 Redis 主从实例的状态,并在主节点出现故障时自动执行故障转移,使得应用能够继续正常工作。Redis Sentinel 的主要功能包括:
- 监控 (Monitoring):Sentinel 持续监控 Redis 主节点和从节点,确保它们按预期工作。如果检测到问题,Sentinel 会采取相应的措施。
- 通知 (Notification):当 Sentinel 发现 Redis 实例有问题时,它可以通过 API 向系统管理员或其他程序发送通知,告知存在的问题。
- 自动故障转移 (Automatic failover):如果主节点出现问题,Sentinel 会启动自动故障转移,将其中一个从节点提升为新的主节点,并重新配置其他从节点以指向新主节点。应用程序也会得到通知以连接到新的主节点。
- 配置提供者 (Configuration provider):Sentinel 作为服务发现的权威信息源,客户端可以通过连接 Sentinel 来获取当前负责服务的主节点地址。如果发生故障转移,Sentinel 会向客户端报告新的主节点地址。
通过这些功能,Redis Sentinel 能够确保 Redis 实例在出现故障时能够自动恢复和重新配置,从而实现高可用性。
Redis Sentinel像一个分布式系统
Redis Sentinel 是设计成多个 Sentinel 进程协同工作的架构。多个 Sentinel 进程共同运行的优势如下:
- 降低误报概率:只有当多个 Sentinel 同意某个主节点不可用时,才会判定为主节点失效。这种机制减少了误报的概率,提高了检测的准确性。
- 系统具备容错能力:即使并非所有的 Sentinel 进程都在工作,系统仍然能够继续运作。这使得 Sentinel 系统在故障情况下仍然健壮可靠,避免了 Sentinel 本身成为单点故障的问题。
整个系统,包括 Sentinels、Redis 实例(主节点和从节点)以及连接到 Sentinel 和 Redis 的客户端,是一个更大的分布式系统。通过这种设计,Redis Sentinel 系统不仅能够自动故障转移,而且具有高度的可靠性,适合在高可用场景中使用。
运行Redis Sentinel
如果您使用的是 redis-sentinel
可执行文件(或如果您有一个指向 redis-server
可执行文件的符号链接,并命名为 redis-sentinel
),您可以使用以下命令行启动 Sentinel:
redis-sentinel /path/to/sentinel.conf
另外,您也可以直接使用 redis-server
可执行文件,以 Sentinel 模式启动:
redis-server /path/to/sentinel.conf --sentinel
这两种方式的效果是一样的。
需要注意的是,在运行 Sentinel 时,强制要求使用配置文件,因为该文件将用于保存当前状态,以便在重启时重新加载。如果未提供配置文件或配置文件路径不可写,Sentinel 将拒绝启动。
默认情况下,Sentinel 监听 TCP 26379 端口,因此,为了使 Sentinel 正常工作,您的服务器必须开放 26379 端口,以接收来自其他 Sentinel 实例的连接。否则,Sentinel 之间无法通信,也无法达成一致,故障转移将无法进行。
配置Redis Sentinel
Redis Sentinel 的配置文件 sentinel.conf
是一个自我文档化的示例配置文件,可以用来配置 Sentinel。一个典型的最小配置文件如下所示:
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5
您只需指定要监控的主节点,并为每个主节点(可以有任意数量的从节点)指定不同的名称。无需指定从节点,因为它们会自动发现。Sentinel 会自动更新配置以保留关于从节点的额外信息(以防重启)。每当从节点被提升为主节点或发现新的 Sentinel 时,配置都会重写。
上述示例配置基本上监控了两组 Redis 实例,每组由一个主节点和一个未定义数量的从节点组成。第一组实例称为 mymaster
,第二组称为 resque
。
sentry monitor <master-name> <ip> <port> <quorum>
的参数含义如下:
- master-name:要监控的主节点名称(例如
mymaster
)。 - ip:主节点的 IP 地址(例如
127.0.0.1
)。 - port:主节点的端口(例如
6379
)。 - quorum:达成共识以标记主节点为失败的 Sentinel 数量。
quorum参数
如果您有 5 个 Sentinel 进程,并且为某个主节点设置了 quorum 为 2,具体情况如下:
- 故障检测:如果有两个 Sentinel 同时认为主节点不可达,那么这两个 Sentinel 中的一个将尝试启动故障转移过程。
- 故障转移授权:只有当至少有三个 Sentinel 处于可用状态时,故障转移才会获得授权并实际启动。这意味着在进行故障转移之前,需要有大多数 Sentinel 进程能够互相通信。
在实践中,这意味着在故障发生期间,如果大多数 Sentinel 进程无法通信(例如,在网络分区或部分故障的情况下),Sentinel 将不会启动故障转移。此设计的目的是避免错误的故障转移,确保系统的稳定性和可用性。通过设置适当的 quorum,Sentinel 可以在不必要的情况下避免错误的判断,从而增强整个系统的稳定性。
其它Sentinel 选项
在 Redis Sentinel 的配置中,其他选项通常采用以下形式:
sentinel <option_name> <master_name> <option_value>
这些选项用于以下目的:
- down-after-milliseconds:这是一个以毫秒为单位的时间,当某个实例在此时间内未能响应(无论是未回复 PING 请求,还是回复错误)时,Sentinel 将开始认为该实例处于“下线”状态。
- parallel-syncs:设置故障转移后能够同时重新配置为使用新主节点的副本数量。较低的值将使故障转移过程的完成时间变长,但如果副本配置为服务旧数据,您可能不希望所有副本同时与主节点重新同步。尽管副本的复制过程大部分是非阻塞的,但在从主节点加载大量数据的瞬间,副本需要停止操作。通过将此选项设置为 1,您可以确保一次只有一个副本不可达。
配置参数可以在运行时修改:
- 针对特定主节点的配置参数:可以使用
SENTINEL SET
命令进行修改。 - 全局配置参数:可以使用
SENTINEL CONFIG SET
命令进行修改。
有关其他选项的详细描述,请参见文档的其余部分以及与 Redis 发行版一起提供的示例 sentinel.conf
文件。 运行时重新配置 Sentinel 的更多信息,可以查看相关章节,确保 Sentinel 在运行中能够灵活调整其配置,以适应不断变化的环境需求。
Redis Sentinel部署模式探索
ASCII 图示说明
我们使用 ASCII来以图形格式显示配置示例,不同的符号表示:
- 方框:表示独立失败的计算机或虚拟机,称为“盒子”。
- 线条:不同盒子之间的连接,表示它们能够相互通信。
实例名称约定:
- 主节点:M1、M2、M3、…、Mn
- 副本:R1、R2、R3、…、Rn(R 代表副本)
- Sentinel:S1、S2、S3、…、Sn
- 客户端:C1、C2、C3、…、Cn
- 当实例因 Sentinel 操作而改变角色时,我们用方括号表示,例如 [M1] 表示由于 Sentinel 干预而成为主节点的实例。
模式1:只用两个 Sentinel,不要这样做
quorum = 1
+----+ +----+
| M1 |---------| R1 |
| S1 | | S2 |
+----+ +----+
在这种设置中,如果主节点 M1 失败,R1 将被提升为新的主节点,因为两个 Sentinel 能够就故障达成一致(当然,仲裁设置为 1),并且也能授权故障转移,因为大多数节点的数量为 2。这种情况下看似有效,但实际上存在一些严重问题:
- 单点故障:如果运行 M1 的机器停止工作,S1 也会停止工作。此时,运行在其他机器上的 Sentinel(S2)无法授权故障转移,整个系统将变得不可用。
- 缺乏多数:故障转移需要多数节点的同意。只有当大多数 Sentinel 能够就主节点的状态达成一致时,才能安全地进行故障转移。否则,可能导致数据不一致和系统可用性降低。
- 分裂脑问题:如图所示:
在这个配置中,我们可能创建了两个主节点(M1 和 S2)。假设 S2 在没有授权的情况下进行了故障转移,那么客户端可能会同时向两个主节点写入数据。在网络分区恢复时,不可能确定哪个配置是正确的,这将导致“分裂脑”问题。+----+ +------+ | M1 |----//-----| [M1] | | S1 | | S2 | +----+ +------+
因此,为了避免这些潜在的灾难,请务必始终在三个不同的盒子中部署至少三个 Sentinel。这样可以确保:
- 提高可用性:即使一个 Sentinel 失效,其他 Sentinel 仍然能够监控并进行故障转移,确保系统的可用性。
- 避免数据不一致:通过确保大多数节点的同意,可以避免在分裂的情况下形成两个主节点。
综上所述,至少三个 Sentinel 的配置是确保 Redis 高可用性的最佳实践。
模式2:基本设置,三个盒子
quorum = 2
+----+
| M1 |
| S1 |
+----+
|
+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+
如果主节点 M1 失败,S2 和 S3 将会就故障达成一致,并能够授权进行故障转移,从而使客户端能够继续正常操作。
在任何 Sentinel 设置中,由于 Redis 使用异步复制,总是存在丢失写入数据的风险,因为某个已确认的写入可能无法到达被提升为主节点的副本。上述设置中由于客户端可能与旧主节点隔离,风险更高,如下图所示:
+----+
| M1 |
| S1 | <- C1 (写入将会丢失)
+----+
|
/
/
+------+ | +----+
| [M2] |----+----| R3 |
| S2 | | S3 |
+------+ +----+
在这种情况下,网络分区将旧主节点 M1 隔离,因此副本 R2 被提升为主节点。然而,像 C1 这样的客户端仍然在与旧主节点相同的分区内继续写入数据。这些数据将永远丢失,因为当分区恢复时,旧主节点会被重新配置为新主节点的副本,从而丢弃其数据集。
可以使用以下 Redis 复制功能来缓解这一问题,允许主节点在检测到无法将写入传输到指定数量的副本时停止接受写入:
min-replicas-to-write 1
min-replicas-max-lag 10
在上述配置中,当 Redis 实例作为主节点时,如果无法向至少一个副本写入数据,则会停止接受写入。由于复制是异步的,无法写入实际上意味着副本要么断开连接,要么在指定的最大延迟秒数内未发送异步确认。
使用此配置,旧的 Redis 主节点 M1 在上述示例中将在 10 秒后变得不可用。当分区恢复时,Sentinel 配置将收敛到新的配置,客户端 C1 将能够获取有效的配置并继续使用新主节点。
然而,没有免费的午餐。如果两个副本都宕机,主节点将停止接受写入。这是一种权衡:在确保数据一致性和可用性之间进行选择。在设计 Redis 高可用性解决方案时,需要根据具体的使用场景仔细考虑这些设置。
模式3:在客户端盒子中放置 Sentinel
quorum = 2
+----+ +----+
| M1 |----+----| R1 |
| | | | |
+----+ | +----+
|
+------------+------------+
| | |
+----+ +----+ +----+
| C1 | | C2 | | C3 |
| S1 | | S2 | | S3 |
+----+ +----+ +----+
在这个设置中,Sentinel 的视角与客户端相同:如果主节点可以被大多数客户端访问,那就是可行的。这里的 C1、C2 和 C3 是通用客户端,并不意味着 C1 代表一个单一的连接到 Redis 的客户端。更可能的是,它们代表的是应用服务器、Rails 应用程序等。
如果 M1 和 S1 所在的服务器发生故障,故障转移将顺利进行。然而,容易看出,不同的网络分区将导致不同的行为。例如,如果客户端与 Redis 服务器之间的网络断开,Sentinel 将无法进行配置,因为 Redis 的主节点和副本都会不可用。
模式4:客户端侧的 Sentinel 设置,少于三个客户端
quorum = 3
+----+ +----+
| M1 |----+----| R1 |
| S1 | | | S2 |
+----+ | +----+
|
+------+-----+
| |
+----+ +----+
| C1 | | C2 |
| S3 | | S4 |
+----+ +----+
这种设置与示例 3 类似,但这里我们在可用的四个盒子中运行四个 Sentinel。如果主节点 M1 变得不可用,其他三个 Sentinel 将执行故障转移。理论上,如果仅移除运行 C2 和 S4 的盒子,并将 quorum 设置为 2,此设置也能工作。然而,通常我们希望在 Redis 层实现高可用性的同时,应用层也能实现高可用性。
Sentinel, Docker, NAT和可能的问题
Docker 使用了一种称为端口映射的技术:在 Docker 容器内运行的程序可能会以不同于其认为正在使用的端口进行公开。这对于在同一台服务器上同时运行多个使用相同端口的容器非常有用。然而,端口和地址的重映射会给 Sentinel 带来两个主要问题:
-
Sentinel 自动发现失效:
- Sentinel 之间的自动发现依赖于它们发送的 hello 消息,这些消息包含它们监听的端口和 IP 地址。
- 当使用端口映射时,Sentinel 无法了解地址或端口已经被重映射,因此它们会宣布一个不正确的信息供其他 Sentinel 连接。
-
主节点的 INFO 输出问题:
- 在 Redis 主节点的 INFO 输出中,副本的信息是通过主节点检查 TCP 连接的远程对等体来检测的,而副本在握手期间会发布端口。
- 由于上述第一个问题,端口可能是错误的,从而导致副本无法被正确识别。
因为 Sentinel 使用主节点的 INFO 输出信息自动检测副本,所以在 Docker 部署的主副本实例中,Sentinel 将无法找到可用的副本,进而无法进行故障转移。为了在使用 Docker 的情况下运行一组 Sentinel 实例并解决上述问题,可以使用以下两个 Sentinel 配置指令,强制 Sentinel 宣布特定的 IP 和端口:
sentinel announce-ip <ip>
sentinel announce-port <port>
Redis Sentinel部署示例
在本节中,我们假设 Sentinel 实例在端口 5000、5001 和 5002 上运行,同时 Redis 主节点在端口 6379 上,副本在端口 6380 上。我们将整个教程中使用 IPv4 回环地址 127.0.0.1,假设您在个人计算机上运行模拟。
Sentinel 配置文件示例
sentinel5000.conf
:
port 5000
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel5001.conf
和 sentinel5002.conf
配置文件与 sentinel5000.conf
相同,但将端口号更改为 5001 和 5002。
启动 Sentinel
一旦您启动这三个 Sentinel 实例,您将看到它们记录的一些消息,例如:
+monitor master mymaster 127.0.0.1 6379 quorum 2
这是一个 Sentinel 事件,您可以通过 Pub/Sub 接收此类事件,如果您订阅后文中的事件名称。Sentinel 在故障检测和故障转移期间会生成和记录不同的事件。
查询主节点状态
要开始使用 Sentinel,最直接的方法是检查其监控的主节点的状态:
$ redis-cli -p 5000
127.0.0.1:5000> sentinel master mymaster
您应该看到类似以下的信息:
1) "name"
2) "mymaster"
3) "ip"
4) "127.0.0.1"
5) "port"
6) "6379"
7) "runid"
8) "953ae6a589449c13ddefaee3538d356d287f509b"
9) "flags"
10) "master"
11) "link-pending-commands"
12) "0"
13) "link-refcount"
14) "1"
15) "last-ping-sent"
16) "0"
17) "last-ok-ping-reply"
18) "735"
19) "last-ping-reply"
20) "735"
21) "down-after-milliseconds"
22) "5000"
23) "info-refresh"
24) "126"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "532439"
29) "config-epoch"
30) "1"
31) "num-slaves"
32) "1"
33) "num-other-sentinels"
34) "2"
35) "quorum"
36) "2"
37) "failover-timeout"
38) "60000"
39) "parallel-syncs"
40) "1"
一些关键信息如下:
- num-other-sentinels:显示为 2,表明 Sentinel 已检测到两个其他 Sentinel 监控此主节点。如果您检查日志,您会看到生成的 +sentinel 事件。
- flags:显示为 master。如果主节点宕机,则这里应会看到 s_down 或 o_down 标志。
- num-slaves:正确设置为 1,表明 Sentinel 检测到有一个副本与主节点相连。
查询副本和其他 Sentinels
要进一步探索该实例,您可以尝试以下两个命令:
SENTINEL replicas mymaster
SENTINEL sentinels mymaster
第一个命令将提供有关连接到主节点的副本的类似信息,第二个命令则提供关于其他 Sentinels 的信息。
获取当前主节点的地址
正如我们所述,Sentinel 还充当配置提供者,供希望连接到一组主节点和副本的客户端使用。由于可能存在故障转移或重新配置,客户端无法知道给定实例的当前活动主节点,因此 Sentinel 提供了一个 API 来询问此问题:
127.0.0.1:5000> SENTINEL get-master-addr-by-name mymaster
1) "127.0.0.1"
2) "6379"
测试故障转移
此时,我们的 Sentinel 部署已准备好进行测试。我们可以通过终止主节点来检查配置是否更改。为此,我们可以执行以下命令:
redis-cli -p 6379 DEBUG sleep 30
此命令将使我们的主节点在 30 秒内变得不可达,模拟主节点因某种原因挂起。如果您检查 Sentinel 日志,应该会看到大量活动:
- 每个 Sentinel 都会检测到主节点故障,并记录 +sdown 事件。
- 此事件随后升级为 +odown,表示多个 Sentinel 就主节点不可达达成一致。
- Sentinel 进行投票,选出一个 Sentinel 开始第一次故障转移尝试。
- 故障转移发生。
如果您再次询问 mymaster
的当前主节点地址,最终我们应该得到不同的回复:
127.0.0.1:5000> SENTINEL get-master-addr-by-name mymaster
1) "127.0.0.1"
2) "6380"
到此为止,一切顺利……此时您可以开始创建自己的 Sentinel 部署,或者可以继续阅读以了解所有 Sentinel 命令和内部机制。
Sentinel API
Sentinel 提供了一个 API,以便检查其状态、监控主节点和副本的健康状况、订阅以接收特定通知,并在运行时更改 Sentinel 配置。
默认情况下,Sentinel 运行在 TCP 26379 端口(请注意,6379 是普通的 Redis 端口)。Sentinel 使用 Redis 协议接受命令,因此您可以使用 redis-cli 或任何其他未修改的 Redis 客户端与 Sentinel 通信。
您可以直接查询 Sentinel,以检查被监控的 Redis 实例的状态,查看它所知道的其他 Sentinel 等等。或者,通过使用 Pub/Sub,您可以接收来自 Sentinel 的推送式通知,每当某个事件发生时,例如故障转移或实例进入错误状态等。
Sentinel 命令
以下是 SENTINEL 命令的主要 API 及其子命令列表(适用的最低版本已注明):
- SENTINEL CONFIG GET <name> (>= 6.2) 获取全局 Sentinel 配置参数的当前值。指定的名称可以是通配符,类似于 Redis 的 CONFIG GET 命令。
- SENTINEL CONFIG SET <name> <value> (>= 6.2) 设置全局 Sentinel 配置参数的值。
- SENTINEL CKQUORUM <master name> 检查当前 Sentinel 配置是否能够达到故障转移主节点所需的法定人数,以及授权故障转移所需的多数。此命令应在监控系统中使用,以检查 Sentinel 部署是否正常。
- SENTINEL FLUSHCONFIG 强制 Sentinel 重新将其配置写入磁盘,包括当前 Sentinel 状态。通常,Sentinel 会在状态更改时每次重新写入配置(在重启时持久化的状态子集的上下文中)。但是,有时由于操作错误、磁盘故障、软件包升级脚本或配置管理器,配置文件可能会丢失。在这种情况下,强制 Sentinel 重新写入配置文件的方法很方便。此命令即使在之前的配置文件完全丢失的情况下也能工作。
- SENTINEL FAILOVER <master name> 强制进行故障转移,仿佛主节点不可达,并且不需要其他 Sentinel 的同意(但会发布新版本的配置,以便其他 Sentinel 更新其配置)。
- SENTINEL GET-MASTER-ADDR-BY-NAME <master name> 返回具有该名称的主节点的 IP 和端口号。如果正在进行故障转移或该主节点的故障转移成功终止,则返回提升后的副本的地址和端口。
- SENTINEL INFO-CACHE (>= 3.2) 返回来自主节点和副本的缓存 INFO 输出。
- SENTINEL IS-MASTER-DOWN-BY-ADDR 检查当前 Sentinel 视角下,指定的 ip:port 主节点是否处于关闭状态。此命令主要供内部使用。
- SENTINEL MASTER <master name> 显示指定主节点的状态和信息。
- SENTINEL MASTERS 显示被监控主节点的列表及其状态。
- SENTINEL MONITOR 启动 Sentinel 的监控。有关更多信息,请参阅“运行时重新配置 Sentinel”部分。
- SENTINEL MYID (>= 6.2) 返回 Sentinel 实例的 ID。
- SENTINEL PENDING-SCRIPTS 此命令返回有关待处理脚本的信息。
- SENTINEL REMOVE 停止 Sentinel 的监控。有关更多信息,请参阅“运行时重新配置 Sentinel”部分。
- SENTINEL REPLICAS <master name> (>= 5.0) 显示该主节点的副本列表及其状态。
- SENTINEL SENTINELS <master name> 显示该主节点的 Sentinel 实例列表及其状态。
- SENTINEL SET 设置 Sentinel 的监控配置。有关更多信息,请参阅“运行时重新配置 Sentinel”部分。
- SENTINEL SIMULATE-FAILURE (crash-after-election|crash-after-promotion|help) (>= 3.2) 此命令模拟不同的 Sentinel 崩溃场景。
- SENTINEL RESET <pattern> 此命令将重置所有匹配名称的主节点。pattern 参数是一个通配符模式。重置过程将清除主节点的任何先前状态(包括进行中的故障转移),并删除与该主节点已发现并关联的每个副本和 Sentinel。
对于连接管理和管理目的,Sentinel 支持以下 Redis 命令的子集:
- ACL (>= 6.2) 此命令管理 Sentinel 访问控制列表。有关更多信息,请参阅 ACL 文档页面和 Sentinel 访问控制列表身份验证。
- AUTH (>= 5.0.1) 进行客户端连接的身份验证。有关更多信息,请参阅 AUTH 命令和“使用身份验证配置 Sentinel 实例”部分。
- CLIENT 此命令管理客户端连接。有关更多信息,请参阅其子命令页面。
- COMMAND (>= 6.2) 此命令返回有关命令的信息。有关更多信息,请参阅 COMMAND 命令及其各种子命令。
- HELLO (>= 6.0) 切换连接的协议。有关更多信息,请参阅 HELLO 命令。
- INFO 返回有关 Sentinel 服务器的信息和统计信息。有关更多信息,请参阅 INFO 命令。
- PING 此命令仅返回 PONG。
- ROLE 此命令返回字符串“sentinel”和被监控主节点的列表。有关更多信息,请参阅 ROLE 命令。
- SHUTDOWN 关闭 Sentinel 实例。
最后,Sentinel 还支持 SUBSCRIBE
、UNSUBSCRIBE
、PSUBSCRIBE
和 PUNSUBSCRIBE
命令。有关更多详细信息,请参阅 Pub/Sub 消息部分。
在运行时重新配置Sentinel
从 Redis 版本 2.8.4 开始,Sentinel 提供了一个 API 来添加、移除或更改指定主节点的配置。请注意,如果您有多个 Sentinel 实例,您应该对所有实例应用更改,以便 Redis Sentinel 正常工作。这意味着更改单个 Sentinel 的配置不会自动传播到网络中的其他 Sentinel。
以下是用于更新 Sentinel 实例配置的 SENTINEL 子命令列表:
- SENTINEL MONITOR <name> <ip> <port> <quorum> 此命令指示 Sentinel 开始监控具有指定名称、IP、端口和法定人数的新主节点。它与 sentinel.conf 配置文件中的 sentinel monitor 配置指令相同,唯一的区别是您不能在 IP 中使用主机名,而需要提供 IPv4 或 IPv6 地址。
- SENTINEL REMOVE <name> 用于移除指定的主节点:该主节点将不再被监控,并将完全从 Sentinel 的内部状态中移除,因此它将不再被 SENTINEL masters 等命令列出。
- SENTINEL SET <name> [<option> <value> …] 此命令与 Redis 的 CONFIG SET 命令非常相似,用于更改特定主节点的配置参数。可以指定多个选项/值对(或根本不指定)。所有可以通过 sentinel.conf 配置的参数也可以使用 SET 命令进行配置。
以下是一个使用 SENTINEL SET
命令来修改名为 objects-cache 的主节点的 down-after-milliseconds 配置的示例:
SENTINEL SET objects-cache-master down-after-milliseconds 1000
如前所述,SENTINEL SET
可以用于设置启动配置文件中可以设置的所有配置参数。此外,可以仅通过使用以下命令来更改主节点的法定人数配置,而无需使用 SENTINEL REMOVE
删除并重新添加主节点:
SENTINEL SET objects-cache-master quorum 5
请注意,没有等效的 GET
命令,因为 SENTINEL MASTER
提供了所有配置参数,以易于解析的格式(作为字段/值对数组)。
增加或删除Sentinel
将新的 Sentinel 添加到您的部署中是一个简单的过程,因为 Sentinel 实现了自动发现机制。您只需启动新的 Sentinel,并配置它以监控当前活动的主节点。在 10 秒内,Sentinel 将获取其他 Sentinels 的列表以及附加到主节点的副本集。
如果您需要一次添加多个 Sentinels,建议逐个添加,每添加一个就等待所有其他 Sentinels 知道第一个 Sentinel 后再添加下一个。这是为了确保在添加新 Sentinels 的过程中,如果发生故障,仍能保证在分区的一侧达成多数。
这可以通过为每个新 Sentinel 添加 30 秒的延迟轻松实现,并且在没有网络分区的情况下进行。
在该过程结束时,可以使用命令 SENTINEL MASTER mastername 来检查所有 Sentinels 是否一致同意监控主节点的 Sentinel 总数。
移除一个 Sentinel 的过程则复杂一些:Sentinels 永远不会忘记已经看到的 Sentinels,即使它们长时间无法访问,因为我们不想动态改变授权故障转移所需的多数,并创建新的配置编号。因此,移除 Sentinel 的步骤如下,要求在没有网络分区的情况下执行:
- 停止您想要移除的 Sentinel 的进程。
- 向所有其他 Sentinel 实例发送 **SENTINEL RESET *** 命令(如果只想重置单个主节点,可以使用确切的主节点名称代替 *)。一个接一个地发送,每个实例之间至少等待 30 秒。
- 通过检查每个 Sentinel 的 SENTINEL MASTER mastername 输出,确保所有 Sentinels 对当前活动的 Sentinel 数量达成一致。
移除旧的主节点和无法到达的从节点
Sentinels 永远不会忘记给定主节点的副本,即使这些副本长时间无法访问。这是有用的,因为 Sentinels 应该能够在网络分区或故障事件后正确地重新配置返回的副本。
此外,在故障转移后,故障转移的主节点实际上会被添加为新主节点的副本,这样它将在再次可用时被重新配置为与新主节点进行复制。
然而,有时您可能希望永久地从 Sentinels 监控的副本列表中移除一个副本(这可能是旧的主节点)。
为此,您需要向所有 Sentinels 发送 SENTINEL RESET mastername 命令:它们将在接下来的 10 秒内刷新副本列表,只添加从当前主节点的 INFO 输出中列出的正确复制的副本。
发布/订阅消息
客户端可以将 Sentinel 作为一个与 Redis 兼容的 Pub/Sub 服务器(但不能使用 PUBLISH),以便订阅或模式订阅(PSUBSCRIBE
)频道,并接收特定事件的通知。
频道名称与事件名称相同。例如,名为 +sdown 的频道将接收与实例进入 SDOWN(SDOWN 意味着该实例在您查询的 Sentinel 的视角中不再可达)状态相关的所有通知。
要获取所有消息,只需使用 PSUBSCRIBE *
进行订阅。
以下是您可以使用此 API 接收的频道和消息格式的列表。第一个单词是频道/事件名称,其余部分是数据格式。
注意:当指定实例详细信息时,意味着提供以下参数以识别目标实例:
<instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port>
识别主节点的部分(从 @ 参数到最后)是可选的,仅在实例本身不是主节点时指定。
- +reset-master – 主节点已重置。
- +slave – 检测到并附加了新的副本。
- +failover-state-reconf-slaves – 故障转移状态已更改为重新配置副本状态。
- +failover-detected – 检测到由其他 Sentinel 或任何外部实体启动的故障转移(附加副本转变为主节点)。
- +slave-reconf-sent – 领导 Sentinel 向该实例发送了 REPLICAOF 命令,以将其重新配置为新的副本。
- +slave-reconf-inprog – 正在重新配置的副本显示为新主节点的副本 ip:port 对,但同步过程尚未完成。
- +slave-reconf-done – 副本现在已与新主节点同步。
- -dup-sentinel – 为指定主节点删除了一个或多个重复的 Sentinel(例如,当 Sentinel 实例重启时会发生这种情况)。
- +sentinel – 检测到并附加了该主节点的新 Sentinel。
- +sdown – 指定实例现在处于主观下线状态。
- -sdown – 指定实例不再处于主观下线状态。
- +odown – 指定实例现在处于客观下线状态。
- -odown – 指定实例不再处于客观下线状态。
- +new-epoch – 当前纪元已更新。
- +try-failover – 新的故障转移正在进行中,等待多数选举。
- +elected-leader – 赢得指定纪元的选举,可以进行故障转移。
- +failover-state-select-slave – 新的故障转移状态为选择副本:我们正在寻找合适的副本进行提升。
- no-good-slave – 没有合适的副本可供提升。当前我们将稍后尝试,但可能会更改,并且状态机在这种情况下将完全中止故障转移。
- selected-slave – 我们找到了指定的良好副本进行提升。
- failover-state-send-slaveof-noone – 我们正在尝试将提升的副本重新配置为主节点,等待其切换。
- failover-end-for-timeout – 故障转移因超时而终止,副本最终将被配置为与新主节点进行复制。
- failover-end – 故障转移成功结束。所有副本似乎已重新配置为与新主节点进行复制。
- switch-master – 主节点的新 IP 和地址是在配置更改后指定的。这是外部用户最感兴趣的消息。
- +tilt – 进入倾斜模式。
- -tilt – 退出倾斜模式。
Redis Replication
在 Redis 复制的基础上(不包括 Redis 集群或 Redis Sentinel 提供的高可用性功能),存在一种简单易用的主从(领导者-跟随者)复制机制。这种机制允许副本 Redis 实例成为主实例的精确副本。每当连接中断时,副本会自动重新连接到主节点,并会尝试成为主节点的精确副本,无论主节点发生什么情况。这个系统主要通过三个机制工作:
- 命令流复制:当主节点和副本实例连接良好时,主节点通过向副本发送命令流来保持副本更新,以复制主节点上的数据集的变化。这些变化可能是由于客户端写入、键过期、被驱逐或任何其他更改主节点数据集的操作引起的。
- 部分重同步:当主节点和副本之间的链接因网络问题或超时而断开时,副本会重新连接并尝试进行部分重同步。这意味着副本会试图仅获取在断开连接期间错过的命令流的一部分。
- 全重同步:当无法进行部分重同步时,副本会请求全重同步。这将涉及一个更复杂的过程,主节点需要创建其所有数据的快照,将其发送给副本,然后继续发送数据集更改时的命令流。
Redis 默认使用异步复制,具有低延迟和高性能的特性,这使得它成为绝大多数 Redis 用例的自然复制模式。然而,Redis 副本会定期与主节点异步确认接收到的数据量。因此,主节点在每次处理命令时并不等待副本的确认,但它会知道在需要时哪个副本已经处理了哪个命令。这使得可以选择性地实现同步复制。
客户端可以使用 WAIT
命令请求某些数据的同步复制。然而,WAIT
只能确保在其他 Redis 实例中存在指定数量的已确认副本,并不会将一组 Redis 实例转变为强一致性的 CP 系统:在故障转移期间,已确认的写入仍可能丢失,这取决于 Redis 持久性配置的具体情况。然而,WAIT
显著降低了在故障事件后丢失写入的可能性,尤其是在难以触发的故障模式下。
有关高可用性和故障转移的更多信息,请参阅 Redis Sentinel 或 Redis Cluster 文档。本文档的其余部分主要描述了 Redis 基本复制的基本特性。
注意:
- Redis使用异步复制,副本与主节点之间以异步方式确认已处理的数据量。
- 一个主节点可以有多个副本。
- 副本可以接受来自其他副本的连接。除了将多个副本连接到同一主节点,副本也可以以级联结构连接到其他副本。从Redis 4.0开始,所有子副本将从主节点接收完全相同的复制流。
- Redis复制在主节点上是非阻塞的。这意味着当一个或多个副本执行初始同步或部分重新同步时,主节点将继续处理查询。
- 复制在副本端也大体上是非阻塞的。在副本执行初始同步时,假如您在
redis.conf
中进行了相应配置,它可以使用旧版本的数据集处理查询。否则,您可以将Redis副本配置为在复制流断开时向客户端返回错误。然而,在初始同步后,旧数据集必须被删除并加载新数据集。在此短暂窗口期间(对于非常大的数据集,这个时间可能会持续很多秒),副本将阻塞传入连接。自Redis 4.0起,您可以配置Redis使得旧数据集的删除在不同的线程中进行,但加载新的初始数据集仍将在主线程中进行,并且会阻塞副本。 - 复制可以用于可扩展性,拥有多个副本以处理只读查询(例如,慢速O(N)操作可以转移到副本上),或者仅仅是为了提高数据安全性和高可用性。
- 您可以利用复制避免主节点将完整数据集写入磁盘的成本:一种典型的技术是在主节点的
redis.conf
中配置不持久化到磁盘,然后连接一个配置为定期保存或启用AOF的副本。然而,这种设置必须谨慎处理,因为重启的主节点将从一个空数据集开始:如果副本尝试与其同步,副本也将被清空。
主服务器关闭持久化时复制的安全性
在使用Redis复制的环境中,强烈建议在主节点和副本上启用持久化。当由于慢磁盘等原因无法实现持久化时,应将实例配置为禁止自动重启。为了更好地理解为什么启用自动重启且未开启持久化的主节点是危险的,我们来看一个可能导致数据丢失的故障模式:假设有一个设置,其中节点A充当主节点,持久化关闭,并且节点B和C从节点A复制数据。
- 节点A崩溃,但它有一个自动重启系统,可以重新启动进程。然而,由于持久化已关闭,节点将以空数据集重新启动。
- 节点B和C将从空的节点A进行复制,从而有效地销毁它们的数据副本。
当使用Redis Sentinel实现高可用性时,在主节点上关闭持久化并自动重启进程是非常危险的。例如,主节点可能重启得足够快,以至于Sentinel没有检测到故障,从而导致上述故障模式发生。
Redis Replication是如何工作的
每个Redis主节点都有一个复制ID,这是一个大的伪随机字符串,用于标记数据集的特定历史记录。每个主节点还有一个偏移量(offset),该偏移量在发送给副本的复制流的每个字节时递增,以更新副本中修改数据集的新更改的状态。即使没有副本实际连接,复制偏移量也会递增。因此,每对:
复制 I D ,偏移量 复制ID,偏移量 复制ID,偏移量
都可以唯一标识主节点的确切数据集版本。当副本连接到主节点时,它们使用PSYNC
命令发送其旧主节点的复制ID和迄今为止处理的偏移量。这样,主节点可以仅发送所需的增量部分。如果主节点的缓冲区没有足够的回溯数据,或者副本引用的历史(复制ID)不再被知晓,那么就会发生完全重新同步。在这种情况下,副本将从头开始获取完整的数据集副本。以下是完全同步的更详细的工作方式:
- 主节点启动一个后台保存进程以生成RDB文件。与此同时,它开始缓冲所有来自客户端的新的写命令。
- 当后台保存完成后,主节点将RDB文件传输给副本,副本将其保存在磁盘上,然后将其加载到内存中。
- 主节点随后会将所有缓存的新的写命令发送到副本。这是以命令流的形式进行的,格式与Redis协议本身相同。
复制ID
在之前的部分,我们提到如果两个实例具有相同的复制 ID 和复制偏移量,它们的数据是完全相同的。然而,了解复制 ID 的具体含义以及实例实际上具有两个复制 ID(主 ID 和次 ID)是很有用的。
- 复制 ID 的定义
复制 ID 基本上标记了一组特定的数据集历史。每当实例从头开始重新启动为主节点,或副本被提升为主节点时,都会为该实例生成一个新的复制 ID。在连接到主节点的副本会在握手后继承其复制 ID。因此,两个实例具有相同的 ID 表明它们持有相同的数据,但可能在不同的时间。这时,复制偏移量则用于理解在给定历史(复制 ID)下,哪个实例拥有最新的数据集。
- 为何 Redis 实例具有两个复制 ID
Redis 实例之所以有两个复制 ID,是因为副本被提升为主节点的情况。在故障转移后,提升的副本需要记住其过去的复制 ID,因为该复制 ID 是前主节点的复制 ID。这样,当其他副本与新主节点同步时,它们会尝试使用旧主节点的复制 ID 进行部分重新同步。这样的工作是可行的,因为当副本被提升为主节点时,它将其次 ID 设置为主 ID,记住在这个 ID 切换发生时的偏移量。随后,它会选择一个新的随机复制 ID,因为新的历史开始了。
在处理新副本连接时,主节点会将它们的 ID 和偏移量与当前 ID 和次 ID 进行匹配(在一定偏移量内,以确保安全)。简而言之,这意味着在故障转移后,连接到新提升的主节点的副本不必执行完全同步。
- 为什么副本在故障转移后需要更改其复制 ID
副本在故障转移后需要更改其复制 ID 的原因是,旧主节点可能由于某种网络分区仍在正常工作。如果保留相同的复制 ID,将违反任何两个随机实例具有相同 ID 和相同偏移量表示它们拥有相同数据集的事实。这种设计确保了数据一致性和系统的健壮性。
无盘复制
通常,完全重新同步需要在磁盘上创建一个 RDB 文件,然后从磁盘重新加载同一个 RDB 文件,以将数据传输到副本。在慢速磁盘上,这对主节点来说可能是一个非常有压力的操作。Redis 2.8.18 版本首次支持无盘复制(diskless replication)。在这种设置中,子进程直接通过网络将 RDB 文件发送给副本,而不使用磁盘作为中介存储。这种方法的优点包括:
- 降低延迟:由于不涉及磁盘读写,数据传输速度更快,可以减少数据同步的时间。
- 减轻负担:主节点在执行数据同步时不会受到慢速磁盘的影响,从而提高了整体性能和响应速度。
- 资源优化:可以在没有磁盘 IO 竞争的情况下更有效地使用系统资源,尤其是在高负载情况下。
这种无盘复制技术大大改善了 Redis 的性能,特别是在对磁盘 IO 速度要求较高的场景中。
配置
配置基本的 Redis 复制非常简单:只需在副本的配置文件中添加以下行:
replicaof 192.168.1.1 6379
当然,你需要将 192.168.1.1
和 6379
替换为你的主节点的 IP 地址(或主机名)和端口号。或者,你可以通过调用 REPLICAOF
命令来使主节点与副本开始同步。
要启用无盘复制,可以使用 repl-diskless-sync
配置参数。延迟开始传输以等待第一个副本到达后更多副本的时间由 repl-diskless-sync-delay
参数控制。
只读副本
自 Redis 2.6 以来,副本支持默认启用的只读模式。该行为由 redis.conf
文件中的 replica-read-only
选项控制,并且可以使用 CONFIG SET
在运行时启用或禁用。
只读副本将拒绝所有写命令,因此不可能因为错误而向副本写入数据。这并不意味着该特性旨在将副本实例暴露在互联网上或更一般的存在不可信客户端的网络中,因为像 DEBUG
或 CONFIG
这样的管理命令仍然是启用的。
你可能会想知道为什么可以恢复只读设置,并使副本实例可以接受写操作。答案是,允许可写副本的存在仅仅是历史原因。使用可写副本可能导致主节点和副本之间的不一致,因此不建议使用可写副本。要理解在什么情况下这可能会导致问题,我们需要了解复制是如何工作的。主节点上的更改通过将常规 Redis 命令传播到副本来进行复制。当主节点上的键过期时,这将作为 DEL
命令传播。如果主节点上存在的键在副本上被删除、过期或与主节点的类型不同,那么对于从主节点传播过来的 DEL
、INCR
或 RPOP
等命令,副本将以不同的方式反应。传播的命令可能在副本上失败,或者产生不同的结果。为了最小化风险(如果你坚持使用可写副本),我们建议遵循以下建议:
-
不要向可写副本中的键写入与主节点上也使用的键相同的值。(如果你无法控制所有向主节点写入的客户端,这可能很难保证。)
-
不要将实例配置为可写副本作为在运行系统中升级一组实例的中间步骤。一般来说,如果希望保证数据一致性,请不要将实例配置为可写副本,如果它可能被提升为主节点。
Redis Replication如何处理键过期
Redis 允许键具有有限的生存时间(TTL),这是一个非常实用的功能。然而,这个功能依赖于实例能够准确计时。尽管如此,Redis 副本在正确复制具有过期时间的键时表现良好,即使这些键是通过 Lua 脚本进行更改的。
为了实现这一功能,Redis 并不依赖主节点和副本之间的时钟同步,因为这无法解决,并且会导致竞争条件和数据集分歧。因此,Redis 使用了三种主要技术,使过期键的复制能够正常工作:
- 主节点驱动的过期
副本不会主动过期键,而是等待主节点过期这些键。当主节点过期一个键(或因 LRU 被驱逐时),它会合成一个 DEL
命令并将其传输到所有副本。
但是,由于是主节点驱动的过期,有时副本可能仍会在内存中保留已在逻辑上过期的键,因为主节点未能及时发送 DEL
命令。为了处理这种情况,副本使用其逻辑时钟来报告在不违反数据集一致性的读取操作中键不存在(因为来自主节点的新命令将会到达)。通过这种方式,副本避免报告仍然存在的逻辑上已过期的键。在实际应用中,使用副本进行扩展的 HTML 片段缓存将避免返回已超出期望生存时间的项目。
- Lua 脚本执行时的时间冻结
在 Lua 脚本执行过程中,不会执行键的过期操作。当 Lua 脚本运行时,概念上主节点的时间是“冻结”的,因此给定键在脚本运行期间要么存在,要么不存在。这可以防止在脚本中途键过期,并确保在副本中发送相同的脚本时,保证对数据集产生相同的效果。
- 提升副本为主节点后独立过期
一旦副本被提升为主节点,它将开始独立地过期键,并且不需要旧主节点的任何帮助。这意味着副本在成为主节点后,可以自行管理键的生命周期,而不再依赖于之前的主节点来发送过期命令。
通过这些技术,Redis 能够有效地管理键的过期,同时确保副本和主节点之间的数据一致性。
INFO 和 ROLE 命令
Redis 提供了两个命令,可以获取有关主节点和副本实例当前复制参数的大量信息:
- INFO:当使用
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
Redis Cluster
Redis 通过称为 Redis 集群的部署拓扑实现横向扩展。本主题将教你如何在生产环境中设置、测试和操作 Redis 集群。你将从最终用户的角度了解 Redis 集群的可用性和一致性特性。
Redis Cluster 101
Redis 集群提供了一种可以将数据自动分片到多个 Redis 节点上的解决方案。它还能够在某些节点故障或无法通信时,继续保持一定的可用性。然而,如果出现较大规模的故障(例如多数主节点不可用),集群将无法正常运行。
因此,使用 Redis 集群,你可以实现以下功能:
- 自动将数据分布到多个节点。
- 当部分节点出现故障或无法与集群通信时,系统仍能继续运行。
Redis Cluster TCP端口
在 Redis 集群中,每个节点需要两个开放的 TCP 连接:一个用于服务客户端的 Redis TCP 端口,例如 6379;另一个称为集群总线端口。默认情况下,集群总线端口通过在数据端口上加 10000 来设置(例如,16379);不过,你可以在 cluster-port 配置中覆盖这个设置。
集群总线是一个节点间通信通道,使用二进制协议,更适合节点之间的信息交换,因为它占用的带宽和处理时间较少。节点使用集群总线进行故障检测、配置更新、故障转移授权等操作。客户端不应尝试与集群总线端口通信,而应使用 Redis 命令端口。但请确保在防火墙中同时开放两个端口,否则 Redis 集群节点将无法通信。
为了让 Redis 集群正常工作,每个节点需要:
- 用于客户端通信的端口(通常是 6379),用于与客户端通信,并对需要访问集群的所有客户端开放,还要允许其他集群节点通过此客户端端口进行键迁移。
- 集群总线端口必须能够从其他集群节点访问。
如果不同时开放这两个 TCP 端口,集群将无法正常工作。
Redis Cluster和Docker
目前,Redis 集群不支持 NAT 环境,或者一般情况下的 IP 地址或 TCP 端口被重新映射的环境。为了使 Docker 兼容 Redis 集群,你需要使用 Docker 的主机网络模式。有关详细信息,请参阅 Docker 文档中的 --net=host
选项。
Redis Cluster数据分片
Redis 集群并不使用一致性哈希,而是采用另一种形式的分片技术,每个键概念上都是我们称之为哈希槽的一部分。Redis 集群中共有 16384 个哈希槽,要计算给定键的哈希槽,我们只需将该键的 CRC16 结果对 16384 取模即可。每个 Redis 集群节点负责一部分哈希槽。例如,在一个有 3 个节点的集群中:
- 节点 A 负责哈希槽 0 到 5500;
- 节点 B 负责哈希槽 5501 到 11000;
- 节点 C 负责哈希槽 11001 到 16383。
这使得增加和移除集群节点变得简单。例如,如果我想添加一个新节点 D,只需从节点 A、B 和 C 中移动一些哈希槽到 D。类似地,如果我想从集群中移除节点 A,我可以将 A 负责的哈希槽移动到 B 和 C,一旦 A 为空,就可以完全将其移除。
从一个节点移动哈希槽到另一个节点并不需要停止任何操作。因此,增加或移除节点,或者改变节点持有的哈希槽比例,都不需要停机。
Redis 集群支持多键操作,只要所有涉及的键在同一个哈希槽内。用户可以通过一种叫做哈希标签(hash tags)的功能强制多个键属于同一个哈希槽。
哈希标签在 Redis 集群规范中有详细说明,基本原理是:如果一个键中有包含在 {} 大括号内的子字符串,那么只有这个字符串会被哈希。例如,键 user:{123}:profile
和 user:{123}:account
因为共享相同的哈希标签,所以它们会被分配到相同的哈希槽。因此,可以在同一多键操作中对这两个键进行操作。
Redis Cluster主副本模型
为了在部分主节点发生故障或无法与大多数节点通信时保持可用性,Redis 集群采用主-从(主节点-副本节点)模型,每个哈希槽至少有 1 个主节点和最多 N 个副本节点(即 N-1 个额外的副本节点)。 例如,在我们的集群中,如果节点 B 发生故障,集群将无法继续运行,因为我们无法再服务范围为 5501-11000 的哈希槽。然而,当集群创建时(或稍后),我们会为每个主节点添加一个副本节点,因此最终的集群将由 A、B、C 这三个主节点和 A1、B1、C1 这三个副本节点组成。这样,如果节点 B 发生故障,系统仍能继续运行。节点 B1 复制节点 B 的数据,如果节点 B 发生故障,集群将会将节点 B1 提升为新的主节点,并继续正常操作。但是,请注意,如果节点 B 和 B1 同时发生故障,Redis 集群将无法继续运行。
Redis Cluster一致性保证
Redis 集群并不保证强一致性。从实际角度来看,这意味着在某些情况下,Redis 集群可能会丢失已向客户端确认的写操作。Redis 集群可能会丢失写操作的第一个原因是它使用异步复制。这意味着在写操作过程中,发生的事情是:
- 客户端向主节点 B 写入数据。
- 主节点 B 向客户端回复 OK。
- 主节点 B 将写入操作传播到其副本 B1、B2 和 B3。
正如您所看到的,B 在回复客户端之前并不等待 B1、B2、B3 的确认,因为这样会导致 Redis 延迟显著增加。因此,如果您的客户端写入了数据,B 确认了写入,但在能够将写入传播给其副本之前崩溃了,那么没有接收到写入的副本将被提升为主节点,从而永远丢失该写入。
这与许多配置为每秒将数据刷新到磁盘的数据库的情况非常相似,因此您可以通过以往对传统数据库系统的经验来理解这一场景。类似地,您可以通过强制数据库在回复客户端之前将数据刷新到磁盘来提高一致性,但这通常会导致性能大幅降低。这在 Redis 集群中相当于同步复制。
基本上,这在性能和一致性之间存在权衡。
Redis 集群支持在绝对需要时进行同步写入,通过 WAIT
命令实现。这大大降低了写入丢失的可能性。然而,请注意,即使在使用同步复制的情况下,Redis 集群也并不实现强一致性:在更复杂的故障场景下,仍然有可能没有接收到写入的副本被选举为主节点。
还有一个显著的场景是在网络分区期间,客户端与包括至少一个主节点在内的少数实例隔离,这时 Redis 集群也会丢失写入。
以我们的 6 节点集群为例,由 A、B、C、A1、B1 和 C1 组成,包含 3 个主节点和 3 个副本。还有一个客户端,我们称之为 Z1。
在发生分区后,可能在分区的一侧有 A、C、A1、B1 和 C1,而在另一侧有 B 和 Z1。
Z1 仍然能够向 B 写入数据,B 将接受这些写入。如果分区在短时间内恢复,集群将正常继续运行。然而,如果分区持续足够长的时间,以至于 B1 在分区的多数一侧被提升为主节点,那么 Z1 在此期间发送给 B 的写入将会丢失。
注意:Z1 能够发送给 B 的写入数量有一个最大窗口:如果经过足够的时间使分区的多数一侧选举出一个副本作为主节点,少数一侧的每个主节点将停止接受写入。这个时间量是 Redis 集群的一个非常重要的配置指令,称为节点超时。
在节点超时过后,主节点将被视为故障,并可以被其副本替换。同样,在节点超时过后,如果没有主节点能够感知到其他主节点的大多数,节点将进入错误状态并停止接受写入。
Redis Cluster配置参数
我们即将创建一个示例集群部署。在继续之前,让我们介绍 Redis 集群在 redis.conf
文件中引入的配置参数。
- cluster-enabled <yes/no>:如果设置为 yes,则在特定的 Redis 实例中启用 Redis 集群支持。否则,实例将像往常一样以独立实例启动。
- cluster-config-file :请注意,尽管此选项的名称如此,但这并不是用户可编辑的配置文件,而是 Redis 集群节点在每次发生变化时自动持久化集群配置的文件(基本上是状态),以便在启动时能够重新读取。该文件列出了集群中的其他节点、它们的状态、持久变量等。通常,此文件会在接收到某些消息后被重写并刷新到磁盘。
- cluster-node-timeout :Redis 集群节点可以不可用的最大时间,超过此时间将被视为故障。如果主节点在指定的时间内不可达,则会由其副本进行故障切换。此参数控制 Redis 集群中的其他重要内容。值得注意的是,每个无法在指定时间内到达大多数主节点的节点将停止接受查询。
- cluster-slave-validity-factor :如果设置为零,则副本将始终认为自己是有效的,因此无论主节点与副本之间的链接断开多长时间,副本都将尝试故障切换主节点。如果值为正,则最大断开时间将被计算为节点超时值乘以该选项提供的因子。如果节点是副本,并且主链接断开超过指定时间,它将不会尝试启动故障切换。例如,如果节点超时设置为 5 秒,validity factor 设置为 10,则与主节点断开连接超过 50 秒的副本将不会尝试故障切换其主节点。请注意,任何非零值可能会导致在主节点故障后 Redis 集群不可用,如果没有副本能够进行故障切换。在这种情况下,集群将仅在原主节点重新加入集群后恢复可用。
- cluster-migration-barrier :主节点将保持与最小数量副本连接,以便另一个副本迁移到不再有任何副本覆盖的主节点。有关副本迁移的更多信息,请参见本教程中的相关章节。
- cluster-require-full-coverage <yes/no>:如果设置为 yes(默认设置),则如果某些键空间的百分比未被任何节点覆盖,集群将停止接受写操作。如果将选项设置为 no,则集群即使只有关于一部分键的请求可以被处理,仍将继续服务查询。
- cluster-allow-reads-when-down <yes/no>:如果设置为 no(默认设置),当集群标记为故障时,Redis 集群中的节点将停止服务所有流量,原因包括节点无法达到主节点的法定人数或未满足完全覆盖条件。这可以防止从无法感知集群中变化的节点读取潜在不一致的数据。可以将此选项设置为 yes,以允许在故障状态下从节点读取数据,这对于希望优先考虑读取可用性的应用程序非常有用,但仍希望防止不一致的写入。在只有一个或两个分片的情况下使用 Redis 集群时,也可以使用此选项,因为它允许节点在主节点故障但无法自动故障切换时继续提供写入服务。
创建和使用一个Redis Cluster
创建Redis Cluster的要求
要创建一个集群,您首先需要运行几个空的 Redis 实例,并将其设置为集群模式。至少在 redis.conf
文件中设置以下指令:
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
要启用集群模式,请将 cluster-enabled
指令设置为 yes
。每个实例还包含一个文件路径,用于存储该节点的配置,默认情况下为 nodes.conf
。此文件从不由人工修改;它仅在 Redis 集群实例启动时生成,并在每次需要时更新。
请注意,能够正常工作的最小集群必须包含至少三个主节点。对于部署,我们强烈建议使用六个节点的集群,其中包含三个主节点和三个副本节点。
您可以通过在任何给定目录中创建以下以实例端口号命名的目录来在本地进行测试。例如:
mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005
在每个目录中创建一个 redis.conf
文件,从 7000 到 7005。作为配置文件的模板,您可以使用上面的简短示例,但确保根据目录名称将端口号 7000 替换为正确的端口号。
您可以通过在每个终端选项卡中分别运行以下命令来启动每个实例:
cd 7000
redis-server ./redis.conf
您会在日志中看到每个节点为自己分配了一个新 ID:
[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1
此 ID 将在此特定实例的整个生命周期中使用,以便为该实例提供唯一的名称。在集群上下文中,每个节点都使用这些 ID 记住其他节点,而不是通过 IP 或端口。IP 地址和端口可能会更改,但唯一的节点标识符在节点的整个生命周期中将始终不变。我们将此标识符称为 Node ID。
创建一个Redis Cluster
现在我们有了一些正在运行的实例,您需要通过为节点编写一些有意义的配置来创建集群。
您可以手动配置和执行单个实例,或者使用 create-cluster
脚本。接下来我们将讨论如何手动进行操作。
要创建集群,请运行以下命令:
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
这里使用的命令是 create
,因为我们想要创建一个新的集群。选项 --cluster-replicas 1
意味着我们希望为每个创建的主节点设置一个副本。
其他参数是我想要用来创建新集群的实例地址列表。
redis-cli
将会提出一个配置。通过输入 yes
来接受建议的配置。集群将被配置并连接,这意味着实例将被初始化为相互通信。最后,如果一切顺利,您将看到如下消息:
[OK] All 16384 slots covered
这意味着至少有一个主实例正在服务于每一个可用的 16384 个槽。
如果您不想按照上述方式手动配置和执行单个实例来创建 Redis 集群,还有一个更简单的系统(但您将不会学到相同数量的操作细节)。
请在 Redis 发行版中找到 utils/create-cluster
目录。里面有一个名为 create-cluster
的脚本(与目录名相同),这是一个简单的 bash 脚本。要启动一个由 3 个主节点和 3 个副本节点组成的 6 节点集群,只需输入以下命令:
create-cluster start
create-cluster create
在第二步中,当 redis-cli
工具要求您接受集群布局时,请回复 yes
。
现在您可以与集群进行交互,默认情况下,第一个节点将启动在 30001 端口。当您完成时,可以使用以下命令停止集群:
create-cluster stop
请阅读此目录中的 README
以获取有关如何运行该脚本的更多信息。
与Redis Cluster的交互
要连接到 Redis 集群,您需要一个支持集群的 Redis 客户端。请查看您所选客户端的文档以确定其集群支持。
您也可以使用 redis-cli
命令行工具来测试您的 Redis 集群:
$ redis-cli -c -p 7000
接下来,您可以执行一些命令:
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7002> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"
注意: 如果您是使用脚本创建集群的,那么您的节点可能会监听不同的端口,默认情况下从 30001 开始。
redis-cli
的集群支持非常基本,因此它总是利用 Redis 集群节点能够将客户端重定向到正确节点的事实。一个更为高级的客户端能够做得更好,缓存哈希槽与节点地址之间的映射,以直接使用正确的连接到正确的节点。该映射仅在集群配置发生变化时更新,例如在故障转移之后,或系统管理员通过添加或删除节点更改集群布局之后。
用redis-rb-cluster编写一个示例应用
在继续展示如何操作 Redis 集群,比如故障转移或重新分片之前,我们需要创建一些示例应用程序,或至少能够理解简单的 Redis 集群客户端交互的语义。
这样,我们可以运行一个示例,同时尝试使节点失效或启动重新分片,以查看 Redis 集群在现实世界条件下的表现。在没有人写入集群的情况下观察发生的事情并没有什么帮助。
本节介绍了 redis-rb-cluster
的一些基本用法,展示了两个示例。第一个示例是 redis-rb-cluster
发行版中的 example.rb
文件:
require './cluster'
if ARGV.length != 2
startup_nodes = [
{:host => "127.0.0.1", :port => 7000},
{:host => "127.0.0.1", :port => 7001}
]
else
startup_nodes = [
{:host => ARGV[0], :port => ARGV[1].to_i}
]
end
rc = RedisCluster.new(startup_nodes, 32, :timeout => 0.1)
last = false
while not last
begin
last = rc.get("__last__")
last = 0 if !last
rescue => e
puts "error #{e.to_s}"
sleep 1
end
end
((last.to_i + 1)..1000000000).each { |x|
begin
rc.set("foo#{x}", x)
puts rc.get("foo#{x}")
rc.set("__last__", x)
rescue => e
puts "error #{e.to_s}"
end
sleep 0.1
}
该应用程序做了一件非常简单的事情,它依次将键 foo<number>
设置为对应的数字。因此,如果您运行程序,结果将是以下命令流:
SET foo0 0
SET foo1 1
SET foo2 2
依此类推...
程序看起来比实际复杂,因为它设计为在屏幕上显示错误,而不是在出现异常时退出,因此每个与集群的操作都用 begin
和 rescue
块包裹。
第 14 行是程序中第一个有趣的地方。它创建了 Redis 集群对象,使用可启动节点的列表、该对象允许对不同节点进行的最大连接数,以及在某个操作被视为失败后的超时时间作为参数。
启动节点不需要是集群的所有节点。重要的是至少有一个节点是可到达的。请注意,redis-rb-cluster
在能够连接到第一个节点后会立即更新这个启动节点列表。您应该期望任何其他成熟客户端也会有这样的行为。
现在我们在 rc
变量中存储了 Redis 集群对象实例,准备像使用普通 Redis 对象实例一样使用该对象。
这正是在第 18 到 26 行中发生的:当我们重新启动示例时,我们不想从 foo0
开始,因此我们将计数器存储在 Redis 本身中。上面的代码旨在读取这个计数器,如果计数器不存在,则将其值赋为零。
但是注意这是一个 while
循环,因为我们希望即使集群关闭并返回错误也要不断尝试。正常应用程序不需要这么小心。
第 28 到 37 行之间开始了主要循环,在该循环中设置键或显示错误。
注意循环末尾的 sleep
调用。在您的测试中,您可以移除 sleep
,如果您希望尽可能快地写入集群(相对来说,这是一个繁忙的循环,当然没有真正的并行,因此在最佳条件下通常会达到 10k ops/秒)。
通常,写入操作会减慢,以便让示例应用程序更容易被人类跟踪。
启动应用程序会产生以下输出:
ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (我在这里停止了程序)
这不是一个非常有趣的程序,我们将在稍后使用一个更好的程序,但我们已经可以看到在程序运行期间发生的重新分片情况。
重启Cluster
现在我们准备尝试集群的重新分片。在此之前,请确保保持 example.rb
程序的运行状态,以便观察程序运行时是否会受到影响。您可能还想注释掉 sleep
调用,以便在重新分片期间产生更高的写入负载。
重新分片基本上意味着将哈希槽从一组节点移动到另一组节点。与集群创建一样,这也是使用 redis-cli
工具完成的。要开始重新分片,只需输入以下命令:
redis-cli --cluster reshard 127.0.0.1:7000
您只需指定一个节点,redis-cli
会自动找到其他节点。
当前,redis-cli
只能在管理员支持下进行重新分片,您不能仅仅说将 5% 的槽从一个节点移动到另一个节点(但这很简单,可以实现)。因此,它会开始询问。第一个问题是您希望进行多少重新分片:
您希望移动多少个槽(1 到 16384)?
我们可以尝试重新分片 1000 个哈希槽,这已经包含了相当数量的键,如果示例仍在运行并且没有 sleep
调用的话。
接下来,redis-cli
需要知道重新分片的目标,即将接收哈希槽的节点。我将使用第一个主节点,即 127.0.0.1:7000
,但我需要指定实例的节点 ID。该 ID 已经在 redis-cli
列表中打印出来,但如果需要,我可以使用以下命令查找节点的 ID:
$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460
所以我的目标节点是 97a3a64667477371c4479320d683e4c8db5858b1
。
现在,您会被询问要从哪些节点获取这些键。我将输入 all
,以便从所有其他主节点获取一些哈希槽。
在最终确认后,您将看到每个槽的消息,表示 redis-cli
将从一个节点移动到另一个节点,并且每移动一个实际的键,都会打印一个点。
在重新分片进行过程中,您应该能够看到示例程序在不受影响的情况下运行。如果需要,您可以在重新分片期间多次停止和重新启动程序。
在重新分片结束后,您可以使用以下命令测试集群的健康状况:
redis-cli --cluster check 127.0.0.1:7000
所有的槽将照常覆盖,但这次位于 127.0.0.1:7000
的主节点将拥有更多的哈希槽,数量大约在 6461 左右。
重新分片可以在不需要手动输入参数的情况下自动执行。这可以使用如下命令实现:
redis-cli --cluster reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes
这允许在您经常需要重新分片的情况下建立一些自动化功能,但是目前 redis-cli
还无法自动重新平衡集群,检查集群节点之间的键分布并智能地移动槽。此功能将在将来添加。
--cluster-yes
选项指示集群管理器自动回答“yes”以响应命令的提示,从而允许以非交互方式运行。请注意,此选项也可以通过设置 REDISCLI_CLUSTER_YES
环境变量来激活。
一个更有趣的示例应用程序
我们之前编写的示例应用程序并不是很好。它以简单的方式写入集群,甚至没有检查所写的内容是否正确。
从我们的角度来看,接收写入的集群可以在每次操作中将键 foo
始终写入 42,而我们根本不会注意到。
因此,在 redis-rb-cluster
仓库中,有一个更有趣的应用程序,名为 consistency-test.rb
。它使用一组计数器,默认情况下为 1000,并发送 INCR
命令来递增计数器。
然而,这个应用程序除了写入外,还做了两个额外的事情:
- 当计数器通过
INCR
更新时,应用程序会记住这次写入。 - 在每次写入之前,应用程序会随机读取一个计数器,并检查其值是否符合预期,与内存中的值进行比较。
这意味着这个应用程序是一个简单的一致性检查器,能够告诉您集群是否丢失了一些写入,或者是否接受了我们没有收到确认的写入。在第一种情况下,我们会看到一个计数器的值小于我们记住的值,而在第二种情况下,值会更大。
运行 consistency-test
应用程序会每秒产生一行输出:
$ ruby consistency-test.rb
925 R (0 err) | 925 W (0 err) |
5030 R (0 err) | 5030 W (0 err) |
9261 R (0 err) | 9261 W (0 err) |
13517 R (0 err) | 13517 W (0 err) |
17780 R (0 err) | 17780 W (0 err) |
22025 R (0 err) | 22025 W (0 err) |
25818 R (0 err) | 25818 W (0 err) |
每行显示执行的读取和写入次数,以及由于系统不可用而未接受的查询错误数。
如果发现某些不一致,输出将添加新行。例如,如果我在程序运行时手动重置一个计数器:
$ redis-cli -h 127.0.0.1 -p 7000 set key_217 0
OK
(在另一个终端中,我看到……)
94774 R (0 err) | 94774 W (0 err) |
98821 R (0 err) | 98821 W (0 err) |
102886 R (0 err) | 102886 W (0 err) | 114 lost |
107046 R (0 err) | 107046 W (0 err) | 114 lost |
当我将计数器设置为 0 时,实际值为 114,因此程序报告了 114 次丢失的写入(未被集群记住的 INCR
命令)。
这个程序作为测试用例更有趣,因此我们将使用它来测试 Redis 集群的故障转移。
测试故障转移
为了触发故障转移,最简单的方法是崩溃单个进程,这里指的是单个主节点。
在此测试中,您应该在另一标签页中运行一致性测试应用程序。我们可以通过以下命令识别一个主节点并将其崩溃:
$ redis-cli -p 7000 cluster nodes | grep master
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385482984082 0 connected 5960-10921
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 master - 0 1385482983582 0 connected 11423-16383
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422
可以看到,7000、7001和7002是主节点。我们可以通过以下命令崩溃节点7002:
$ redis-cli -p 7002 debug segfault
接下来,我们可以查看一致性测试的输出,观察报告的结果:
18849 R (0 err) | 18849 W (0 err) |
23151 R (0 err) | 23151 W (0 err) |
27302 R (0 err) | 27302 W (0 err) |
… 随后会出现许多错误警告 …
29659 R (578 err) | 29660 W (577 err) |
33749 R (578 err) | 33750 W (577 err) |
37918 R (578 err) | 37919 W (577 err) |
42077 R (578 err) | 42078 W (577 err) |
如您所见,在故障转移期间,系统无法接受578个读取和577个写入请求,但数据库中没有产生不一致。这听起来可能出乎意料,因为在本教程的第一部分中我们提到过,Redis集群在故障转移期间可能会丢失写入,但我们并没有说明这种情况发生的可能性非常小。这是因为Redis同时向客户端发送回复和向副本发送复制命令,因此丢失数据的窗口非常小。然而,这并不意味着不可能发生,所以这并不改变Redis集群所提供的一致性保证。
现在我们可以检查故障转移后的集群设置(在此期间,我重新启动了崩溃的实例,以便它重新加入集群作为副本):
$ redis-cli -p 7000 cluster nodes
3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385503418521 0 connected
a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385503419023 0 connected
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5959 10922-11422
3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385503419023 3 connected
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385503417005 0 connected
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385503418016 3 connected
现在主节点运行在7000、7001和7005端口。先前的主节点(7002)现在是7005的副本。
CLUSTER NODES
命令的输出可能看起来很复杂,但实际上很简单,由以下标记组成:
- 节点ID
- ip:port
- 标志:master, replica, myself, fail 等等
- 如果是副本,则是主节点的Node ID
- 上一次等待回复的PING的时间
- 上一次收到的PONG的时间
- 该节点的配置纪元(见集群规范)
- 与该节点的链接状态
- 提供的槽位
手动故障转移
有时,强制故障转移而不实际对主节点造成任何问题是很有用的。例如,要升级其中一个主节点的Redis进程,故障转移到副本是一个不错的主意,这样对可用性的影响最小。
Redis集群通过在您想要故障转移的主节点的一个副本中执行 CLUSTER FAILOVER
命令来支持手动故障转移。
手动故障转移是特殊的,相比于因实际主节点故障而导致的故障转移,它更安全。手动故障转移的过程可以避免数据丢失,通过确保新的主节点处理完所有来自旧主节点的复制流后,再将客户端从原主节点切换到新的主节点。
以下是您在执行手动故障转移时副本日志中的输出:
# Manual failover user request accepted.
# Received replication offset for paused master manual failover: 347540
# All master replication stream processed, manual failover can start.
# Start of election delayed for 0 milliseconds (rank #0, offset 347540).
# Starting a failover election for epoch 7545.
# Failover election won: I'm the new master.
基本上,与我们正在故障转移的主节点连接的客户端会被停止。与此同时,主节点会将其复制偏移发送给副本,副本会等待在其端达到该偏移。当复制偏移达到后,故障转移就开始,旧主节点会被通知配置切换。当旧主节点上的客户端被解除阻塞时,它们会被重定向到新的主节点。
需要注意的是,为了将副本提升为主节点,它必须首先被集群中大多数主节点识别为副本。否则,它无法赢得故障转移选举。如果副本刚刚被添加到集群中(见“将新节点添加为副本”),您可能需要等待一段时间再发送 CLUSTER FAILOVER
命令,以确保集群中的主节点已经意识到新的副本。
添加一个主节点
添加新节点基本上是将一个空节点添加到集群中,然后将一些数据移入该节点(如果它是新的主节点),或者告诉它作为已知节点的副本进行设置(如果它是副本)。
我们将展示这两种情况,从添加新的主实例开始。
在这两种情况下,执行的第一步都是添加一个空节点。
这个过程很简单,只需在端口7006启动一个新节点(我们已经使用了7000到7005的现有6个节点),并使用与其他节点相同的配置,唯一的区别是端口号。因此,您应该在您的设置中执行以下操作:
- 在终端应用程序中创建一个新标签页。
- 进入
cluster-test
目录。 - 创建一个名为
7006
的目录。 - 创建一个名为
redis.conf
的文件,内容与其他节点使用的相似,但将端口号更改为7006。 - 最后使用命令
../redis-server ./redis.conf
启动服务器。
此时,服务器应该正在运行。
现在,我们可以像往常一样使用 redis-cli
将节点添加到现有集群中:
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000
在这里,我使用 add-node
命令,指定新节点的地址作为第一个参数,以及集群中一个随机现有节点的地址作为第二个参数。
实际上,redis-cli
在这里做的很少,它只是向节点发送了一个 CLUSTER MEET
消息,这也是可以手动完成的。但是,redis-cli
在操作之前也会检查集群的状态,因此,即使您知道内部工作原理,也建议始终通过 redis-cli
执行集群操作。
现在我们可以连接到新节点,查看它是否真的加入了集群:
redis 127.0.0.1:7006> cluster nodes
输出可能如下所示:
3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 127.0.0.1:7001 master - 0 1385543178575 0 connected 5960-10921
3fc783611028b1707fd65345e763befb36454d73 127.0.0.1:7004 slave 3e3a6cb0d9a9a87168e266b0a0b24026c0aae3f0 0 1385543179583 0 connected
f093c80dde814da99c5cf72a7dd01590792b783b :0 myself,master - 0 0 0 connected
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543178072 3 connected
a211e242fc6b22a9427fed61285e85892fa04e08 127.0.0.1:7003 slave 97a3a64667477371c4479320d683e4c8db5858b1 0 1385543178575 0 connected
97a3a64667477371c4479320d683e4c8db5858b1 127.0.0.1:7000 master - 0 1385543179080 0 connected 0-5959 10922-11422
3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7005 master - 0 1385543177568 3 connected 11423-16383
请注意,由于此节点已连接到集群,因此它能够正确重定向客户端查询,并且总体上是集群的一部分。但是,与其他主节点相比,它有两个特性:
- 它不持有数据,因为它没有分配的哈希槽。
- 由于它是没有分配槽的主节点,因此在副本想要成为主节点时,它不参与选举过程。
现在,可以使用 redis-cli
的重新分片(resharding)功能为此节点分配哈希槽。实际上展示这个过程并没有太大意义,因为我们在之前的部分中已经做过,唯一的区别就是目标是这个空节点。
添加一个副本节点
添加新副本可以通过两种方式进行。显而易见的一种是再次使用 redis-cli
,但需要加上 --cluster-slave
选项,如下所示:
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave
请注意,这里的命令行与我们之前添加新主节点时完全相同,因此我们没有指定要将副本添加到哪个主节点。在这种情况下,redis-cli
会将新节点作为一个随机主节点的副本添加,该主节点是副本数量较少的主节点之一。
不过,您可以通过以下命令行精确指定要针对的主节点:
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
通过这种方式,我们将新副本分配给特定的主节点。
添加副本的另一种更手动的方法是将新节点添加为空主节点,然后使用 CLUSTER REPLICATE
命令将其转变为副本。如果节点已经作为副本添加,但您想将其移动为不同主节点的副本,这种方法也适用。
例如,为了为节点 127.0.0.1:7005
添加副本,该节点当前正在服务哈希槽范围为 11423-16383
,并且其节点 ID 为 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
,我们只需连接到新节点(已添加为空主节点)并发送以下命令:
redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
就这样。现在我们有了这个哈希槽集的新副本,集群中的所有其他节点在更新其配置后也已经知道这一点(需要几秒钟的时间)。我们可以使用以下命令进行验证:
$ redis-cli -p 7000 cluster nodes | grep slave | grep 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
输出可能如下所示:
f093c80dde814da99c5cf72a7dd01590792b783b 127.0.0.1:7006 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617702 3 connected
2938205e12de373867bf38f1ca29d31d0ddb3e46 127.0.0.1:7002 slave 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 0 1385543617198 3 connected
节点 3c3a0c...
现在有两个副本,分别运行在端口 7002
(现有副本)和 7006
(新副本)。
删除一个节点
要移除副本节点,可以使用 redis-cli
的 del-node
命令:
redis-cli --cluster del-node 127.0.0.1:7000 <node-id>
第一个参数是集群中的任意节点,第二个参数是您想要移除的节点的 ID。
对于主节点,您可以使用相同的方法进行移除,但主节点必须是空的。如果主节点不为空,则需要将数据重新分片到其他主节点上。
移除主节点的另一种选择是对其进行手动故障转移,然后在其转变为新主节点的副本后移除该节点。显然,这种方法对于减少集群中实际的主节点数量并没有帮助,在这种情况下,需要进行数据重新分片。
在特殊情况下,如果要移除一个失败的节点,则不应使用 del-node
命令,因为该命令会尝试连接所有节点,可能会遇到“连接被拒绝”的错误。相反,您可以使用 call
命令:
redis-cli --cluster call 127.0.0.1:7000 cluster forget <node-id>
此命令将在每个节点上执行 CLUSTER FORGET
命令。
副本迁移
在 Redis 集群中,您可以随时使用以下命令将副本重新配置为与不同的主节点进行复制:
CLUSTER REPLICATE <master-node-id>
然而,在某些特殊情况下,您可能希望副本在没有系统管理员干预的情况下自动从一个主节点迁移到另一个主节点。这种自动副本重新配置称为副本迁移,可以提高 Redis 集群的可靠性。
您可能希望让集群中的副本在特定条件下自动移动的原因是,通常情况下,Redis 集群的故障抵抗能力与附加到给定主节点的副本数量有关。例如,一个每个主节点只有一个副本的集群,如果主节点和其副本同时故障,则无法继续操作,因为没有其他实例可以保留主节点所服务的哈希槽的副本。
尽管网络分割可能会同时隔离多个节点,但许多其他类型的故障(例如,单个节点本地的硬件或软件故障)是非常显著的故障类型,因此不太可能同时发生。假设在某个集群中,每个主节点都有一个副本,副本可能在凌晨 4 点被关闭,而主节点在早上 6 点被关闭。这仍会导致集群无法正常操作。
为了提高系统的可靠性,我们可以选择向每个主节点添加额外的副本,但这会导致开销。副本迁移允许您仅向一些主节点添加更多副本。因此,假设您有 10 个主节点,每个主节点都有 1 个副本,总共 20 个实例。然而,您可以添加例如 3 个额外实例作为某些主节点的副本,因此某些主节点将拥有多个副本。
副本迁移的工作原理是,如果某个主节点没有副本,则将从拥有多个副本的主节点迁移一个副本到孤立的主节点。因此,在上述例子中,如果您的副本在凌晨 4 点宕机,那么另一个副本将接替它的位置,当主节点在早上 5 点故障时,仍然可以选举出一个副本,从而使集群继续操作。
关于副本迁移,您需要了解的要点如下:
- 集群将尝试从当前拥有最多副本的主节点迁移一个副本。
- 要受益于副本迁移,您只需向集群中的某个主节点添加更多副本,主节点可以是任意的。
- 有一个配置参数控制副本迁移功能,称为
cluster-migration-barrier
,您可以在 Redis 集群提供的示例redis.conf
文件中找到更多信息。
升级Redis集群中的节点
升级副本节点很简单,因为您只需停止节点并使用更新版本的 Redis 重新启动它。如果有客户端通过副本节点进行读操作,若某个副本不可用,它们应该能够重新连接到其他副本。
升级主节点则稍微复杂一些,建议的步骤如下:
-
触发手动故障转移:使用
CLUSTER FAILOVER
命令将主节点手动故障转移到其中一个副本上。(请参阅此主题中的手动故障转移部分。) -
等待主节点转变为副本:确保原来的主节点成功转变为副本。
-
升级节点:按照升级副本的方式升级节点。
-
重新激活主节点:如果您希望将刚刚升级的节点重新设为主节点,触发一次新的手动故障转移,将升级后的节点转回为主节点。
按照此程序,您应该可以一个接一个地升级所有节点,确保集群的高可用性。
迁移到Redis Cluster
用户想要迁移到 Redis Cluster 时,可能只有一个主节点,或者已经在使用现有的分片设置,其中键被分配到 N 个节点,使用某种内部算法或由其客户端库或 Redis 代理实现的分片算法。
在这两种情况下,迁移到 Redis Cluster 都是可行的,但最重要的细节是应用程序是否使用了多键操作,以及如何使用。可以分为三种不同的情况:
-
不使用多键操作:键是独立访问的(即使通过事务或 Lua 脚本组合多个命令,处理的是相同的键)。
-
使用多键操作且键具有相同的哈希标签:即一起使用的键都有相同的
{...}
子字符串。例如,以下多键操作是在相同哈希标签的上下文中定义的:SUNION {user:1000}.foo {user:1000}.bar
。 -
使用多键操作且键名没有明确或相同的哈希标签:此情况 Redis Cluster 不支持,应用程序需要进行修改,以不使用多键操作或仅在相同哈希标签的上下文中使用它们。
前两种情况是可处理的,因此我们将关注这两种情况,它们的处理方式相同,因此文档中不做区分。
假设您已经将现有的数据集拆分为 N 个主节点,其中 N=1(如果您没有现有的分片),迁移数据集到 Redis Cluster 需要以下步骤:
-
停止您的客户端:目前不支持自动实时迁移到 Redis Cluster。您可以在应用程序/环境中协调实时迁移。
-
为所有 N 个主节点生成追加文件:使用
BGREWRITEAOF
命令,并等待 AOF 文件完全生成。 -
将 AOF 文件从 aof-1 保存到 aof-N:此时您可以停止旧实例(这很有用,因为在非虚拟化部署中,您通常需要重新使用相同的计算机)。
-
创建一个由 N 个主节点和零个副本组成的 Redis Cluster:稍后将添加副本。确保所有节点都使用追加文件进行持久化。
-
停止所有集群节点:用您的现有追加文件替换它们的追加文件,aof-1 替换第一个节点,aof-2 替换第二个节点,依此类推。
-
使用新的 AOF 文件重新启动 Redis Cluster 节点:它们会抱怨配置中不应存在的键。
-
使用
redis-cli --cluster fix
命令:修复集群,以便根据每个节点的哈希槽来迁移键。 -
最后使用
redis-cli --cluster check
检查集群是否正常。 -
重新启动已修改为使用 Redis Cluster 兼容的客户端库的客户端。
还有一种替代方法是使用 redis-cli --cluster import
命令从外部实例导入数据到 Redis Cluster。该命令会将运行实例的所有键移动到指定的现有 Redis Cluster 中(并从源实例中删除这些键)。但请注意,如果您使用 Redis 2.8 作为源实例,则操作可能较慢,因为 2.8 不实现迁移连接缓存,因此您可能希望在执行此操作之前将源实例重新启动为 Redis 3.x 版本。
Redis持久化
持久化指的是将数据写入耐用存储(如固态硬盘 SSD)的过程。Redis 提供了一系列持久化选项,具体如下:
- RDB(Redis 数据库):
- RDB 持久化在指定的时间间隔内执行数据集的时间点快照。
- 这种方式适合于在一定频率下保存数据,可以减少对性能的影响。
- AOF(追加文件):
- AOF 持久化记录服务器接收到的每个写操作。
- 这些操作可以在服务器启动时重放,以重建原始数据集。命令使用与 Redis 协议相同的格式进行记录。
- AOF 提供了更细粒度的持久化方式,适用于需要更高数据可靠性的场景。
- 无持久化:
- 你可以完全禁用持久化。这种设置通常用于缓存场景,其中数据的持久性不重要。
- RDB + AOF:
- 你还可以在同一实例中结合使用 RDB 和 AOF。
- 这种组合能够同时享受两种持久化方式的优势,在数据安全性和性能之间取得平衡。
RDB的优点和缺点
优点如下:
- 备份的理想选择:
- RDB 文件非常适合用于备份。例如,你可以每小时归档 RDB 文件,保存最近 24 小时的数据快照,并每天保存一个 RDB 快照,保留 30 天。这使你能够在灾难发生时轻松恢复数据集的不同版本。
- 灾难恢复:
- RDB 是灾难恢复的优秀选择,因为它是一个单一的紧凑文件,可以方便地传输到远程数据中心或上传到 Amazon S3(可以加密传输)。
- 性能优化:
- RDB 最大化了 Redis 的性能。为了持久化,Redis 主进程只需创建一个子进程来处理所有的 I/O 操作,主进程不会执行磁盘 I/O 操作。这意味着在持久化过程中不会影响主进程的性能。
- 快速重启:
- 在处理大数据集时,RDB 允许比 AOF 更快的重启。这是因为 RDB 文件是一种更紧凑的表示,加载速度更快。
- 支持部分重新同步:
- 在副本上,RDB 支持在重启和故障转移后进行部分重新同步。这使得在主从复制的环境中,系统恢复的效率更高。
缺点如下:
- 数据丢失风险:
- 如果 Redis 停止工作(例如由于断电),你可能会丢失最近的数据。虽然可以配置不同的保存点以生成 RDB 快照(例如,在至少五分钟和 100 次写入后生成快照),但通常情况下,你的 RDB 快照每五分钟生成一次或更久。因此,如果 Redis 没有正常关闭,你可能会失去最近几分钟的数据。
- 频繁的
fork()
调用:- RDB 在持久化到磁盘时需要频繁调用
fork()
创建子进程。如果数据集很大,这个过程可能会耗时,导致 Redis 在执行fork()
时可能会停止为客户端提供服务几毫秒甚至一秒钟,尤其是在 CPU 性能不佳的情况下。
- RDB 在持久化到磁盘时需要频繁调用
AOF的优点和缺点
优点如下:
- 灵活的 fsync 策略:
- 使用 AOF 时,可以根据需要设置不同的
fsync
策略:- 不进行 fsync:最大化写入性能,但会增加数据丢失的风险。
- 每秒 fsync:这是默认策略,性能良好,最多只会丢失一秒钟的数据。
- 每次查询 fsync:提供最大的安全性,但可能会影响性能。
- 使用 AOF 时,可以根据需要设置不同的
- 后台线程处理 fsync:
- AOF 的
fsync
操作是由后台线程处理的,主线程会尽量在没有fsync
进行时进行写入操作。因此,在正常操作下,用户的写入性能不会受到太大影响。
- AOF 的
- append-only 日志格式:
- AOF 使用追加日志格式,这意味着没有随机寻址(seek)操作,并且在电源故障时不会出现数据损坏的问题。即使日志以半写入的命令结束,Redis 提供的
redis-check-aof
工具可以轻松修复。
- AOF 使用追加日志格式,这意味着没有随机寻址(seek)操作,并且在电源故障时不会出现数据损坏的问题。即使日志以半写入的命令结束,Redis 提供的
- 自动重写 AOF:
- Redis 能够在 AOF 文件过大时自动在后台重写 AOF。这一过程是安全的,Redis 在继续向旧文件追加数据的同时,生成一个包含当前数据集最小操作集的新文件。新文件准备好后,Redis 切换到新文件并开始向其追加。
- 可导出和恢复数据:
- AOF 包含所有操作的日志,格式易于理解和解析。如果你意外使用
FLUSHALL
命令清空了所有数据,只要没有进行重写,就可以通过停止服务器、删除最后一条命令并重新启动 Redis 来恢复数据集。
- AOF 包含所有操作的日志,格式易于理解和解析。如果你意外使用
缺点如下:
- 文件大小:
- AOF 文件通常较大:相同数据集的 AOF 文件通常会比 RDB 文件更大。这是因为 AOF 记录了每一个写入操作,而 RDB 则是一个点时间快照。
- 性能:
- AOF 性能受 fsync 策略影响:
- 每秒 fsync:在默认配置下,AOF 的性能仍然很高,通常能满足大多数场景的需求。
- 禁用 fsync:在这种情况下,AOF 的性能应该与 RDB 相当,即使在高负载情况下也能保持快速。
- AOF 性能受 fsync 策略影响:
如何选择
选择合适的持久化策略对于确保数据的安全性和可用性至关重要。以下是对两种持久化方法(RDB 和 AOF)使用建议的总结:
- 同时使用 RDB 和 AOF:
- 如果您希望获得与 PostgreSQL 类似的数据安全性,那么同时使用这两种持久化方法是一个不错的选择。
- 结合 RDB 的快照和 AOF 的日志,可以在系统崩溃后快速恢复数据,并最大限度地减少数据丢失的风险。
- 单独使用 RDB:
- 如果您对数据安全性非常重视,但可以接受在灾难情况下损失几分钟的数据,那么仅使用 RDB 是合适的。
- RDB 的快照功能能够满足大多数备份需求,并在系统恢复时提供更快的启动时间。
- 单独使用 AOF:
- 虽然有许多用户选择仅使用 AOF,但通常不建议这样做。因为 AOF 在遇到问题时可能导致数据丢失,并且在 AOF 引擎出现错误时可能无法快速恢复。
- 定期生成 RDB 快照可以作为有效的备份策略,确保在 AOF 出现故障时能够恢复数据。
快照
Redis 的快照持久化功能允许您将数据集的状态定期保存到磁盘上,以便在需要时进行恢复。快照文件通常称为 dump.rdb
。
配置保存
您可以通过 redis.conf
文件或使用 CONFIG SET
命令来设置快照保存策略。快照保存策略的基本语法如下:
save <seconds> <changes>
<seconds>
:在指定的时间段内(以秒为单位)进行快照。<changes>
:在此时间段内发生的最小键更改数量。
以下是一个示例配置,使 Redis 每隔 60 秒自动保存一次数据集,如果在此期间至少有 1000 个键发生变化:
save 60 1000
这意味着如果在 60 秒内有至少 1000 个键被更改,Redis 将创建一个新的快照并将其保存到 dump.rdb
文件中。
手动保存
除了自动快照,您还可以手动触发快照保存。可以使用以下命令之一:
SAVE
:该命令会阻塞 Redis,直到快照完成。BGSAVE
:该命令会在后台异步保存快照,不会阻塞 Redis 处理其他客户端请求。
只追加文件
Redis 的附加文件(AOF)持久化是一种确保数据持久性的有效策略,特别是在需要避免数据丢失的应用场景中。AOF 在 Redis 1.1 版本引入,提供了在发生意外停机(如系统崩溃或断电)时的更高数据安全性。要启用 AOF,可以在 Redis 的配置文件中设置:
appendonly yes
一旦启用,Redis 将在接收到更改数据集的命令(例如 SET
)时,将该命令附加到 AOF 文件中。
AOF 文件的工作原理
- 重放机制:当 Redis 重启时,它会重新播放 AOF 文件中的所有命令,以重建数据集的状态。这确保了即使在意外情况下,数据也能得到恢复。
- 多文件机制(Redis 7.0.0 及更高版本):
- Redis 7.0.0 引入了多文件 AOF 机制,将原始的单个 AOF 文件分割为基础文件和增量文件。
- 基础文件:最多只有一个,代表在 AOF 重写时的数据快照(可以是 RDB 或 AOF 格式)。
- 增量文件:可以有多个,包含自上一个基础 AOF 文件创建以来的增量更改。
- 所有这些文件都会存储在一个单独的目录中,并由一个清单文件进行跟踪。
AOF 日志重写
随着写操作的进行,AOF 文件会不断增大。例如,如果你对一个计数器进行了 100 次递增操作,数据集中只会有一个键(包含最终值),但在 AOF 文件中将会有 100 条记录,其中 99 条记录对于重建当前状态是多余的。
安全的重写机制
Redis 支持一个有趣的功能:它可以在后台重建 AOF 文件,而不会中断对客户端的服务。具体过程如下:
- 最小命令集重写:当你发出
BGREWRITEAOF
命令时,Redis 会写入重建当前数据集所需的最短命令序列。 - 后台执行:Redis 在后台创建一个新 AOF 文件,记录当前内存中的数据集状态。旧文件继续接受新写入操作。
- 原子切换:一旦新的 AOF 文件准备就绪,Redis 会执行一个原子替换操作,将新文件与旧文件交换,并开始将后续的写入操作追加到新文件中。
自动重写(Redis 2.4 及更高版本)
自 Redis 2.4 版本起,AOF 重写的触发可以自动进行。在配置文件中可以设置重写条件,Redis 会根据这些条件自动调用 BGREWRITEAOF
来优化 AOF 文件。
Redis 7.0.0 的改进
在 Redis 7.0.0 中,当调度 AOF 重写时,Redis 父进程会打开一个新的增量 AOF 文件继续写入。子进程则执行重写逻辑,生成新的基础 AOF 文件。同时,Redis 会使用一个临时清单文件来跟踪新生成的基础文件和增量文件。
- 原子替换:一旦新基础文件和增量文件准备完毕,Redis 将执行一个原子替换操作,使临时清单文件生效。
- 重写限制机制:为避免因 AOF 重写的重复失败和重试而产生过多的增量文件,Redis 引入了 AOF 重写限制机制,确保失败的 AOF 重写会以逐渐减慢的速率重试。
AOF的持久性
**AOF(Append Only File)**提供了多种持久性选项,以控制数据在磁盘上的同步(fsync)频率。可以根据需求选择不同的持久性策略,具体如下:
- fsync 策略选项
appendfsync always
:- 描述:每次将新命令追加到 AOF 时都调用 fsync。
- 性能:非常慢,但安全性极高。
- 注意:命令在执行完来自多个客户端的批量命令或管道后,才会被追加到 AOF 中,因此每次写入后都会有一次 fsync。
appendfsync everysec
:- 描述:每秒调用一次 fsync。
- 性能:相对较快(自 2.4 版本起,这个选项的性能与 RDB 快照相当)。
- 风险:在发生灾难时,可能会丢失最近一秒的数据。
appendfsync no
:- 描述:从不调用 fsync,只将数据交给操作系统。
- 性能:更快,但安全性较低。
- 注意:在这种配置下,Linux 通常会每 30 秒将数据刷新到磁盘,但具体行为取决于内核的调优设置。
- 推荐配置
- 默认策略:建议使用
appendfsync everysec
,这既能提供合理的性能,又能在一定程度上保证数据安全。虽然always
策略提供了最大的安全性,但在实际应用中其性能往往无法满足需求。 - 批量提交:
appendfsync always
策略支持组提交(group commit),对于多个并行写入操作,Redis 会尝试合并为一次 fsync 操作,从而提升性能。
处理 AOF 文件截断
当 AOF 文件被截断时,可能是由于服务器在写入 AOF 文件时崩溃,或者存储 AOF 文件的卷已满。在这种情况下,AOF 仍然包含一致的数据,代表数据集的某个时刻版本(在默认的 AOF fsync 策略下,可能最多为一秒钟),但 AOF 中的最后一条命令可能已被截断。最新版本的 Redis 仍然能够加载 AOF,只需丢弃文件中最后一个格式不正确的命令。此时,服务器会发出类似以下的日志:
* Reading RDB preamble from AOF file...
* Reading the remaining AOF tail...
# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 439 !!!
# AOF loaded anyway because aof-load-truncated is enabled
如果您希望 Redis 在这种情况下停止,可以更改默认配置;但是,默认配置是无论文件最后一条命令是否格式正确都继续运行,以保证在重启后能够恢复可用性。
对于旧版本的 Redis,可能无法恢复,需要采取以下步骤:
- 备份 AOF 文件:
- 在进行任何修复操作之前,请确保备份原始 AOF 文件。
- 使用 redis-check-aof 工具修复文件:
- 运行以下命令修复原始文件:
$ redis-check-aof --fix <filename>
- 可选地,使用
diff -u
检查修复前后两个文件的差异。
- 运行以下命令修复原始文件:
- 用修复后的文件重新启动服务器。
处理 AOF 文件损坏
如果 AOF 文件不仅被截断,而且在中间出现了无效字节序列,情况会更复杂。Redis 在启动时会报错并中止:
* Reading the remaining AOF tail...
# Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>
处理步骤:
- 备份 AOF 文件:
- 同样,首先备份损坏的 AOF 文件。
- 运行 redis-check-aof 工具:
- 初始时,不带
--fix
选项运行工具,以了解问题:$ redis-check-aof <filename>
- 检查工具给出的错误提示,跳转到文件中的指定偏移量,查看是否可以手动修复文件。AOF 使用与 Redis 协议相同的格式,手动修复相对简单。
- 初始时,不带
- 自动修复(如果手动修复不可行):
- 如果无法手动修复,您可以让工具为您修复文件,但在这种情况下,从无效部分到文件末尾的所有 AOF 部分可能会被丢弃,如果损坏发生在文件的初始部分,可能会导致大量数据丢失。
AOF是如何工作的
日志重写(Log rewriting)使用了与快照相同的写时复制(copy-on-write)技巧,具体工作原理如下:
- Redis 7.0 及以上版本
- Redis 进行 fork 操作,此时有一个子进程和一个父进程。
- 子进程开始将新的 base AOF 写入临时文件,这个文件代表重写后的基础 AOF 文件。
- 父进程打开一个新的增量 AOF 文件,用于继续记录后续的更新。如果重写失败,旧的 base 文件和增量文件(如果有的话)加上这个新打开的增量文件,仍然能够完整地表示更新后的数据集,因此不会有数据丢失。
- 当子进程完成 base 文件的重写时,父进程收到信号,并使用新打开的增量文件和子进程生成的 base 文件构建一个临时的清单(manifest 文件),并将其持久化。
- 交换清单文件:Redis 原子性地交换旧的清单文件和新的清单文件,这样日志重写的结果就生效了。之后 Redis 清理旧的 base 文件和未使用的增量文件。
Redis 7.0 以下版本
- Redis 进行 fork 操作,同样有一个子进程和一个父进程。
- 子进程开始将新的 AOF 写入临时文件,这个临时文件存放的是重写后的所有操作。
- 父进程将所有新的更改累积到内存缓冲区中,同时将这些更改写入旧的 append-only 文件中。这样即使重写失败,依然是安全的。
- 当子进程完成文件重写后,父进程收到信号,将内存缓冲区的内容追加到子进程生成的新文件的末尾。
- 原子性地重命名文件:Redis 将新生成的文件原子性地重命名为旧文件,接着开始将新的数据追加到新文件中。
无论是 Redis 7.0 及以上版本,还是更早的版本,Redis 都能确保即使在日志重写期间出现故障,仍然可以保证数据的安全性和一致性。
如果我目前正在使用RDB,我如何切换到AOF?
如果你想在当前使用 RDB 快照的服务器上启用 AOF(追加日志文件),你需要通过 CONFIG
命令在运行的服务器上先启用 AOF 来进行数据转换。
- Redis 2.2 及以上版本
准备工作:
- 备份最新的
dump.rdb
文件。 - 将此备份文件转移到安全的地方。
在活跃的数据库上切换到 AOF:
- 启用 AOF:
redis-cli config set appendonly yes
。 - 可选操作:禁用 RDB:
redis-cli config set save ""
。 - 确保写操作正确地被追加到 AOF 文件中。
- 重要提示:更新
redis.conf
文件(可以通过CONFIG REWRITE
命令更新),确保配置文件与上述配置匹配。如果你忽略这一步,当你重启服务器时,配置更改将丢失,服务器会以旧配置重新启动,导致数据丢失。
下一次重启服务器时:
- 在重启服务器之前,等待 AOF 重写完成并持久化数据。你可以通过
INFO persistence
来监控,确保aof_rewrite_in_progress
和aof_rewrite_scheduled
都为 0,同时确认aof_last_bgrewrite_status
为ok
。 - 重启服务器后,检查数据库中包含的键数量是否与之前相同。
- Redis 2.0
步骤:
- 备份最新的
dump.rdb
文件。 - 将此备份文件转移到安全的地方。
- 停止所有对数据库的写操作!
- 执行
redis-cli BGREWRITEAOF
,这将生成 AOF 文件。 - 在 Redis 完成 AOF 文件生成后,停止服务器。
- 编辑
redis.conf
,启用 AOF 持久化。 - 重启服务器。
- 确保你的数据库中包含的键数量与切换前相同。
- 确保写操作正确地被追加到 AOF 文件中。
AOF和RDB持久性之间的交互
Redis 2.4 及以上版本确保在 RDB 快照操作正在进行时不会触发 AOF 重写操作,或在 AOF 重写过程中允许执行 BGSAVE
。这避免了两个 Redis 后台进程同时进行大量的磁盘 I/O 操作。
当快照正在进行时,如果用户明确请求通过 BGREWRITEAOF
执行日志重写操作,服务器会返回 OK 状态码告知用户该操作已被安排,重写操作会在快照完成后开始。
如果同时启用了 AOF 和 RDB 持久化,在 Redis 重启时会使用 AOF 文件来重建原始数据集,因为 AOF 文件被保证是最完整的数据记录。
RDB持久化备份
Redis 非常适合数据备份,因为您可以在数据库运行时复制 RDB 文件:RDB 文件一旦生成就不会再被修改,并且在生成过程中会使用临时文件名,只有在新快照完全生成后才会通过 rename(2)
原子性地重命名到最终位置。
这意味着在服务器运行时复制 RDB 文件是完全安全的。我们建议的做法是:
- 在服务器中创建一个
cron
任务,每小时生成一个 RDB 文件的快照并存储在一个目录中,每天生成一个快照并存储在另一个目录中。 - 每次
cron
脚本运行时,确保使用find
命令删除过旧的快照。例如,您可以保存过去 48 小时的每小时快照,以及一个或两个月的每日快照。确保为快照命名时包含日期和时间信息。 - 每天至少一次,确保将 RDB 快照转移到数据中心之外,或者至少将其转移到不在运行 Redis 实例的物理机器上。
AOF持久化备份
如果您运行的是仅启用了 AOF 持久化的 Redis 实例,仍然可以执行备份。从 Redis 7.0.0 开始,AOF 文件被分割成多个文件,这些文件存储在由 appenddirname
配置指定的目录中。在正常操作中,您只需复制或打包该目录中的文件即可完成备份。然而,如果备份期间正好发生了 AOF 重写操作,可能会导致备份无效。为了解决此问题,您必须在备份期间禁用 AOF 重写操作:
- 使用以下命令关闭自动重写:
CONFIG SET auto-aof-rewrite-percentage 0
- 确保在此期间不要手动启动重写(例如通过
BGREWRITEAOF
命令)。 - 使用以下命令检查当前是否有正在进行的重写操作:
INFO persistence
确认 aof_rewrite_in_progress
值为 0
。如果为 1
,则需要等待重写完成。
4. 现在您可以安全地复制 appenddirname
目录中的文件。
5. 完成备份后重新启用自动重写:
CONFIG SET auto-aof-rewrite-percentage <之前的值>