资讯详情

资讯详情

分布式任务调度平台AX调度架构设计与实践:从Cron到高可用编排

干调度这行的人多半都经历过被Cron支配的恐惧。业务一复杂单机定时任务根本撑不住凌晨三点的大批量数据同步总是超时日志满天飞却不知道哪个节点执行了哪个任务扩个机器还得手动改配置更别提那玄学一样的重复执行——明明只跑一次一检查数据库多了两倍数据。所以当团队开始自研分布式任务调度平台的时候我其实是举双手赞成的。项目代号就叫AX调度听起来简单做起来一点都不简单。从最开始的统一任务注册到后续的弹性伸缩、失败重试、幂等控制每一步都是踩坑踩出来的经验。这篇文章就把我们在AX调度上的整体设计思路、核心实现细节和实操过程完整梳理一遍写给正准备搞调度平台、或者正在被海量定时任务折磨的兄弟们。你能看到架构选型背后的取舍也能直接照抄我们验证过的部署和接入方案省掉自己摸索的时间。1. 项目背景与定位我们为什么需要AX调度1.1 单机Cron与多机部署之间的矛盾绝大多数系统一开始都是单体应用定时任务用Spring的Scheduled或者Linux的Crontab做就够了。但业务量一旦上来单机调度的问题会像连环雷一样爆开单点故障没有容灾。机器宕机所有任务集体罢工凌晨没人发现等到早上业务反馈才追悔莫及。任务执行时间不可控。大任务和定时任务挤在同一台机器上CPU一竞争执行时长远超预期后面的任务全部积压。无法横向扩展。多台服务器都部署同一套代码如果用Crontab每台机器都会执行一次任务重复触发严重如果只部署在一台机器那这台机器的负载就被单独打满。缺少可视化运维。任务执行成功还是失败没有聚合日志和监控全靠群里问一句“今天任务跑了没”。AX调度的初衷就是把“定时触发”这个能力从业务代码里抽离出来做成独立的调度中心。业务只负责实现执行逻辑调度中心统一管理触发时间、执行记录、失败重试和负载分配让任务从“能不能跑”升级成“一直能跑”。1.2 AX调度的设计目标和适用场景AX调度项目启动时我们在立项文档里写清楚了四个核心目标平台化所有定时任务集中登记、集中启停、集中监控不再散落在各个服务代码里。高可用调度中心支持多节点部署单节点挂了不影响整体触发。弹性扩缩容执行器节点可以随时加减调度中心自动识别新节点并分配任务不用改任何配置。任务编排除了简单的定时触发还要支持依赖触发、分片任务和手动补偿。这套东西适用性其实很广。最常见的场景是数据同步每天凌晨从业务库拉取增量数据到数仓几十张表如果串行跑时间根本不够用分片任务一张表一个分片多台执行器并行处理时间能压缩十分之一。其次是报表生成、缓存刷新、对账清算以及各类状态机驱动的异步任务。凡是“到了某个时间点就该干什么活”的业务逻辑都适合交给AX调度。2. 架构设计与调度引擎的核心思路2.1 调度器、执行器与注册中心的三方协作AX调度的整体架构并不复杂核心就是三个角色加一个数据库。**调度器AX Server**扮演大脑角色负责解析任务定义、计算触发时间、派发任务实例。部署上采用多节点无状态设计几台机器都连同一个数据库通过数据库锁机制保证同一个任务同一时刻只有一个节点负责触发。**执行器AX Worker**是真正干活的角色嵌入在业务应用里以SDK方式运行。执行器启动后向调度器注册自身节点信息并开启HTTP长轮询接口等待调度器派发的执行指令。**注册中心Registry**负责节点发现记录所有在线执行器的地址和分组。执行器每隔一段时间发送心跳超时未心跳的节点会被自动移除。三者之间没有采用RabbitMQ或Kafka这类消息队列做任务分发原因是我们希望任务派发具备更强的可控性队列只能保证“消息能发出去”做不到“节点再负载均衡后精准投递”。AX调度把分发给一台还是多台执行器这件事完全由调度器根据一致性哈希和权重计算决定异常时可以立即重新派发不需要经过消息中间件这层间接跳板。数据库层面我们用了MySQL主要存储任务定义、调度记录、执行日志和注册节点信息。虽然大家对“调度中心用数据库”有性能疑虑但实际场景下定时的频率远低于高并发MQ消息数据库能很好地承担调度状态管理。触发频率高的秒级任务我们会单独优化避免秒级扫描任务对数据库造成太大压力。2.2 时间轮触发引擎与Cron表达式解析调度器内部没有用最简单粗暴的“每秒全表扫描”那样到几千个任务时SQL查询会拖垮数据库。AX调度采用的是基于时间轮的触发引擎。具体说任务注册时会把下一次触发时间换算成时间戳以秒为单位放入一个环形数组槽位中每个槽位对应这一秒内需要触发的任务集合。调度线程每推进一秒只处理当前槽位的任务逐个派发不关心其他任务。这样触发的复杂度从O(N)降到了O(1)无论任务总量是多少单次触发检查都是秒级完成的。Cron表达式解析藏在任务定义里。标准的七段式Cron秒、分、时、日、月、周我们完整支持内部实现是Lex分析加递归适配生成的表达式树。举个例子一个常见的凌晨2点半执行任务0 30 2 * * ?含义是每天2点30分0秒触发。配置这个Cron的时候我们遇到过特别多“日和周冲突”的场景比如用户希望每月10号和每周五都执行但Cron规范里日和周只要有一个为?另一个才生效。这块在控制台表单上特意加了冲突校验提示避免用户配置完后发现任务在这个月一次都没跑。触发之后调度器会生成一条任务实例记录状态标记为待执行然后把指令推送给选定的执行器。执行器收到指令后执行并回传结果调度器更新任务实例状态写执行日志。这一个闭环是整个AX调度的最小单元。2.3 高可用、失败重试与幂等控制的三重保障高可用这块调度器多节点部署只是第一步。真正麻烦的是多节点之间如何避免同一个任务被重复触发。我们的方案是“任务注册表锁”。节点启动后会尝试获取任务表上的分布式锁获取成功的节点成为主调度器负责所有任务的触发。其他节点作为影子节点只提供控制台服务和健康检查接口一旦主节点失联剩余节点重新参与选举新的主节点接管全部任务。这个机制朴实但有效。实测下来主节点宕机后从感知到故障到新主节点开始派发任务最长延迟不超过10秒。对于大多数定时任务来说10秒的容错时间可以接受。失败重试的设计是每类任务可独立配置。遇到网络抖动或下游服务临时不可用任务按指数退避策略自动重试默认最多重试3次间隔分别为30秒、60秒和120秒。但重试带来的最大隐患是重复执行比如任务执行成功但网络回传超时调度器以为失败了又重试一遍就会产生脏数据。所以AX调度要求所有任务回调里必须实现幂等逻辑。我们在控制台任务定义里增加了一个幂等字段支持三种策略幂等策略实现方式适用场景业务唯一键业务代码中检查唯一索引或状态位对账、同步类任务执行记录去重任务实例ID传入业务逻辑重复执行时直接跳过数据抽取类任务版本号乐观锁更新数据前比对版本号状态更新类任务这个设计让“补偿式重试”变得安全。后续做数据对账时即使任务重试3次最终数据依然只有一份不会再出现重复写入的问题。3. 落地实操从部署到跑通第一个定时任务3.1 快速部署一套单机版AX调度中心先声明一下生产环境至少要部署两个调度器节点这里用单机版是为了快速验证功能流程。部署之前的依赖很干净只需要JDK 8 和MySQL 5.7几台服务器或容器都行。第一步初始化数据库。调度器安装包里的ax-server.sql脚本包含了任务表、执行记录表、锁表和执行器节点注册表直接执行mysql -u root -p ax-server.sql脚本执行完数据库里会出现ax_job、ax_instance、ax_lock、ax_worker_node这四张核心表。先别急着忽略这些表结构后面排查调度问题全靠它们。第二步配置调度器连接信息。默认配置文件放在application.yml里主要改数据库地址和服务端口server: port: 8899 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/ax_scheduler?useSSLfalsecharacterEncodingutf8 username: root password: yourpassword ax: server: # 当前节点对外暴露的注册地址集群部署时填本机IP registry-address: 127.0.0.1:8899 schedule: # 线程池大小影响同时触发的任务数 trigger-pool-size: 16第三步启动调度中心nohup java -jar ax-server.jar --spring.config.locationapplication.yml server.log 21 启动完成后访问http://127.0.0.1:8899控制台默认管理员账号密码是admin / admin123登录后第一时间改掉。控制台首页能看到注册节点数、任务总数和今日触发次数这些基础指标能让你在任务出问题的第一时间发现异常而不是等业务方来找你。3.2 注册执行器并接入一个定时任务调度中心只负责触发真正执行任务需要业务应用接入AX Worker。以最常见的Spring Boot项目为例。第一步在pom.xml中引入SDK依赖dependency groupIdcom.axschedule/groupId artifactIdax-worker-spring-boot-starter/artifactId version1.2.1/version /dependency第二步在application.yml里配置执行器信息ax: worker: app-name: order-sync-service # 分组名调度任务按这个分组派发 group: order-group # 调度器注册中心地址多个逗号分隔 server-addresses: 127.0.0.1:8899 # 执行器端口接收调度指令用的轻量HTTP服务 port: 9091执行器启动后会占用配置的9091端口用于接收调度指令。这个端口在防火墙里要放行否则调度器能派发却无法下发指令任务会一直卡在待执行状态。第三步写一个简单的任务处理器。实现AxJobHandler接口加上AxJob注解AxJob(name orderStatTask, cron 0 0 2 * * ?, desc 每日订单统计) public class OrderStatTaskHandler implements AxJobHandler { Resource private OrderStatService orderStatService; Override public void execute(AxJobContext context) throws Exception { // 任务实例ID用于幂等判断 Long instanceId context.getInstanceId(); // 分片序号和总分片数分片任务使用 Integer shardIndex context.getShardIndex(); Integer shardTotal context.getShardTotal(); // 业务逻辑只统计当前分片范围内的订单 orderStatService.statByRange(shardIndex, shardTotal); // 主动上报进度 context.setProgress(100); } }这里有个细节容易忽略AxJob注解里的cron参数用户既可以通过代码里写死也可以在控制台动态覆盖。我们的经验是生产环境尽量不要在代码里写死Cron。调度策略属于配置变更应该由运维在控制台上调整避免发版重启才能改执行时间的尴尬局面。好在这个注解的Cron值会被控制台配置覆盖一段业务逻辑可以灵活绑定多个触发时间。第四步启动业务应用。启动日志里看到“AX Worker registered successfully”就说明已经注册到调度中心了。控制台的执行器管理页面能看到order-sync-service这个节点状态为在线。到这一步一个最简单的定时任务闭环已经跑通了。在控制台的任务管理里可以手动触发一次查看执行日志。第一次跑通的时候我们踩过坑执行日志里一直提示“找不到可用的执行器节点”自查半天才发现是业务应用启动时注册中心还没完全初始化多等了几秒重新注册就好了。3.3 控制台操作与监控告警的实用配置控制台除了CRUD还有很多容易被忽略但关键时刻能救命的功能。手动触发和现时触发很有用。发布新任务时不用等Cron时间到直接手点一下“执行一次”立刻验证业务逻辑对不对。我们团队的习惯是新任务上线手动触发一次查看日志没问题再启用定时调度。日志查看功能内置了全链路追踪。每次任务实例生成一个唯一的InstanceId日志采集的起点是调度器生成实例终点是执行器返回结果。执行器端业务输出的日志也会通过SDK的Appender自动收集到调度中心。排查任务问题不用再登录服务器看文件控制台直接按实例ID搜索日志效率提升了非常多。监控告警推荐配置两条规则任务失败告警和调度超时告警。失败告警在任务连续失败2次时触发通过钉钉Webhook推送超时告警是针对某些执行时间特别长的任务设置一个阈值如300秒超出就告警。别想着把告警配得很密集告警疲劳比不告警更可怕——真正问题来的时候运维反而不看了。4. 常见问题与排查技巧实录4.1 任务重复执行数据库唯一键和幂等策略的双保险AX调度上线后遇到最典型的线上事故是一个库存回写任务重复执行导致商品库存多扣了一次。事件回顾任务执行成功写库但在向调度器回传结果时网络超时调度器触发重试重试任务再次执行了扣减逻辑库存于是少了一倍。这个问题的根源是回传超时无法区分“执行失败”和“执行成功但消息丢失”。单纯依靠调度中心去优化不可能完全避免所有边界情况。最终的防线只能落到执行器业务代码上。我们的解决方案是双保险。首先存储层数据库表的库存变动流水增加唯一索引字段组合是“任务实例ID加业务单据ID”重复插入直接报错业务捕获到唯一键冲突就跳过本次操作。其次在业务逻辑里加一个状态位判断订单已处理过的直接返回成功不再处理。分布式锁在某些特定场景下会有释放不及时问题数据库唯一键反而是最牢靠的兜底机制。这套组合方案上线后重试导致的脏数据基本清零。所以如果你在接入AX调度第一件事最好检查一下你的任务表有没有天然的唯一键。没有的唯一键的业务建议先加一个task_run_record表来承接幂等判断成本不高但能挡住绝大多数“重试爆炸”事故。4.2 调度延迟和任务堆积线程池参数调优某个报表任务Cron配的是每5分钟一次理论上一天跑288次。结果有一天运维发现凌晨2点到3点的数据统计延迟了近40分钟任务积压了几百次执行记录。排查过程分三步。第一步看执行记录表发现大量任务实例状态是“已触发但执行中”很显然是执行器忙不过来。第二步看执行器日志发现线程池队列一直在堆积任务触发速度大于消费速度。第三步看数据库慢查询日志发现统计逻辑里几条SQL没有索引单次查询耗时从200ms涨到了2秒撑爆了执行线程池。根因是统计SQL性能退化但暴露的是调度系统对于慢任务缺乏保护机制。AX调度在任务定义里可以配置“阻塞策略”当上一个任务还没跑完时新触发的任务可以配置为丢弃、覆盖或并行执行。推荐配置“覆盖”这样上一轮卡住的任务会被标记已取消新任务直接执行避免不可控积压。执行器端的线程池参数我们也做了调整。默认核心线程数只有4对于报表类任务明显不够。经验值是核心线程数设置为集群所有执行器节点的CPU总数的一半。如果有4台4核执行器核心线程数设为8是合理的最大线程数可以适当放宽但队列长度不要设太长设置10就够否则内存中的等待任务会成为新的风险点。4.3 扩容后任务分配不均一致性哈希带来的分片倾斜某天数据同步集群从4台扩容到8台本以为任务处理速度会翻倍结果观察了一个小时发现4台老节点CPU跑到80%4台新节点CPU只有20%。这个现象非常典型原因就藏在AX调度的节点选择策略里。AX调度默认用一致性哈希把任务分配给执行器目标是保持任务在节点上的分布稳定。但一致性哈希自带虚拟节点机制新增节点后只有少部分任务的哈希分布会发生迁移大量旧任务依然牢牢固定在了老节点上。这个特性用在缓存数据分布上是优势用在任务负载均衡上就是劣势。解决办法是把分片任务的分配策略改成“加权轮询”模式。AX调度控制台在分片任务的高级配置里提供了load-balancer参数默认是consistent-hash改成round-robin后新任务会在所有在线节点之间轮流分配扩容节点的利用率很快就上来了。其实这里也体现了分片设计的重要性。前期架构上我们把大任务设计成可拆分的分片任务——一个总任务拆成10个分片均匀分给10台执行器。相比单机执行整张表的数据处理分片后总耗时会从几小时缩短到几十分钟。分片数建议设为执行器节点数的2到3倍预留一定的增量弹性防止节点宕机后分片迁移导致其他节点瞬间过载。5. 扩展实践从定时任务到工作流编排5.1 DAG任务编排解决跨任务依赖问题AX调度发展到中后期单纯“到了时间就执行”的模式已经不能满足业务需求。很多流程是A任务跑完才能跑BB成功后再并行跑C和D最后汇总到E。如果全用定时触发你没法确定A到底几点能跑完后置任务的Cron时间只能往后预估预估短了任务堆积预估长了整个流程被无谓拖慢。我们在AX调度的基础上扩展了DAG工作流编排能力。用户可以在控制台上用图形化方式拖拽任务节点建立依赖关系调度引擎按照拓扑顺序依次触发前序任务未完成时后续任务处于等待状态。最初实现时依赖判断的实时性是个难题。毫秒级驱动DAG流转需要的成本和复杂度太高我们采用的是时间窗口扫描每10秒扫描一次等待状态的任务检查其依赖任务是否全部成功满足条件的进入待触发队列。绝大多数业务场景对链路延迟的容忍度都在分钟级每10秒扫描绰绰有余。这个扩展极大拓展了AX调度的适用范围。比如数据同步链路拉取binlog - 清洗转换 - 落仓分区 - 触发下游报表刷新四个节点在DAG里串成一条线彻底告别了手动设置“凌晨1点同步凌晨2点必须清洗完成”这种拍脑袋的时序控制。5.2 按执行时间动态分片实时数据吞吐量翻倍另一个值得一提的实践是按执行时间动态分片。普通的静态分片比如固定分4片数据量小的时候浪费资源数据量大的时候单分片处理时间依然很长。AX调度支持执行器在任务初始化阶段计算总数据量然后根据分片参数动态决定拆分数量。以订单增量同步为例任务启动时先执行一条轻量查询“统计这10分钟产生了多少条新增订单”然后根据数据量动态决定分片数int count orderMapper.countIncrement(lastTime); // 每万条数据分配一个分片最多不超过16个分片 int shardCount Math.min(16, Math.max(1, count / 10000)); context.setShardCount(shardCount);动态分片带来的效果立竿见影。数据量少的凌晨时段可能只拆2个分片执行器不空转白天数据高峰自动扩展到16个分片并行处理同步耗时从原来的45分钟压到了8分钟内。实时数据管道吞吐量翻了一倍不止。这里有个注意点拆分的分片数决定了最终并行度分片太多会产生频繁的小事务反而拖慢性能。按照实际CPU核数和表结构合理上限最好在配置中心做成可调节参数。写在最后的几点体会AX调度从立项到稳定运行我感触最深的不是技术多复杂而是调度系统对细节的敏感度远超普通业务系统。一个时间轮槽位快照的拷贝时机、一个执行结果回传的补偿机制、一个分布式锁的续租间隔每一个看起来不起眼的点都可能在大流量面前变成事故导火索。另外自研调度平台最大的隐性成本不在开发而在长期维护。每次业务方提需求比如“这个任务能不能支持指定日期执行”“能不能按业务维度暂停”你都得评估调度引擎是否支持。所以如果团队规模不大、时间不充裕建议先认真评估开源方案自研更适用于有强定制需求且人力充足的团队。我们当初选择AX调度核心就是为了把任务编排和触发机制牢牢握在自己手里这个决策本身没有对错关键在于你是否准备好了持续投入精力去打磨它。最后分享一个小经验无论用哪套调度系统上线前一定要做混沌演练。把调度器节点直接kill掉看任务能不能自动恢复把执行器网络切断再恢复看任务重试有没有产生脏数据。我们就是通过这种“故意搞破坏”的方式提前暴露了很多边缘问题让AX调度在真正面对生产故障时表现得很稳。调度系统是业务的底层底座底座不稳上面盖的楼再漂亮也白搭。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →