Redis高可用实战——主从复制、哨兵模式

99 阅读23分钟

ChatGPT Image 2026年1月16日 11_18_25.png

概述

本篇文章你会了解到:

  • 主从复制
  • 哨兵模式

一、主从复制

在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。Redis 也是如此,它为我们提供了复制功能,实现了相同数据的多个 Redis 副本。复制功能是高可用 Redis 的基础,哨兵和集群都是在复制的基础上实现高可用的。

默认情况下,Redis 都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点。

1.1 复制的拓扑结构

Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构,下面分别介绍。

1.1.1 一主一从

一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。

image.png

当应用写命令并发量较高且需要持久化时,可以只在从节点上开启 AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是,当主节点关闭持久化功能时,如果主节点脱机要避免自动重启操作。

因为主节点之前没有开启持久化功能自动重启后数据集为空,这时从节点如果继续复制主节点会导致从节点数据也被清空的情况,丧失了持久化的意义。安全的做法是在从节点上执行 slaveof no one 断开与主节点的复制关系,再重启主节点从而避免这一问题。

1.1.2 一主多从

一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点实现读写分离。

image.png

对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。同时在日常开发中如果需要执行一些比较耗时的读命令,如:keyssort 等,可以在其中一台从节点上执行,防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加重了主节点的负载影响服务稳定性。

1.1.3 树状主从

树状主从结构(又称为树状拓扑结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。

image.png

数据写入节点 A 后会同步到 B 和 C 节点,B 节点再把数据同步到 D 和 E 节点,数据实现了一层一层的向下复制。当主节点需要挂载多个从节点时为了避免对主节点的性能干扰,可以采用树状主从结构降低主节点压力。

1.2 复制的配置

1.2.1 建立复制

参与复制的 Redis 实例划分为主节点(master)和从节点(slave)。默认情况下,Redis 都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点。

配置复制的方式有以下三种:

  1. 在配置文件中加入 slaveof {masterHost} {masterPort} 随 Redis 启动生效。
  2. 在 redis-server 启动命令后加入 --slaveof {masterHost} {masterPort} 生效。
  3. 直接使用命令 slaveof {masterHost} {masterPort} 生效。

综上所述,slaveof 命令在使用时,可以运行期动态配置,也可以提前写到配置文件中。

image.png

image.png

slaveof 本身是异步命令,执行 slaveof 命令时,节点只保存主节点信息后返回,后续复制流程在节点内部异步执行,具体细节见之后。主从节点复制成功建立后,可以使用 info replication 命令查看复制相关状态。

1.2.2 断开复制

slaveof 命令不但可以建立复制,还可以在从节点执行 slaveof no one 来断开与主节点复制关系。例如在 6881 节点上执行 slaveof no one 来断开复制。

image.png

断开复制主要流程:

  1. 断开与主节点复制关系。
  2. 从节点晋升为主节点。

从节点断开复制后并不会抛弃原有数据,只是无法再获取主节点上的数据变化。通过 slaveof 命令还可以实现切主操作,所谓切主是指把当前从节点对主节点的复制切换到另一个主节点。

执行 slaveof {newMasterIp} {newMasterPort} 命令即可,例如把 6881 节点从原来的复制 6880 节点变为复制 6879 节点。

切主内部流程如下:

  1. 断开与旧主节点复制关系。
  2. 与新主节点建立复制关系。
  3. 删除从节点当前所有数据。
  4. 对新主节点进行复制操作。

1.2.3 只读

默认情况下,从节点使用 slave-read-only=yes 配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。因此建议线上不要修改从节点的只读模式。

image.png

1.2.4 传输延迟

主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis 为我们提供了 repl-disable-tcp-nodelay 参数用于控制是否关闭 TCP_NODELAY,默认关闭,说明如下:

repl-disable-tcp-nodelay no

当关闭时,主节点产生的命令数据无论大小都会及时地发送给从节点,这样主从之间延迟会变小,但增加了网络带宽的消耗。适用于主从之间的网络环境良好的场景,如同机架或同机房部署。

当开启时,主节点会合并较小的 TCP 数据包从而节省带宽。默认发送时间间隔取决于 Linux 的内核,一般默认为 40ms。这种配置节省了带宽但增大主从之间的延迟。适用于主从网络环境复杂或带宽紧张的场景,如跨机房部署。

1.3 主从复制原理

image.png

在从节点执行 slaveof 命令后,复制过程便开始运作。

1.3.1 保存主节点信息

执行 slaveof 后从节点只保存主节点的地址信息便直接返回,这时建立复制流程还没有开始。

1.3.2 建立主从socket连接

从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。

从节点会建立一个 socket 套接字,专门用于接受主节点发送的复制命令,从节点连接成功后打印日志。如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行 slaveof no one 取消复制。

image.png

1.3.3 发送ping命令

连接建立成功后从节点发送 ping 请求进行首次通信,ping 请求主要目的:检测主从之间网络套接字是否可用、检测主节点当前是否可接受处理命令。

image.png

从节点发送的 ping 命令成功返回,Redis 打印日志,并继续后续复制流程。

1.3.4 权限验证

如果主节点设置了 requirepass 参数,则需要密码验证,从节点必须配置 masterauth 参数保证与主节点相同的密码才能通过验证。如果验证失败复制将终止,从节点重新发起复制流程。

masterauth <master-password>

1.3.5 同步数据集

主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。

1.3.6 命令持续复制

当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

1.4 数据同步

Redis 早期支持的复制功能只有全量复制(sync 命令),它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。

Redis 在 V2.8 版本以后采用新复制命令 psync 进行数据同步,原来的 sync 命令依然支持,保证新旧版本的兼容性。新版同步划分两种情况:全量复制和部分复制。

1.4.1 全量复制

全量复制:一般用于初次复制场景,Redis 早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。

全量复制是 Redis 最早支持的复制方式,也是主从第一次建立复制时必须经历的阶段。触发全量复制的命令是 syncpsync

psync 全量复制流程,它与 V2.8 以前的 sync 全量复制机制基本一致。

image.png

  1. 发送 psync 命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行 ID,所以发送 psync ? -1
  2. 主节点根据 psync ? -1 解析出当前为全量复制,回复 +FULLRESYNC 响应,从节点接收主节点的响应数据保存运行 ID 和偏移量 offset,并打印日志。
  3. 主节点执行 bgsave 保存 RDB 文件到本地。
  4. 主节点发送 RDB 文件给从节点,从节点把接收的 RDB 文件保存在本地并直接作为从节点的数据文件,接收完 RDB 后从节点打印相关日志,可以在日志中查看主节点发送的数据量。

image.png

  1. 对于从节点开始接收 RDB 快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完 RDB 文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。

image.png

需要注意,对于数据量较大的主节点,比如生成的 RDB 文件超过 6GB 以上时要格外小心。传输文件这一步操作非常耗时,速度取决于主从节点之间网络带宽。

常见问题

通过分析全量复制的所有流程,会发现全量复制是一个非常耗时费力的操作。它的时间开销主要包括:

  1. 主节点 bgsave 时间。
  2. RDB 文件网络传输时间。
  3. 从节点清空数据时间。
  4. 从节点加载 RDB 的时间。
  5. 可能的 AOF 重写时间。

因此当数据量达到一定规模之后,由于全量复制过程中将进行多次持久化相关操作和网络数据传输,这期间会大量消耗主从节点所在服务器的 CPU、内存和网络资源。

另外最大的问题,复制还会失败。

例如我们线上数据量在 6G 左右的主节点,从节点发起全量复制的总耗时在 2 分钟左右。如果总时间超过 repl-timeout 所配置的值(默认 60 秒),从节点将放弃接受 RDB 文件并清理已经下载的临时文件,导致全量复制失败。

如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。默认配置为:

client-output-buffer-limit replica 256mb 64mb 60

意思是如果 60 秒内缓冲区消耗持续大于 64MB 或者直接超过 256MB 时,主节点将直接关闭复制客户端连接,造成全量同步失败。

所以除了第一次复制时采用全量复制在所难免之外,对于其他场景应该规避全量复制的发生。正因为全量复制的成本问题。

1.4.2 部分复制

部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施。使用 psync {runId} {offset} 命令实现。

当从节点(slave)正在复制主节点(master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。

image.png

  1. 当主从节点之间网络出现中断时,如果超过 repl-timeout 时间,主节点会认为从节点故障并中断复制连接,打印日志。如果此时从节点没有宕机,也会打印与主节点连接丢失日志。
  2. 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存 1MB。
  3. 当主从节点网络恢复后,从节点会再次连上主节点,打印日志。
  4. 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行 ID。因此会把它们当作 psync 参数发送给主节点,要求进行部分复制操作。
  5. 主节点接到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点。之后根据参数 offset 在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送 +CONTINUE 响应,表示可以进行部分复制。如果不再,则退化为全量复制。
  6. 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。发送的数据量可以在主节点的日志,传递的数据远远小于全量数据。

1.4.3 心跳

主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令。

主从心跳判断机制:

  1. 主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过 client list 命令查看复制相关客户端信息,主节点的连接状态为 flags=M,从节点连接状态为 flags=S。
  2. 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态。可通过参数 repl-ping-slave-period 控制发送频率。
  3. 从节点在主线程中每隔 1 秒发送 replconf ack {offset} 命令,给主节点上报自身当前的复制偏移量。replconf 命令主要作用如下:

实时监测主从节点网络状态、上报自身复制偏移量、检查复制数据是否丢失。如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据。

实现保证从节点的数量和延迟性功能,通过 min-slaves-to-writemin-slaves-max-lag 参数配置定义。

主节点根据 replconf 命令判断从节点超时时间,体现在 info replication 统计中的 lag 信息中,lag 表示与从节点最后一次通信延迟的秒数,正常延迟应该在 0 ~ 1s 之间。如果超过 repl-timeout 配置的值(默认 60 秒),则判定从节点下线并断开复制客户端连接。即使主节点判定从节点下线后,如果从节点重新恢复,心跳检测会继续进行。

1.4.4 异步复制机制

主节点不但负责数据读写,还负责把写命令同步给从节点。写命令的发送过程是异步完成,也就是说主节点自身处理完写命令后直接返回给客户端,并不等待从节点复制完成。

由于主从复制过程是异步的,就会造成从节点的数据相对主节点存在延迟。具体延迟多少字节,我们可以在主节点执行 info replication 命令查看相关指标获得。

在统计信息中可以看到从节点 slave 信息,分别记录了从节点的 ip 和 port,从节点的状态,offset 表示当前从节点的复制偏移量,master_repl_offset 表示当前主节点的复制偏移量,两者的差值就是当前从节点复制延迟量。Redis 的复制速度取决于主从之间网络环境、repl-disable-tcp-nodelay、命令处理速度等。正常情况下,延迟在 1 秒以内。

二、哨兵模式

Redis 的主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址,对于很多应用场景这种故障处理的方式是无法接受的。

Redis 从 2.8 开始正式提供了 Redis Sentinel(哨兵)架构来解决这个问题。

2.1 主从复制问题

Redis 的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作用。

  1. 作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备顶上来,并且保证数据尽量不丢失(主从复制是最终一致性)。
  2. 从节点可以扩展主节点的读能力,一旦主节点不能支撑住大并发量的读操作,从节点可以在一定程度上帮助主节点分担读压力。

但是主从复制也带来了以下问题:

  1. 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。
  2. 主节点的写能力受到单机的限制。
  3. 主节点的存储能力受到单机的限制。

2.2 Redis Sentinel

Redis Sentinel 是一个分布式架构,其中包含若干个 Sentinel 节点和 Redis 数据节点,每个 Sentinel 节点会对数据节点和其余 Sentinel 节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他 Sentinel 节点进行“协商”,当大多数 Sentinel 节点都认为主节点不可达时,它们会选举出一个 Sentinel 节点来完成自动故障转移的工作,同时会将这个变化实时通知给 Redis 应用方。整个过程完全是自动的,不需要人工来介入,所以这套方案很有效地解决了 Redis 的高可用问题。

image.png

以 3 个 Sentinel节点、1 个主节点、2 个从节点组成一个 Redis Sentinel 进行说明。

启动命令:

./redis-sentinel ../conf/reids.conf

或者

./redis-server ../conf/reids.conf  --sentinel

2.3 实现原理

Redis Sentinel 的基本实现中包含以下: Redis Sentinel 的定时任务、主观下线和客观下线、Sentinel 领导者选举、故障转移等等知识点,学习这些可以让我们对 Redis Sentinel 的高可用特性有更加深入的理解和认识。

2.3.1 三个定时监控任务

一套合理的监控机制是 Sentinel 节点判定节点不可达的重要保证,Redis Sentinel 通过三个定时监控任务完成对各个节点发现和监控:

  1. 每隔 10 秒的定时监控

image.png

每隔 10 秒,每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构,Sentinel 节点通过对上述结果进行解析就可以找到相应的从节点。

这个定时任务的作用具体可以表现在三个方面:

  • 通过向主节点执行 info 命令,获取从节点的信息,这也是为什么 Sentinel 节点不需要显式配置监控从节点。
  • 当有新的从节点加入时都可以立刻感知出来。
  • 节点不可达或者故障转移后,可以通过 info 命令实时更新节点拓扑信息。
  1. 每隔 2 秒的定时监控

image.png

每隔 2 秒,每个 Sentinel 节点会向 Redis 数据节点的 __sentinel__:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息,同时每个 Sentinel 节点也会订阅该频道,来了解其他 Sentinel 节点以及它们对主节点的判断,所以这个定时任务可以完成以下两个工作:

发现新的 Sentinel 节点:通过订阅主节点 __sentinel__:hello 了解其他的 Sentinel 节点信息,如果是新加入的 Sentinel 节点,将该 Sentinel 节点信息保存起来,并与该 Sentinel 节点创建连接。

Sentinel 节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据。

  1. 每隔 1 秒的定时监控

image.png

每隔 1 秒,每个 Sentinel 节点会向主节点、从节点、其余 Sentinel 节点发送一条 ping 命令做一次心跳检测,来确认这些节点当前是否可达。

通过上面的定时任务,Sentinel 节点对主节点、从节点、其余 Sentinel 节点都建立起连接,实现了对每个节点的监控,这个定时任务是节点失败判定的重要依据。

image.png

2.3.2 主观下线和客观下线

主观下线:

image.png

客观下线:

image.png

当 Sentinel 主观下线的节点是主节点时,该 Sentinel 节点会通过 sentinel is-master-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断,当超过 <quorum> 个数,Sentinel 节点认为主节点确实有问题,这时该 Sentinel 节点会做出客观下线的决定,这样客观下线的含义是比较明显了,也就是大部分 Sentinel 节点都对主节点的下线做了同意的判定,那么这个判定就是客观的。

领导者 Sentinel 节点选举:

image.png

假如 Sentinel 节点对于主节点已经做了客观下线,那么是不是就可以立即进行故障转移了?当然不是,实际上故障转移的工作只需要一个 Sentinel 节点来完成即可,所以 Sentinel 节点之间会做一个领导者选举的工作,选出一个 Sentinel 节点作为领导者进行故障转移的工作。Redis 使用了 Raft 算法实现领导者选举,Redis Sentinel 进行领导者选举的大致思路如下:

  1. 每个在线的 Sentinel 节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他 Sentinel 节点发送 sentinel is-master-down-by-addr 命令,要求将自己设置为领导者。
  2. 收到命令的 Sentinel 节点,如果没有同意过其他 Sentinel 节点的 sentinel is-master-down-by-addr 命令,将同意该请求,否则拒绝。
  3. 如果该 Sentinel 节点发现自己的票数已经大于等于 max (quorum, num(sentinels)/2+1),那么它将成为领导者。
  4. 如果此过程没有选举出领导者,将进入下一次选举。

选举的过程非常快,基本上谁先完成客观下线,谁就是领导者。

Raft 协议的详细版本:

raft-zh_cn/raft-zh_cn.md at master · maemual/raft-zh_cn · GitHub

2.3.3 故障转移

领导者选举出的 Sentinel 节点负责故障转移,具体步骤如下:

image.png

  1. 在从节点列表中选出一个节点作为新的主节点,选择方法如下:
    • 过滤“不健康”(主观下线、断线)、5 秒内没有回复过 Sentinel 节点 ping 响应、与主节点失联超过 down-after-milliseconds*10 秒。
    • 选择 slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
    • 选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。
    • 选择 runid 最小的从节点。
  2. Sentinel 领导者节点会对第一步选出来的从节点执行 slaveof no one 命令让其成为主节点。
  3. Sentinel 领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和 parallel-syncs 参数有关。
  4. Sentinel 节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。

2.4 高可用读写分离

2.4.1 从节点的作用

  1. 当主节点出现故障时,作为主节点的后备“顶”上来实现故障转移,Redis Sentinel 已经实现了该功能的自动化,实现了真正的高可用。
  2. 扩展主节点的读能力,尤其是在读多写少的场景非常适用。

但上述模型中,从节点不是高可用的:

如果 slave-1 节点出现故障,首先客户端 client-1 将与其失联,其次 Sentinel 节点只会对该节点做主观下线,因为 Redis Sentinel 的故障转移是针对主节点的。所以很多时候,Redis Sentinel 中的从节点仅仅是作为主节点一个热备,不让它参与客户端的读操作,就是为了保证整体高可用性,但实际上这种使用方法还是有一些浪费,尤其是在有很多从节点或者确实需要读写分离的场景,所以如何实现从节点的高可用是非常有必要的。

2.4.2 读写分离设计思路参考

Redis Sentinel 在对各个节点的监控中,如果有对应事件的发生,都会发出相应的事件消息,其中和从节点变动的事件有以下几个:

  • switch-master

切换主节点(原来的从节点晋升为主节点),说明减少了某个从节点。

  • convert-to-slave

切换从节点(原来的主节点降级为从节点),说明添加了某个从节点。

  • sdown

主观下线,说明可能某个从节点可能不可用(因为对从节点不会做客观下线),所以在实现客户端时可以采用自身策略来实现类似主观下线的功能。

  • reboot

重新启动了某个节点,如果它的角色是 slave,那么说明添加了某个从节点。

所以在设计 Redis Sentinel 的从节点高可用时,只要能够实时掌握所有从节点的状态,把所有从节点看做一个资源池,无论是上线还是下线从节点,客户端都能及时感知到(将其从资源池中添加或者删除),这样从节点的高可用目标就达到了。