1. 程式人生 > 其它 >Elastic Job 同城主備、同城雙活,高可用必備~

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 基礎教程推薦看這裡:

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機房,那麼流量就一直跨機房寫了,這樣對於效能來說是不好的事情。

那麼有沒有辦法達到如下效果了:

  1. 保證兩個機房都隨時可用,也就是一個機房的服務如果全部不可用了,另外一個機房能提供對等的服務
  2. 但一個任務可以優先指定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機房例項的,例如:

  1. 指定任務的主機房讓其是B機房優先排程(例如挑選部分只讀任務,佔10%的任務數)
  2. 對於分片的分配,把末尾(如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開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!