基於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. 效果
啟動兩個定時任務例項,從後臺日誌可以看到,僅主節點真正執行了任務
如果此時殺掉主節點繼承,從節點自動變為主節點