基于Zookeeper的定时任务应用改造和高可用部署

前言

很多应用场景下需要使用定时任务,本文不讨论定时任务的实现,而是讨论在简单的定时任务基础上,如何实现高可用部署。比如有个结账服务在每天0点对前一天的交易进行结账处理,普通的定时任务下可能只能运行一个结账服务实例,否则会结两次帐,但仅部署一个实例则不能保证结账服务的稳定运行。

为解决这个问题比较合适的方式是将这个计算加入工作流(airflow、XXL-job等),如果是新增业务来说比较合适,但是对于既有业务系统的改造难度就比较大了。本文提供一种侵入性较小的改造方案:利用zookeeper的主节点选举方式来决定定时任务的执行与否即可。以下对具体步骤进行说明。

1. 改造

1.1 引入依赖

maven引入curator,注意:curator自带zookeeper依赖包,最好将其排除,自行引入于服务器对应版本的依赖


    org.apache.curator
    curator-framework
    4.0.1


    org.apache.curator
    curator-recipes
    4.0.1


    org.apache.curator
    curator-client
    4.0.1
    
        
            
                org.apache.zookeeper
                zookeeper
            
        
    




    org.apache.zookeeper
    zookeeper
    3.4.10

1.2 curator的LeaderLatch实现

只用curator比仅使用zookeeper类库会简单许多。而curator进行主节点选举的方式有LeaderLatch和LeaderSelector两种。LeaderLatch仅在初始化时候抢占Leader,之后仅在Leader故障时候在进行节点变更,而LeaderSelector每次都进行一次抢占,执行任务结束后释放Leader位置。两种实现方式各有利弊,请自行斟酌。

本文由于是定时任务场景,在Leader选举完毕后除非主节点故障否则不需要频繁变更,因此采用LeaderLatch方式。代码如下:

@Component
public class ZkLeaderLatch {
	
	private static final Logger logger = LoggerFactory.getLogger(ZkLeaderLatch.class);
	private static CuratorFramework zkClient;
	private static LeaderLatch leaderLatch;
	
	
	public ZkLeaderLatch(@Value("${zookeeper.task.servers}") String connectString,@Value("${zookeeper.task.masterkey}") String masterKey) {
		try {
			String id = String.format("zkLatchClient#%s", InetAddress.getLocalHost().getHostAddress());
			logger.info("zk {} 客户端初始化... server:{}, masterKey:{}",id,connectString,masterKey);
			RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
			zkClient = CuratorFrameworkFactory.builder().connectString(connectString)
					.sessionTimeoutMs(6000).retryPolicy(retryPolicy).build();
			logger.info("zk 客户端启动....");
			zkClient.start();
			leaderLatch = new LeaderLatch(zkClient, masterKey,id);
			
			LeaderLatchListener leaderLatchListener = new LeaderLatchListener() {
				
				@Override
				public void notLeader() {
					logger.info("客户端: {} 不是主节点. ",id);
				}
				
				@Override
				public void isLeader() {
					logger.info("客户端: {} 成为主节点. YEAH!",id);
				}
			};
			
			leaderLatch.addListener(leaderLatchListener);
			
			logger.info("leaderLatch启动....");
			leaderLatch.start();
		} catch(Exception e) {
			logger.error("客户端初始化异常. "+e.getMessage(),e);
		}
		
	}
	
	
	public boolean isLeader() {
		return leaderLatch.hasLeadership();
	}
	
	public CuratorFramework getClient(){
		return zkClient;
	} 
	
	public LeaderLatch getLatch(){
		return leaderLatch;
	} 
}

1.3 定时任务中LeaderLatch的使用

在LeaderLatch启动完毕后,主节点已选举完毕,可以通过hasLeaderShip()方法来判定。因此仅需要对定时任务的原代码执行前判断一下本应用实例的latch是否是是否是Leader,如果是则继续定时任务,否则放弃本次执行。代码如下:

@Component
public class XXXTaskClass {

    private ZkLeaderLatch zkLeaderLatch;
	
    @Autowired
    public XXXTaskClass(ZkLeaderLatch zkLeaderLatch) {
        logger.info("LogReportTask 初始化....");
        this.zkLeaderLatch = zkLeaderLatch;
    }


    @PostConstruct
    @Scheduled(cron="${report.cron}")
    public void doReport(){
        if(this.zkLeaderLatch.isLeader()) {
            // 具体的任务代码
        } else {
            // 跳过任务执行
        }
    }
}

2. 效果

启动两个定时任务实例,从后台日志可以看到,仅主节点真正执行了任务

基于Zookeeper的定时任务应用改造和高可用部署_第1张图片

 

如果此时杀掉主节点继承,从节点自动变为主节点

基于Zookeeper的定时任务应用改造和高可用部署_第2张图片

你可能感兴趣的:(后端)