timer触发延迟是系统调度导致的固有特性,因事件循环受cpu负载、进程抢占等影响,首次注册需等待循环就绪,高频定时器应改用c扩展或swoole,多进程需redis分布式锁防重复。

Timer触发时间比设定值晚,是系统调度导致的
Workerman的Timer::add()底层依赖事件循环(如libevent、ev或event扩展),而事件循环本身受操作系统调度影响。当CPU负载高、进程被抢占、或内核中断延迟时,定时器回调不会“准时”进入执行队列——它只能在下一次事件循环tick中被检查并触发。实测中,在CentOS上可能做到±1ms偏差,Ubuntu上常见±4ms,这并非Bug,而是用户态定时器的固有特性。
onWorkerStart里注册的定时器,为什么偶尔第一次就延迟?
常见错误是在onWorkerStart里直接Timer::add(1, $cb),期望1秒后立刻执行。但此时Worker刚启动,事件循环尚未完全就绪,首次tick可能滞后。更稳妥的做法是加一层“等待循环就绪”的判断:
$worker->onWorkerStart = function ($worker) {
// 确保事件循环已启动后再注册
Timer::add(0.01, function () use ($worker) {
Timer::add(1, function () {
echo "真正开始计时\n";
});
});
};
- 避免用
0秒作为首次延迟(某些版本会忽略) - 首次延迟设为
0.01(10ms)比1更可靠 - 不要依赖“绝对准时”,而应关注“相对稳定间隔”
高频定时器(如10ms级)误差放大,怎么压低抖动?
低于50ms的间隔在普通Linux服务器上本身就接近极限。误差来源不只是调度,还包括PHP自身GC暂停、内存分配、甚至echo等I/O操作阻塞当前tick。若业务真需要高精度(比如RTP包发送),必须换方案:
- 改用
pcntl_alarm()+ 信号处理(仅限CLI,需自行管理上下文) - 把定时逻辑下沉到C扩展或Swoole的
swoole_timer_tick()(精度更高) - 用Redis的
ZSET + BRPOPLPUSH做外部延迟队列,牺牲毫秒级精度换取稳定性 - 接受误差,改用滑动窗口校准:记录每次实际触发时间戳,动态调整下次间隔补偿偏差
跨进程部署时,多个Worker都跑同一个定时器,结果重复执行
Workerman多进程模式下,每个Worker子进程都会独立运行自己的Timer::add(),没有跨进程协调。比如你写了个“每分钟清理过期订单”的定时器,开了4个Worker,就会每分钟执行4次。
- 必须加分布式锁:在
onWorkerStart里用Redis::set($key, $value, ['nx', 'ex' => 60])抢锁,成功才注册定时器 - 锁失效时间要略大于定时器间隔(如间隔60秒,锁设65秒)
- 别依赖
Timer::del()自动清理锁——进程意外退出时锁残留,要用带租约的方案
真正难缠的不是“不准”,而是“不准却没报错”:它不抛异常、不打警告,只悄悄让任务慢半拍,查日志也看不出问题。上线前务必在压测环境用microtime(true)打点记录实际触发时间,画出偏差分布图。











