分布式系统 - 分布式任务及实现方案

arcstack2023年5月26日约 6062 字大约 20 分钟

分布式系统 - 分布式任务及实现方案

本文主要介绍定时任务的基础和单体方式下定时任务方案的演化,以及常见的分布式任务方案和技术实现要点。@pdai

定时任务和分布式任务介绍

主要介绍定时任务及其方案和演化。

定时任务应用场景

比如每天/每周/每月生成日志汇总,定时发送推送信息,定时生成数据表格等

定时任务的基础

Cron表达式是定时任务的基础。Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:

具体可以看如下文章:

单体应用定时任务的演化

单体中定时任务的演化大概如下,(后续章节逐步介绍分布式场景下的方案)

cron+脚本定时任务

JDK内置之Timer

JDK内置的Timer, 现在很少被使用。更多内容和集成可以看:SpringBoot集成定时任务 - Timer实现方式

简单示例如下

执行定时任务,延迟1秒开始执行。

    @SneakyThrows
    public static void timer() {
        // start timer
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            public void run() {
                log.info("timer-task @{}", LocalDateTime.now());
            }
        }, 1000);

        // waiting to process(sleep to mock)
        Thread.sleep(3000);

        // stop timer
        timer.cancel();
    }

输出

    10:05:47.440 [Timer-0] INFO tech.pdai.springboot.schedule.timer.timertest.TimerTester - timer-task @2021-10-01T20:05:47.436

schedule 和 scheduleAtFixedRate 有何区别

为什么几乎很少使用Timer这种方式

Timer底层是使用一个单线来实现多个Timer任务处理的,所有任务都是由同一个线程来调度,所有任务都是串行执行,意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。

如果有一个定时任务在运行时,产生未处理的异常,那么当前这个线程就会停止,那么所有的定时任务都会停止,受到影响。

JDK内置之ScheduleExecutorService

ScheduledExecutorService是基于线程池的实现方式。更多内容和集成可以看:SpringBoot集成定时任务 - ScheduleExecutorService实现方式

简单案例如下

延迟1秒执行一个进程任务。

    @SneakyThrows
    public static void schedule() {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.schedule(
                new Runnable() {
                    @Override
                    @SneakyThrows
                    public void run() {
                        log.info("run schedule @ {}", LocalDateTime.now());
                    }
                },
                1000,
                TimeUnit.MILLISECONDS);
        // waiting to process(sleep to mock)
        Thread.sleep(10000);

        // stop
        executor.shutdown();
    }

输出

    21:07:02.047 [pool-1-thread-1] INFO tech.pdai.springboot.schedule.executorservice.ScheduleExecutorServiceDemo - run schedule @ 2022-03-10T21:07:02.046

为什么用ScheduledExecutorService 代替 Timer

上文我们说到Timer底层是使用一个单线程来实现多个Timer任务处理的,所有任务都是由同一个线程来调度,所有任务都是串行执行,意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。

如果有一个定时任务在运行时,产生未处理的异常,那么当前这个线程就会停止,那么所有的定时任务都会停止,受到影响。

而ScheduledExecutorService是基于线程池的,可以开启多个线程进行执行多个任务,每个任务开启一个线程; 这样任务的延迟和未处理异常就不会影响其它任务的执行了。

Netty之HashedWheelTimer

时间轮(Timing Wheel)是George Varghese和Tony Lauck在1996年的论文'Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility 在新窗口打开open in new window '实现的,它在Linux内核中使用广泛,是Linux内核定时器的实现方法和基础之一。

时间轮(Timing Wheel)是一种环形的数据结构,就像一个时钟可以分成很多格子(Tick),每个格子代表时间的间隔,它指向存储的具体任务(timerTask)的一个链表。

springboot-timer-timewheel-1.png
springboot-timer-timewheel-1.png

以上述在论文中的图片例子,这里一个轮子包含8个格子(Tick), 每个tick是一秒钟;

任务的添加:如果一个任务要在17秒后执行,那么它需要转2轮,最终加到Tick=1位置的链表中。

任务的执行:在时钟转2Round到Tick=1的位置,开始执行这个位置指向的链表中的这个任务。(# 这里表示剩余需要转几轮再执行这个任务)

在Netty中的一个典型应用场景是判断某个连接是否idle,如果idle(如客户端由于网络原因导致到服务器的心跳无法送达),则服务器会主动断开连接,释放资源。判断连接是否idle是通过定时任务完成的,但是Netty可能维持数百万级别的长连接,对每个连接去定义一个定时任务是不可行的,所以如何提升I/O超时调度的效率呢?

Netty根据时间轮(Timing Wheel)开发了HashedWheelTimer工具类,用来优化I/O超时调度(本质上是延迟任务);之所以采用时间轮(Timing Wheel)的结构还有一个很重要的原因是I/O超时这种类型的任务对时效性不需要非常精准。

通过构造函数看主要参数

    public HashedWheelTimer(
            ThreadFactory threadFactory,
            long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
            long maxPendingTimeouts, Executor taskExecutor) {

    }

具体参数说明如下:

具体案例请看:SpringBoot集成定时任务 - Netty HashedWheelTimer方式

Spring Tasks

Spring提供的schedule任务,更多内容和集成可以看:SpringBoot集成定时任务 - Spring tasks实现方式

具体使用方式如下:

    @EnableScheduling
    @Configuration
    public class ScheduleDemo {

        /** * 每隔1分钟执行一次。 */
        @Scheduled(fixedRate = 1000 * 60 * 1)
        public void runScheduleFixedRate() {
            log.info("runScheduleFixedRate: current DateTime, {}", LocalDateTime.now());
        }

        /** * 每个整点小时执行一次。 */
        @Scheduled(cron = "0 0 */1 * * ?")
        public void runScheduleCron() {
            log.info("runScheduleCron: current DateTime, {}", LocalDateTime.now());
        }

    }

Quartz

来源百度百科 在新窗口打开open in new window , 官网地址:http://www.quartz-scheduler.org/;更多内容和集成可以看:SpringBoot集成定时任务 - 基础quartz实现方式

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标准的Java组件或 EJBs。

它的特点如下

Quartz的体系结构

springboot-job-quartz-1.png
springboot-job-quartz-1.png

注: 上图来源于https://www.cnblogs.com/jijm123/p/14240320.html

分布式任务的方案

常见的分布式任务的方案有:Quartz Cluster,XXL-Job,Elastic-Job等。@pdai: 综合代码质量,License, 维护方,拓展性等,选择的建议:

Quartz Cluster

Quartz 提供的持久化方式,更多内容和集成可以看: SpringBoot集成定时任务 - 分布式quartz cluster方式

为什么要持久化

当程序突然被中断时,如断电,内存超出时,很有可能造成任务的丢失。 可以将调度信息存储到数据库里面,进行持久化,当程序被中断后,再次启动,仍然会保留中断之前的数据,继续执行,而并不是重新开始。

Quartz提供了两种持久化方式

Quartz提供两种基本作业存储类型:

在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。

所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务。

XXL-Job

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。实现案例SpringBoot集成定时任务 - 分布式xxl-job方式

如下内容来源于xxl-job官网 在新窗口打开open in new window ; 支持如下特性:

xxl-job的架构设计

设计思想

将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。

将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。

因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;

系统组成
架构图
springboot-xxl-job-8.png
springboot-xxl-job-8.png

Elastic-Job

ElasticJob 是面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。 它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。ElasticJob 已于 2020 年 5 月 28 日成为 Apache ShardingSphere 的子项目。

使用 ElasticJob 能够让开发工程师不再担心任务的线性吞吐量提升等非功能需求,使他们能够更加专注于面向业务编码设计; 同时,它也能够解放运维工程师,使他们不必再担心任务的可用性和相关管理需求,只通过轻松的增加服务节点即可达到自动化运维的目的。

ElasticJob-Lite: 定位为轻量级无中心化解决方案,使用 jar 的形式提供分布式任务的协调服务。

springboot-elasticjob-lite-1.png
springboot-elasticjob-lite-1.png

Elasticjob-lite的案例- SpringBoot集成定时任务 - 分布式Elasticjob-lite方式

ElasticJob-Cloud: 采用自研 Mesos Framework 的解决方案,额外提供资源治理、应用分发以及进程隔离等功能。

springboot-elasticjob-cloud-1.png
springboot-elasticjob-cloud-1.png

ElasticJob-Lite和ElasticJob-Cloud的区别

ElasticJob-LiteElasticJob-Cloud无中心化是否资源分配不支持支持作业模式常驻常驻 + 瞬时部署依赖ZooKeeperZooKeeper + Mesos

分布式任务的技术要点

站在设计一个分布式任务的中间件的角度看,会需要考虑(结合上述中间件的功能看)哪些功能设计呢? @pdai

基础功能

从基础功能看,主要包括Job类型支持,Job生命周期管理,Job异常处理,接口,拓展性和UI等。

高性能和分布式

从性能和分布式的角度看,主要包括:线程池,分片,Transient Job(分如下具体项),注册中心等

生态构建

从生态构建角度看,主要包括 开发拓展接口,三方和平台集成,文档国际化,社区建设等。

参考文章

https://shardingsphere.apache.org/elasticjob/current/cn/overview/