Redis Cluster 里 epoch 到处都是:CLUSTER INFO 里有 cluster_current_epoch 和 cluster_my_epoch,nodes.conf 里每一行有 epoch,日志里选举叫「for epoch 7」。它们各是什么、谁比谁大意味着什么?这篇通过三个可复现的实验来看:新建集群、人为制造冲突、以及冻结/解冻一个旧主库时 epoch 如何决定「谁的槽位声明有效」——顺便发现一个能丢数据的窗口。
7430 M current_epoch = 6 my_epoch(configEpoch) = 1 7431 M current_epoch = 6 my_epoch(configEpoch) = 2 7432 M current_epoch = 6 my_epoch(configEpoch) = 3 7433 S current_epoch = 6 my_epoch(configEpoch) = 1 7434 S current_epoch = 6 my_epoch(configEpoch) = 2 7435 S current_epoch = 6 my_epoch(configEpoch) = 3
每个节点的 CLUSTER INFO 有两个 epoch:cluster_current_epoch(整个集群的逻辑时钟,所有节点尽量保持一致,这里都是 6)和 cluster_my_epoch(本节点的 configEpoch)。三个主库的 configEpoch 依次是 1、2、3,是 --cluster create 时分配的;每个从库的 my_epoch 等于它主库的。
握手前 7440: my_epoch = 5
握手前 7441: my_epoch = 5
握手前 7442: my_epoch = 0
+ 1 ms 各节点 (my_epoch, current_epoch): {7440: (5, 5), 7441: (5, 5), 7442: (0, 0)}
+ 116 ms 各节点 (my_epoch, current_epoch): {7440: (6, 6), 7441: (5, 6), 7442: (0, 6)}configEpoch 用来在两个节点都声称拥有同一个槽时分出胜负,所以每个主库必须有不同的值。我用 CLUSTER SET-CONFIG-EPOCH 手工把两个新节点都设成 5,让它们握手:约 116 ms 内其中一个的 configEpoch 被推进到 6(同时 current_epoch 成为 6),另一个保持 5——冲突消失了。第三个节点没有设置,它的 configEpoch 保持 0,但 current_epoch 与集群对齐。哪一个被推进由什么规则决定(我推测与节点 ID 有关,官方文档也是这么说),我没有做对照验证。
第 1 次:X=7451 从库=7454;从库升主用了 5809 ms
X 的 (current,my) epoch:冻结前 (6, 2) → 解冻后 (7, 7);新主库 7454:(6, 2) → (7, 7)
解冻后 0.7 ms 内经旧连接 SET 的结果:OK;旧主库变成从库用了 10 ms;最终 key 在新主库上的值 = 'after-failover',在旧主库上 = Err('MOVED 9094 127.0.0.1:7454')
第 2 次:X=7451 从库=7453;从库升主用了 5871 ms
X 的 (current,my) epoch:冻结前 (6, 2) → 解冻后 (7, 7);新主库 7453:(6, 2) → (7, 7)
解冻后 0.9 ms 内经旧连接 SET 的结果:OK;旧主库变成从库用了 11 ms;最终 key 在新主库上的值 = 'after-failover',在旧主库上 = Err('MOVED 9094 127.0.0.1:7453')
第 3 次:X=7451 从库=7454;从库升主用了 5449 ms
X 的 (current,my) epoch:冻结前 (6, 2) → 解冻后 (7, 7);新主库 7454:(6, 2) → (7, 7)
解冻后 1.4 ms 内经旧连接 SET 的结果:OK;旧主库变成从库用了 13 ms;最终 key 在新主库上的值 = 'after-failover',在旧主库上 = Err('MOVED 9094 127.0.0.1:7454')冻结旧主库直到从库升主(约 5.8 s)。升主的从库申请了新的 epoch:current_epoch 6 → 7,新主库的 configEpoch 也是 7,比旧主库的 2 大。解冻旧主库之后,它收到新主库对同一批槽位的声明——configEpoch 更大者胜出,于是旧主库把自己重新配置为新主库的从库,epoch 变成 (7, 7),用时 10–13 ms。
关键在解冻的那一瞬间:旧主库进程恢复运行,但它还没收到新 epoch 的消息,仍然以为自己是这些槽位的主库。我提前打开了一条到旧主库的连接,解冻后立刻(约 0.7 ms 内)发一次 SET:三次实验里它都返回了 OK。随后旧主库降为从库、与新主库同步,这次被 OK 的写入被丢弃:最终该 key 在新主库上的值是故障转移之后写入的 after-failover。这是「已确认的写入在故障转移中丢失」的一个具体窗口,长度在毫秒量级,并且需要「客户端还连着旧主库」这个条件才会撞上。这类窗口的更多形态见第 93 篇。
注意这里用的是冻结/解冻(进程暂停后恢复),它对应的是「主库因 GC/虚拟机暂停等原因暂时假死」这类场景;和第 75 篇 kill -9(进程彻底消失)不同,后者不会有「旧主库醒来」的这一幕。