简介:一款基于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_pass@tcp(mysql:3306)/onetool?charset=utf8mb4&parseTime=true" 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_pass@tcp(mysql:3306)/onetool?charset=utf8mb4&parseTime=true" 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_pass@tcp(mysql:3306)/onetool?charset=utf8mb4&parseTime=true" 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 里我一直带着 parseTime=true 和 charset=utf8mb4,少了前者,执行器在把时间字段写回 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"]这段配置里值得注意的参数有三个。第一个是 timezone,cron 表达式里的 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 --tail=50 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 --tail=100 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 的配置里加了一行 TIMEZONE=Asia/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 --tail=100 scheduler # 3. 在日志里看到状态从 running -> timeout -> pending -> running 即通过验证的逻辑是:杀死节点后,调度器感知到任务超时并重新投递,另一个执行器节点把它捡起来继续执行,管理台最终出现 succeeded。如果看到任务一直卡在 timeout 不再投递,说明巡检线程或重试队列配置有问题。这里要提醒的是,节点故障期间会有一段“任务不可用”的空窗期,空窗期长度由 stale_running_after 决定,调短能让故障恢复更快,调长能降低误判概率,生产环境我一般设置在 3 到 5 分钟之间。经过这么一折腾,我后来每次上线任何多节点配置,都会把这个故障演练脚本放进发布检查单。宁可多花十分钟在演练上,也不要等凌晨被调度器的告警拉起来再排错。这套分片加补投的组合,是我从 onetool 拆完以后用得最顺手的部分,希望也能帮你在自己的任务体系里少走两步弯路。
本文还有配套的精品资源,点击获取