文章概览
名词解释
背景
需求
设计
任务分类
任务状态
架构
总结
欢迎加入华仔的星球,你将获得: 专属的中间件专栏 / 1v1 提问 / 简历修改指导/ 学习打卡 / 每月赠书 / 社群讨论
《从四大维度开始带你精通 RocketMQ》 已爆肝完毕,基于 RocketMQ 5.1.2 版本进行源码讲解。 《从四大维度开始带你精通 Kafka》 已爆肝完毕,基于 Kafka 2.8 以及 3.x 版本进行源码讲解。 截止目前,累计输出 500w+ 字,讲解图 2000+ 张,还在持续爆肝中.. 后续还会上新更多项目和专栏,目标是打造地表最强中间件星球,戳我加入学习,已有430+小伙伴加入,电商实战项目火热更新中,结束时会有简历指导包装,需要的抓紧来。
这里说几点,解答一些疑惑,可以认真看下:
1、星球内容只会越来越完善,价格越来越贵,一年时间从69元开始发售到现在已经涨到了189元,下周正式涨价,下周正式涨价,下周正式涨价。
2、只筛选认可且支持我的老铁,我不喜欢白嫖怪,尊重别人就是尊重自己。
3、对于星球年费说下,只是到期后新内容看不到,已经更完的内容相当于一次付费永久看,所以认可我内容的可以放心来,有疑问文末加我好友进行答疑。
4、除专栏外,我会提供专属的一对一答疑服务,包括不限于工作中、专栏中遇到的问题,简历修改指导、职业规划服务等。这也是星球的特色服务。
钱芳园,多年MySQL、Redis等DB运维经验,现在专注于数据库自动化平台开发。
一、名词解释
Task:任务通常是为了达成某项目的而执行的一组命令、动作的集合。
Celery:一个分布式任务调度框架,简单、高可用、组件之间可以扩展。
Worker:Celery 中执行任务的节点。
Broker:Celery 中下发任务的队列,一般是 Redis 或RabbitMQ。
Backend:Celery 中任务执行完成之后会将任务结果保存在Backend 中,通常为数据库。
TaskID:每个任务需要具备一个全局唯一的ID。
TaskName:所有的任务都需要有任务名,用于标识一组任务的名称。
TaskType:任务类型,业务可以自定义的任务类型。
TaskWorker:任务执行器,执行具体的某项任务。
TaskScheduler:任务调度器,负责调度任务执行。
TaskConsole:任务请求入口,负责转发任务请求到TaskScheduler和TaskWorker上。
DistributeLock:分布式锁,同一时间只能有一个申请者加锁成功,其余返回失败,并且在锁有效期间其他失败者无法再次加锁。
TaskQueue:任务队列,TaskScheduler将可执行的任务放入到任务队列中,TaskWorker从任务队列中获取任务,并执行任务。
二、背景
任务,作为实现指定目标的命令、动作集合,涵盖广泛,如实例部署、数据库关闭、日志分析、数据库备份与恢复等。面对大量并行任务,资源争夺成为潜在冲突源,可能导致执行意外。设想一场景:一任务负责文件清理,另一任务则欲修改同一文件。若两者并行不悖,先行的清理可能阻断修改,反之亦然,结果难以预测。
为确保任务执行的有序性与高效性,避免资源冲突带来的不确定性及整体执行时间的无谓延长,任务调度器的引入显得尤为关键。此调度器旨在精心编排各类任务,确保它们以最优化方式执行,从而最大化任务执行效率,保障系统运行的流畅与稳定。
当前数据库运维平台集成了Celery作为任务调度器,自动化地管理任务执行流程。无论是通过手动操作还是预设的定时程序,任务都能推送至Broker队列中。随后,各Worker节点主动从Broker中拉取任务详情,迅速执行并处理,最终将任务成果安全地存储至Backend系统中,确保整个任务处理链条的流畅与高效。
当前任务调度系统存在如下几个问题:
任务类型局限性:平台对任务类型的支持较为有限,擅长处理简单任务,但在应对复杂或高耗时任务时显得力不从心。
任务信息记录不足:系统记录的任务状态信息较为基础,仅限于PENDING、STARTED、SUCCESS、FAILURE、RETRY、REVOKED等,缺乏详尽的执行明细,难以满足深入分析和监控的需求。
Broker扩展性受限:当前Broker仅支持Redis、RabbitMQ、Amazon SQS,缺乏对其他类型队列的兼容性,限制了系统的灵活性和扩展能力。
任务幂等性问题:由于Celery高度依赖Broker来避免任务重复执行,这导致任务本身难以达到幂等性,即在相同条件下多次执行应产生相同的结果,这在某些应用场景下可能成为障碍。
大规模任务部署挑战:平台在设计之初未充分考虑大规模任务执行场景,以及各组件间的可用性保障,虽然这些问题并非无解,但解决起来需投入较高成本,增加了运维难度和成本。
鉴于Celery任务调度系统存在的问题与局限性,正值DBA团队开发新一代自动化运维平台的契机,我们决心彻底革新,从零构建一套分布式任务调度系统,旨在彻底消除既有痛点,为自动化平台提供一个高效、可靠的任务调度系统。
新平台的架构如下:
这个是我们构建的全新的自动化平台。
整个平台架构层次分明,各层各司其职,模块间独立部署,确保无相互依赖,且均支持灵活的横向扩展。同层模块灵活互调,上层可调用下层,而下层则遵循规则,不能越界调用上层。特别指出,Server作为核心管控模块,引领平台启动,统揽全局;Meta则作为元数据枢纽,为各模块提供元数据服务,两者超脱层级束缚,独立存在。此架构彻底摒弃单点故障隐患,各模块均展现出卓越的性能、高可用性及高度可扩展性。接下来,简要概述下该平台架构各层级的主要功能。
底层为插件层,专注于执行多元化任务处理。
插件管控层,精准管理各插件运作,确保高效协同。
基础服务层,稳固地构建了平台基石,包括高可用(HA)、监控(Monitor)、备份恢复(BackupRestore)等核心模块,并深度融合任务调度系统,全面提升系统效能。
高级服务层,展现高级功能之精髓,如分析(Analyze)、报告(Report)、自愈(Healing)等模块,引领平台向智能化迈进。
接入层,作为门户与枢纽,无缝连接展示层与业务交互,集请求转发、权限认证、安全防护于一体,确保访问的安全与顺畅。
展示层,则是平台的直观展现,汇聚各类功能与信息。
目前平台已初步成型,分层架构稳固搭建。在此基础上,我们重构了分布式任务调度系统,以创新驱动平台未来发展。
三、需求
针对全新分布式任务调度系统的构建,我们设定了前瞻性与实效性并重的目标体系,为未来十年的技术迭代与需求增长奠定坚实基础:
全面兼容各类任务场景,无论是同步、异步,或是高耗时、复杂流程任务,均能实现无缝集成。
系统需详尽记录任务全周期信息,涵盖调度详情、错误追踪、状态监控及执行日志等,确保信息透明可追溯。
在任务队列策略上,我们倡导灵活适配原则,小规模任务内嵌高效队列,大规模场景则开放对接多元外置队列,实现队列功能的纯粹分发,精简系统架构。
任务执行支持幂等性,确保同一个任务不允许重复执行。
各组件间必须遵循高性能、高扩展性及高可用性的设计原则。
四、设计
为达成上述目标,首要之务是明晰任务相关的具体信息,确切界定所支持的任务类型。
1. 任务分类
按照阻塞属性,任务分为同步任务和异步任务。
1.1 同步任务
同步任务即是在调度系统分配任务执行之际,必须等待Worker完成任务并回馈结果后,方可终结其执行流程。
1.2 异步任务
异步任务指的是调度系统分发任务时,仅需向Worker发出执行指令,无需阻塞等待任务完成。Worker在独立处理任务后,会主动将执行结果回馈给调度系统。
按照重复属性,任务分为简单任务、定时任务、永久任务、拓扑任务。
1.3 简单任务
简单任务是指任务只需要下发给执行者一次,无论任务是否执行成功。但如果任务失败时,根据配置任务可以选择是否需要重试执行。例如发送邮件、部署数据库实例、关闭数据库等,正常情况下只需要执行一次就可以,如果执行失败,需要根据错误信息和当前的配置选择是否需要重试执行。可以重复执行的任务,都需要任务自身保证幂等的结果。
1.4 定时任务
定时任务是指调度器在指定时间生成简单任务,并执行任务。这种任务具备定时重复的属性,例如定时清理日志、定时分析数据生成报告等。
1.5永久任务
永久任务是指调度器需要确保任务一旦下发,那就必须要在执行。任务需要永久执行,且不可停止,除非遇到不可恢复的错误或者手动停止。例如binlog备份、监控服务等任务在正常情况下都需要不停的执行。
1.6 拓扑任务(Topology)
拓扑任务是指一系列任务的集合,任意任务之间可以是父子、中心的关系。
父子关系的任务是指每个任务必须有一个父任务(第一个任务除外),父任务执行完成才可以触发子任务执行,一个父任务可以触发多个子任务执行。子任务之间是并行执行的关系。这种任务场景比较多,例如部署数据库集群,需要创建初始化环境任务、部署节点任务、部署监控任务、部署 HA任务等。每个任务之间是父子关系,需要等待上一个任务执行完成,才可以执行下一个任务。
中心关系的任务是指每个子任务必须由一个中心任务创建,每个子任务执行完成之后需要通知中心任务,由中心任务确定是否需要创建下一个子任务。这种关系一般是因为任务之间的资源是有相互依赖,后一个子任务需要依赖前一个子任务执行的结果。例如恢复数据库,需要创建选取恢复服务器务、恢复数据库任务、数据导出任务、清理任务,每个子任务需要等前一个子任务执行成功,并获取子任务的结果之后才能创建下一个子任务,并执行子任务。
新任务调度系统必须兼容各类任务需求。至于任务执行时机的确定及成功与失败的辨识,关键在于为每项任务设定一个明确的状态标识。此状态不仅反映任务当前进展,还指引后续操作路径,因此,任务状态是不可或缺的一环。
2. 任务状态
根据当前需要支持的任务类型、场景,任务调度系统需要支持的任务状态如下:
Init | 不可调度状态。一般只有任务创建时会处于这个状态,需要将任务状态修改为可执行的状态,任务才可以调度执行。 |
Create | 创建状态。任务初始化状态,处于这个状态下的任务是可以被调度执行。 |
Prepare | 准备状态。任务已经初始化完成相关的资源,可以进入执行状态。 |
Postpone | 推迟状态。任务已经被调度执行,但是因为条件不满足,所以需要被推迟执行,等待下一轮调度。 |
Running | 执行中状态。任务已经在执行。 |
Pause | 暂停状态。任务由于条件不满足,所以需要暂停,等待条件满足之后再执行。一般情况下永久任务才会有这个状态,且是需要手动执行的。 |
Success | 成功状态。任务已经执行,并且结果为成功。 |
Fail | 失败状态。任务已经执行,但是结果为失败。 |
Finish | 完成状态。处于 Finish 状态的任务不可以被调度,Finish 为任务的最终状态,所有任务状态均可以转化为 Finish 状态。 |
None | 未知状态。任务的状态不可知晓,一般情况下是获取不存在的任务信息时,会返回这个状态。 |
Rollback | 回滚状态。任务失败之后已经回滚完成。 |
任务状态流程图:
3. 架构
明确需要支持哪些任务以及任务状态之后,根据需求分析,我们设计的系统架构如下:
整个系统中分为三个大模块:
TaskScheduler 负责记录任务的基础信息,并获取需要执行的任务,负责调度任务的执行。任务执行的调度信息和任务状态也都需要记录。
TaskWorker 是各种任务类型的集合,负责执行具体的任务,并记录任务的结果。
TaskConsole 主要负责转发任务请求,所有的任务均需要接入 TaskConsole,由 TaskConsole 负责转发请求到TaskSchedule 或指定的 TaskWorker。
3.1 TaskScheduler
根据TaskScheduler的功能划分,我们设计了TaskScheduler的子模块架构图:
各个模块的作用如下:
API:对外提供的接口,接受请求,将任务写入到库中。
Produer:定时任务生成模块,持有定时任务的触发器,触发器会在指定的时间生成任务,并写入到数据库中。
DB:数据库模块,用于记录任务信息、任务状态、触发器信息、任务的执行日志等。
LogDumper:数据库日志变更模块,用于获取数据库的操作记录,如果记录是与任务相关,则根据规则将任务下发到 Dispatcher 中,启用这个模块在部分场景下可以加快任务执行速度。
Dispatcher:任务分发模块,根据任务的优先级将任务下发到队列中,并负责监控队列的延迟、堆积等信息。
Checker:任务检查模块,负责检查任务状态是否处于正常,对于处于异常状态的任务,会做进一步的处理。
Queue:任务队列,只做简单的FIFO,不做其他的策略。
Scheduler:任务调度模块,从 Queue 中获取任务,并调度任务到 TaskWorker 中执行。
任务的来源有两种,通过API创建或者由定时任务创建,API 提供了外部创建的接口,Producer 提供了定时任务创建的功能。所有的任务都会写入到数据库中,LogDumper 接受到任务的变更,会将任务按照规则下发给 Dispatcher。Dispatcher 收到任务请求,会将任务下发到队列中。Scheduler 会从队列中获取任务,并检查任务是否可以执行。如果可以则调度任务执行,那么任务将会进入到执行流程。
3.2 TaskWorker
TaskWorker负责各种任务的实现。TaskScheduler和TaskWorker之间通过 HTTP 接口实现通讯。由于任务种类较多,为了避免每种任务都需要实现各自的调度逻辑,故我们制定任务执行接口的规范,所有任务都需要实现的接口。即所有的任务都只需要按照规范实现相应的接口就可以无缝接入到任务调度系统中。
3.3 通用的任务接口规范
所有任务接口统一前缀为:/{version}/task/{task_name},所有的数据提交方式均是:application/json
,创建、修改任务的数据需要放在http 请求的body中,query为过滤参数需要传入。在请求的url的query中会有一个action
参数,标识着本次任务的执行动作类型。
POST | 是 | 创建任务,并返回任务 ID,所有任务都必须要实现这个接口。 | |
GET | 是 | 获取任务的信息,所有任务都必须要实现这个接口。 | |
GET | status | 是 | 获取任务的状态,所有任务都必须要实现这个接口。 |
PUT | notify | 否 | 任务通知,任务执行完成后,通知 TaskScheduler 任务执行完成。一般情况下只有异步类型任务需要实现这个接口。 |
PUT | running | 是 | 执行任务,所有任务都必须要实现这个接口。 |
PUT | pause | 否 | 任务暂停,任务执行时,出于某种原因需要暂停执行。一般情况下只有永久任务需要实现这个接口。 |
PUT | continue | 否 | 任务继续执行,任务处于暂停状态,需要继续执行时,需要调用这个接口。一般情况下只有永久任务需要实现这个接口。 |
PUT | prepare | 否 | 任务准备资源,任务在执行前,将需要的资源分配完成,等待任务执行。 |
PUT | prepare_rollback | 否 | 任务回滚预备资源,由于某些原因,任务不需要执行,则需要将准备阶段分配的资源回滚掉,避免资源泄露。 |
PUT | change | 否 | 任务修改接口,需要对任务状态、开始时间进行修改。 |
PUT | clean | 否 | 任务清理,任务成功之后需要将某些资源清理掉。 |
PUT | clean_fail | 否 | 任务失败清理,任务失败之后,需要将某些资源清理掉。 |
PUT | heartbeat | 否 | 任务心跳信息,任务执行过程中可以向TaskScheduler主动通知当前任务心跳信息,一般情况下永久任务需要实现这个动作。 |
GET | heartbeat_status | 否 | 任务心跳信息,TaskScheduler如果没有收到心跳信息,那么会主动向TaskWorker查询任务心跳信息,一般情况下永久任务需要实现这个接口。 |
POST | process | 否 | 任务进度,TaskWorker在执行任务过程中可以向TaskScheduler通知任务的执行进度。 |
GET | process_info | 否 | 任务进度信息获取接口。 |
GET | summary | 否 | 任务查询汇总接口,包括各种类型的汇总、报告查询,这个是TaskScheduler需要实现的接口。 |
以上接口需要TaskWorker中的每个任务根据自身的需求实现。除了必须的接口,其他接口可以根据自身需求进行实现。
3.4 通用任务属性
定义了每个任务的请求接口,还需要定义任务的共同属性。为了避免各种类型的任务传输的数据差异比较大,系统无法识别,故我们规范了接口请求数据的结构体。
创建任务需要传输的数据如下:
create_info | object | 任务基础信息 |
process_arg | object | 任务运行控制参数 |
task_arg | object | 任务参数 |
create_info
记录任务基础信息,在任务执行过程中不会变更,核心参数如下:
task_type | string | 任务类型 |
task_name | string | 任务名 |
important_level | int | 任务重要程度 |
http_url | string | 任务请求的url |
parent_task_id | string | 父任务ID |
control_task_id | string | 中心任务ID |
pr
ocess_arg
控制任务执行过程的参数,核心参数如下:
start_time | time | 任务开始时间,默认为当前时间 |
deadline_second | int | 任务超时时间 |
lock_timeout | int | 任务加锁超时时间 |
max_retry | int | 任务最大重试次数 |
task_status | string | 任务开始的状态,只能是Init或者Create(默认) |
除了创建任务和查询任务汇总接口,其他任务接口请求均需要在query
中传入以下三个参数task_name、task_type、task_id,例如:
GET /{version}/task/{task_type}?task_name={task_name}&task_type={task_type}&task_id={task_id}
修改任务接口需要在http 请求的body中传入的核心参数为:
task_status | string | 修改任务状态任务状态 |
start_time | time | 修改任务开始运行时间 |
所有的任务接口返回的结果必须为如下格式:
status | string | 任务状态 |
message | string | 任务错误信息 |
next_start_time | time | 下次任务调度时间,仅当status为postpone时有意义 |
result | object | 任务的详细结果 |
任务返回结果中status是最关键的,它标志着任务当前处于什么状态,以及任务下一步需要执行什么。
3.5 通用任务执行流程
所有的任务有了统一的接口和同一的请求、返回结构数据,那就可以按照统一的执行流程,执行任务。通用的任务流程执行如下:
任务开始执行。
首先会检查任务状态,如果任务不是处于可执行状态,则直接返回,不进入任务执行流程。
任务加锁,如果加锁失败则返回错误,本次流程结束。
加锁成功之后会调用 Prepare 接口分配资源,如果 Prepare 接口分配资源失败,则会调用回滚接口 PrepareRollback,并检查返回状态,如果是状态为 Postpone,则推迟任务,否则返回错误,任务结束。
执行任务。
检查任务结果,如果是 Success 则调用 Clean 接口;如果是 Fail 则调用 CleanFail 接口,如果是 Running 或 Postpone 则修改对应的状态。
如果是任务执行完成(Success/Fail),并且任务处于Topology 结构中,则继续调用子任务或者中心任务。
解锁任务。
任务执行流程结束。
对于同步或异步任务执行的主流程是相同的,区别在于 Running 之后返回的任务结果,异步任务会返回 Running 状态,同步任务则会返回 Success或 Fail,但不会有 Running 状态。故异步任务还多了一次回调的任务流程。
异步任务回调的流程:
任务收到通知。
首先任务会加锁,如果加锁失败则返回错误,本次流程结束。
加锁成功之后会检查任务结果,如果是 Success 则调用 Clean 接口;如果是 Fail 则调用 CleanFail 接口,如果是 Running 或 Postpone 则修改对应的状态。
如果是任务执行完成(Success/Fail),并且任务处于Topology 结构中,则继续调用子任务或者中心任务。
解锁任务。
任务回调执行流程结束。
以上是简单任务的同步和异步执行流程,但是在所有任务类型中定时任务比较特殊,需要有一个模块持有触发器,并确保在指定的时间内生成简单任务。
3.6 触发器如何持有
定时任务触发器的高可用方案大致分为冷备和分布式两种方案。
说明 | 集群所有节点分为master和standby角色。master 持有所有的触发器,standby时刻做冷备,不停的探测 master 存活。一旦 master不可用,则 standy角色中所有节点会选举一个成为 master,并立即持有所有的触发器。 | 集群所有节点均是对等的,节点之间相互探测存活。如果发现某个节点不可用则立即抢占这个节点下所持有所有的触发器。 |
优点 | 架构简单,对分布式锁依赖较少。 | 节点接近"无状态",可以横向扩展 对触发器数量支持非常多,单个节点不存在瓶颈。 |
缺点 | 如果触发器较多,则 master 会成为瓶颈,尤其是同一时刻触发器触发的任务较多时。 master是单点,所以master故障时间内所有触发器均不可用。 | 节点持有触发器可能会存在不均衡。 节点故障会存在有一部分触发器在一段时间内不可用。 架构复杂,对分布式锁依赖较多。 |
针对分布式方案的缺点:
触发器的不均衡问题,可以通过触发器定时均衡或手动迁移来临时解决。
节点故障导致触发器不可用,可以通过缩短触发器失效时间减少故障时间。
架构复杂,对分布式锁依赖较多,可以通过流程设计,减少对分布式锁的依赖。
为了消除瓶颈,减少单点的影响,故我们采用分布式的方案。
TaskScheduler的Produer模块是定时任务的触发器持有者,需要确保每个触发器的持有是唯一,即每个触发器有且仅有一个TaskScheduler的 Producer 模块持有。由于TaskScheduler是分布式部署,每个节点都是对等的,所以需要设计一套方案确保触发器可以在不同的 TaskScheduler节点之间自动 Failover。
3.7 Producer
定时任务触发器的运作机制严格依赖于Producer的掌控,唯有在其掌控之下,方能启动并管理定时任务。因此,我们致力于将触发器无人持有的时间缩至最短,确保效率与连续性。一旦发现触发器可能处于无主之状,所有Producer需即刻行动,竞相夺取其控制权,以维持任务的顺畅执行。基于这一核心理念,我们特制定以下规则:
持有触发器需要注册分布式锁,只有注册成功的分布式锁才可以持有。
触发器持有的过程中也需要定期更新分布式锁,但是更新故障(分布式锁不可用)不影响持有。
一旦检测到分布式锁已经被其他 Producer 持有时,需要主动放弃触发器。
根据规则,每当Producer启动时,它会自动检索未注册的触发器及其先前持有的触发器列表,随后将这些触发器注册至分布式锁系统。仅当注册操作成功完成,Producer方能合法持有这些触发器。为确保锁的有效性,Producer在持有触发器锁期间,会定期更新其持有时间,以此防止锁因超时而失效。
此外,各Producer还承担着相互监控的责任,通过检测机制确认其他Producer的存活状态。一旦发现某Producer停止运作,系统将迅速接管其持有的触发器,并尝试通过分布式锁抢占注册这些触发器,确保资源不被长期滞留于无法维护的状态。
主动抢占触发器虽然能大大减少无法持有的时间,但同时也会存在一个问题:触发器可能会在短时间内被两个 producer 持有。试想这样一个场景:Producer1 抢占触发器 Trigger 成功,但是在定时更新触发器持有时间时短暂更新失败(网络分区、网络抖动等),那会导致 Producer2 去抢占这个触发器,此时 Producer1 和 Producer2 同时持有Trigger,那样如果此时触发器生成任务时,这两个触发器会同时生成两个任务,任务就会重复,与我们的需求不符。
如何解决这个问题,就需要重新声明一下任务的唯一属性 TaskID 是如何定义的。定时任务 ID 的格式如下:
由图中可以看出定时任务的 TaskID 是由任务类型+任务名称+时间戳,所以持有同一个任务触发器两个 Producer在同一时间生成任务 TaskID 是相同的。所有任务在写入到数据库之前,会检查是否有相同的任务,一旦任务重复则会直接返回错误。故两个持有同一个任务触发器的Producer 在同一时间生成的任务,最多仅有一个可以写入成功,如此以来,这个问题就得以解决。
3.8 锁方案
任务在生成、持有、执行过程中需要使用到很多锁,一个高效的锁方案可以确保任务执行的效率。当前系统需要支持的锁方案有:
局部锁
需要限制在单个程序内部的并发执行。例如在同一时刻,程序只需要启动一个线程获取任务等,可以通过程序内部自带的锁实现。
分布式锁
需要限制在集群内的并发。同一时刻只有集群内的一个节点可以获取到锁,其他节点只能等待。可以通过MySQL、Redis、Zookeeper、etcd等实现分布式锁。
分布式锁+watch
实时持有,锁需要始终持有在一个节点中。针对cron任务,如果锁失效之后需要有其他节点可以立即获取锁,尽可能减少cron任务中断的执行。可以通过zookeeper、etcd或订阅数据变更实现。
3.9 分布式锁的可靠性
分布式锁需要在分布式系统环境下,一个方法或者变量同一时间只能被一个线程操作的特性。我们以基于Zookeeper的分布式锁方案为例,探讨一下分布式锁的可靠性。假如两个客户端A和B 同时需要抢占分布式锁/lock
,则流程如下:
A和 B 都尝试创建
/lock
;假设 A先到达,则加锁成功,B加锁失败;
A 操作共享资源,期间 A 与Zookeeper保持链接;
A操作完成,并删除 /lock 节点,释放锁;
但是可能会存在如下情况:
A 申请加锁 /lock,Zookeeper返回加锁成功。
A发生了长时间的GC,没有发送心跳给Zookeeper。
Zookeeper 发现A长时间没有返回心跳,则以为session过期,将临时节点 /lock 删除。
B发现锁释放于是申请加锁,Zookeeper返回加锁成功。
B操作共享资源。
A gc结束,但是认为自己依然持有锁,在心跳周期内将会操作共享资源,与B发生冲突。
虽然可以通过优化 gc、调整过期时间在一定程度上避免这个问题,但是这种情况无法完全彻底消除。
所以针对依赖分布式锁的接口,我们需要制定如下规则:
分布式锁的过期时间不要太大,尽量避免上述问题。
不要将整个任务执行过程都依赖分布式锁,尤其是针对长时间运行的任务,可以分段依赖。
能用局部锁解决的,不要用分布式锁。
所有任务在执行前都需要做状态、结果校验,如果不符合要求则直接返回。
五、总结
借助抽象的任务架构与接口设计,我们实现了任务执行流程、状态及属性的统一标准化,有效规避了现有任务调度器的局限,确保了各节点在性能、可用性及扩展性方面的高水准。全新的任务调度系统精准满足我们的需求,实现了各类任务的高效、精准调度。
最后最后推荐下两个不错的产品,感兴趣的可以 上车了,这里只吸引同频的人,如果加入几分钟 就直接退出的就不要来了,浪费我的名额。 第一个来自码哥的小报童,仅需 19 元,刚开始更新,需要的可以扫码加入。 本专栏内容涵盖 Java 基础、Java 高级进阶、Redis、MySQL、消息中间件、微服务
架构设计等面试必考点、面试高频点。本专栏不只是单纯教大家学会背八股文知识,
更多是结合实际大厂高并发项目的场景,去学习面试技术要点。从面试官的角度去出
发,介绍互联网 Java 流行技术体系各个面试要点。
本专栏适合于准备进阶 Java 后端技术、有面试或提升技术需求,希望学习互联网大
厂 Java 流行技术体系的程序员和面试官,助你拿到名企高薪 Offer。
第二个是我的知识星球,仅需 189 元,限时特惠 赠送上面小册,只需169元,月底涨价,月底涨价, 月底涨价,需要的可以扫码加入。 关于星球介绍点击: 超 500 万字详解,从零到一带你彻底吃透 Kafka + RocketMQ 小红书实战 需要续费的扫这个,优惠15元 另外必须要注意的是上车的老铁一定要加我微信 好友,拉你们加入星球专属交流群。