【Redis从入门到精通】第51篇:Cluster复制与故障转移——集群高可用机制
上一篇【第50篇】集群重新分片——不停服迁移槽位的黑魔法
下一篇【第52篇】sentinel vs cluster——redis高可用方案怎么选
在上一篇中,我们搞懂了Redis Cluster的重新分片机制——数据是怎么在节点之间搬家的。但你有没有想过一个问题:如果某个主库突然挂了怎么办?难道整个集群就瘫了?
当然不是。Redis Cluster内置了一套完整的主从复制 + 自动故障转移机制。今天我们就来扒一扒这套机制的底裤,看看它是怎么在主库挂掉之后,自动"扶正"一个从库上位的。
集群模式下的主从复制
复制拓扑
在Redis Cluster中,每个主库(Master)都可以有零个或多个从库(Replica/Slave)。从库实时复制主库的数据,当主库下线时,从库可以被选举为新主库。
集群复制拓扑示意 Node A (Master) Node D (Master) 槽: 0-5460 槽: 5461-10922 | | | v v v A-1 A-2 D-1 (Replica) Node G (Master) Node J (Master) 槽: 10923-16383 槽: (无,从库) | | v v G-1 (Replica) (属于其他主库)这里有个值得注意的点:Node J没有分配任何槽位。在Cluster模式中,一个节点可以既不持有槽位,也不做任何主库的从库——但这其实是一种资源浪费,你应该避免这种情况。
配置主从复制
有两种方式建立主从关系:
方式一:使用CLUSTER REPLICATE命令
# 先连上从库节点redis-cli-p7001# 告诉它"你去给7000号节点当小弟"CLUSTER REPLICATE<node-id-of-7000># 返回 OK 表示复制关系建立成功方式二:在启动时使用--cluster-replica参数
# 创建集群时直接指定每个主库的从库数量redis-cli--clustercreate\127.0.0.1:7000127.0.0.1:7001127.0.0.1:7002\127.0.0.1:7003127.0.0.1:7004127.0.0.1:7005\--cluster-replicas1--cluster-replicas 1表示每个主库配1个从库。上面的命令会自动把7003、7004、7005分别配置为7000、7001、7002的从库。
查看复制关系
CLUSTER REPLICAS<node-id># 返回该主库所有从库的 node-idCLUSTER NODES# 查看所有节点及其复制关系# 输出示例:# 07c37d... 127.0.0.1:7000@17000 myself,master - 0 ... 0-5460# 67a2ab... 127.0.0.1:7001@17001 master - 0 ... 5461-10922# e685e5... 127.0.0.1:7002@17002 master - 0 ... 10923-16383# 7d3aea... 127.0.0.1:7003@17003 slave 07c37d... 0 ... 0-5460# 388f12... 127.0.0.1:7004@17004 slave 67a2ab... 0 ... 5461-10922# f2c197... 127.0.0.1:7005@17005 slave e685e5... 0 ... 10923-16383踩坑提示:在Redis 5.0+中,配置文件和命令已经把
slave相关术语改为replica。但如果你用老版本的redis-cli连接新版本,输出的CLUSTER NODES仍然会显示slave。这不是bug,是兼容性处理。
主库下线检测——从怀疑到确诊
故障检测是故障转移的前提。Redis Cluster采用了一套两阶段检测机制:先主观下线(PFAIL),再客观下线(FAIL)。这跟医生看病有点像——先觉得你不对劲,再确诊。
PFAIL(主观疑似下线)
每个集群节点每秒都会向其他节点发送PING消息(通过Gossip协议),同时等待对方的PONG回复。
如果一个节点A在cluster-node-timeout时间内没有收到节点B的PONG回复:
- 节点A把节点B标记为PFAIL(Possibly Failed)——“疑似下线”
- PFAIL状态只存在于节点A的本地视角中
PFAIL 检测流程 节点A 节点B | | |--- PING (每秒) ------->| | | |--- PING -------------->| (B没有回复) | | |--- PING -------------->| (B还是没有回复) | | | 等待 cluster-node-timeout 超时... | | | [B 被标记为 PFAIL] |注意:
cluster-node-timeout的默认值是15秒。这意味着从B停止响应到A标记它PFAIL,最快也要15秒。这个值不是越小越好——设置太小会导致频繁的误判(比如网络抖动),设置太大会导致故障发现太慢。
FAIL(客观下线)
PFAIL只是"我觉得你挂了",但集群不会因为一个人的判断就采取行动。需要大多数节点都认为某个节点挂了,才能确认为FAIL。
具体流程:
- 节点A发现B为PFAIL后,通过Gossip协议把B的PFAIL状态传播给其他节点
- 每个收到PFAIL信息的节点都会记录:“A认为B疑似下线”
- 当一个节点C发现超过半数的主库节点都认为B是PFAIL时:
- C将B标记为FAIL(客观下线)
- C向集群广播一条FAIL 消息
FAIL 判定流程 时刻T0: 节点B无响应 时刻T1 (超时后): A标记B为PFAIL ─── Gossip ───> C收到: "A说B=PFAIL" D标记B为PFAIL ─── Gossip ───> C收到: "D说B=PFAIL" E标记B为PFAIL ─── Gossip ───> C收到: "E说B=PFAIL" 时刻T2 (收集到多数票): C统计: A/D/E都说B=PFAIL (3票 > N/2) C标记B为FAIL ─── 广播FAIL消息 ───> 全集群FAIL消息的广播
FAIL消息不是Gossip消息,而是通过全员广播的方式传播:
- 收到FAIL消息的节点会立即将B标记为FAIL,不需要等待自己的超时
- FAIL消息会确保全集群快速达成共识
踩坑提示:FAIL消息是单向传播的。如果某个节点因为网络分区错过了FAIL消息,它会通过后续的Gossip交换来更新状态。所以最终一致性是有保证的,但可能需要一些时间。
从库投票选举——谁来当新主库
当主库被标记为FAIL后,它的从库们就要开始"竞选"了。这个过程类似于公司CEO突然离职后的紧急董事会选举。
选举流程
从库选举流程 Step 1: 自我提名 ┌──────────────────────────────────────┐ │ 从库A、从库B 检查自己是否满足选举条件 │ │ • 与主库的复制连接正常 │ │ • 主库处于FAIL状态 │ │ • 没有触发选举超时 │ │ 满足条件 → 向集群广播FAILOVER_AUTH_ │ │ REQUEST消息 │ └──────────────────────────────────────┘ | v Step 2: 集群投票 ┌──────────────────────────────────────┐ │ 每个收到AUTH_REQUEST的主库节点投票: │ │ • 对每个故障主库,只投一票 │ │ • 先到先得(谁先发请求投给谁) │ │ • 回复FAILOVER_AUTH_ACK │ └──────────────────────────────────────┘ | v Step 3: 统计结果 ┌──────────────────────────────────────┐ │ 从库收到 N/2 + 1 张ACK票: │ │ → 当选!晋升为主库 │ │ 未收到足够票数: │ │ → 下一轮再试 │ └──────────────────────────────────────┘ | v Step 4: 执行故障转移 ┌──────────────────────────────────────┐ │ 新主库: │ │ 1. 撤销对原主库的从库关系 │ │ 2. 接管原主库的槽位 │ │ 3. 广播PONG消息(让全集群知道新主库) │ │ 4. 开始处理客户端请求 │ └──────────────────────────────────────┘选举条件详解
不是每个从库都有资格参选。必须同时满足以下条件:
| 条件 | 说明 |
|---|---|
| 复制连接正常 | 从库与原主库的复制连接断开时间不能超过cluster-node-timeout |
| 主库FAIL | 只有当主库被标记为FAIL时,从库才会发起选举 |
| 选举延迟 | 不是所有从库同时选举,而是按复制偏移量排序,偏移量越大延迟越短 |
选举延迟的计算:
// 伪代码delay=500ms+random(0,500ms)// 基础延迟 + 随机抖动delay+=(rank-1)*1000ms// rank越小延迟越短// rank的计算:// 按复制偏移量从大到小排序// 偏移量最大的从库 rank=0,最小的 rank=N-1这意味着数据最新的从库(偏移量最大)会最先发起选举,大大增加了选出"最佳候选人"的概率。
投票规则
投票有几个重要规则:
- 每个主库对每个故障主库只能投一票—— 不会同时给同一故障主库的两个从库投票
- 先到先得—— 如果从库A先发AUTH_REQUEST,主库C把票投给A,之后从库B的请求会被拒绝
- 只有主库才能投票—— 从库没有投票权(自己都泥菩萨过江了)
- 不给自己投—— 从库不给自己投
踩坑提示:如果你的集群有3个主库,其中一个主库故障后,其从库需要获得2票(3/2+1=2,向上取整)才能当选。但如果另一个主库也挂了(只剩2个主库在线),那从库只需要1票就能当选。这也是为什么建议集群至少有3个主库。
手动故障转移
有时候你不想等自动故障转移——比如你想在维护窗口期做一次计划内的主库切换。这时候可以用手动故障转移。
CLUSTER FAILOVER 命令
# 在从库上执行CLUSTER FAILOVER# 强制故障转移(即使主库还活着也转)CLUSTER FAILOVER FORCE# 带超时的故障转移(等待主库同步完成再转)CLUSTER FAILOVER TAKEOVER三种模式对比:
| 选项 | 说明 | 主库状态要求 |
|---|---|---|
| 无参数(默认) | 安全模式,等主库和从库数据同步完成后再转移 | 主库必须在线且同步中 |
| FORCE | 强制模式,不等同步完成直接转移 | 主库可以在线 |
| TAKEOVER | 接管模式,从库不等其他主库投票,直接接管 | 适用于紧急情况 |
手动故障转移的使用场景:
# 场景1: 计划内维护(升级主库Redis版本)# Step 1: 在从库上执行手动故障转移redis-cli-p7003CLUSTER FAILOVER# Step 2: 确认转移成功redis-cli-p7003CLUSTER NODES# 确认自己变成了master# Step 3: 升级原来的主库redis-cli-p7000SHUTDOWN NOSAVE# 升级7000的Redis版本...# Step 4: 把升级后的节点作为从库加入集群redis-cli-p7000CLUSTER REPLICATE<new-master-id># 场景2: 某个主库CPU 100%,但进程还活着# 从库上执行强制转移redis-cli-p7003CLUSTER FAILOVER FORCE踩坑提示:
TAKEOVER模式非常危险!它绕过了正常的投票流程,直接让从库接管。如果你在脑裂场景下使用了TAKEOVER,可能导致数据丢失。除非你确定自己在做什么,否则不要用。
故障转移超时与参数调优
cluster-node-timeout
这是集群故障检测最核心的参数,默认值为15000毫秒(15秒)。
# redis.confcluster-node-timeout15000这个参数影响着集群的多个行为:
| 行为 | 计算方式 |
|---|---|
| PFAIL检测时间 | = cluster-node-timeout |
| 从库迁移超时 | = cluster-node-timeout * slave-announce-factor(已废弃) |
| 选举超时 | 从最后一次收到主库PONG后开始计时 |
| 故障转移完成时间 | 约 = cluster-node-timeout + 选举时间(1-2秒) |
调优建议:
# 低延迟网络(同机房)cluster-node-timeout5000# 5秒# 跨机房部署cluster-node-timeout30000# 30秒# 公网部署(不推荐,但如果你非要...)cluster-node-timeout60000# 60秒踩坑提示:不要把
cluster-node-timeout设得太大。因为Redis Cluster有一个"惩罚"机制——当从库发现和主库断连超过cluster-node-timeout后,会尝试重新连接。但如果超时设为60秒,那故障发现就需要60秒,再加上选举时间,整个故障转移可能要70秒以上。对于高可用要求高的系统来说,这太慢了。
cluster-require-full-coverage
# redis.confcluster-require-full-coverageyes# 默认值这个参数控制一个关键行为:当集群中有槽位没有被任何节点负责时,是否拒绝服务。
| 设置 | 行为 |
|---|---|
| yes(默认) | 如果有任何槽位不可用,整个集群拒绝所有写操作 |
| no | 只有不可用槽位的写操作会被拒绝,其他槽位正常工作 |
cluster-require-full-coverage = yes 时: 槽 0-5460 正常可用 ✓ 槽 5461-10922 主库挂了 ✗ 槽 10923-16383 正常可用 ✓ → 客户端写任何key都会报错: (error) CLUSTERDOWN Hash slot not served cluster-require-full-coverage = no 时: 槽 0-5460 正常可用 ✓ 槽 5461-10922 主库挂了 ✗ 槽 10923-16383 正常可用 ✓ → 只有落在5461-10922的key会报错,其他key正常工作生产建议:如果对可用性要求极高,可以考虑设置为
no。但要注意,这意味着在部分节点故障期间,有些数据可能无法访问。你需要确保应用层能处理这种情况(比如对用户显示"该功能暂时不可用")。
孤立主库与集群分区
孤立主库(no replica)
如果某个主库没有从库(no replica),那它一旦挂掉,它负责的槽位就没有节点接管了。
孤立主库风险示意 Node A (Master) Node B (Master) Node C (Master) 槽: 0-5460 槽: 5461-10922 槽: 10923-16383 从库: 无 ← 危险! 从库: D 从库: E A 挂了 → 槽 0-5460 没人管 → 集群不可用!解决方案:
# 检查是否有孤立主库redis-cli-p7000CLUSTER NODES|grep"master"|whilereadline;donode_id=$(echo$line|awk'{print $1}')replicas=$(redis-cli-p7000CLUSTER REPLICAS $node_id|wc-l)if["$replicas"-eq0];thenecho"WARNING: Node$node_idhas no replicas!"fidone集群分区与脑裂
当网络分区发生时,集群可能被分成两个互相无法通信的部分。这就是经典的**脑裂(Split Brain)**问题。
集群脑裂场景 ┌─────────────────────────┐ 网络断开 ┌─────────────────────────┐ │ 分区1(多数派) │ ──────────── │ 分区2(少数派) │ │ │ │ │ │ Master-A (alive) │ │ Replica-A (alive) │ │ Master-B (alive) │ │ │ │ Replica-C (alive) │ │ │ │ │ │ │ │ 3个主库 → 正常工作 │ │ 0个主库 → 只读/拒绝服务 │ └─────────────────────────┘ └─────────────────────────┘ 少数派中的从库不会被选举为主库(因为拿不到多数票) 所以不会发生真正的脑裂写入Redis Cluster通过投票机制天然防止了脑裂写入——少数派分区的从库拿不到多数票,无法被选举为新主库。这是一个非常优雅的设计。
但有一个极端情况需要警惕:
极端脑裂场景(6节点,3主3从) 分区前: A(Master) ← B(Slave) C(Master) ← D(Slave) E(Master) ← F(Slave) 网络分区后: ┌─────────────────┐ ┌─────────────────┐ │ 分区1 (2主+0从) │ │ 分区2 (1主+3从) │ │ A, C │ │ B, D, E, F │ │ 正常工作 │ │ B无法晋升(1/3票) │ │ B从库丢失 │ │ D无法晋升(1/3票) │ └─────────────────┘ └─────────────────┘ → 分区1中A和C变成了没有从库的主库 → 如果此时A或C再挂掉,对应槽位就没人管了防御措施:
- 确保每个主库至少有1个从库,最好分布在不同机房
- 使用
cluster-node-timeout合理值,避免误判 - 设置
cluster-require-full-coverage no来提高部分故障时的可用性
故障转移的完整流程总结
让我们把整个流程串起来:
Redis Cluster 故障转移完整流程 ① 正常运行 Master-A ← Replica-A1, Replica-A2 所有节点互相 PING/PONG ② Master-A 崩溃 其他节点超时后标记 A = PFAIL PFAIL 状态通过 Gossip 传播 ③ 确认故障 超过半数主库认为 A = PFAIL 触发 A = FAIL,广播 FAIL 消息 ④ 发起选举 Replica-A1(数据最新的从库)延迟最短,率先发起 广播 FAILOVER_AUTH_REQUEST ⑤ 投票 其他主库投票(FAILOVER_AUTH_ACK) Replica-A1 获得 N/2+1 票 ⑥ 执行转移 Replica-A1 撤销从库身份 Replica-A1 接管 A 的槽位 Replica-A1 广播新身份(PONG) Replica-A2 自动转为 Replica-A1 的从库 ⑦ 恢复服务 客户端重新路由请求到新主库 故障转移完成!本章小结
Redis Cluster的故障转移机制可以总结为一句话:Gossip发现 + 两阶段确认 + 投票选举。
| 组件 | 作用 | 关键参数 |
|---|---|---|
| PING/PONG | 心跳检测 | cluster-node-timeout |
| PFAIL | 主观下线 | 单节点判断 |
| FAIL | 客观下线 | 半数以上主库确认 |
| AUTH_REQUEST | 从库发起选举 | 复制偏移量决定优先级 |
| AUTH_ACK | 主库投票 | 先到先得,每主库一票 |
| CLUSTER FAILOVER | 手动故障转移 | FORCE / TAKEOVER |
这套机制虽然没有Sentinel那么"专职",但它是内嵌在集群协议中的,不需要额外的组件。而且通过投票机制天然防止了脑裂,这在分布式系统中是非常难得的。
上一篇【第50篇】集群重新分片——不停服迁移槽位的黑魔法
下一篇【第52篇】sentinel vs cluster——redis高可用方案怎么选
