Elastic Job 同城主備、同城雙活,高可用必備~
作者:薛定諤的風口豬
來源: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 基礎教程推薦看這裡:
單點部署到高可用
如本文開頭所說,很多系統的部署是採取以下部署架構:
原因是開發者擔心定時任務在同一時刻被觸發多次,導致業務有問題。實際上這是對於框架最基本的原理不瞭解。在官方文件的功能列表裡:
就已說明其最基本的功能之一就是:
作業分片一致性,保證同一分片在分散式環境中僅一個執行例項
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 重磅釋出,黑暗模式太炸了!
覺得不錯,別忘了隨手點贊+轉發哦!