Laravel schedule 按秒使用时的 cron 注意事项
我真的花了10个小时才解决这个问题,太折腾了。 先简单说明一下。 有时会像这样把 Laravel schedule 设置为按秒运行。 这时文档里是这样说的 文档要求像这样设置。 但这里有一个很大的问题。 schedule:run 会启动一个进程,然后在这一分钟内每秒重复运行一次进程。 Laravel 的代码就是这么实现
我真的花了10个小时才解决这个问题,太折腾了。
先简单说明一下。
我真的花了10个小时才解决这个问题,太折腾了。 先简单说明一下。 有时会像这样把 Laravel schedule 设置为按秒运行。 这时文档里是这样说的 文档要求像这样设置。 但这里有一个很大的问题。 schedule:run 会启动一个进程,然后在这一分钟内每秒重复运行一次进程。 Laravel 的代码就是这么实现
我真的花了10个小时才解决这个问题,太折腾了。
先简单说明一下。
有时会像这样把 Laravel schedule 设置为按秒运行。
这时文档里是这样说的
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * php /var/www/artisan schedule:run > /tmp/cron.last 2>&1文档要求像这样设置。
但这里有一个很大的问题。
schedule:run 会启动一个进程,然后在这一分钟内每秒重复运行一次进程。
Laravel 的代码就是这么实现的。
一开始我怀疑是 onOneServer 的问题?还是 runInBackground 的问题? 我调试了 command、job 这些方面,但问题并不是出在这里。 于是我检查 PHP 本身有没有运行,结果它根本就没有运行。
所以一开始我在 Alpine Linux 镜像中安装了 dcron 来使用。 我怀疑是不是 dcron 的问题,于是删除了 dcron,试着换成 busybox cron。
但是换成 busybox cron 之后,问题依然没有解决。
于是最后我给 cron 加上了日志记录。
cat cron.log
crond: crond (busybox 1.37.0) started, log level 8
crond: USER cron pid 86 cmd php /var/www/artisan schedule:run > /tmp/cron.last 2>&1
crond: USER cron pid 87 cmd echo "Cron is working: $(date)" >> /tmp/cron_test.log
crond: user cron: process already running: php /var/www/artisan schedule:run > /tmp/cron.last 2>&1
crond: USER cron pid 896 cmd echo "Cron is working: $(date)" >> /tmp/cron_test.log
crond: USER cron pid 904 cmd php /var/www/artisan schedule:run > /tmp/cron.last 2>&1
crond: USER cron pid 905 cmd echo "Cron is working: $(date)" >> /tmp/cron_test.log然后得到了这样的结果。
crond: user cron: process already running
原来是因为进程重复,导致无法运行……
啊……我竟然现在才知道这件事,ㅠㅠ
于是我查了一下,发现
就是通过这样的检查,可以确认如果进程已经在运行,就不会重复启动。 真的……我竟然知道 cron 会抑制重复的 PID 进程。不对?我以前知道,只是忘了吗?ㅡ. ㅡ?
啊……所以我的解决办法是……
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * php /var/www/artisan schedule:run > /tmp/cron.last 2>&1 &在末尾加上 &,改为在后台运行,这样就解决了。
以前我经常在末尾加上 & 来使用,但上次加入按秒运行的调度器时,我看了文档后把 & 去掉了,我觉得问题可能就出在这里。
那么,为什么会发生这个问题呢??? php schedule:run 应该最晚在第59秒结束。
protected function repeatEvents($events)
{
$hasEnteredMaintenanceMode = false;
while (Date::now()->lte($this->startedAt->endOfMinute())) {
foreach ($events as $event) {
if ($this->shouldInterrupt()) {
return;
}
if (! $event->shouldRepeatNow()) {
continue;
}
$hasEnteredMaintenanceMode = $hasEnteredMaintenanceMode || $this->laravel->isDownForMaintenance();
if ($hasEnteredMaintenanceMode && ! $event->runsInMaintenanceMode()) {
continue;
}
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
Sleep::usleep(100000);
}
}但是,即使设置成每2秒运行一次,也不会严格在第2、4、6、8秒运行。
每次读取 command 时都会有一点延迟,所以有时会出现运行时间逐渐错开的情况,比如第2、4、7、9、11、12、14秒。
那么,如果在第59秒运行的 command 无法在1秒内全部完成,
虽然该进程应该在第59秒结束,但由于第59秒启动的命令还没有执行完,
cron 就会出现 crond: user cron: process already running 问题。
所以,调度器运行时当然必须使用 runInBackground()。
如果要使用 schedule:run,就必须在末尾加上 &,让它在后台运行。
重复运行的管理则需要在代码层面控制。
Schedule::command(Test2Command::class)->everyTwoSeconds()->onOneServer()->withoutOverlapping()->runInBackground();也就是使用 withoutOverlapping()。
顺便说一下,无论是全部使用 job,还是把 command dispatch 后交给 job 执行,当 command 达到40个、50个时,都无法保证进程不会超过60秒。
当然,这个问题也可能只是 busybox cron 或 dcron 的问题。 也有可能是我换成 Alpine Linux 镜像后才出现的。
总之,如果要使用 schedule:run 来运行调度器,我建议在后台运行。