hyperf定时任务无法常驻运行的根本原因是crontab-dispatcher进程脱离swoole生命周期管理,持续新建实例却不回收,导致内存缓慢泄漏而被系统kill;应停用@crontab注解,改用beforestart协程定时器,并注意资源释放、超时控制、异步日志及redis分布式锁。

Hyperf定时任务无法常驻运行,根本原因不是“没启动”,而是 crontab-dispatcher 进程脱离 Swoole 生命周期管理,持续新建实例却不回收,最终因内存缓慢泄漏被系统 kill 或主动退出。它不随 worker 重启,也不响应优雅信号,表面看是“进程挂了”,实则是静默崩溃。
确认 crontab-dispatcher 是否真在跑
执行 ps aux | grep crontab-dispatcher,若无输出或 PID 频繁变动,说明它已异常退出。别只看 php bin/hyperf.php start 是否成功——启动时 dispatcher 能起来,但几小时后就可能因内存涨到 800MB+ 被 OOM Killer 杀掉。日志里通常没有报错,只有 worker 进程 RSS 持续上升的痕迹。
停用 @Crontab 注解,改用 beforeStart 协程定时器
这是最直接有效的解法,绕过 dispatcher 进程本身:
- 删掉所有类上的 @Crontab 注解,清空 config/autoload/crontab.php 中的 enable => true(或干脆卸载 hyperf/crontab 组件)
- 在 ServerProvider 的 beforeStart() 方法中,用 Coroutine::create 启动一个独立协程循环
- 每次执行完业务逻辑后调用 Coroutine::sleep(1),确保调度器可切换
- 加兜底判断:while (ServerManager::isRunning()),避免服务停止后协程还活着
防止协程内资源泄漏
即使用了原生协程定时器,若写法不当,照样会内存泄漏:
- 不要在循环里 new 大对象(如 Service 实例、DB 查询构建器),改用 $this->container->get() 每次获取新实例
- 避免静态变量缓存数据,尤其不能存 PDO、Redis 客户端或未关闭的协程 Channel
- DB 操作务必设 query timeout,防止某次慢查询卡住整个协程
- 日志写入用异步方式(如 Hyperf\Logger\LoggerFactory),别用 file_put_contents 同步写
分布式环境必须加锁,但别依赖 singleton
singleton=true 只防单机多 worker 重复,在 Kubernetes 多副本下完全无效。要真正防重复,得用 Redis 分布式锁:
- 在协程循环开头,用 Redis::set($lockKey, $value, ['nx', 'ex' => 30]) 尝试占锁
- 失败则直接 sleep(1) 进入下一轮,不执行业务
- 成功后执行业务,最后再 del 锁;也可用 Hyperf\Contract\LockInterface 简化操作
- 注意锁过期时间要比单次任务执行时间长至少 2 倍,防误释放











