面试官问,怎么实现一个定时任务调度器?如果是分布式下的呢?

首先要思考一下通用的定时器大概包含哪些要素?首先,应该听过Quartz,Spring Schedule 等框架;往分布式研究,又有 SchedulerX,ElasticJob 等分布式任务调度。那么往底层实现看,又有多种定时器实现方案的原理、工作效率、数据结构等等可以进行思考。

那么抽象来说,定时器大概包含如下属性,判断一个任务是否到期,基本会采用轮询的方式,** 每隔一个时间片 ** 去检查 ** 最近的任务 ** 是否到期,并且,在 NewTask 和 Cancel 的行为发生之后,任务调度策略也会出现调整。

  • 存储一系列的任务集合,并且 Deadline 越接近的任务,拥有越高的执行优先级
  • NewTask:将新任务加入任务集合
  • Cancel:取消某个任务
  • Run:执行一个到期的定时任务

框架列举

  • 【单机】ScheduledExecutorService:相对延迟或者周期作为定时任务调度,缺点没有绝对的日期或者时间
  • 【单机】spring定时框架:配置简单功能较多,如果系统使用单机的话可以优先考虑spring定时器
  • 【分布式】Quartz:Java事实上的定时任务标准但是Quartz可以基于数据库实现作业的高可用,有侵入性,而且缺少分布式并行调度的功能
  • 【分布式】Spring batch:轻量级的,完全面向Spring的批处理框架,可以应用于企业级大量的数据处理系统。Spring Batch可以提供大量的,可重复的数据处理功能,包括日志记录/跟踪,事务管理,作业处理统计工作重新启动、跳过,和资源管理等重要功能。
  • 【分布式】elastic-job:当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片
  • 【分布式】xxl-job: 是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。

支持集群部署

  • x-job:保证每个集群节点配置(db和登陆账号等)保持一致。调度中心通过db配置区分不同集群。执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。集群部署唯一要求为:保证集群中每个执行器的配置项 “xxl.job.admin.addresses/调度中心地址” 保持一致,执行器根据该配置进行执行器自动注册等操作。
  • e-job:重写Quartz基于数据库的分布式功能,用Zookeeper实现注册中心。作业注册中心: 基于Zookeeper和其客户端Curator实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。

多节点部署时任务不能重复执行

  • x-job:使用Quartz基于数据库的分布式功能
  • e-job:将任务拆分为n个任务项后,各个服务器分别执行各自分配到的任务项。一旦有新的服务器加入集群,或现有服务器下线,elastic-job将在保留本次任务执行不变的情况下,下次任务开始前触发任务重分片。

日志

  • x-job:支持日志可追溯,有日志查询界面
  • e-job:可通过事件订阅的方式处理调度过程的重要事件,用于查询、统计和监控。Elastic-Job目前提供了基于关系型数据库两种事件订阅方式记录事件

监控报警

  • x-job:调度失败时,将会触发失败报警,如发送报警邮件。任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔
  • e-job:通过事件订阅方式可自行实现

弹性扩容缩容

  • x-job:使用Quartz基于数据库的分布式功能,服务器超出一定数量会给数据库造成一定的压力
  • e-job:通过zk实现各服务的注册、控制及协调

支持并行调度

  • x-job:调度系统多线程(默认10个线程)触发调度运行,确保调度精确执行,不被堵塞
  • e-job:采用任务分片方式实现。将一个任务拆分为n个独立的任务项,由分布式的服务器并行执行各自分配到的分片项

高可用策略

  • x-job:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行
  • e-job:调度器的高可用是通过运行几个指向同一个ZooKeeper集群的Elastic-Job-Cloud-Scheduler实例来实现的。ZooKeeper用于在当前主Elastic-Job-Cloud-Scheduler实例失败的情况下执行领导者选举。通过至少两个调度器实例来构成集群,集群中只有一个调度器实例提供服务,其他实例处于”待命”状态。当该实例失败时,集群会选举剩余实例中的一个来继续提供服务

失败处理策略

  • x-job:调度失败时的处理策略,策略包括:失败告警(默认)、失败重试
  • e-job:弹性扩容缩容在下次作业运行前重分片,但本次作业执行的过程中,下线的服务器所分配的作业将不会重新被分配。失效转移功能可以在本次作业运行中用空闲服务器抓取孤儿作业分片执行。同样失效转移功能也会牺牲部分性能

对比总结

  • x-Job 侧重的业务实现的简单和管理的方便,学习成本简单,失败策略和路由策略丰富。推荐使用在“用户基数相对少,服务器数量在一定范围内”的情景下使用
  • e-Job 关注的是数据,增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。但是学习成本相对高些,推荐在“数据量庞大,且部署服务器数量较多”时使用

数据结构

我们主要衡量 NewTask(新增任务),Cancel(取消任务),Run(执行到期的定时任务)这三个指标,分析时间复杂度和空间复杂度。

双向有序链表(LinkedList)

NewTask:O(N)
Cancel:O(1)
Run:O(1)
N:任务数

堆(PriorityQueue)

在 Java 中,PriorityQueue 是一个天然的堆,可以利用传入的 Comparator 来决定其中元素的优先级。

NewTask:O(logN)
Cancel:O(logN)
Run:O(1)
N:任务数

expireTime 是 Comparator 的对比参数。NewTask O(logN) 和 Cancel O(logN) 分别对应堆插入和删除元素的时间复杂度 ;Run O(1),由 expireTime 形成的小根堆,我们总能在堆顶找到最快的即将过期的任务。

时间轮

Netty 针对 I/O 超时调度的场景进行了优化,实现了 HashedWheelTimer 时间轮算法。

面试官问,怎么实现一个定时任务调度器?如果是分布式下的呢?_第1张图片

HashedWheelTimer 是一个环形结构,可以用时钟来类比,钟面上有很多 bucket ,每一个 bucket 上可以存放多个任务,使用一个 List 保存该时刻到期的所有任务,同时一个指针随着时间流逝一格一格转动,并执行对应 bucket 上所有到期的任务。任务通过 取模 决定应该放入哪个 bucket 。和 HashMap 的原理类似,newTask 对应 put,使用 List 来解决 Hash 冲突。

NewTask:O(1)
Cancel:O(1)
Run:O(M)
Tick:O(1)
M: bucket ,M ~ N/C ,其中 C 为单轮 bucket 数,Netty 中默认为 512

构造 Netty 的 HashedWheelTimer 时有两个重要的参数:tickDuration 和 ticksPerWheel。

  1. tickDuration:即一个 bucket 代表的时间,默认为 100ms,Netty 认为大多数场景下不需要修改这个参数;
  2. ticksPerWheel:一轮含有多少个 bucket ,默认为 512 个,如果任务较多可以增大这个参数,降低任务分配到同一个 bucket 的概率。

层级时间轮

Kafka 针对时间轮算法进行了优化,实现了层级时间轮 TimingWheel

调度和任务分开执行

把调度和任务执行,隔离成两个部分:调度中心和执行器。调度中心模块只需要负责任务调度属性,触发调度命令。执行器接收调度命令,去执行具体的业务逻辑,而且两者都可以进行分布式扩容。

MQ模式

面试官问,怎么实现一个定时任务调度器?如果是分布式下的呢?_第2张图片

调度中心依赖Quartz集群模式,当任务调度时候,发送消息到RabbitMQ 。业务应用收到任务消息后,消费任务信息。

这种模型充分利用了MQ解耦的特性,调度中心发送任务,应用方作为执行器的角色,接收任务并执行。

但这种设计强依赖消息队列,可扩展性和功能,系统负载都和消息队列有极大的关联。这种架构设计需要架构师对消息队列非常熟悉。

XXL-JOB

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

面试官问,怎么实现一个定时任务调度器?如果是分布式下的呢?_第3张图片

你可能感兴趣的:(程序人生,深度学习,职场和发展,java,面试)