延迟任务调度系统—技术选型与设计(上篇)

本文探讨了延迟任务调度的常见场景,如习题考试截止提醒、课程开课通知等,分析了现有解决方案的问题,如精确度不足、系统单点等。提出了实现目标,包括精确性、通用性、高性能等。并详细介绍了几种实现方案,如RabbitMQ通过死信和死信路由、延迟消息插件,Redis有序集合,以及DelayQueue等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文来自网易云社区

    • 延迟任务的场景是?
    • 现有的解决方案是?
    • 存在的问题是什么?
    • 希望达到的目标是?
    • 可以实现的方案有?
      • RabbitMQ实现
        • 通过死信和死信路由实现
        • 通过延迟消息插件来实现
      • Redis实现
      • DelayQueue实现
      • 时间轮实现
        • 单表时间轮
        • 分层时间轮
      • 之前的设计(DB/DelayQueue/ZooKeeper)
      • 另一种方案(DB/DelayQueue/ZooKeeper/MQ

延迟任务的场景是?

  • 习题考试截止前3天,给未提交用户发送消
  • 学习项目开课前2小时,给参与用户发送通知
  • 问卷开始收集时,才对用户可见
  • 问卷结束收集时,触发一些操作
  • 指定时间发布课件
  • 课程结束时,开始计算用户结业信息
  • 直播时间到了,给用户发送消息
  • 用户下单后,30分钟内未付款,关闭订单
  • 用户付款后,24小时内未发货,提示发货
  • 用户打车后,48小时后自动评价为5星
  • 这类业务的特点是:延迟执行。一种比较简单的方法是使用后台线程扫描符合条件的业务数据,逐一处理。 这种方法扫描间隔时间不好设置,间隔时间过大影响精确度,过小则影响效率和性能。

现有的解决方案是?

  • 通过linux的crontab触发定时任务
  • 扫描业务表,筛选出符合条件的数据对其进行操作

存在的问题是什么?

  • 由于每种类型的任务都设有扫描间隔,任务不能精确处理
  • 扫描业务库,影响业务正常操作
  • 任务的执行过于密集,容易导致服务器间隔性压力
  • 存在系统单点,触发定时调度的服务挂了,所有任务都不会执行
  • 系统不具容错能力,一旦错过了,任务就不会再被执行
  • 没有统一的视图来查看任务的执行情况
  • 没有告警来提示失败的任务

希望达到的目标是?

  • 精确性(可在指定时间触发任务处理)
  • 通用性
  • 高性能(集群能力不少于1000TPS)
  • 高可用(支持多实例部署)
  • 可伸缩(增加和减少服务时,任务会重新分配)
  • 可重试(任务失败可重试)
  • 多协议(支持http\dubbo调用)
  • 可管理(业务使用方可修改、删除任务)
  • 能告警(失败次数达到阈值可触发告警)
  • 统一视图(方便查看任务执行情况,可手动干预任务执行)

 

下面所讨论技术方案的前提是精确触发,所以我们不讨论目前业界的一些分布式调度系统如:elastic-job,xxl-job,tbschedule等, 这些系统解决不了延迟任务精确触发问题。

可以实现的方案有?

RabbitMQ实现

通过死信和死信路由实现

原理如下:

何为死信:

  • 消息被拒绝
  • 消息已过期
  • 队列达到最大长度

RabbitMQ可以对队列和消息设置x-message-tt、expiration来控制消息的存活时间,如果超时,消息变为死信。

 

何为死信路由:

RabbitMQ可以对队列设置x-dead-letter-exchange和x-dead-letter-routing-key两个参数。
当消息在一个队列中变成死信后会按这两个参数路由,消息就可以重新被消费。

 

实例操作:

  1. 创建延迟队列(设置死信路由)
  2. 创建就绪队列
  3. 创建死信路由
  4. 绑定死信路由与就绪队列
  5. 发送延迟消息
  6. 消息过期后进入就绪队列

 

优点:

  • 高效,可以利用RabbitMQ的分布式特性轻易进行横向扩展,且支持持久化

缺点:

  • 不支持对已发送的消息进行管理
  • 一个消息比在同一队列中的其他消息提前过期,提前过期的消息也不会优先进入死信队列。

所以需要确保业务上每个任务的延迟时间是一致的。如果有不同延时的任务,需要为每种不同延迟的任务单独创建消息队列,缺乏灵活性。

通过延迟消息插件来实现

原理如下:

核心代码流程:

 

其原理是延迟消息会被保存到Mnesia表,在Exchange中根据每个message头设置的延迟时间x-delay,消息过期后才路由到对应队列。

实例操作:

  1. 下载插件
    https://bintray.com/rabbitmq/community-plugins/download_file?file_path=rabbitmq_delayed_message_exchange-0.0.1.ez
  2. 安装插件
docker-compose.xml(将插件安装到容器中) version: '2' services:   rabbitmq:     hostname: rabbitmq     image: rabbitmq:3.6.8-management     mem_limit: 200m     ports:       - "5672:5672"       - "15672:15672"     volumes:       - ~/dockermapping/rabbitmq/lib:/var/lib/rabbitmq/
       - /Users/oldlu/workspace/document/docker-compose/rabbitmq/rabbitmq_delayed_message_exchange-0.0.1.ez:/usr/lib/rabbitmq/lib/rabbitmq_server-3.6.8/plugins/rabbitmq_delayed_message_exchange-0.0.1.ez

 启用插件
 rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  • 创建类型为x-delayed-message的路由
  • 创建就绪队列
  • 绑定队列和路由
  • 发布延迟消息(设置x-delay=延迟的毫秒数)

源码分析
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/blob/master/src/rabbit_delayed_message.erl

核心函数
消息入队:internal_delay_message
启动Timer:maybe_delay_first
消息处理:handle_info

优点:

  • 一个消息比其他消息提前过期,提前过期的消息会被提前路由到队列,不需要为不同延迟的消息创建单独的消息队列。

缺点:

  • 不支持对已发送的消息进行管理
  • 集群中只有一个数据副本(保存在当前节点下的Mnesia表中),如果节点不可用或关闭插件会丢失消息。
  • 目前该插件只支持disk节点,不支持RAM节点
  • 性能比原生差一点(普通的Exchange收到消息后直接路由到队列,而延迟队列需要判断消息是否过期,未过期的需要保存在表中,时间到了再捞出来路由)

Redis实现

有序集合(Sorted Set)是Redis提供的一种数据结构,具有set和hash的特点。
其中每个元素都关联一个score,并以这个score来排序。
其内部实现用到了两个数据结构:hash table和 skip list(跳跃表)

 

skip list的特点

  • 由很多层结构组成,level是通过一定的概率随机产生的
  • 每一层都是一个有序的链表,默认是升序
  • 最底层的链表包含所有元素
  • 如果一个元素出现在Level i的链表中,则它在Level i之下的链表也都会出现
  • 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
  • 插入和删除的时间复杂度是O(logn),当达到了一定的数据规模之后,它的效率与红黑树差不多

主要命令

  • zadd:向Sorted Set中添加元素
  • zrem:删除Sorted Set中的指定元素
  • zrange:按照从小到大的顺序返回指定区间内的元素

实现延迟队列

  1. 将延迟任务加到Sorted Set,将延迟时间设为score
  2. 启动一个线程不断判断Sorted Set中第一个元素的score是否大于当前时间
  3. 如果大于,从Sorted Set中移除任务并添加到执行队列中
  4. 如果小于,进行短暂休眠后重试

实例操作

root@redis:/usr/local/bin# redis-cli127.0.0.1:6379> zadd delayqueue 1 task1
(integer) 1127.0.0.1:6379> zadd delayqueue 2 task2
(integer) 1127.0.0.1:6379> zadd delayqueue 4 task4
(integer) 1127.0.0.1:6379> zadd delayqueue 3 task3
(integer) 1127.0.0.1:6379>127.0.0.1:6379> zrange delayqueue 0 0 withscores1) "task1"

优点:

  • 实现简单
  • 任务可管理(可删除、修改任务)

缺点:

  • 需要有短轮询线程不断判断第一个元素是否过期,造成CPU空耗
  • 分布式场景中,容易引起多个节点读取到相同任务

DelayQueue实现

DelayQueue是一个使用优先队列实现的BlockingQueue,优先队列比较的是时间,内部存储的是实现Delayed接口的对象。 只有在对象过期后才能从队列中获取对象。

内部结构

  • 可重入锁
  • 用于根据delay时间排序的优先级队列
  • 用于优化阻塞通知的线程leader
  • 用于实现阻塞和通知的Condition对象

Leader/Followers
Leader/Followers是多个工作线程轮流进行事件监听、分发、处理的一种模式。 该模式最大的优点在于,它是自己监听事件并处理客户请求,从接收到处理都是在同一线程中完成, 所以不需要在线程之间传递数据,解决线程频繁切换带来的开销。

该模式工作的任何时间点,只有一个线程成为Leader ,负责事件监听,而其他线程都是Follower,在休眠中等待成为Leader。 该模式的工作线程存在三种状态,工作线程同一时间只能处于一种状态,这三种状态为

  • Leading:线程处于领导者状态,负责事件监听。Leader监听到事件后,有两种处理方式:
    • 可以转移至Processing状态,自己处理该事件,并调用方法推选新领导者。
    • 也可以指定其他Follower来处理事件,此时Leader状态不变。
  • Processing:线程正在处理事件,处理完事件如果当前线程集中没有领导者,它将成为新领导者,否则转为追随者。
  • Following:线程处于追随者状态,等待成为新的领导者也可能被领导者指定来处理新的事件。

核心源码分析:

入队public boolean offer(E e) {
 final ReentrantLock lock = this.lock;
  lock.lock();
 try {
      q.offer(e);
 if (q.peek() == e) {//入队对象延迟时间是队列中最短的          leader = null;//重置leader          available.signal();//唤醒一个线程去监听新加入的对象      }
 return true;
  } finally {
      lock.unlock();
  }
}

 

出队public E take() throws InterruptedException {
 final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
 try {
 for (;;) {
          E first = q.peek();
 if (first == null)
              available.await();//队列为空,无限等待 else {
 long delay = first.getDelay(TimeUnit.NANOSECONDS);
 if (delay <= 0)//延迟时间已过,直接返回 return q.poll();
 else if (leader != null)//已有leader在监听了,无限等待                  available.await();
 else {
                  Thread thisThread = Thread.currentThread();
                  leader = thisThread;//当前线程成为leader try {
                      available.awaitNanos(delay);//在delay纳秒后唤醒                  } finally {
 if (leader == thisThread)// 入队一个最小延迟时间的对象时leader会被清空                          leader = null;
                  }
              }
          }
      }
  } finally {
 if (leader == null && q.peek() != null)//leader不存在且队列不为空,唤醒一个follower去成为leader去监听          available.signal();
      lock.unlock();
  }
}
 

 

优点:

  • 效率高,任务触发时间延迟低

缺点:

  • 数据是保存在内存,需要自己实现持久化
  • 不具备分布式能力,需要自己实现高可用

未完待续。

 

本文来自网易云社区,经作者陈志良授权发布。

原文:延迟任务调度系统(技术选型与设计)

<think>我们正在设计一个自动化运维平台中的任务调度子系统。根据引用[3],任务调度系统包含核心业务流:任务管理系统、作业管理系统、任务调度、作业调度、作业执行、执行主机(Worker)以及执行目标主机及目标程序。我们将基于此展开设计。1.**整体架构设计**-任务调度子系统采用分层设计:-**管理层**:提供用户接口,管理任务和作业的元数据(任务管理系统、作业管理系统)。-**调度层**:负责任务的实例化、执行规划(任务调度器TaskScheduler)和作业的调度执行(作业调度器JobScheduler)。-**执行层**:由作业执行器(JobExecutor)和Worker节点组成,负责具体执行。-**目标层**:实际运行脚本或程序的目标主机。2.**核心组件详解**-**任务管理系统**:-功能:任务的定义、修改、删除、查询,以及任务依赖关系管理。-存储:任务元数据(如任务名称、类型、执行命令、依赖任务、调度周期等)存入数据库。-**作业管理系统**:-功能:作业(任务的一次实例化)的生命周期管理,包括状态跟踪(等待、运行、成功、失败)、日志记录等。-**任务调度器(TaskScheduler)**:-根据任务配置(如定时规则)实例化任务生成作业。-进行作业执行规划:确定作业执行的先后顺序(考虑任务依赖关系)。-**作业调度器(JobScheduler)**:-从作业队列中取出待执行作业,根据资源状况(如Worker负载)分配执行资源。-支持优先级调度、故障转移等策略。-**作业执行器(JobExecutor)**:-在Worker节点上运行,接收JobScheduler分配的作业。-调用目标主机上的脚本或程序,并监控执行状态。-**Worker节点**:-部署在计算节点上,负责实际执行作业。可横向扩展。-向调度层注册并定期上报心跳和资源状态。3.**关键流程**-**任务提交**:用户通过管理界面或API创建任务,任务管理系统存储任务配置。-**任务实例化**:-TaskScheduler扫描任务配置,当满足触发条件(如定时到达)时,生成作业实例并推入作业队列。-对于有依赖的任务,需检查父任务状态(例如父任务成功后才触发)。-**作业调度**:-JobScheduler监听作业队列,根据调度策略(如优先级、资源匹配)选择作业。-将作业分配给负载较低的Worker节点(通过JobExecutor执行)。-**作业执行**:-JobExecutor在Worker节点上启动进程执行目标命令,并捕获输出和返回码。-实时上报执行状态给作业管理系统。-**状态同步**:作业管理系统更新作业状态,供用户查询和告警使用。4.**高可用扩展性**-**去中心化设计**:Worker节点可动态加入,JobScheduler支持多实例部署(通过选举或分片实现负载均衡)。-**故障恢复**:-Worker失联时,JobScheduler将其上运行的作业重新分配。-任务调度器采用分布式锁保证实例化过程幂等。-**监控集成**:Worker节点上报资源使用率,结合引用[2]的监控系统,可定义规则自动处理故障(如作业失败时重试)。5.**技术选型建议**-调度框架:Celery(Python)或Quartz(Java),但需二次开发满足分布式调度需求。-存储:MySQL(任务/作业元数据),Redis(作业队列)。-通信:RPC(gRPC)或消息队列(RabbitMQ/Kafka)用于组件间通信。6.**高级特性**-**任务依赖**:使用DAG(有向无环图)描述任务依赖关系,调度时进行拓扑排序。-**超时控制**:作业执行设置超时,避免僵尸任务。-**资源隔离**:Worker节点支持容器化(Docker)运行作业。>示例:一个简单的任务调度流程>1.用户定义任务A(每天00:00执行)和任务B(依赖A成功)。>2.TaskScheduler在00:00生成A的作业实例。>3.JobScheduler分配Worker执行A作业。>4.A成功后,TaskScheduler生成B的作业实例。>5.JobScheduler分配Worker执行B作业。§§相关问题§§1.如何设计任务调度子系统的高可用架构?2.任务依赖关系在调度系统中如何实现?3.自动化运维平台中任务调度子系统的监控指标有哪些?</think>### 自动化运维平台任务调度子系统设计方案 #### 一、核心架构设计 任务调度子系统采用分层架构,主要分为以下模块: 1. **任务管理层** - 功能:提供任务定义、配置管理、版本控制等接口 - 技术实现:RESTful API + 数据库(MySQL/PostgreSQL) - 关键数据结构: $$ \text{Task} = \{ \text{id, name, type, command, params, dependencies, schedule\_policy} \} $$ 2. **调度引擎层** - **任务调度器(TaskScheduler)** - 解析任务DAG(有向无环图),处理依赖关系 - 基于时间/事件触发任务实例化 - 算法示例: ```python def schedule_tasks(task_dag): # 拓扑排序生成执行序列 sorted_tasks = topological_sort(task_dag) for task in sorted_tasks: if check_dependencies(task): # 检查前置条件 create_job_instance(task) # 生成作业实例 ``` - **作业调度器(JobScheduler)** - 采用资源感知调度算法(如DRF算法) - 动态分配Worker资源 - 状态机管理: $$ \text{JobState} \in \{ \text{PENDING, RUNNING, SUCCESS, FAILED, TIMEOUT} \} $$ 3. **执行层** - **Worker节点** - 部署监控代理(如Prometheus Exporter) - 支持容器化执行环境(Docker/Kubernetes) - **作业执行器(JobExecutor)** - 命令执行模板: ```bash ssh {target_host} "nohup {command} > {log_path} 2>&1 &" ``` #### 二、关键流程设计 1. **任务提交流程** ```mermaid graph TD A[用户提交任务] --> B[任务管理系统] B --> C[持久化任务配置] C --> D[TaskScheduler] D --> E[生成作业实例] E --> F[JobScheduler队列] ``` 2. **故障处理机制** - 基于引用[2]的IFTTT模型: ```python if check_failure(job): # 触发条件检测 execute_action(job, 'retry') # 执行预设动作 ``` - 支持三级容错策略: 1. 本地重试(≤3次) 2. Worker转移 3. 人工介入告警 #### 三、高可用设计 1. **分布式部署方案** - ZooKeeper实现调度器主从选举 - Redis分布式锁控制并发调度 $$ \text{LockKey} = \text{job}\_\text{}\{\text{id}\}\_\text{schedule} $$ 2. **数据持久化** - 元数据:关系型数据库(ACID事务保障) - 执行日志:Elasticsearch集群 - 检查点:定期持久化DAG状态 #### 四、监控体系设计 | 监控层级 | 核心指标 | 告警阈值 | |----------------|-------------------------|------------------| | 调度器 | 队列积压量 | >100任务/分钟 | | Worker节点 | CPU/MEM使用率 | >85%持续5分钟 | | 作业执行 | 失败率 | >5%/小时 | | 网络通信 | P99延迟 | >500ms | > 通过集成引用[2]的Action Channel实现自动化故障处理[^2] #### 五、典型技术栈 | 模块 | 推荐技术 | |--------------|-----------------------------------| | 调度引擎 | Celery/Airflow + Redis/RabbitMQ | | 执行环境 | Docker + Kubernetes CRD | | 状态存储 | PostgreSQL + Redis | | 监控 | Prometheus + Grafana | ### 设计验证案例 **场景:** 每日凌晨批量服务器巡检 1. 任务配置: ```json { "name": "nightly_inspection", "cron": "0 2 * * *", "command": "/scripts/inspection.sh", "timeout": 1800 } ``` 2. 执行过程: - TaskScheduler 02:00 生成作业 - JobScheduler 分配低负载Worker - JobExecutor 通过SSH在多目标机并行执行 - 结果写入Elasticsearch供可视化展示
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值