前言:
RDS系统致力于MySQL数据的高可用,高可靠,高性能以及在线扩展功能,实现这些特性的主要逻辑功能都运行在管理服务器上,一旦管理服务器宕机,数据库的在线扩展功能/备份功能/故障恢复功能等都无从谈起。然而,之前RDS系统管理服务器却是单点服务,为了保证整个系统的稳定性,管理服务器需要实现高可用,结合当前主流的高可用方案,决定使用Zookeeper来实现服务的高可用。
基本设计方案原理:
如下图所示,管理服务器A B C会在zk的root节点上注册临时序列节点/root/manager000000001 /root/manager000000002 /root/manager0000000003,序列最小的节点会被选为leader对外提供服务,其他节点作为热备节点随时准备升为leader。在此图中,A是leader,B C是standby。一旦A服务器因为某些原因宕机,zk就会将该服务器注册的临时节点移除掉,然后通知所有其他节点B C,B C会选出序列号最小的节点作为新的leader对外提供服务,此时B就会被选为新主。
血案现场:
会有这么一种比较特殊的场景需要考虑,比如当前leader是A BC都是备机。假如A和zk集群之间的网络出现了异常,A会收到一个连接状态被持久化为Disconnected的event,但是ZK Server并没有在这时移除A注册的临时节点,所以理论上A还是leader直至session timeout,session timeout后zk会将A注册的临时节点移除掉,然后通知B C选出新的leader,显而易见,B因为序列号小会成为新的leader。但是问题来了,session timeout的时候A的客户端并没有接收到任何notification,换句话说,它依然会认为自己是leader,这个时候就出现了这样的场景,A认为自己是leader,而B同样会认为自己是leader,即同时出现两个leader对外提供服务的情况。这很显然是不合理的,但如何深入地理解并解决这个问题呢?
个人认为理解并解决这个问题需要理解下面三个子问题:
1. 理解Zookeeper中Session的含义以及Connection Loss和Session Expired的关系
2. 理解Zookeeper中Session为什么由Server维护,而不由Client维护
3. 理解作为leader的A在整个流程中应该如何转变自己的角色,来避免脑裂
对zookeeper中Connection Loss和Session Expired的理解
Session是指当Client创建一个同Server的连接时产生的会话。连接Connected之后Session状态就开启,Zookeeper服务器和Client采用长连接方式(Client会不停地向Server发送心跳)保证session在不出现网络问题、服务器宕机或Client宕机情况下可以一直存在。因此,在正常情况下,session会一直有效,并且ZK集群上所有机器都会保存这个Session信息。
在ZK中,很多数据和状态都是和会话绑定的,一旦会话失效,那么ZK就开始清除和这个会话有关的信息,包括这个会话创建的临时节点和注册的所有Watcher。
一旦网络连接因为某种原因断开或者zk集群发生宕机,ZK Client会马上捕获到这个异常,封装为一个ConnectionLoss的事件,然后启动自动重连机制在地址列表中选择新的地址进行重连。重连会有三种结果:
(1)在session timeout时间内重连成功,client会重新收到一个syncconnected的event,并将连接重新持久化为connected状态
(2)超过session timeout时间段后重连成功,client会收到一个expired的event,并将连接持久化为closed状态
(3)一直重连不上,client将不会收到任何event
很显然,无论重连成功与否,在session timeout那个重要的时间点,ZK Client是接收不到任何ZK Server清理临时节点的信息的。这也就导致ZK会通知了B C节点A已经不再是Leader,A自身却没有接收到这样的信息,依旧对外提供服务,进而产生脑裂的问题。
Zookeeper中Session为什么由Server维护,而不由Client维护
可能很多朋友看了上面的讨论就会想为什么ZK Client不维护Session信息,如果这样做了,ZK Client就会在Session Timeout时得到相应的通知。
好,现在假如这样实现了,看看会发生什么。设想有这么一种真实场景,某个连接的Session Timeout是15s,ZK集群因为未知原因发生宕机,5min之后集群恢复成功。在Session Timeout时,ZK Client确实可以知道Session失效,然后做降主操作,但是ZK Server却不知道Session已经失效,也就不会通知其他节点选出新的Leader,此时整个系统实际上处于没有Leader的状态。即使5min之后重连成功,因为旧Session对应的临时节点没有被清理且序号最小,ZK依然会认为Leader是该临时节点,而实际上该临时节点对应不到任何的ZK Client,所以这种情况下系统依然选不出Leader。
可见,如果由Client维护Session,在某些场景下(网络异常或者集群宕机时间超过Session Timeout),由于逻辑问题根本选不出Leader。
因此这种方案是不可行的。
那能不能从应用层面避免脑裂问题呢?带着问题进入下个部分。
避免脑裂问题:作为leader的A在整个流程中应该如何转变自己的角色
因为ZK本身的设计使得这种场景下没有一个完美的解决方案,可以考虑采用退化的方案进行处理。
A在接收到DisConnected事件后就降主,不对外提供服务。然后等待接下来的发生的事情,首先可能发生的事件是在Session Timeout时间段内重连成功得到SyncConnected事件,这时A可以重新升级为主,对外提供服务。如果这段时间内没有重连成功,ZK Server在Session Timeout时会将A注册的临时节点移除,并通知B和C A已经停止对外服务了,需要选出新的leader。因为A自己已经降主了,所以在选出新leader后也不会出现多主现象。如果A在Session Timeout时间段外重连又成功了,那此时肯定新的leader已经选出来了,A需要重新注册作为新的备机候选。
可以使用如下的流程图解释这个过程:
这种应用层面的方案在一定程度上解决了脑裂问题,但是会出现一段时间系统无Leader的情况,持续时间最长为Session超时时间。
总结:
结合上述分析,可以针对Session Timeout的问题得出一个应用层面的解决方案,即Client分阶段转变角色方案。如下图所示,箭头上方红色标示表示Client所处不同阶段,箭头下方蓝色标示表示Client在不同阶段的角色转变。
本文章为作者原创