脑裂是说:同一时刻有两个节点都以为自己是主库、都接受写入。Sentinel 架构里,「sentinel 联系不上主库」并不等于「客户端联系不上主库」。这一篇做出这种情形:切断 sentinel 与主库之间的通路,让一个不知情的客户端继续写旧主库,看故障转移之后有多少已确认的写入消失。
三个 sentinel 监控的主库地址是我写的一个 TCP 代理(127.0.0.1:7900 → 主库 7910)。cut() 关闭代理并断开它上面的所有连接,等价于「sentinel 与主库之间网络断了」;而客户端 A 是直连主库的、不受影响。从库直连主库做复制。另一个客户端 B 每次写之前先向 sentinel 询问当前主库地址。
sentinel 完成主库切换:4.3 s(切断后);分区持续到 10.3 s 后恢复代理
客户端 A(直连旧主库)整个过程共 1540 次写入被回复 OK,其中「分区期间(切断 → 恢复)」内的 740 次;失败 288 次 [('BrokenPipeError', 287), ('EOFError', 1)]
恢复后旧主库被 sentinel 降级为从库,用了 10.3 s
这 1540 条被 OK 的写入,最终在新主库上还在的有 314 条,丢失 1226 条切断后 4.3 s,sentinel 提升了一个从库,B 开始写新主库。而 A 仍在写旧主库,分区期间又有 740 次写入被回复 OK。恢复代理之后,sentinel 花了 10.3 s 才把旧主库降级成从库(第 89 篇里也观察到旧主库回归耗时约 10 s),这期间 A 继续写。降级后旧主库从新主库同步,自己的写入被覆盖。最终:A 共 1540 次写入被回复 OK,还在的只有 314 条——故障转移之前复制过去的那些;其余 1226 条(约 80%)已经被回复 OK 的写入丢失了。(A 的失败 288 次主要是降级之后连接被断开。)
Sentinel 本身不能阻止旧主库继续接受写入——它只能在恢复联系后把旧主库降级。缓解办法是让旧主库自己在联系不上从库时拒绝写入(min-replicas-to-write,第 94、95 篇),以及让客户端通过 sentinel 发现主库、并在连接断开或收到只读错误后重新发现(第 92 篇)。
这次实验是「sentinel 与主库分区、客户端与主库连通」这一种拓扑;真实的分区形态很多(包括客户端也被分区),这里没有覆盖。