分布式任务调度探索

2021-05-01  本文已影响0人  PioneerYi

定时任务是大家在开发中一个不可避免的业务,比如在一些电商系统中可能会定时给用户发送生日券,一些对账系统中可能会定时去对账。如果服务只有一台机子,那直接用ScheduledExecutorService就可以了,但是现在服务一般是集群部署,这个时候,ScheduledExecutorService很多时候就没法解决问题,因为我们需要保证只执行一次,同时,我们还需要保证高可用,任务监控等等。

因此,鉴于这些场景,出现了较多的分布式调度框架,本文将对也就主流的开源调度器进行分析对比,然后总结出分布式调度器的系统设计关键点。

需求分析

设计一个分布式调度器,首先需要明确面临哪些场景,以及需要解决哪些问题。按照需求特点,我们将其分为功能性需求和非功能性需求。

功能性需求

调度器功能性需求.png

功能性需求,主要包括:

非功能性需求

调度器非功能性需求.png

对于调度器,一般需要满足:高可用、高性能、支持水平处理等能力,我们将这些能力成为非功能性需求。

开源框架分析

Java Timer

在ScheduledExecutorService未出现前,在业务开发中如果遇到执行一些简单定时任务的需求,为了避免做一些看起来复杂的控制逻辑,一般考虑使用 Timer 来实现定时任务的执行,下面给出一个最简单用法的例子:

Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
    @Override
    public void run() {
        // scheduledExecutionTime() 返回此任务最近开始执行的时间
        Date date = new Date(this.scheduledExecutionTime());
        System.out.println("timeTask run " + date);
    }
};

// 从现在开始每间隔 1000 ms 计划执行一个任务
timer.schedule(timerTask, 0, 1000);

Timer现在已经不建议使用了,推荐使用ScheduledExecutorService,为什么不建议使用Timer了?原因如下:

由于上述缺陷,尽量不要使用Timer, idea中也会明确提示,使用ScheduledThreadPoolExecutor替代Timer 。

Java ScheduledExecutorService

ScheduledExecutorService对于Timer的缺陷进行了修补,首先ScheduledExecutorService内部实现是ScheduledThreadPool线程池,可以支持多个任务并发执行。

对于某一个线程执行的任务出现异常,也会处理,不会影响其他线程任务的执行,另外ScheduledExecutorService是基于时间间隔的延迟,执行不会由于系统时间的改变发生变化。

当然,ScheduledExecutorService也有自己的局限性:只能根据任务的延迟来进行调度,无法满足基于绝对时间和日历调度的需求。

Spring Task

spring task 是spring自主开发的轻量级定时任务框架,不需要依赖其他额外的包,配置较为简单,此处使用注解配置:


image.png

Spring Task 本身不支持持久化,也没有推出官方的分布式集群模式,只能靠开发者在业务应用中自己手动扩展实现,无法满足可视化,易配置的需求。

Quartz

Quartz框架是Java领域最著名的开源任务调度工具,也是目前事实上的定时任务标准,几乎全部的开源定时任务框架都是基于Quartz核心调度构建而成。

image.png

关键概念

实现细节

1.Quartz如何保证任务只在一个节点运行?
答案是使用了数据库锁。在quartz的集群解决方案里有张表scheduler_locks,quartz采用了悲观锁的方式对triggers表进行行加锁,以保证任务同步的正确性。一旦某一个节点上面的线程获取了该锁,那么这个Job就会在这台机器上被执行,同时这个锁就会被这台机器占用。同时另外一台机器也会想要触发这个任务,但是锁已经被占用了,就只能等待,直到这个锁被释放。之后会看trigger状态,如果已经被执行了,则不会执行了。

简单地说,quartz的分布式调度策略是以数据库为边界资源的一种异步策略。各个调度器都遵守一个基于数据库锁的操作规则从而保证了操作的唯一性。同时多个节点的异步运行保证了服务的可靠。但这种策略有自己的局限性:集群特性对于高CPU使用率的任务效果很好,但是对于大量的短任务,各个节点都会抢占数据库锁,这样就出现大量的线程等待资源。这种情况随着节点的增加会越来越严重。

2.在集群的哪个节点上运行,Quartz是如何进行选取的?
随缘的。集群中各个节点做到时间同步很重要。如果待触发的任务少且运行快,那么很可能一直在时间最早的那一个节点上执行。

3.任务太多,而线程池中配置的线程太少时怎么办?
没事,反正选取Trigger的时候也会考虑空闲线程的数量,空闲线程少的话实例就少选取几个Trigger来执行。

Quartz不足

Quartz通过故障转移和负载均衡实现了任务的高可用,通过数据库的锁机制来确保任务执行的唯一性,但是集群特性仅仅只是用来HA,节点数量的增加并不会提升单个任务的执行效率,不能实现水平扩展。

总结起来,其缺陷和不足在于:
1)需要把任务信息持久化到业务数据表,和业务有耦合;
2)调度逻辑和执行并存于同一个项目中,在机器性能固定的情况下,业务和调度之间不可避免的会相互影响;
3)quartz集群模式下,是通过数据库独占锁来唯一获取任务,任务执行并没有实现完善的负载均衡;

轻量级神器 XXL-JOB

XXL-JOB是一个轻量级分布式任务调度平台,主打特点是平台化,易部署,开发迅速、学习简单、轻量级、易扩展,代码仍在持续更新中。

“调度中心”是任务调度控制台,平台自身并不承担业务逻辑,只是负责任务的统一管理和调度执行,并且提供任务管理平台, “执行器” 负责接收“调度中心”的调度并执行,可直接部署执行器,也可以将执行器集成到现有业务项目中。 通过将任务的调度控制和任务的执行解耦,业务使用只需要关注业务逻辑的开发。

主要提供了任务的动态配置管理、任务监控和统计报表以及调度日志几大功能模块,支持多种运行模式和路由策略,可基于对应执行器机器集群数量进行简单分片数据处理。

功能特性

主要功能特性如下:

系统设计

设计思路

将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求;将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑;因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;

image.png

系统组成

系统HA设计

1、调度中心高可用
调度中心支持多节点部署,基于数据库行锁保证同时只有一个调度中心节点触发任务调度,参考com.xxl.job.admin.core.thread.JobScheduleHelper#start

2、任务调度高可用

工作原理

XXL-JOB的工作原理如下如所示:


工作原理

Elastic-Job

Elastic-Job是一个分布式调度解决方案,由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成。Elastic-Job-Lite定位为轻量级无中心化解决方案,使用jar包的形式提供分布式任务的协调服务。Elastic-Job-Cloud使用Mesos + Docker的解决方案,额外提供资源治理、应用分发以及进程隔离等服务。

原理解析

框架

Saturn

Saturn是唯品会开源的一个分布式任务调度平台,在Elastic Job的基础上进行了改造。

SIA-TASK

是宜信开源的分布式任务调度平台。

分布式调度关键点

分布式调度核心要素.png

分布式调度一般分为三部分,分别是:任务调度器、任务执行器、任务。详细如下:

高可用

为了避免单点故障,任务调度系统通常需要通过集群实现系统高可用,同时通过扩展提高系统的任务负载量上限。

鉴于任务调度系统的特殊性,“调度”和“执行”两个模块需要均支持集群部署,由于职责不同,因此各自集群侧重点也有有所不同。

“调度器”集群,目标为避免调度模块单点故障。同时,集群节点需要通过锁或命名服务保证单个任务的单次触发,只在其中一个节点上生效,以防止任务的重复触发。

调度器为何采用集群而不是主从?
主从模式只能提供HA的特性,不能提高提高系统的任务负载量上限。
主从需要实现选举算法,保证CP或者AP
集群只需要将状态都迁移到全局的存储器中(例如DB),任务可以采用抢占机制(全局锁),抢占后在任务后标识任务状态即可。

“执行器”集群:目标为避免任务模块单点故障。进一步可以通过自定义路由策略实现Failover等高级功能,从而在执行器某台机器节点故障时自动转移不会影响到任务的正常触发执行。

失败处理

这里的失败策略指的是业务发生失败的处理策略,而不是因为节点故障导致任务没有执行完成导致的失败

任务失败是一种很常见的情况,当任务失败时有两点非常重要,一个是快速发现问题,另一个是及时解决问题。

任务业务逻辑千差万别,如索引同步、pv统计、订单超时处理等等。任务失败可能会导致非常严重的后果,比如索引同步任务失败可能导致搜索不匹配,pv统计失败可能导致打点报表的生成,订单超时处理任务的失败可能导致商品库存的大量无效占用等等。

针对上述情况,通常有几种处理方案:

路由策略

由于任务执行器存在多个实例,调度器如何选择任务执行器同样是个问题。为每个任务配置不同的路由策略(为配置则使用默认路由策略)常见的策略如下:

阻塞策略

在调度比较密集,而执行器来不及处理的情况下,任务阻塞策略可以指导执行器快速处理阻塞的触发请求。常见的阻塞策略有以下几种:

分布式锁实现

对于分布式定时任务系统来说,最重要的是分布式锁,实现方式有三种:

  1. 基于数据库的实现方式:唯一索引原理,比如一个定时任务一天执行一次,我们就以日期作为唯一索引,谁第一个把当天日期插入成功,谁就有资格执行;
  2. 基于Redis的实现方式
  3. 基于zk的实现方式
上一篇 下一篇

猜你喜欢

热点阅读