资讯动态

onetool十二合一云任务平台源码拆解:调度、状态机与多节点落地

发布时间:2026/10/27 22:40:13 来源:尧图企业网站定制
简介一款基于PHP8.0构建的十二合一云任务/多任务挂机平台源码面向站长和有一定PHP基础的开发者用于将网易云音乐、哔哩哔哩、贴吧签到、视频解析、王者战力查询等多平台操作集中到统一后台省去逐一登录不同平台的时间成本方便自动化挂机与日常管理。资源包共3个文件包含zip源码、txt搭建教程和html说明页整体约30.26MB精简实用下载后按文档即可部署。已有377人学习/下载。平台还整合了Epic商城、爱奇艺、米游社、QQ及收款码合一等模块二改版本在去授权基础上修复了视频解析新增王者战力查询并美化了首页、登录页与用户页UI同时提供PHP8.0环境下的ThinkPHP伪静态与运行目录设置说明适合需要快速搭建多任务管理后台的个人或团队直接参考和二次开发。1. 云任务平台不是定时器onetool 十二合一到底解决了什么问题如果你还在用“一台服务器一堆 crontab 抽空翻一眼日志”的方式维护任务大概率会在某个凌晨被一条“刚才那批数据全没落库”的消息拉起来。生产环境里的任务不是孤立的定时抓数的、凌晨做报表的、轮询第三方接口的、消息积压时补推的每一样都要跑挂一个就牵动全局。onetool 十二合一云任务平台多任务挂机平台解决的就是这一层不把任务当“定时器”而是当成一套状态完整的业务系统来管统一调度、统一重试、统一告警。拆这份源码后我的感受是它适合运维、后端开发和同时管着多条业务线任务的团队。真正的难度不在任务本身而在于把调度骨架搭对、把状态流转做稳这两点想清楚了后面接什么业务都顺。2. 把“十二合一”拆成最小可用模型调度器、执行器与任务状态流转2.1 十二类任务别背功能记住三个承载角色先看一张功能映射表。这份资源号称“十二合一”但它十二在哪里、源码里各功能放在什么位置必须一开始就对清楚否则后面调参数时你会找不到地方改。任务分类对应执行器模块典型使用场景定时 HTTP/资源抓取http_fetcher定时拉取第三方接口或静态文件数据库迁移与数据回补db_syncer从生产库抽取数据进分析库第三方 API 限频转慢速api_proxy上游接口限频把大量任务排队慢速推送消息推送pusher钉钉、企微、邮件等告警通知文件上传下载ftp_syncer报表上传、对账单定时下载日志扫描聚合log_analyzer定时扫描业务日志中的异常关键字SQL 例行运维sql_runner分区归档、临时表清理服务健康检查health_checker按固定间隔探活业务节点爬虫任务启停控制spider_controller定时触发爬虫并维持运行状态失败数据重放replay_executor上游故障恢复后重新消费积压数据Mock 数据生成data_mocker压测或联调环境的数据填充延迟消费delay_executor消息延迟处理、订单超时未支付关闭这张表要这么读十二不是十二个独立工程。我拆源码的时候发现目录里确实是 executor_http_fetcher.py、executor_db_syncer.py 这一路小模块但它们共享的是同一套底座——任务入库调度器按时把任务从 pending 切出来执行器节点取走跑完再写回结果。变化只发生在“末尾执行”那几行代码上。换句话说定时抓取和 SQL 例行运维走的是同一条调度链路只是执行器不同真正撑住“挂机平台”这四个字的是前面那套统一调度、断点续跑和状态回写机制而不是某个 py 文件写得有多炫。这也是判断一份同类资源值不值得用的标准如果项目里 12 个功能各写各的入口没有统一任务表那它只是 12 个脚本的合集扩到 20 个任务时调度会乱成一团。onetool 这套的好处是新加一类任务只需要新写一个执行器然后登记到执行器注册表里不用动调度核心。资源里自带的插件发现机制一般有两种一种是扫描固定目录下的 executor_*.py 文件自动加载另一种是读配置文件里的执行器清单。我建议用扫描目录的方式新增任务类型时少改一处配置出问题的概率也更低。2.2 任务描述模型优先级、去重键与重试参数在哪里一个任务在 onetool 里不是简单一行 cron 表达式而是一个结构化对象。我一般把它理解成调度器只看头部字段执行器只看 payload 字段。先看一个最简的 HTTP 抓取任务定义{ task_id: fetch-product-20260212-01, task_type: http_fetcher, schedule_type: interval, interval: 3600, payload: { url: https://api.example.com/products, method: GET, timeout: 15, headers: {User-Agent: onetool/1.0} }, priority: 5, dedup_key: fetch-product-20260212, max_retry: 3, backoff: 30, tags: [product, import] }逐个字段说task_id 是全局唯一标识必须稳定同一个任务每次调度生成时都不能变否则重试会被当成新任务task_type 决定消息分发到哪个执行器schedule_type 支持 one_shot、interval、cron 三种interval 只需要写秒数cron 则走 crontab 语法payload 是执行器专用的参数区调度器不解析执行器自己取priority 数值越小越优先任务队列拥塞时优先级差别的效果很明显dedup_key 是去重键同一窗口内生成过的任务如果 dedup_key 相同调度器会直接丢弃max_retry 和 backoff 决定失败后的重试次数和间隔单位秒tags 字段则用于后续多节点部署时的路由——带同样 tag 的任务会被调度到同一个执行器分组里。这里最容易被忽略的是 task_id 和 dedup_key 的区别。我见过有人把当前时间戳拼进 task_id结果每次重试都生成一个新 id旧任务永远处于失败状态新任务又被反复创建数据库里堆了几万条脏任务。正确做法是task_id 按业务维度生成比如 fetch-product-{date}-{page}让它天然幂等dedup_key 用来在更短的窗口内挡重复比如同一批数据在 5 分钟内只允许下发一次。两者配合才能做到重试不重、补投不漏。流的理解就是“task_id 管身份dedup_key 管窗口”。2.3 状态机与两次投递窗口pending → running → succeeded任务在 onetool 里的生命周期不是“跑完就没了”而是有一张状态表跟着。源码里任务状态大体是这样流转的状态含义写入方pending已入库等待调度调度器创建任务时dispatching已从队列取出等待认领分发线程running执行器已认领并开始执行执行器回调succeeded执行完成且结果校验通过执行器回调failed执行异常进入重试等待执行器回调skipped去重命中或依赖条件不满足调度器timeout执行超时被强制回收调度巡检线程这套状态机不是摆设。它解决了“这个任务到底跑没跑”的问题管理台看到的每个任务都有明确状态失败的任务会进入重试队列而不是静默消失调度巡检线程每隔几十秒扫一次 running 状态的任务如果发现某条记录超过执行超时上限还没有回调就把它强制标记为 timeout然后把重试次数退一。这个动作在源码里叫“悬挂任务回收”也是后面第四章要展开的重点坑位之一。有一点值得提前说任务从 pending 变成 running 的瞬间存在一个“两次投递窗口”。调度器把任务发给执行器的网络请求成功后如果执行器在回写 succeeded 之前宕机任务会变回超时状态等待另一节点再次领取。这不是 bug是 at-least-once 语义的必然结果要做的是在业务侧接受重复执行用 dedup_key 或业务主键把后一次执行变成空操作。想追求 exactly-once 就得在 payload 里带上事务边界让执行器自己保证幂等这是平台帮不了的部分。任务状态流转中间还有一个容易被忽略的细节重试次数用完之后的最终态。默认情况下max_retry 用完后任务会进入 failed 终态管理台只标记失败不再自动投递。但如果打开 dead_letter 配置任务会进入失败桶由运维手动确认后重新投递。生产环境我建议打开这个开关并且接一个告警重试次数用尽算高危事件应该让值班人员看到而不是躺在一个日志页面里。后面第五章验证多节点分片时会再用到 failed 终态来判断补投是否正确。3. 落地部署让 onetool 在十分钟内跑起来3.1 用 Docker Compose 拉起调度与依赖部署这套平台不复杂难点在依赖关系的梳理。它一共四个部分MySQL 存任务定义和状态Redis 存队列和分布式锁scheduler 负责任务计算和下发executor 节点真正执行业务逻辑admin 是管理台和 API。下面是一个能直接用的 compose 文件骨架version: 3.8 services: mysql: image: mysql:8.0 container_name: onetool-mysql environment: MYSQL_ROOT_PASSWORD: onetool_pass MYSQL_DATABASE: onetool volumes: - ./data/mysql:/var/lib/mysql ports: - 3306:3306 networks: [onetool-net] redis: image: redis:7-alpine container_name: onetool-redis command: redis-server --appendonly yes ports: - 6379:6379 networks: [onetool-net] scheduler: build: ./scheduler container_name: onetool-scheduler depends_on: - mysql - redis environment: DB_DSN: root:onetool_passtcp(mysql:3306)/onetool?charsetutf8mb4parseTimetrue REDIS_ADDR: redis:6379 NODE_ROLE: scheduler TIMEZONE: Asia/Shanghai volumes: - ./config:/app/config networks: [onetool-net] executor: build: ./executor container_name: onetool-executor depends_on: - mysql - redis environment: DB_DSN: root:onetool_passtcp(mysql:3306)/onetool?charsetutf8mb4parseTimetrue REDIS_ADDR: redis:6379 NODE_ROLE: executor NODE_ID: executor-1 TIMEZONE: Asia/Shanghai volumes: - ./config:/app/config - ./data/workspace:/data/workspace networks: [onetool-net] admin: build: ./admin container_name: onetool-admin depends_on: - mysql - redis ports: - 8080:8080 environment: DB_DSN: root:onetool_passtcp(mysql:3306)/onetool?charsetutf8mb4parseTimetrue REDIS_ADDR: redis:6379 networks: [onetool-net] networks: onetool-net: driver: bridge参数说明mysql 和 redis 是调度器和执行器的公共状态层scheduler 负责扫描任务表并生成待执行任务不直接跑业务逻辑所以它不需要挂载工作目录executor 是真正跑脚本的地方必须挂载一个 workspace 目录让脚本、临时文件、产物都有地方放admin 是管理台它既是 Web 界面也是 API 入口创建任务、查看状态、手动触发都走这个服务。这里有两个容易踩的细节。第一个executor 的 NODE_ID 不能重复如果起了多个 executor 节点每个节点的 NODE_ID 必须唯一否则节点心跳互相覆盖调度器会把两个节点当成同一个任务认领时就会出现乱序。第二个DB_DSN 里我一直带着 parseTimetrue 和 charsetutf8mb4少了前者执行器在把时间字段写回 MySQL 时会报驱动错误少了后者任务内容里带 emoji 或其他多字节字符会直接写入失败。这些都属于“配好了就不吭声、配错了卡半天”的问题。3.2 最小配置一个 cron 抓取任务的完整写法服务起来了下一步是建一个任务。管理台或 API 创建任务时本质上写入的是一条任务配置。以每天凌晨 3 点抓取前一天的销售汇总为例最小配置是这样task_id: sales-report-daily task_type: http_fetcher schedule_type: cron cron_expr: 0 3 * * * timezone: Asia/Shanghai priority: 6 max_retry: 3 backoff: 60 payload: url: https://api.example.com/report/daily?date method: GET timeout: 120 headers: Authorization: Bearer ${ONETOOL_TOKEN} output: save_to: /data/workspace/sales/reports format: csv notify: on_failed: true channels: [dingtalk, email]这段配置里值得注意的参数有三个。第一个是 timezonecron 表达式里的 0 3 * * * 默认按调度器的系统时区解析如果调度器容器是 UTC而业务方要的是北京时间就必须显式声明 timezone否则任务会在北京时间早上 7 点跑第一轮业务方以为是凌晨 3 点的数据抓到的是 7 点的快照数据口径直接错位。第二个是 output.save_to这个路径要落在 executor 挂载的 workspace 下而不是容器内临时目录一旦容器被重建容器内文件会全部丢失。第三个是 notify建议把 on_failed 打开并至少配一个渠道这样任务失败是主动通知到你而不是等业务方发现数据不对再来追查。payload 里我刻意放了 ${ONETOOL_TOKEN} 这种写法。这套平台支持环境变量插值敏感信息不要硬编码在任务配置里而是从 executor 的环境变量里读。一方面避免配置库泄露另一方面同一套配置在不同环境之间迁移时只需要改环境变量不需要改任务本体。我一般会在 executor 的 compose 里挂一个 .env 文件把这类敏感变量全部收进去配置文件里只留引用。3.3 启动自检新部署后先看这三处刚把容器拉起来的时候别急着创建一堆任务先做三轮自检每次都看一个明确迹象# 1. 检查容器之间依赖是否起来 docker compose ps docker compose logs --tail50 scheduler executor admin # 2. 确认调度器能正常连接依赖 docker compose exec scheduler ./onetoolctl health docker compose exec redis redis-cli ping # 3. 手动投递一个 once 任务验证全链路 docker compose exec scheduler ./onetoolctl trigger --task-type once --payload {url:http://127.0.0.1:8080/health} docker compose logs --tail100 executor代码逻辑不复杂第一步看所有容器是不是 running 状态重点看 executor 是否反复重启反复重启通常是 workspace 目录权限或 DB_DSN 配错第二步是确认依赖连接onetoolctl health 会一次性检查 MySQL、Redis、心跳线程和调度器之间的连接池如果这里有一项红后面所有任务都会卡在 pending第三步是手动投递一个一次性的任务这里用 /health 接口当目标是为了验证“创建、取走、执行、回写”这一整条链路是否通如果日志里出现 executor received → executor done → task succeeded说明平台本身是健康的。三步走完再导入真实任务后面就很少有“所有任务都不跑”这种全局性翻车。4. 避坑上线三个月遇到的五个典型故障排查记录下面这几条不是从文档里抄来的“最佳实践”是我实际操作里真踩过的坑。每条按现象、原因、解决的顺序写照这个顺序排错能省大半天的日志翻找时间。4.1 定时任务到点没跑管理台却显示一切正常现象在管理台建了一个每天 2 点的 SQL 归档任务第二天去看任务表里最后一条成功记录还是前天管理台状态却显示“正常”没有失败日志也没有告警。原因任务根本没被调度器“算”出来。排查发现 scheduler 里配置的 cron 解析器默认使用 UTC 时间管理台写时区只对展示层生效调度线程直接拿着配置里的 cron_expr 用系统时区计算下一次触发时间两者差了 8 个小时2 点的任务实际上是北京时间 10 点才触发而业务方已经上班把问题暴露了。解决统一“调度计算时区”。我在 scheduler 的配置里加了一行 TIMEZONEAsia/Shanghai并且在数据库连接串里带上 loc 参数同时把所有历史任务配置补上 timezone 字段。从那之后我再也没有遇到过“配置看着对就是不到点”的怪事。这类问题最坑的地方在于它不报错日志里一切正常只有把调度器计算出来的 next_run_time 打印出来才能发现偏差。所以我在管理台的任务详情页里加了一列 next_run_time每天抽查几个比单纯看状态列可靠得多。4.2 两个节点“同时”执行了同一个任务数据重复写入现象扩展了第二个 executor 节点后发现每天同一时刻有两个节点同时在跑同一条数据同步任务目标表里出现了整段重复数据。两个节点日志里的 task_id 一模一样时间戳相差不到一秒。原因默认的认领逻辑是“谁先抢到算谁的”执行器节点拉起时向 Redis 注册一个锁拿到锁的节点才允许从任务表里取待执行任务。问题在于调度器把任务从 pending 改成 running 的操作和 Redis 抢锁操作不是原子的。节点 A 抢到锁并开始执行但状态还没回写节点 B 的认领线程读任务表时发现还是 pending于是也正常地把它领走了。解决给认领过程加上原子操作。源码里那段认领逻辑我改成了用 Lua 脚本把“检查状态 置为 running”做成一个原子操作脚本大致如下if redis.call(SETNX, KEYS[1], ARGV[1]) 1 then redis.call(EXPIRE, KEYS[1], ARGV[2]) return 1 else return 0 end// 认领任务时先抢锁再转状态两个动作要保证原子性 lock : redis.SetNX(ctx, task:lock:taskId, nodeID, 30*time.Second) if !lock { // 被另一个节点认领直接跳过 return nil } if err : repo.Transition(ctx, taskId, pending, running); err ! nil { // 状态流转失败释放锁并等待下一轮 redis.Del(ctx, task:lock:taskId) return err }参数说明SETNX 的 key 用 task:lock:{task_id} 来做value 写当前节点 ID过期时间给 30 秒防止执行器异常退出时锁永远不释放。加了这套逻辑后“同时执行”的问题没有再出现过。但这里我要多说一句重复执行的代价在多数场景下是数据重复写入最彻底的兜底是给目标表的关联字段加唯一索引即使真的撞了任务第二次执行也会被数据库挡在唯一约束上而不是产生脏数据。分布式锁能降低撞车概率唯一索引才能兜住最后一公里。4.3 任务卡在“运行中”执行器的日志却无声无息现象管理台上一个跑批任务已经显示 running 五个小时但去执行器节点上看日志最新一行停留在刚开始的“开始处理第 1 批”CPU 占用也低。任务就像是挂起了一样。原因任务进程还在但内部线程被阻塞了。最常见的两个诱因调用第三方接口没设超时时间连接一直挂着或者日志聚合任务里正则表达式写出了一个灾难性回溯CPU 被卡住但进程不退出。调度器巡检线程虽然会把超过执行超时的任务强制标记为 timeout但如果执行器的超时配置没写就会一直等下去。解决我做了三层措施。第一每个 task 的 payload 里必须写 timeout 字段调度器以此为准做超时回收第二执行器内部给业务代码加 context 传播所有 HTTP 调用走 context.WithTimeout防止第三方接口拖死整个任务第三把日志扫描类的 executor 加上正则超时保护Go 的 regexp 自带超时Python 的 regex 库没有原生支持得自己在执行线程外加信号量控制。执行器侧的配置大致是这样executor: default_timeout: 300 watchdog_interval: 10 scheduler: stale_running_after: 3600这里最关键的一点是不要把“进程还在”当成“任务在正常跑”进程在但执行线程冻住本质上是同一类事故。我们后来在 executor 里加了一个看门狗协程每 10 秒检查一次当前任务的心跳如果连续三次没有输出新的进度日志就直接执行杀线程并回写 failed。从那以后这种“僵尸 running”的任务最多存活 30 秒就会被回收。4.4 定时抓取任务每次都触发全量更新跑得越来越慢现象一个每天晚上同步商品数据的任务数据量从 10 万涨到 100 万之后单次执行从 15 分钟涨到 2 小时还出现了部分批次超时。原因payload 里根本没有增量条件。http_fetcher 的 URL 写死了一个不带参数的地址服务端全量返回执行器全量覆盖随着数据增长越跑越慢。这算平台 bug 吗不算是逻辑设计埋在 payload 里的隐患。平台只是忠实地执行了任务定义问题出在定义本身。解决把增量字段显式写进 payload让执行器从公共参数里拼带 since 的请求。我把任务配置改成了{ url: https://api.example.com/products?since{last_success_ts}, incremental_field: updated_at, state_store: redis, state_key: task:state:product-sync:last_success_ts }逻辑说明执行器拿到任务后先读 state_key 对应的上次成功时间拼到 URL 的 since 参数里服务端只返回增量数据执行成功后把本次窗口的最大时间戳写回 state_key。改动不大效果是运行时间从 2 小时降到了 20 分钟。这里我养成了一个习惯凡是带 http_fetcher 的任务建任务之前先问一句“数据量会一直涨吗能不能按时间增量”如果答案是能第一时间就把增量参数写进 payload绝不等到跑不动了再补。4.5 新任务全部在队列里排队执行器节点却“装死”现象管理台上新建的几十个任务全部堆在 pending执行器日志也没有报错CPU 和内存都很健康但就是不去领任何新任务。原因执行器的心跳线程挂了。平台要求执行器每 15 秒向调度器汇报一次心跳调度器只给心跳正常的节点下发任务。某个执行器节点之前出过一次网络闪断心跳线程重连失败后没有重启保护导致这个节点在调度器侧被标记为离线任务自然就不会派给它。从执行器本地看进程还在、日志不报错没有一个人会觉得它瘫痪了。解决给心跳加看门狗。在进程级别做两件事一是心跳线程本身要捕获所有异常并做指数退避重连二是启动一个后台协程监视最近一次心跳上报时间超过 45 秒没有成功上报就整进程退出由 Docker 或 Supervisor 自动拉起。自愈的效果在无人值守的任务平台里很关键。另外一个辅助手段是管理台里加节点列表页直接展示每个节点的 last_heartbeat 时间排查时先用 Redis 看一眼心跳是否有更新# 查看节点心跳 redis-cli -h 127.0.0.1 -p 6379 keys node:heartbeat:* redis-cli -h 127.0.0.1 -p 6379 get node:heartbeat:executor-1如果 get 出来的时间戳停留在几分钟前不再变化基本可以断定这个节点已经和调度器失联。心跳是执行器最容易被忽略的命门节点再健康心跳断了就等于从集群里被摘除所有任务都会堆在 pending 里干等。5. 进阶把任务从单节点扩展到多节点——如何做到不重、不漏5.1 一致性哈希分片让同一个任务永远落在同一个节点执行器从 1 个扩到 4 个之后第一个要回答的问题是某条业务数据到底由谁来跑如果每次调度都把任务随机发给一个节点本地缓存、临时文件和数据库连接池都会被打散还会出现第四章那种双节点同时执行的情况。平台源码里默认提供的路由策略是拿任务上的 tags 或 payload 里的业务键做一致性哈希把相同业务键的任务路由到同一个节点。相比“节点数取模”的简单做法一致性哈希在节点增减时只会影响哈希环上很小一段区间不会让全量任务重新洗牌。核心参数在路由配置里route: strategy: consistent_hash hash_key: ${payload.shop_id} ring_size: 1024 replicas: 128参数说明hash_key 决定同一业务聚集到同一节点比如某个店铺的所有同步任务都会路由到同一个 executor该节点的本地缓存能持续命中ring_size 是哈希环总槽位数replicas 是每个物理节点在环上拥有的虚拟节点数节点少时调大 replicas 能让分布更均匀。如果任务天然有强隔离要求比如某大客户的同步任务必须独占一个节点也可以把 strategy 换成固定绑定在节点配置里写死支持的 tag调度器只把对应 tag 的任务发到这个节点。5.2 节点故障后的补投策略与验证步骤一致性哈希只解决了“不重”还要解决“不漏”。节点故障时分配出去的任务会处于 running 或已认领状态如果没有超时回收机制这条任务就丢了。平台里的调度巡检会周期性扫 running 任务超过 stale_running_after 阈值还没收到完成回调就强制放回 pending 并重试。所以节点闪断后任务不会消失只会等待超时后换个节点再跑一次。这也是 at-least-once 语义在平台侧的体现。上线验证这套机制我一般走这样一个流程# 1. 在管理台创建一个每 5 分钟执行一次的探活任务 # 2. 手动把 executor-1 容器 stop观察任务是否在 timeout 后重新被其他节点领取 docker stop onetool-executor-1 docker compose logs --tail100 scheduler # 3. 在日志里看到状态从 running - timeout - pending - running 即通过验证的逻辑是杀死节点后调度器感知到任务超时并重新投递另一个执行器节点把它捡起来继续执行管理台最终出现 succeeded。如果看到任务一直卡在 timeout 不再投递说明巡检线程或重试队列配置有问题。这里要提醒的是节点故障期间会有一段“任务不可用”的空窗期空窗期长度由 stale_running_after 决定调短能让故障恢复更快调长能降低误判概率生产环境我一般设置在 3 到 5 分钟之间。经过这么一折腾我后来每次上线任何多节点配置都会把这个故障演练脚本放进发布检查单。宁可多花十分钟在演练上也不要等凌晨被调度器的告警拉起来再排错。这套分片加补投的组合是我从 onetool 拆完以后用得最顺手的部分希望也能帮你在自己的任务体系里少走两步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑