如何设计一个分布式任务调度系统?
嗨,你好啊,我是猿java
什么是分布式任务调度器?为什么需要分布式任务调度系统?如何设计一套分布式任务调度系统?这篇文章,我们来详细分析。
什么是分布式任务调度系统?
分布式任务调度系统是一种软件系统,用于在多个计算节点上协调和管理任务的执行,这类系统的主要目标是提高任务调度的效率、可靠性和可扩展性。分布式任务调度系统通常用于处理需要在多个服务器或计算节点上并行执行的复杂计算任务。
如何设计分布式任务调度系统?
需求分析
在深入设计之前,让我们列出功能和非功能需求。
功能需求
- 用户可以提交一次性或周期性任务进行执行。
- 用户可以取消已提交的任务。
- 系统应将任务分布到多个工作节点进行执行。
- 系统应提供任务状态监控(排队中、运行中、已完成、失败)。
- 系统应防止同一任务被多次并发执行。
非功能需求
- 可扩展性:系统应能够调度和执行数百万个任务。
- 高可用性:系统应具有容错能力,且无单点故障。如果工作节点失败,系统应将任务重新调度到其他可用节点。
- 延迟:任务应以最小的延迟进行调度和执行。
- 一致性:任务结果应一致,确保任务只执行一次(或最小化重复)。
附加需求(超出范围):
- 任务优先级:系统应支持基于任务优先级的调度。
- 任务依赖性:系统应处理有依赖关系的任务。
High-level 设计
在高层次上,我们的分布式任务调度器将包含以下组件:
任务提交服务
任务提交服务是客户端与系统交互的入口。它提供用户或服务通过API提交、更新或取消任务的接口。
该层暴露一个RESTful API,接受任务详细信息,如:
- 任务名称
- 频率(一次性、每日)
- 执行时间
- 任务负载(任务详情)
它将任务元数据(例如,execution_time、frequency、status = pending)保存在任务存储(数据库)中,并返回一个唯一的任务ID给客户端。
任务存储
任务存储负责持久化任务信息并维护系统中所有任务和工作节点的当前状态。
任务存储包含以下数据库表:
任务表
该表存储任务的元数据,包括任务ID、用户ID、频率、负载、执行时间、重试次数和状态(待处理、运行中、已完成、失败)。
任务执行表
任务在失败时可以多次执行。
该表跟踪每个任务的执行尝试,存储信息如执行ID、开始时间、结束时间、工作节点ID、状态和错误信息。
如果任务失败并重试,每次尝试都将在此记录。
任务调度表
调度表存储每个任务的调度详情,包括next_run_time。
对于一次性任务,next_run_time与任务的执行时间相同,last_run_time保持为空。
对于周期性任务,next_run_time在每次执行后更新,以反映下次计划的运行时间。
工作节点表
工作节点表存储每个工作节点的信息,包括其IP地址、状态、最后心跳、容量和当前负载。
调度服务
调度服务负责根据任务调度表中的next_run_time选择待执行的任务。
它定期查询表中计划在当前分钟运行的任务:
1 | SELECT * FROM JobSchedulesTable WHERE next_run_time = 1726110000; |
一旦取回到期任务,它们将被推送到分布式任务队列中供工作节点执行。
同时,任务表中的状态更新为SCHEDULED。
分布式任务队列
分布式任务队列(例如,Kafka、RabbitMQ)作为调度服务和执行服务之间的缓冲区,确保任务高效地分布到可用的工作节点。
它持有任务,并允许执行服务拉取任务并分配给工作节点。
执行服务
执行服务负责在工作节点上运行任务并在任务存储中更新结果。
它由一个协调器和一组工作节点组成。
协调器
协调器(或编排器)节点负责:
分配任务:将任务从队列分发到可用的工作节点。
管理工作节点:跟踪活跃工作节点的状态、健康状况、容量和工作负载。
处理工作节点故障:检测工作节点故障并将其任务重新分配给其他健康节点。
负载均衡:确保工作负载根据可用资源和容量均匀分布在工作节点上。
工作节点
工作节点负责执行任务并将结果(例如,已完成、失败、输出)更新到任务存储中。
当工作节点被分配一个任务时,它会在任务执行表中创建一个新条目,任务状态设为运行中并开始执行。
执行完成后,工作节点在任务表和任务执行表中更新任务的最终状态(例如,已完成或失败)以及任何输出。
如果工作节点在执行期间失败,协调器会将任务重新排队到分布式任务队列中,允许其他工作节点拾取并完成任务。
Low-level设计
系统API设计
以下是系统中一些重要的 API。
提交任务(POST /jobs)
获取任务状态(GET /jobs/{job_id})
取消任务(DELETE /jobs/{job_id})
列出待处理任务(GET /jobs?status=pending&user_id=u003)
获取某个工作节点上正在运行的任务(GET /job/executions?worker_id=w001)
深入分析关键组件
SQL vs NoSQL
为了选择适合我们需求的数据库,让我们考虑一些可能影响选择的因素:
我们需要每天存储数百万个任务。
读写查询大致相同。
数据是具有固定模式的结构化数据。
我们不需要ACID事务或复杂的连接。
SQL和NoSQL数据库都可以满足这些需求,但考虑到工作负载的规模和性质,像DynamoDB或Cassandra这样的NoSQL数据库可能更适合,特别是在处理每天数百万个任务并支持高吞吐量的写入和读取时。
扩展调度服务
调度服务每分钟定期检查任务调度表中的待处理任务并将它们推送到任务队列中进行执行。
例如,以下查询检索在当前分钟内到期执行的所有任务:
1 | SELECT * FROM JobSchedulesTable WHERE next_run_time = 1726110000; |
优化从JobSchedulesTable读取:
由于我们使用next_run_time列查询JobSchedulesTable,最好在next_run_time列上分区表,以高效检索计划在特定分钟内运行的所有任务。
如果任何分钟内的任务数量较少,一个节点就足够了。
然而,在高峰期,如在一分钟内需要处理50,000个任务时,依赖一个节点可能会导致执行延迟。
节点可能会过载并减慢速度,造成性能瓶颈。
此外,只有一个节点会引入单点故障。
如果该节点由于崩溃或其他问题而不可用,则在节点恢复之前不会调度或执行任何任务,导致系统停机。
为了解决这个问题,我们需要一个分布式架构,其中多个工作节点并行处理任务调度任务,由一个中央节点协调。
但是,我们如何确保任务不会被多个工作节点同时处理呢?
解决方案是将任务划分为段。每个工作节点只处理JobSchedulesTable中分配给它的特定子集任务,专注于分配的段。
这是通过添加一个名为segment的额外列来实现的。
segment列逻辑上将任务分组(例如,segment=1,segment=2,等等),确保没有两个工作节点同时处理同一个任务。
协调器节点通过分配不同的段给工作节点来管理工作负载的分布。
它还通过心跳或健康检查监控工作节点的健康状况。
在工作节点故障、新工作节点的添加或流量激增的情况下,协调器通过调整段分配动态重新平衡工作负载。
每个工作节点使用next_run_time和其分配的段查询JobSchedulesTable,以检索它负责处理的任务。
以下是工作节点可能执行的查询示例:
1 | SELECT * FROM JobSchedulesTable WHERE next_run_time = 1726110000 AND segment in (1,2); |
处理任务失败
当任务在执行期间失败时,工作节点会增加任务表中的retry_count。
如果retry_count仍低于max_retries阈值,工作节点会从头开始重试任务。
一旦retry_count达到max_retries限制,任务将被标记为失败,不会再次执行,状态更新为失败。
注意:任务失败后,工作节点不应立即重试任务,特别是如果失败是由瞬态问题(例如,网络故障)引起的。
相反,系统在延迟后重试任务,并且每次重试的延迟会呈指数增加(例如,1分钟、5分钟、10分钟)。
处理执行服务中工作节点的故障
工作节点负责执行由执行服务中的协调器分配给它们的任务。
当工作节点失败时,系统必须检测到故障,将未完成的任务重新分配给健康节点,并确保任务不会丢失或重复。
有几种检测故障的方法:
心跳机制:每个工作节点定期向协调器发送心跳信号(每几秒一次)。协调器跟踪这些心跳信号,如果在预定义时间段内(例如,连续3次心跳信号未收到),将工作节点标记为“不健康”。
健康检查:除了心跳信号,协调器还可以对每个工作节点进行定期健康检查。健康检查可能包括CPU、内存、磁盘空间和网络连接,以确保节点不过载。
一旦检测到工作节点故障,系统需要恢复并确保分配给故障工作节点的任务仍然被执行。
有两种主要场景需要处理:
待处理任务(未开始)
对于分配给工作节点但尚未开始的任务,系统需要将这些任务重新分配给其他健康的工作节点。
协调器应将它们重新排队到任务队列中,让另一工作节点拾取。
进行中的任务
在工作节点故障时正在执行的任务需要小心处理,以防止部分执行或数据丢失。
一种方法是使用任务检查点,工作节点定期将长时间运行任务的进度保存到持久存储(如数据库)。如果工作节点失败,另一工作节点可以从最后一个检查点重新开始任务。
如果任务部分执行但未完成,协调器应将任务标记为“失败”并重新排队到任务队列中,让另一个工作节点重试。
解决单点故障
我们在调度服务和执行服务中使用了协调器节点。
为了防止协调器成为单点故障,部署多个协调器节点并使用领导选举机制。
这确保了一个节点是活动领导者,而其他节点处于待命状态。如果领导者失败,将选举新的领导者,系统继续运行不中断。
领导选举:使用像Raft或Paxos这样的共识算法从协调器池中选举领导者。像Zookeeper或etcd这样的工具通常用于管理分布式领导选举。
故障切换:如果领导协调器失败,其他协调器检测到故障并选举新的领导者。新领导者立即接管职责,确保任务调度、工作节点管理和健康监测的连续性。
数据同步:所有协调器应访问相同的共享状态(例如,任务调度数据和工作节点健康信息)。这可以存储在分布式数据库中(例如,Cassandra、DynamoDB)。这样可以确保当新的领导者接管时,它有最新的数据可用。
速率限制
1. 任务提交级别的速率限制
如果一次性提交给调度系统的任务过多,系统可能会过载,导致性能下降、超时或甚至调度服务失败。
在客户端级别实现速率限制,以确保没有单个客户端可以压垮系统。
例如,限制每个客户端每分钟最多提交1,000个任务。
2. 任务队列级别的速率限制
即使控制了任务提交速率,如果任务队列(例如,Kafka、RabbitMQ)被过多任务淹没,系统可能会过载,导致工作节点速度变慢或消息积压。
限制任务推送到分布式任务队列的速率。这可以通过实现队列级别的节流来实现,每秒或每分钟只允许一定数量的任务进入队列。
3. 工作节点级别的速率限制
如果系统允许工作节点同时执行过多任务,可能会导致基础设施(例如,CPU、内存、数据库)过载,导致性能下降或崩溃。
因此,在工作节点级别实现速率限制,以防止任何单个工作节点一次执行过多任务。在工作节点上设置最大并发限制,以控制每个工作节点可以同时执行的任务数量。
分布式任务的方案
上面的内容我们详细分析了分布式任务的设计和实现细节,但是,在实际工作中,我们一般都会采用一些三方的方案来实现分布式任务。国内常见的分布式任务的方案有:Quartz Cluster,XXL-Job,Elastic-Job等
是的,你提到的Quartz Cluster、XXL-Job和Elastic-Job都是常见的分布式任务调度方案。它们各自有不同的特点和适用场景。以下是对这些方案的简要介绍:
Quartz Cluster
Quartz是一个功能强大的开源任务调度框架,支持创建复杂的定时任务。Quartz Cluster是其集群版本,用于实现高可用性和负载均衡。
特点
- 丰富的定时任务功能:支持简单任务、复杂任务、重复任务等多种类型。
- 集群支持:通过数据库实现任务调度的分布式协调,多个节点可以共享任务调度的负载。
- 持久化:支持任务的持久化存储,以确保调度信息在系统重启后不丢失。
- 灵活性:支持多种触发器和任务类型,可以根据业务需求进行灵活配置。
适用场景
- 需要高精度定时任务调度的场景。
- 需要任务持久化和高可用性的场景。
XXL-Job
XXL-Job是一个分布式任务调度平台,旨在提供一个简单、高效、可扩展的任务调度方案。
特点
- 简单易用:提供了易于使用的Web管理界面,方便用户管理和监控任务。
- 分布式执行:支持分布式执行任务,能够根据负载动态分配任务。
- 失败重试:支持任务失败后的自动重试机制。
- 调度策略:支持多种调度策略,包括Cron表达式、固定频率等。
- 任务依赖:支持任务依赖关系管理,确保任务按正确顺序执行。
适用场景
- 需要一个简单易用的Web界面进行任务管理的场景。
- 需要分布式任务调度和负载均衡的场景。
Elastic-Job
Elastic-Job是当当网开源的分布式调度解决方案,分为Elastic-Job-Lite和Elastic-Job-Cloud两个版本。
特点
- 分布式协调:利用Zookeeper进行任务的分布式协调和管理。
- 弹性伸缩:支持任务的动态伸缩,能够根据负载自动调整任务分配。
- 高可用:通过任务分片和故障转移机制实现高可用性。
- 任务分片:支持将任务拆分成多个小任务并行执行,提高执行效率。
- 多租户支持:支持多租户任务调度和管理。
适用场景
- 需要高可用性和弹性伸缩能力的分布式任务调度场景。
- 需要任务分片并行执行以提高效率的场景。
综合来说:
- Quartz Cluster适用于需要高精度和任务持久化的场景。
- XXL-Job适用于需要简单易用的Web管理界面和分布式任务调度的场景。
- Elastic-Job适用于需要高可用性、弹性伸缩和任务分片的复杂分布式任务调度场景。
国外一些常见的分布式任务调度系统包括:
- Apache Airflow:一个开源的工作流管理平台,用于编写、调度和监控工作流。
- Celery:一个基于分布式消息传递的异步任务队列系统。
- Kubernetes CronJobs:Kubernetes中用于定期执行任务的调度机制。
- Apache Oozie:一个用于Hadoop工作流调度和协调的服务。
总结
本文,我们从需求到架构再到实现细节,详细地介绍了如何设计一个可扩展、高可用的分布式任务调度系统。在实际工作中,我们一般都会采用一些三方的方案来实现分布式任务,但是理解分布式任务调度系统的设计可以帮助我们更好的理解和使用三方工具。
学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。