乐趣区

关于java:Elastic-Job-同城主备同城双活高可用必备~

作者:薛定谔的风口猪

起源:https://jaskey.github.io/blog…

在应用 Elastic Job Lite 做定时工作的时候,我发现很多开发的团队都是间接部署单点,这对于一些离线的非核心业务(如对账、监控等)或者无关紧要,但对于一些高可用弥补、外围数据定时批改(如金融场景的利息更新等),单点部署则“十分危险”。实际上,Elastic Job Lite 是反对高可用的。

网上对于 Elastic Job 的较高级的博文甚少,本文试图联合本身实际的一些教训,大抵解说其计划原理,并延长至同城双机房的架构实际。

注:本文所有探讨均基于开源版本的 Elastic Job Lite,不波及 Elastic Job Cloud 局部。

Elastic Job 基础教程举荐看这里:

http://www.javastack.cn/tags/…

单点部署到高可用

如本文结尾所说,很多零碎的部署是采取以下部署架构:

起因是开发者 放心定时工作在同一时刻被触发屡次,导致业务有问题。实际上这是对于框架最根本的原理不理解。在官网文档的性能列表里:

http://elasticjob.io/docs/ela…

就已阐明其最根本的性能之一就是:

作业分片一致性,保障同一分片在分布式环境中仅一个执行实例

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/ela…),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 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版