workerman/crontab 必须在 onWorkerStart 中 new 实例且用6位表达式,否则静默失效;耗时任务需拆独立进程,毫秒级调度应改用协程 Timer::tick。

workerman/crontab 不是“写个函数就能跑”的定时器,它依赖进程注册、事件循环和正确的表达式格式。没在 onWorkerStart 里 new 实例,任务根本不会启动;漏写秒位或 reload 没做对,任务就静默失效。
必须在 onWorkerStart 中 new Crontab 实例
所有定时逻辑必须绑定到 Worker 进程的事件循环上,而只有 onWorkerStart 是事件循环就绪后的第一个可靠钩子。其他位置都会失败:
- 类属性初始化(
protected $c = new Crontab(...))→ PHP 解析阶段就报错或被忽略 -
__construct()里 → 此时 Worker 还没启动,事件循环未创建 - 放在
onMessage或自定义方法里且没被调用 → 完全不执行
正确写法只有一种:
public function onWorkerStart($worker)
{
new Crontab('*/5 * * * * *', function () {
echo "每5秒执行\n";
});
}注意:每个 new Crontab() 都是独立实例,可共存于同一进程,但共享同一个事件循环 —— 这就是阻塞根源。
立即学习“PHP免费学习笔记(深入)”;
Crontab 表达式必须是 6 位(含秒),不能省略
Webman 的 workerman/crontab 使用「秒 分 时 日 月 周」6 字段格式,和系统 crontab 不兼容。写成 5 位会触发静默降级:
-
'*/5 * * * *'→ 被解析为「每小时第 5 分钟执行一次」,不是每 5 秒 -
'0 0 2 * * *'✅ 每天凌晨 2:00:00 执行 -
'*/10 * * * * *'✅ 每 10 秒执行一次 -
'* * * * * *'⚠️ 每秒执行,极易打满 CPU,务必压测后使用
表达式错误不会抛异常,也不会打日志,只会让任务不注册 —— 查不到、等不到、也报不了错。
耗时任务必须隔离到独立进程,否则必然阻塞
同一个 Worker 进程内所有 Crontab 回调串行执行。一个 sleep(30) 或慢 SQL 就会让后续任务全部排队延迟:
- 本该每秒执行的心跳检测,实际间隔变成 32 秒、65 秒…
- HTTP 请求响应变慢甚至超时,因为 Worker 进程被占满
- 日志时间戳显示“执行完成”比“开始执行”晚几十秒
解决方式不是优化单个任务,而是拆进程:
// config/process.php
return [
'heartbeat' => ['handler' => app\process\Heartbeat::class],
'report' => ['handler' => app\process\Report::class],
'sync' => ['handler' => app\process\Sync::class],
];每个进程只跑一类任务,互不影响。修改后必须执行 php start.php reload(不是 restart),否则旧进程还在跑老逻辑。
毫秒级调度别用 Crontab,改用协程 Timer::tick
workerman/crontab 最小精度是 1 秒,且内部靠字符串解析 + 对齐逻辑,无法支持毫秒级表达式(如 */500)。要实现 500ms 级别精准调度,必须绕过它:
- 确认已安装
Swoole或Swow扩展(Fiber驱动不推荐) - 在进程配置中显式指定 eventLoop:
'eventLoop' => Workerman\Events\Swoole::class - 在
onWorkerStart中使用:Workerman\Coroutine\Timer::tick(500, fn() => {...}) - 回调内禁用
sleep()、同步 DB 查询、文件读写等阻塞操作
协程 Timer 是底层事件驱动,不受 Crontab 解析开销影响,但代价是失去表达式语义 —— 你得自己算好周期,不能写 @hourly 这种东西。
真正难的从来不是“怎么写”,而是“怎么不让它坏”。表达式少一位、reload 忘了做、耗时任务混进心跳进程、协程里偷偷调了同步 IO —— 这些地方不出问题则已,一出就是线上告警连响半小时。



















