背景
最近一周,一直在做squirrel-ha-service高可用的改进。简单介绍下squirrel-ha-service。squirrel-ha-service(后文都简写为ha)是线上持续监控redis集群,保证redis集群高可用的一个服务。会通过redis cluster nodes命令获取redis集群所有节点的状态,如果某个节点宕机了,ha一旦发现宕机节点,首先会通过修改zk通知redis客户端不再访问该节点,然后自动替换宕机节点。替换完节点后,再次通过修改zk通知redis客户端刷新本地路由,将新添加的节点加入本地路由表。
上述左图是我们目前线上ha大致架构,基本能保证redis集群的整体可用性。但是最近我们在做机房容灾相关,这个架构就有问题了。
上述是右图我们线上一个redis集群的部署情况,基本都是3机房部署。图中是dx,yf,gh三个机房,每个机房都有一个主节点一个从节点。redis集群这样部署,能保证最高的可用性。即使线上某个机房发生故障,剩下的两个机房也能继续提供服务。
这里我们假设现在gh机房发生网络故障,先忽视图中红色框。gh-master节点原来有一个dx-slave节点,一旦光环机房不可用,dx-slave节点会发起提升自己为master的请求,dx和yf机房的两个主节点投票通过,这样一个新的集群dx(2个master节点),yf(一个master,一个slave)可以继续对外提供服务。
从redis集群角度看,如果机房是三机房主从均匀部署,单个机房发生故障另外两个机房依然能继续提供服务。但是此时我们的squirrel-ha服务因为也处于gh机房(我们线上一个redis集群唯一对应一个ha监控服务),由于gh机房和另外两个机房dx,yf网络不通,此时ha服务无法刷新zk,通知dx和yf的redis客户端更新路由了。之所以发生这个问题,是因为ha自身没有保证高可用,所以我们考虑引进zk选举来保证ha服务的可用性。如上图红框,当gh机房发生网络故障,之前dx-ha-watcher会替代gh-ha成为新的leader,开始监控redis集群。
Curator选主
上面我们说了,在发生机房网络分区时候,ha自身不能保证高可用。所以接下来我们会将ha接入zk,通过zk的选举功能保证ha自身的高可用。我们会使用Netflix开源的curator来实现选主,这个框架解决了原生zookeeper client断线重连相关问题,并且提供了2套选主方案。
- LeaderLatch:随机从候选着中选出一台作为leader,选中之后除非调用close()释放leadship,否则其他的后选择无法成为leader。这种策略适合主备应用,当主节点意外宕机之后,多个从节点会自动选举其中一个为新的主节点
- Leader Election:这种选举策略跟Leader Latch选举策略不同之处在于每个实例都能公平获取领导权,而且当获取领导权的实例在释放领导权之后,该实例还有机会再次获取领导权。另外,选举出来的leader不会一直占有领导权,当 takeLeadership(CuratorFramework client) 方法执行结束之后会自动释放领导权。
具体选择哪种策略,还是要用户根据自己的需求选择。
LeaderLatch
|
|
上述代码,创建了3个LeaderLatch实例,然后sleep 2s,让3个实例进行选主。最后依次调用close方法,释放leader。
控制台会随机输出:
2:I am leader. I am doing jobs!
重复执行几次,可以看到不同的client随机获得leader。
Leader Election
|
|
上述代码ExampleClient主要关注以下四点
- 构造方法中leaderSelector.autoRequeue();这个确保了leaderSelector在释放leader后,还可以重新获取leader。
- takeLeaderShip方法,一旦进入这个方法,就表示leaderSelector已经成为leader,从这个方法退出,就释放leader。可以看到这就同LeaderLatch不一样,LeaderLatch只能通过主动close释放leader。
- releaseLeader方法我们通过设置isLeaderRelease为true,让takeLeaderShip能退出循环,达到释放leader目的。
- 通过继承了LeaderSelectorListenerAdapter类,一旦出现SUSPENDED或者LOST连接问题,能主动释放leader,这个下面会详细说下。
LeaderSelectorListenerAdapter
一旦LeaderSelector启动,它会向curator客户端添加监听器。 使用LeaderSelector必须时刻注意连接的变化。一旦出现连接问题如SUSPENDED,或者LOST,curator实例必须确保其不再是leader并且其takeLeadership()应该直接退出。
推荐的做法是,如果发生SUSPENDED或者LOST连接问题,最好直接抛CancelLeadershipException,此时,leaderSelector实例会尝试中断并且取消正在执行takeLeadership()方法的线程。 建议扩展LeaderSelectorListenerAdapter, LeaderSelectorListenerAdapter中已经提供了推荐的处理方式 。
LeaderSelector
可以看到一旦catch到listener.stateChanged抛出的CancelLeadershipException异常,会调用leaderSelector.interruptLeadership()尝试中断,所以我们上面的ExampleClient的takeLeaderShip方法必须要是可以响应中断的
上述方法Thread.sleep确实可以响应中断,所以一旦出现SUSPENDED或者LOST连接问题,就会从takeLeaderShip方法退出并释放leader。
下面我们来测试下上述的ExampleClient
上述我们创建了3个ExampleClient,如果成为leader就会打印日志。后续每隔7s,又会主动释放leader,这样其他follower就会成为leader。
控制台输出如下:
可以看到每隔7s,leader确实会切换一次。
上图是我们通过zkCli命令连接到zk server获取的信息。我们创建3个ExampleClient,会在/test节点下面分别创建3个临时节点,观察后面的数字0019,0020,0021。
其实Leader Election内部通过一个分布式锁来实现选主;并且选主结果是公平的,zk会按照各节点请求的次序成为主节点,当前最小序号的节点成为主节点,其他节点会添加一个对于当前最小节点的监听watcher。一旦发现最小节点不存在,第二小的节点就会成为leader。