1. 程式人生 > >基於Zookeeper的定時任務應用改造和高可用部署

基於Zookeeper的定時任務應用改造和高可用部署

前言

很多應用場景下需要使用定時任務,本文不討論定時任務的實現,而是討論在簡單的定時任務基礎上,如何實現高可用部署。比如有個結賬服務在每天0點對前一天的交易進行結賬處理,普通的定時任務下可能只能執行一個結賬服務例項,否則會結兩次帳,但僅部署一個例項則不能保證結賬服務的穩定執行。

為解決這個問題比較合適的方式是將這個計算加入工作流(airflow、XXL-job等),如果是新增業務來說比較合適,但是對於既有業務系統的改造難度就比較大了。本文提供一種侵入性較小的改造方案:利用zookeeper的主節點選舉方式來決定定時任務的執行與否即可。以下對具體步驟進行說明。

1. 改造

1.1 引入依賴

maven引入curator,注意:curator自帶zookeeper依賴包,最好將其排除,自行引入於伺服器對應版本的依賴

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.0.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-client</artifactId>
    <version>4.0.1</version>
    <exclusions>
        <exclusion>
            <dependency>
                <groupId>org.apache.zookeeper</groupId>
                <artifactId>zookeeper</artifactId>
            </dependency>
        </exclusion>
    </exclusions>
</dependency>

<!-- zookeeper -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.10</version>
</dependency>
<!-- zookeeper -->

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. 效果

啟動兩個定時任務例項,從後臺日誌可以看到,僅主節點真正執行了任務

如果此時殺掉主節點繼承,從節點自動變為主節點