当前位置: 首页 > 后端技术 > Java

ElasticJob同城主备,同城双活,高可用必备~

时间:2023-04-02 01:49:26 Java

作者:薛定谔的风口猪来源:https://jaskey.github.io/blog...使用ElasticJobLite时做定时任务,我发现很多开发团队直接单点部署,对于一些线下的非核心业务(比如对账、监控等)可能无所谓,但是对于一些高可用的补偿和核心的定期修改数据(如金融场景、利息更新等),单点部署“非常危险”。事实上,ElasticJobLite是支持高可用的。网上关于ElasticJob的比较高级的博文很少。本文试图结合自身的一些实践经验,大致阐述其解决方案的原理,并将其推广到同城双机房的架构实践中。注:本文所有讨论均基于开源版ElasticJobLite,不涉及ElasticJobCloud部分。ElasticJob基础教程推荐看这里:http://www.javastack.cn/tags/...单点部署到高可用本文开头提到,很多系统的部署采用如下部署架构:原因是开发者担心定时任务同时触发多次,导致业务出现问题。其实这是对框架最基本原理的无知。在官方文档的功能列表中:http://elasticjob.io/docs/ela...已经说明了其最基本的功能之一是:jobshards的一致性,保证同一个shard只能被在分布式环境下使用一个执行实例ElasticJob会依赖zookeeper选出对应的实例进行分片,从而保证只有一个实例在执行同一个分片(如果任务不取分片(即分片的个数)shards为0),则表示这个任务只有一个实例在执行)所以下图所示的部署架构是完全没问题的——首先,服务只会被一个实例调用,其次,如果某个服务挂了起来,其他实例也可以接管并继续提供服务。高可用性。双机房高可用随着互联网业务的发展,对架构的高可用也会有更高的要求。下一步可能会在同城部署两个机房。这时候为了保证两个机房定时服务的高可用,我们的架构可能会变成这样:那么如果A机房的所有定时任务都不可用,那么B机房确实也可以接手提供服务。并且因为集群是一个,ElasticJob可以保证同一个分片在两个机房只有一个实例运行。看起来很完美。注:本文不讨论zookeeper是如何实现双机房高可用的。其实从zookeeper的原理来看,只有两个机房组成一个大集群,无法实现双机房的高可用。优先调度?上述架构解决了定时任务在两个机房都可用的问题,但是在实际生产中,定时任务很可能依赖于存储的数据源。而这个数据源通常是分主备的(这里不考虑单元化架构的情况):比如主在A机房,备在B机房进行实时同步。如果这个定时任务只有读操作,可能还好,因为它只需要配置数据源连接到同一机房的数据源即可。但是如果要写的话,就有一个问题——如果所有的任务都安排在B机房,那么这些数据的写入会跨过机房写到A机房,这样延迟会大大改善,如下图所示。如图所示,如果ElasticJob将所有任务都调度到B机房,那么流量会一直跨机房写入,对性能不利。那么有没有什么办法可以实现如下效果:保证两个机房随时可用,即如果一个机房的所有服务都不可用,另一个机房可以提供同等的服务,但是一个任务可以优先分配给A机房执行ElasticJob分片策略在回答这个问题之前,我们需要先了解一下ElasticJob的分片策略。根据官网(http://elasticjob.io/docs/ela...)的描述,ElasticJob内置了一些可以可选的分片策略,包括平均分布算法,奇偶数作业名的哈希值决定IP升序和降序算法,作业名的哈希值轮转服务器列表;也支持自定义策略,实现了JobShardingStrategy接口,实现了sharding方法。publicMap>sharding(ListjobInstances,StringjobName,intshardingTotalCount)假设我们可以实现这个自定义策略:让我们知道哪些实例属于机房A,哪些实例属于机房B,那么我们就知道机房A是优先级。在执行分片策略时,先踢掉B机房的实例,再重新使用原来的策略进行分配。这不就解决了我们就近访问的问题(靠近数据源)吗?下面是一个使用装饰器模式定制的装饰器类(抽象类,由哪些子类决定哪些实例属于备实例)。读者可以结合自己的业务场景使用。另外,Java系列面试题和答案都整理好了。微信搜索Java技术栈,后台发送:面试,网上可以看。publicabstractclassJobShardingStrategyActiveStandbyDecoratorimplementsJobShardingStrategy{//内置分配策略采用原始默认策略:averageprivateJobShardingStrategyinner=newAverageAllocationJobShardingStrategy();/***判断一个实例是否为备实例,每次在所有实例上调用该方法都会触发sharding方法。*如果主备实例同时存在于列表中,则分片前移除备实例*@paramjobInstance*@return*/protectedabstractbooleanisStandby(JobInstancejobInstance,StringjobName);@OverridepublicMap>sharding(ListjobInstances,StringjobName,intshardingTotalCount){ListjobInstancesCandidates=newArrayList<>(jobInstances);ListremoveInstance=newArrayList<>();布尔removeSelf=false;对于(JobInstancejobInstance:jobInstances){booleanisStandbyInstance=false;try{isStandbyInstance=isStandby(jobInstance,jobName);}catch(Exceptione){log.warn("isStandBythrowserror,considerasnotstandby",e);}if(isStandbyInstance){if(IpUtils.getIp().equals(jobInstance.getIp())){removeSelf=true;}jobInstancesCandidates.remove(jobInstance);removeInstance.add(jobInstance);}}if(jobInstancesCandidates.isEmpty()){//如果移除后没有instance,不移除,使用原列表(备份)topjobInstancesCandidates=jobInstances;log.info("[{}]注意!!只存在备份作业实例,但无论如何都要对它们进行分片{}",jobName,JSON.toJSONString(jobInstancesCandidates));}if(!jobInstancesCandidates.equals(jobInstances)){log.info([{}]在真正进行分片之前删除备份,removeSelf:{},删除实例:{}",jobName,removeSelf,JSON.toJSONString(removeInstance));log.info([{}]删除备份后:{}",jobName,JSON.toJSONString(jobInstancesCandidates));}else{//都是master或者都是slavelog.info("[{}]jobinstancesjustremainingthesame{}",jobName,JSON.toJSONString(jobInstancesCandidates));}//为了安全起见,对它们进行排序,保证每个实例得到的列表必须是相同的返回内部.sharding(jobInstancesCandidates,jobName,shardingTotalCount);}使用自定义策略实现同城两个机房的优先级调度。下面是一个很简单的就近访问的例子:在ip白名单中指定的优先执行,不在的我们都认为是备用的,下面看看如何实现。1.继承这个装饰器策略,指定哪些实例是备用实例publicclassActiveStandbyESJobStrategyextendsJobShardingStrategyActiveStandbyDecorator{@OverrideprotectedbooleanisStandby(JobInstancejobInstance,StringjobName){StringactiveIps="10.10.10.1,10.10.10.2";//两个ip实例首先执行,其他的是备用的Stringss[]=activeIps.split(",");return!Arrays.asList(ss).contains(jobInstance.getIp());//不在activelist中的为backup}}很简单!这样实现之后,可以达到以下类似的效果。2.在任务开始之前,指定使用这个策略。Java中显示如下,JobCoreConfigurationsimpleCoreConfig=JobCoreConfiguration.newBuilder(jobClass.getName(),cron,shardingTotalCount).shardingItemParameters(shardingItemParameters).build();SimpleJobConfigurationsimpleJobConfiguration=newSimpleJobConfiguration(simpleCoreConfig,jobClass.getCanonicalName());returnLiteJobConfiguration.newBuilder(simpleJobConfiguration).jobShardingStrategyClass("com.xxx.yyy.job.ActiveStandbyESJobAssignment)/UsingMaster/Standby"策略,分为主备实例(输入你的实现类名)。。建造();你就完成了。同城双活改造后,定时任务解决了两个问题:1、定时任务可以在两个机房高可用。2.可先将任务分派到指定机房。从任务上来说,B机房其实只是一个备用机房——因为A机房总是被优先调度。其实我们可能不知道B机房是否存在一些实际问题(常见的,比如没有申请数据库权限),因为没有进行流量验证,此时真的出现容灾问题,不是100%机房B能否安全接受%安全。能否更进一步,实现同城双住?也就是说,B机房也要承担一部分流量?例如10%?回到自定义策略的分片界面:publicMap>sharding(ListjobInstances,StringjobName,intshardingTotalCount)做分配的时候,可以得到一个任务实例的全景图(list所有实例的数量)、当前任务名称和分片数量。基于此,其实可以做一些事情将流量分流到B机房实例,例如:指定任务的主机房,使其优先调度B机房(例如,选择部分只读任务,占任务数量的10%)进行分片分配,将最后的(比如1/10)分片先分配给B机房。以上两种方案都可以实现A机房和B机房的流量(有任务正在调度),从而实现了所谓的双活。下面是上面抛出的第一个方案的双活的代码架构示意图。假设我们有两个定时任务,TASK_A_FIRST,TASK_B_FIRST,其中TASK_B_FIRST是一个只读任务,那么我们可以让他配置读B机房的备库,让他先在B机房运行,TASK_A_FIRST是一个更频繁任务对于有写操作的任务,我们优先在A机房运行,这样两个机房都有流量。注意:这里任何机房都不可用,可以在其他机房安排任务。这里增强的是不同任务的针对性优先级调度,实现双活publicclassActiveStandbyESJobStrategyextendsJobShardingStrategyActiveStandbyDecorator{@OverrideprotectedbooleanisStandby(JobInstancejobInstance,StringjobName){StringactiveIps="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";}Stringss[]=activeIps.split(",");return!Arrays.asList(ss).contains(jobInstance.getIp());//不在active列表中的为后备}}近期热点文章推荐:1.1000+Java面试题及答案(2021最新版)试试策略模式,真香!!3.操!Java中xx≠null的新语法是什么?4、SpringBoot2.5发布,深色模式太炸了!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!