作者:薛定諤的風口豬
來源:https://jaskey.github.io/blog/2020/05/25/elastic-job-timmer-active-standby/
在使用Elastic Job Lite做定時任務的時候,我發現很多開發的團隊都是直接部署單點,這對于一些離線的非核心業務(如對賬、監控等)或許無關緊要,但對于一些高可用補償、核心資料定時修改(如金融場景的利息更新等),單點部署則“非常危險”,實際上,Elastic Job Lite是支持高可用的,
網上關于Elastic Job的較高級的博文甚少,本文試圖結合自身實踐的一些經驗,大致講解其方案原理,并延伸至同城雙機房的架構實踐,
注:本文所有討論均基于開源版本的Elastic Job Lite, 不涉及Elastic Job Cloud部分,
Elastic Job 基礎教程推薦看這里:
http://www.javastack.cn/tags/Elastic-Job/
單點部署到高可用
如本文開頭所說,很多系統的部署是采取以下部署架構:

原因是開發者擔心定時任務在同一時刻被觸發多次,導致業務有問題,實際上這是對于框架最基本的原理不了解,在官方檔案的功能串列里:
http://elasticjob.io/docs/elastic-job-lite/00-overview/
就已說明其最基本的功能之一就是:
作業分片一致性,保證同一分片在分布式環境中僅一個執行實體
Elastic Job會依賴zookeeper選舉出對應的實體做sharding,從而保證只有一個實體在執行同一個分片(如果任務沒有采取分片(即分片數是0),就意味著這個任務只有一個實體在執行)
所以如下圖所示的部署架構是完全沒問題的——一來,服務只會被一個實體呼叫,二來,如果某個服務掛了,其他實體也能接管繼續提供服務從而實作高可用,

雙機房高可用
隨著互聯網業務的發展,慢慢地,對架構的高可用會有更高的要求,下一步可能就是需要同城兩機房部署,那這時候為了保證定時服務在兩個機房的高可用,我們架構上可能會變成這樣的:

這樣如果A機房的定時任務全部不可用了,B機房的確也能接手提供服務,而且由于集群是一個,Elastic Job能保證同一個分片在兩個機房也只有一個實體運行,看似挺完美的,
注:本文不討論zookeeper如何實作雙機房的高可用,實際上從zookeeper的原理來看,僅僅兩個機房組成一個大集群并不可以實作雙機房高可用,
優先級調度?
以上的架構解決了定時任務在兩個機房都可用的問題,但是實際的生產中,定時任務很可能是依賴存盤的資料源的,而這個資料源,通常是有主備之分(這里不考慮單元化的架構的情況):例如主在A機房,備在B機房做實時同步,
如果這個定時任務只有讀操作,可能沒問題,因為只要配置資料源連接同機房的資料源即可,但是如果是要寫入的,就有一個問題——如果所有任務都在B機房被調度了,那么這些資料的寫入都會跨機房地往A機房寫入,這樣延遲就大大提升了,如下圖所示,

如圖所示,如果Elastic Job把任務都調度到了B機房,那么流量就一直跨機房寫了,這樣對于性能來說是不好的事情,
那么有沒有辦法達到如下效果了:
- 保證兩個機房都隨時可用,也就是一個機房的服務如果全部不可用了,另外一個機房能提供對等的服務
- 但一個任務可以優先指定A機房執行
Elastic Job分片策略
在回答這個問題之前,我們需要了解下Elastic Job的分片策略,根據官網的說明(http://elasticjob.io/docs/elastic-job-lite/02-guide/job-sharding-strategy/ ) ,Elastic Job是內置了一些分片策略可選的,其中有平均分配演算法,作業名的哈希值奇偶數決定IP升降序演算法和作業名的哈希值對服務器串列進行輪轉;同時也是支持自定義的策略,實作實作JobShardingStrategy介面并實作sharding方法即可,
public Map<JobInstance, List<Integer>> sharding(List<JobInstance> jobInstances, String jobName, int shardingTotalCount)
假設我們可以實作這一的自定義策略:讓做分片的時候知道哪些實體是A機房的,哪些是B機房的,然后我們知道A機房是優先的,在做分片策略的時候先把B機房的實體踢走,再復用原來的策略做分配,這不就解決我們的就近接入問題(接近資料源)了嗎?
以下是利用裝飾器模式自定義的一個裝飾器類(抽象類,由子類判斷哪些實體屬于standby的實體),讀者可以結合自身業務場景配合使用,
另外,Java 系列面試題和答案全部整理好了,微信搜索Java技術堆疊,在后臺發送:面試,可以在線閱讀,
public abstract class JobShardingStrategyActiveStandbyDecorator implements JobShardingStrategy {
//內置的分配策略采用原來的默認策略:平均
private JobShardingStrategy inner = new AverageAllocationJobShardingStrategy();
/**
* 判斷一個實體是否是備用的實體,在每次觸發sharding方法之前會遍歷所有實體呼叫此方法,
* 如果主備實體同時存在于串列中,那么備實體將會被剔除后才進行sharding
* @param jobInstance
* @return
*/
protected abstract boolean isStandby(JobInstance jobInstance, String jobName);
@Override
public Map<JobInstance, List<Integer>> sharding(List<JobInstance> jobInstances, String jobName, int shardingTotalCount) {
List<JobInstance> jobInstancesCandidates = new ArrayList<>(jobInstances);
List<JobInstance> removeInstance = new ArrayList<>();
boolean removeSelf = false;
for (JobInstance jobInstance : jobInstances) {
boolean isStandbyInstance = false;
try {
isStandbyInstance = isStandby(jobInstance, jobName);
} catch (Exception e) {
log.warn("isStandBy throws error, consider as not standby",e);
}
if (isStandbyInstance) {
if (IpUtils.getIp().equals(jobInstance.getIp())) {
removeSelf = true;
}
jobInstancesCandidates.remove(jobInstance);
removeInstance.add(jobInstance);
}
}
if (jobInstancesCandidates.isEmpty()) {//移除后發現沒有實體了,就不移除了,用原來的串列(后備)的頂上
jobInstancesCandidates = jobInstances;
log.info("[{}] ATTENTION!! Only backup job instances exist, but do sharding with them anyway {}", jobName, JSON.toJSONString(jobInstancesCandidates));
}
if (!jobInstancesCandidates.equals(jobInstances)) {
log.info("[{}] remove backup before really do sharding, removeSelf :{} , remove instances: {}", jobName, removeSelf, JSON.toJSONString(removeInstance));
log.info("[{}] after remove backups :{}", jobName, JSON.toJSONString(jobInstancesCandidates));
} else {//全部都是master或者全部都是slave
log.info("[{}] job instances just remain the same {}", jobName, JSON.toJSONString(jobInstancesCandidates));
}
//保險一點,排序一下,保證每個實體拿到的串列肯定是一樣的
jobInstancesCandidates.sort((o1, o2) -> o1.getJobInstanceId().compareTo(o2.getJobInstanceId()));
return inner.sharding(jobInstancesCandidates, jobName, shardingTotalCount);
}
利用自定義策略實作同城雙機房下的優先級調度
以下是一個很簡單的就近接入的例子:指定在ip白名單的,就是優先執行的,不在的都認為是備用的,我們看如何實作,
一、繼承此裝飾器策略,指定哪些實體是standby實體
public class ActiveStandbyESJobStrategy extends JobShardingStrategyActiveStandbyDecorator{
@Override
protected boolean isStandby(JobInstance jobInstance, String jobName) {
String activeIps = "10.10.10.1,10.10.10.2";//只有這兩個ip的實體才是優先執行的,其他都是備用的
String ss[] = activeIps.split(",");
return !Arrays.asList(ss).contains(jobInstance.getIp());//不在active名單的就是后備
}
}
很簡單吧!這樣實作之后,就能達到以下類似的效果

二、 在任務啟動前,指定使用這個策略
以下以Java的方式示意,
JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder(jobClass.getName(), cron, shardingTotalCount).shardingItemParameters(shardingItemParameters).build();
SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(simpleCoreConfig, jobClass.getCanonicalName());
return LiteJobConfiguration.newBuilder(simpleJobConfiguration)
.jobShardingStrategyClass("com.xxx.yyy.job.ActiveStandbyESJobStrategy")//使用主備的分配策略,分主備實體(輸入你的實作類類名)
.build();
這樣就大功告成了,
同城雙活模式
以上這樣改造后,針對定時任務就已經解決了兩個問題:
1、定時任務能實作在兩個機房下的高可用
2、任務能優先調度到指定機房
這種模式下,對于定時任務來說,B機房其實只是個備機房——因為A機房永遠都是優先調度的,
對于B機房是否有一些實際問題其實我們可能是不知道的(常見的例如資料庫權限沒申請),由于沒有流量的驗證,這時候真的出現容災問題,B機房是否能安全接受其實并不是100%穩妥的,
我們能否再進一步做到同城雙活呢?也就是,B機房也會承擔一部分的流量?例如10%?
回到自定義策略的sharding介面:
public Map<JobInstance, List<Integer>> sharding(List<JobInstance> jobInstances, String jobName, int shardingTotalCount)
在做分配的時候,是能拿到一個任務實體的全景圖(所有實體串列),當前的任務名,和分片數,
基于此其實是可以做一些事情把流量引流到B機房實體的,例如:
- 指定任務的主機房讓其是B機房優先調度(例如挑選部分只讀任務,占10%的任務數)
- 對于分片的分配,把末尾(如1/10)的分片優先分配給B機房,
以上兩種方案都能實作讓A、B兩個機房都有流量(有任務在被調度),從而實作所謂的雙活,
以下針對上面拋出來的方案一,給出一個雙活的示意代碼和架構,
假設我們定時任務有兩個任務,TASK_A_FIRST,TASK_B_FIRST,其中TASK_B_FIRST是一個只讀的任務,那么我們可以讓他配置讀B機房的備庫讓他優先運行在B機房,而TASK_A_FIRST是一個更為頻繁的任務,而且帶有寫操作,我們則優先運行在A機房,從而實作雙機房均有流量,
注:這里任意一個機房不可用了,任務均能在另外一個機房調度,這里增強的只是對于不同任務做針對性的優先調度實作雙活
public class ActiveStandbyESJobStrategy extends JobShardingStrategyActiveStandbyDecorator{
@Override
protected boolean isStandby(JobInstance jobInstance, String jobName) {
String activeIps = "10.10.10.1,10.10.10.2";//默認只有這兩個ip的實體才是優先執行的,其他都是備用的
if ("TASK_B_FIRST".equals(jobName)){//選擇這個任務優先調度到B機房
activeIps = "10.11.10.1,10.11.10.2";
}
String ss[] = activeIps.split(",");
return !Arrays.asList(ss).contains(jobInstance.getIp());//不在active名單的就是后備
}
}

近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.別在再滿屏的 if/ else 了,試試策略模式,真香!!
3.臥槽!Java 中的 xx ≠ null 是什么新語法?
4.Spring Boot 2.5 重磅發布,黑暗模式太炸了!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/295976.html
標籤:Java
