# 任务调度系统 **Repository Path**: brisklan/bs_tasks ## Basic Information - **Project Name**: 任务调度系统 - **Description**: 基于thinkphp8+workerman+redis的任务调度系统 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-28 - **Last Updated**: 2026-06-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 任务调度管理系统 基于 `ThinkPHP8 + Workerman + Redis Stream + MySQL + Vue2(ElementUI)` 的轻量任务调度系统。 适用场景: - HTTP 接口定时调度 - 秒级 / 分钟级 / 每日固定时间 / Cron 表达式任务 - 多 Worker 异步消费 - 执行日志留痕 - 失败恢复、超时恢复、Pending 恢复 - 后台可视化查看任务状态、执行记录、系统状态 ## 技术栈 - ThinkPHP 8 - Workerman - Redis Stream - MySQL 5.7+ - Vue2 + ElementUI ## 核心角色 当前任务系统由 3 个核心进程组成: 1. `Scheduler` 2. `Worker` 3. `Monitor` Linux 下推荐统一使用: - `php think TaskAll start` - `php think TaskAll start d` - `php think TaskAll status` Windows 下由于 Workerman 多进程限制,通常分开启动: - `php think TaskScheduler start` - `php think TaskWorker start` - `php think TaskMonitor start` ## 任务流程总览 ```text 后台新增/修改任务 ↓ TaskService 保存任务 ↓ 计算 next_run_time ↓ 发布 schedule 通知到 Redis ↓ Scheduler 同步任务到调度集合 ↓ 到达执行时间后,写入 Redis Stream ↓ 生成一条 task_execute_log 排队记录 ↓ Worker 消费 Stream 消息 ↓ 发起 HTTP 请求执行任务 ↓ 更新执行日志(success/fail) ↓ 更新任务最近结果、下次执行时间、运行状态 ↓ Monitor 持续巡检异常并恢复 ``` ## 流程图 ### 1. 主流程 ```text ┌──────────────┐ │ 后台任务管理 │ └──────┬───────┘ │ 新增 / 编辑 / 启停 / 删除 v ┌──────────────────────────────┐ │ TaskService │ │ - 保存任务 │ │ - 自动生成 task_code │ │ - 计算 next_run_time │ └──────┬───────────────────────┘ │ publishSchedule v ┌──────────────────────────────┐ │ Redis 调度通知 │ └──────┬───────────────────────┘ v ┌──────────────────────────────┐ │ Scheduler │ │ - rebuildSchedule │ │ - syncTaskToSchedule │ │ - dispatchDueScheduledTasks │ └──────┬───────────────────────┘ │ 到期后 enqueueTask v ┌──────────────────────────────┐ │ Redis Stream │ │ - 任务消息入队 │ └──────┬───────────────────────┘ │ 同时写入 ├──────────────► task_execute_log(status=0 排队中) v ┌──────────────────────────────┐ │ Worker │ │ - 读取 Stream │ │ - 标记执行中 │ │ - 调用 HTTP 任务 │ └──────┬───────────────────────┘ │ 执行完成 ├──────────────► task_execute_log(status=2/3) └──────────────► task 表最近结果 / 下次执行时间 / 运行状态 ``` ### 2. 异常恢复流程 ```text ┌──────────────┐ │ Monitor │ └──────┬───────┘ │ 每 10 秒巡检 v ┌──────────────────────────────────────┐ │ 检查 1:Pending 消息 │ │ - 长时间未确认 ACK │ │ - 重新 claim │ │ - 超过最大重试则标记失败 │ │ - 未超过则重新投递 │ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ 检查 2:执行超时 │ │ - running 日志超时 │ │ - 标记失败 │ │ - 清理 worker_consumer │ │ - 重置任务运行态 │ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ 检查 3:Worker 心跳 │ │ - 心跳过期视为死 Worker │ │ - 清理心跳 │ │ - 回收任务运行状态 │ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ 检查 4:Scheduler 卡住的排队任务 │ │ - queue_status=1 但长时间未运行 │ │ - 自动恢复为可重新调度状态 │ └──────────────────────────────────────┘ ``` ## 各进程职责 ### 1. Scheduler 只负责调度,不负责执行。 主要动作: - 启动时全量重建调度集合 - 接收任务变更通知 - 将启用任务同步到 Redis 调度集合 - 扫描到期任务 - 将到期任务写入 Redis Stream - 预先计算下一次执行时间 - 恢复卡住的排队任务 对应代码: - `app/service/TaskProcessService.php` - `app/service/TaskDispatchService.php` ### 2. Worker 只负责消费和执行,不负责调度。 主要动作: - 从 Redis Stream 消费消息 - 写入消费者名称和开始执行时间 - 更新执行日志状态为“执行中” - 通过 Guzzle 发起 HTTP 请求 - 将结果写回任务表和执行日志 - ACK 并删除已处理消息 - 定时上报心跳 对应代码: - `app/service/TaskProcessService.php` - `app/service/TaskDispatchService.php` - `app/service/TaskService.php` ### 3. Monitor 只负责巡检和恢复。 主要动作: - 恢复 Pending 消息 - 处理执行超时 - 检测死 Worker - 清理异常运行状态 对应代码: - `app/service/TaskProcessService.php` - `app/service/TaskDispatchService.php` ## 数据流说明 ### 1. 任务表 `task` 保存任务定义和任务当前运行态,例如: - 任务名称 - 自动生成的任务编码 `task_code` - 请求地址、请求方式、请求头、请求参数 - 调度规则 - `next_run_time` - `queue_status` - `running_status` - `last_status` - `last_http_code` - `last_error` ### 2. 执行日志表 `task_execute_log` 每次入队或执行都会落一条记录,状态含义: - `0` 排队中 - `1` 执行中 - `2` 成功 - `3` 失败 用途: - 追踪每次执行 - 后台执行记录查看 - 统计成功率、失败率、执行耗时 - 恢复 Pending / Timeout / 死 Worker 时提供依据 ### 3. Redis 当前任务系统主要用到三类 Redis 数据: 1. 调度集合 - 保存任务和到期时间 - Scheduler 用它判断哪些任务到期 2. Redis Stream - 真正的执行队列 - Worker 从这里消费任务 3. Heartbeat - Worker 心跳 - Monitor 和后台统计用它判断 Worker 是否存活 ## 后台状态面板是怎么来的 首页统计面板主要来自以下数据源: - MySQL - 任务总数 - 启用 / 禁用数量 - 今日执行次数 - 今日成功 / 失败 - 当前排队 / 执行中数量 - 平均耗时 - Redis - `ping` - Redis 版本 - Stream / 心跳是否正常 - Worker 运行状态 - `TaskRuntimeStateService` 写入 `runtime` 状态文件 - Redis Heartbeat - 系统进程命令行辅助判断 状态判断逻辑不是只看进程名,而是综合: - runtime 状态文件最近写入时间 - Worker 心跳是否在有效期内 - 当前系统里是否存在对应命令进程 ## 1 秒任务为什么能支持 为了兼容高频任务,当前系统已做两类处理: ### 1. 更高频轮询 通过环境变量控制: ```env TASK_SCHEDULER_POLL_SECONDS=0.2 TASK_WORKER_POLL_SECONDS=0.2 TASK_WORKER_BLOCK_MS=200 ``` 说明: - Scheduler 每 `0.2s` 扫描一次到期任务 - Worker 每 `0.2s` 拉取一次消息 - Stream 阻塞读取时间为 `200ms` - 当前 Worker 仍然是“单进程串行执行” - 真正并发依赖 `TASK_WORKER_COUNT` - 同一任务要支持并发 2 个及以上,必须保证 `TASK_WORKER_COUNT` 也足够 ### 2. 可选允许重叠执行 任务支持 `allow_overlap`: - `0` 不允许重叠执行 - `1` 允许重叠执行 同时支持 `max_concurrency`: - 默认 `1` - 表示同一任务同时允许存在的 `排队中 + 执行中` 总数 - 只有开启“允许重叠执行”后,这个值才会大于 `1` 生效 当任务频率很高,例如 `1 秒一次`,如果上一次还没执行完,且又不允许重叠,就会被拦住。 当开启异步并发后,如果某个任务达到自己的并发上限,系统不会继续为这个任务新增排队日志,而是直接顺延到下一次调度时间,避免执行记录里长期堆积大量 `排队中`。 所以高频任务建议: - 适当缩短任务本身执行时长 - 必要时开启“允许重叠执行” ## 运行链路对应代码 ### 后台任务管理 - `app/admin/view/task/index.html` - `app/result/admin/TaskResult.php` - `app/service/TaskService.php` ### 执行记录 - `app/admin/controller/TaskLog.php` - `app/result/admin/TaskLogResult.php` - `app/admin/view/task_log/index.html` ### 调度与执行 - `app/service/TaskDispatchService.php` - `app/service/TaskProcessService.php` - `app/service/TaskStreamService.php` - `app/service/TaskRuntimeStateService.php` ### 首页统计 - `app/admin/controller/Index.php` - `app/admin/view/index/dashboard.html` ### 命令入口 - `app/command/TaskAll.php` - `app/command/TaskScheduler.php` - `app/command/TaskWorker.php` - `app/command/TaskMonitor.php` ## 一句话理解整个流程 ```text 任务配置保存在 MySQL,Scheduler 负责“什么时候入队”,Redis Stream 负责“排队”,Worker 负责“真正执行”,Monitor 负责“异常恢复”,task_execute_log 负责“每次执行留痕”,后台面板负责“状态可视化”。 ``` ## Linux 资源监控 Linux 下如果使用 `TaskAll` 守护启动,进程名通常会显示为: ```text WorkerMan: master process WorkerMan: worker process ``` 所以不要只用 `grep TaskAll` 判断进程是否存在,建议直接看 Workerman 主进程和子进程。 ### 1. 查看任务进程是否在运行 ```bash ps -ef | grep WorkerMan ps -ef | grep think pgrep -a -f php ``` 如果能看到类似下面这条,说明任务系统已经启动: ```text WorkerMan: master process start_file=/www/wwwroot/your_project/think ``` ### 2. 查看主进程和子进程资源 假设主进程 PID 是 `14273`: ```bash ps -o pid,ppid,user,cmd,%cpu,%mem,rss,vsz -p 14273 --ppid 14273 ``` 示例输出: ```text PID PPID USER CMD %CPU %MEM RSS VSZ 14273 25615 www WorkerMan: master process 0.0 0.9 36548 563592 14283 14273 www WorkerMan: worker process 0.7 0.7 28640 567688 14284 14273 www WorkerMan: worker process 0.5 0.8 31504 568912 14285 14273 www WorkerMan: worker process 0.5 0.8 30896 567944 14286 14273 www WorkerMan: worker process 0.0 0.5 22632 567688 ``` 说明: - 第一行一般是 `master` - 后面几行是 `scheduler / worker / worker / monitor` - `RSS` 是实际占用内存,单位 `KB` - `%CPU` 看 CPU 占用 - `%MEM` 看内存占比 ### 3. 实时刷新查看 ```bash watch -n 1 "ps -o pid,ppid,user,cmd,%cpu,%mem,rss,vsz -p 14273 --ppid 14273" ``` 或者: ```bash top -H -p 14273 ``` ### 4. 查看进程树 ```bash pstree -ap 14273 ``` 可以用来确认当前到底拉起了几个子进程。 ### 5. 如果没有 `pidstat` 有些 CentOS 默认没装: ```bash pidstat -rud -p 1 ``` 如果提示 `command not found`,安装: ```bash yum install -y sysstat ``` ## 资源判断经验 ### 1. 当前这套系统的典型资源结构 通常会有这些进程: - 1 个 `master` - 1 个 `scheduler` - `N` 个 `task worker` - 1 个 `monitor` 例如 `TASK_WORKER_COUNT=2` 时,通常总共 5 个进程。 ### 2. 2核4G 的经验值 根据当前线上实测,单个进程常见内存大概在: - `master`:`30MB ~ 40MB` - `scheduler`:`20MB ~ 35MB` - `worker`:`25MB ~ 35MB / 个` - `monitor`:`20MB ~ 30MB` 所以 `TASK_WORKER_COUNT=2` 时,整套任务系统常见总内存大概: - `120MB ~ 180MB` 这个占用对 `2核4G` 机器来说是比较轻的。 ### 3. 吞吐量粗算 当前系统是“多 Worker 并行 + 单 Worker 串行”模型。 粗算公式: ```text 每秒吞吐 ≈ Worker 数 / 单次平均耗时(秒) ``` 示例: - 平均耗时 `50ms`,`2` 个 Worker - 约 `40 次/秒` - 平均耗时 `100ms`,`2` 个 Worker - 约 `20 次/秒` - 平均耗时 `200ms`,`2` 个 Worker - 约 `10 次/秒` - 平均耗时 `500ms`,`2` 个 Worker - 约 `4 次/秒` - 平均耗时 `1s`,`2` 个 Worker - 约 `2 次/秒` 实际值会比理论值更低,因为还要扣除: - Redis 读写 - MySQL 写日志 - PHP 进程调度 - 目标 HTTP 接口波动 ## 什么时候说明机器扛不住了 出现以下现象时,说明该优化或扩容: - `worker` 进程 `%CPU` 持续偏高 - `worker` 的 `RSS` 持续上涨且不回落 - 后台统计面板里 `排队中` 持续增长 - 后台统计面板里 `执行中` 长时间等于 `TASK_WORKER_COUNT` - 1 秒任务开始明显堆积 - 执行日志里超时和失败开始变多 ## 优化建议 - 先把 `TASK_WORKER_COUNT` 调到 `2` - 如果任务多数是短请求,可以尝试 `3 ~ 4` - 如果任务多数是慢请求,不要盲目加太大,先观察目标接口是否扛得住 - 高频任务尽量缩短接口耗时 - 高频慢任务建议开启 `allow_overlap` - 定期观察执行记录、排队数、平均耗时、失败率