尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

分布式调度引擎ax:从选型架构到落地踩坑全解析

分布式调度引擎ax:从选型架构到落地踩坑全解析 说实话这个项目一开始没人看好。内部代号就叫“ax”听起来像个随手拍的文件夹但最后我们把它变成了一个支撑几十条业务线的分布式调度引擎。你问什么是“ax调度”简单说就是系统里所有“定时跑的活”、“依赖上游的批处理”、“需要重试和路由的异步任务”都由这一套引擎统一接管。干这行的都知道调度这种东西看似是外围支撑一旦出问题整个数据链路都跟着抖。这篇就围绕 ax 的选型、架构、落地和踩坑把能说的实操细节全盘托出来。我过去断断续续用过 Quartz、XXL-JOB也接触过一点 Airflow。说实话各有各的好但落到我们这种“既要快速接入又要强管控还不想被中间件绑架”的团队总觉得差一口气。ax 的出现并不是要再造一个全套调度平台而是想做一个足够朴素的“调度内核”对外暴露稳定 API对内收敛任务编排和执行策略把最脏最累的控制逻辑藏在底层。下面的内容主要按五块走为什么用 ax、核心模型怎么设计、关键参数怎么配、实操怎么落地、线上常见故障怎么排查。如果你是维护过任务系统的人应该能从这里翻出不少有用的东西。1. 内容整体设计与思路拆解1.1 这个项目到底解决什么问题在没有统一调度之前各业务线怎么跑定时任务无非是 Linux crontab 一份代码里 Thread.sleep 一份某个运维同学手打的 Jenkins 定时任务再来一份。表面上都能跑实际上谁也说不清今天哪些任务成功、哪些失败、哪个先跑、哪个被跳过。ax 要解决的核心问题就是三件事依赖调度、分布式执行、失败自动衔接。依赖调度是指任务之间可以声明“上游成功之后我才能开始”分布式执行是指任务可以在多个 worker 节点上并行跑不至于单点一挂全部瘫痪失败自动衔接则要求在任务失败时按策略重试、告警或者直接触发下游的补偿逻辑。这一层想清楚之后后面的技术选型就好做了。再也没有“为了用框架而用框架”的折腾所有组件都围着这三个核心需求转。1.2 为什么不直接选现成的调度中间件接触过 XXL-JOB 和 Quartz 的朋友应该会有同感它们把“调度”这件事本身做得很好但“编排”和“结合公司内部基础设施”常常得靠大量二次开发。比如我们需要按业务线隔离权限需要把若干任务组合成有向无环图一键执行需要把执行结果回写到公司内部的数据平台这些在原生功能里并不直接支持。所以我没选“全家桶”而是选了一个足够薄的“调度内核”路线。假设调度体系分三层顶层是业务侧的可视化流程编排中间是调度的核心服务底层是执行任务的 worker 集群。ax 主要承担中间这一层把任务模型、触发条件、锁机制、失败转移这些底层能力做扎实上层允许各自团队接入。这样既保留平台灵活度又不至于让调度逻辑散落到业务项目里怎么改都改不动。1.3 总体架构设计的选择依据ax 的总体架构参考的是“中心调度 无状态执行”的模型。调度端只有一个逻辑上的调度中心它负责解析任务、计算触发时间、派发指令实际干活的是多个 worker。worker 本身不存任务状态全部状态回写存储层这样即使某个 worker 宕机调度中心也能快速把未完成任务重新派发给其他节点。当时也有另一个方案使用分布式队列把任务全部灌入消息中间件再消费。但后来否了原因是很多任务并不适合做成纯消息。部分任务需要固定在某台机器上执行毕竟要读取本机文件或者访问内网 IP部分任务有严格时序不能单纯靠消费速度来保证。ax 的做法是支持“分片执行”和“节点路由”把该并行的并行该钉死的钉死最终才符合业务实际情况。2. 核心细节解析与实操要点2.1 任务的四种基本模型ax 里的任务模型没有搞得很玄归根结底就四种一次性任务只跑一次适合迁移数据、手工触发补偿。定时任务按 cron 表达式周期触发适合常规数据同步。依赖任务等待上游事件或上游任务完成后再执行。长驻任务持续运行的流式处理更像常驻进程。这四种模型覆盖了线上绝大多数场景。但要注意模型不是越复杂越好反而应该让业务方明确自己到底属于哪一类。如果一个任务又定时又依赖又长驻建议拆成多个任务再编排否则排查问题的时候没人能定位是它没触发还是触发后没结果。2.2 调度时间计算背后的坑定时任务的触发时间计算看起来就是一个 Cron 表达式解析实际上有点隐蔽问题。最典型的是“上一个任务执行太久影响了下一个触发点”。有点像你定好了每天早上八点跑步但昨天熬夜今天起晚了那今天的跑步是取消、顺延、还是立刻补跑ax 默认的策略是“跳过”也就是当前触发时间到达后如果上一个执行还没结束本轮不再触发而是等下一轮。这里有一个我强烈建议调整的配置misfire 策略。有些系统默认会立刻补跑错过的任务这就容易造成雪崩。比如一个任务凌晨三点因为数据库缓慢没跑完三点零五分还没释放结果系统认为它错过了三点这轮立刻补跑两个实例同时操作同一张表锁冲突就来了。正确做法是先把 misfire 固定为“不补跑”然后再根据业务是否允许延后执行来判断是否单独补录。2.3 调度器时钟同步的细节调度中心负责算时间worker 负责干活这中间有个假设是时钟是可信的。可现实中虚拟机经常发生时间漂移我曾经见过一个节点时钟慢了 3 分钟导致它执行的任务全部比预期晚 3 分钟。数据同步晚三分钟对某些场景来说完全不可接受。排查方式很简单在调度中心心跳包中附带时间戳worker 每次上报心跳时和本地时间做差值。如果差值超过阈值就直接标记节点异常不再派发新任务。这个机制听起来轻微但实际省了很多“任务为什么延迟”的排查时间。你不需要人工逐个检查机器时间调度平台自己在后台就挡掉一批问题。3. 实操过程与核心环节实现3.1 环境搭建与目录规划ax 落地不需要很重的外部依赖最少只需要一台调度中心节点和一台 worker 节点。但建议无论多小的集群都预留独立的配置目录目录里分三块conf存放调度中心与 worker 的配置store放本地磁盘缓存与临时文件logs滚动执行日志。分开存放不是洁癖而是方便之后排查磁盘占用和日志路径问题。调度中心配置里最核心的一项是数据库连接串。ax 把任务定义、触发记录、运行日志都持久化到 MySQL所以这条连接串的质量直接决定系统稳定性。我的建议是不要用默认超时时间显式配置连接超时和 socket 超时否则数据库做一次主从切换调度中心半天没感知整个任务派发就卡住了。3.2 接入第一个定时任务的完整步骤下面我以“每小时同步一次订单表”为例说明在 ax 里注册一个任务需要做什么。假设你已经部署好调度中心和 worker并且配置了任务对应的执行器。第一步在调度中心后台新增执行器。一个执行器可以理解为一组任务的归属分组名称建议与业务模块保持一致比如order-sync-worker。这里有个细节注册方式选择“自动注册”还是“手动录入”。如果你在容器环境里经常扩缩容建议选择自动注册让 worker 启动时自动上报地址。如果 worker 在固定的内网物理机上手动录入反而更稳妥避免临时 IP 被安排在漂移之后注册到错误节点。第二步新增任务并填写处理参数。关键参数包括cron 表达式、运行模式单节点还是分片、阻塞处理策略、失败重试次数、超时时间。对订单同步这个场景运行模式选分片利用多台 worker 各自同步一部分数据超时时间我设置为 120 秒超过后调度中心直接强制中断。第三步编写执行器代码。ax 暴露的执行器接口很简单核心方法是接收一个包含任务 ID、分片序号、业务参数的 ExecuteContext然后返回执行状态。有一点必须提醒执行器里不做复杂的本地线程管理。以前有同事喜欢在任务里自己创建线程池结果 worker 节点一重启线程池里的任务全部变成孤儿。所有异步逻辑要么交给 ax 的分布式执行要么等任务结束后外部系统统一收口不要图一时爽快埋雷。3.3 分片参数的计算方式“分片参数”听起来高大上本质上就是把一个大批量任务切碎。假设订单同步任务要处理 100 万条数据现在有 4 台 worker 节点那么理想的分片策略是按主键 ID 区间分段。ax 支持给每个分片传入shardIndex和shardSize业务侧拿到这两个参数后自行整除即可。例如本次分片大小为 4第 2 片处理的数据范围是2 * (100万 / 4)到3 * (100万 / 4)。但要注意如果数据量不是均匀分布的这种简单平均并不合理。有个很快的优化技巧业务侧先通过数据库查询出一个最小 ID 和最大 ID再根据实际数据条数做动态区间切分让每个分片条数接近均匀。通过在分片逻辑里增加一次COUNT(*)查询哪怕多花几十毫秒也能避免某个分片执行 5 秒、另一个分片执行 5 分钟的极端情况。3.4 阻塞策略的取舍阻塞处理策略是 ax 里一个容易被忽略但必须提前决定的参数。什么叫阻塞就是当前任务还没跑完新的触发时间又到了系统应该怎么办。ax 提供三种基本策略丢弃新触发、立即执行、单机串行。我的习惯是普通任务统一用“丢新触发”。从业务上看上一个周期还在处理中说明数据还没消化完再来新周期意义不大。对于必须周期连续、不能漏跑的指标统计任务则单独设为“单机串行”宁可时间拉长也不能中间断档。最不建议的是“立即执行”这等于把并发压力直接放大到数据库或下游接口上多数线上事故都是从这里开始的。4. 常见问题与排查技巧实录4.1 任务堆积怎么排查线上最恐慌的场景之一就是任务堆积。比如同步任务每小时跑一次结果某次数据源卡了 40 分钟所有 worker 都在等结果后续任务全堆积。第一件事不是补跑而是查“是不是同一把锁卡住了所有并发”。ax 默认用数据库乐观锁确保同一个任务同一个时间窗口只有一个调度线程。如果锁记录没有释放后面的触发全部失败。这时先查调度记录里任务状态是不是一直是“运行中”如果是而对应 worker 已经没了心跳那就需要手动把执行状态置为失败让锁释放。第二件事是看堆积任务的“消费速率”。有时根本原因是某条 SQL 查询特别慢我遇到过上游接口单条耗时 3 秒、一次性请求 5000 条的情况。排查出接口超时后最直接的办法是把任务改成批量拉取而不是让调度平台去调高并发。4.2 重复执行和分布式锁失效都说调度平台要保证任务不重复执行但实际很难。一是在网络不稳定时调度中心发送执行指令后没收到 worker 的回执便重发指令worker 实际执行了两遍。二是数据库锁在极端情况下因连接超时提前释放。ax 的解法是两层保障。第一层是任务执行前检查执行 ID 是否已存在这里可以用 Redis 幂等标记。第二层是业务侧去重尤其是写操作尽量使用数据库唯一索引而不是先查后插。很多团队指望调度平台做到“绝不重复”这是把责任放错了地方。平台只能尽量降低重复概率真正的防线还是业务系统自己处理好幂等。4.3 调度记录与日志不同步的毛病有段时间我们发现任务重试了好几次但 Java 日志里一点内容都没有。后来一查是 worker 端日志滚动配置把 info 级别日志写到了不同目录而调度记录里只保留了 “ERROR 时打印的错误堆栈”的字段造成平台显示失败日志里却找不到报错的假象。建议从一开始就把结构化 log 接入任务上下文把 taskId、shardIndex、attempt 序号都打进日志。这样后续无论查询 ELK 还是翻本地文件都能按任务 ID 串联起来。另一个技巧是采取“双写策略”调度记录只存摘要信息详情日志以文件或日志流方式保留避免把大量运行内容塞进数据库。4.4 线上真实案例复盘这里分享一个印象非常深的问题。某个业务方在 ax 上配置了一个每小时执行的数据修复任务正常情况一秒跑完。后来业务量涨了修复时间变成五秒。他们自己把任务 cron 从每小时改成每五分钟但没考虑阻塞策略结果前一轮还没释放新一轮触发直接失败失败又触发重试重试再次产生并发最终数据库连接被打满。复盘下来的根子在于业务方只考虑了任务执行频率没有理解调度系统的资源隔离机制。最后我们专门做了一个限制逻辑单个执行器组的并发线程数不能超过配置上限一旦达到上限后续任务进入等待队列而不是无限重试。这在 ax 里实现起来并不复杂但价值很大。后来我再给别人设计任务调度方案都会问一句你允许这个任务在同一时间的最大并行数是多少如果答不上来那调度策略一定有问题。5. 一些操作习惯与心得根据我碰过这么多调度系统的经验有几个根深蒂固的准则可以说一下。第一个准则是“调度平台只负责触发不负责业务重试逻辑的完整补偿”。失败重试可以做但一定要限制次数和总耗时。比如某个接口调对方服务对方已经处理成功但是响应超时重试又调一次就会产生重复数据。这种补偿诉求不能全部交给调度平台应该在业务代码里判断查询结果是否已经有数据。第二个准则是“任何任务定义都要像代码一样走评审”。任务的 cron、超时、重试次数、执行队列这些看起来是配置其实是生产逻辑。我见过有人把最终生产环境的 cron 表达式错误复制成测试环境的因为账号权限控制不到位导致生产凌晨执行了全量数据清理。现在团队里调度配置改动必须提交工单还要关联变更说明。第三个建议是定期做一次调度演练。比如手动把某个 worker 节点停掉观察任务是否自动转移把数据库连接池调小看看任务失败是否会在预期时间内触发告警。演练的目的不是证明平台多稳定而是让值班同学熟悉最快恢复的路径。最后分享一个小技巧对于执行频率较高的短任务不要每次都走数据库记录完整日志先缓存到本地文件异步批量上报。否则调度中心在高频任务下磁盘 IO 和数据库压力都很大原本用来提升效率的调度器反而成了新的性能瓶颈。ax 这套体系从最开始无人关注到后来成为业务侧默认的调度入口靠的不是炫技而是一点点把底层控制力做扎实。希望对正在摸索任务调度的你有参考价值。
返回列表