首页
/ Coolify 的 Laravel 任务调度最佳实践:withoutOverlapping、onOneServer 与 Schedule Groups 深度解析

Coolify 的 Laravel 任务调度最佳实践:withoutOverlapping、onOneServer 与 Schedule Groups 深度解析

2026-09-04 16:56:35作者:何将鹤

Coolify 是一个可自托管的 PaaS(私有云应用平台),其服务端管理逻辑完全构建在 Laravel 之上,大量后台工作——SSL 证书续期、服务器健康检查、孤儿容器清理、版本更新检查、用户级备份与定时任务——都依赖 Laravel 的任务调度器(Scheduler)驱动。本文以仓库内置的 Laravel 调度最佳实践规则 scheduling.md 为主体,逐条讲解六项核心调度规则的设计动机,并结合 Console Kernel、生产环境 s6 调度服务调度缓存去重助手 的真实源码,展示这些规则在 Coolify 中的落地方式,读完即可掌握编写生产级 Laravel 定时任务的完整方法论。

一、背景:Coolify 的调度器是如何跑起来的

在展开六条规则之前,先明确适用前提:Laravel 调度器必须有一个进程每分钟触发一次 schedule:run。Coolify 采用 schedule:work 常驻模式,由 s6-overlay 进程守护:

所有调度条目集中声明在 app/Console/Kernel.phpschedule() 方法中。值得注意的是,Coolify 用 isDev() 将调度表分成两套:开发环境跑更高频的任务(如每分钟执行 CleanupInstanceStuffsJob、每十分钟执行 CheckHelperImageJob),生产环境则降频(每两分钟、每五分钟),避免自托管用户的生产服务器承担不必要的开销。这种"按环境裁剪任务频率"的思路,本身就是下一节 environments() 规则的延伸用法。

二、规则一:变长任务必须加 withoutOverlapping()

原文档第一条规则指出:没有 withoutOverlapping(),一个长时间运行的任务会在下一个触发点再被拉起第二个实例,导致重复处理或资源耗尽。

Coolify 的调度表中有直接实证:

$this->scheduleInstance->command('cleanup:stucked-resources')
    ->daily()
    ->onOneServer()
    ->withoutOverlapping(60);

(见 app/Console/Kernel.php#L49-L52

cleanup:stucked-resources 负责扫描并修复卡死的资源部署状态,其执行时长取决于数据库里有多少"卡住"的记录,属于典型的变长任务。->withoutOverlapping(60) 的参数表示锁的过期时间为 60 秒:一旦上一轮执行在 60 秒内未结束,新一轮触发时会自动跳过。

更深一层的教训来自 app/Jobs/ScheduledJobManager.php#L52-L92。Coolify 的用户级定时任务管理器使用了队列层的 WithoutOverlapping 中间件:

return [
    (new WithoutOverlapping('scheduled-job-manager'))
        ->expireAfter(90)   // 高负载环境下锁 90 秒过期
        ->dontRelease(),    // 锁冲突时不重新入队
];

源码注释明确记录了生产中遇到的真实坑:升级、Redis 重启等边界情况可能留下 TTL 为 -1 的"僵尸锁",永久阻塞所有调度执行。为此专门实现了 clearStaleLockIfPresent() 自愈逻辑——每次任务派发前检查锁的 TTL,若为 -1 则主动删除并写告警日志。结论:withoutOverlapping 类机制必须配套考虑锁的过期与清理策略,否则它自己会成为新的单点故障。

三、规则二:多服务器部署必须加 onOneServer(),且依赖共享缓存驱动

原文档第二条规则:多服务器部署下不加 onOneServer(),每台服务器会同时执行同一任务;该机制要求配置共享缓存驱动(Redis、database、Memcached)

这条规则在 Coolify 中是默认姿势。浏览 app/Console/Kernel.php 可以看到,几乎所有会产生副作用的任务都挂上了 ->onOneServer()

任务 频率 出处
sanctum:prune-expired --hours=1 每小时 Kernel.php#L53
ApiTokenExpirationWarningJob 每小时 Kernel.php#L54
ServerManagerJob(服务器连接/状态管理) 每分钟 Kernel.php#L82
ScheduledJobManager(备份与定时任务派发) 每分钟 Kernel.php#L87
RegenerateSslCertJob(SSL 证书再生成) 每天两次 Kernel.php#L89
CheckTraefikVersionJob 每周日 00:00 Kernel.php#L91
CleanupOrphanedPreviewContainersJob 每天 Kernel.php#L97

这些任务如果每台服务器各跑一遍,后果从重复发通知到重复续证书不等。而 onOneServer() 的锁正是通过共享缓存实现的——Coolify 的默认缓存驱动就是 Redis,见 config/cache.php#L18'default' => env('CACHE_DRIVER', 'redis')。这恰好满足文档所述的前提约束;如果换成 filearray 驱动,onOneServer() 会退化为每台机器各自独立锁,形同虚设。

四、规则三:并发长任务使用 runInBackground()

原文档第三条规则:默认情况下,同一触发点(tick)的任务串行执行,一个慢任务会阻塞其后所有任务;runInBackground() 让每个任务作为独立进程并发运行。

这条规则在 Coolify 的 Kernel 中没有直接调用(当前调度表里的任务大多是"快速派发"型——真正耗时的工作被投递为队列任务,例如 ServerManagerJob 每分钟只负责派发和状态检查)。但从源码结构看,这正是该规则成立的原因:Coolify 把重活从调度器里剥离到 Horizon 队列(config/horizon.phpapp/Console/Kernel.php 中 dev 环境每分钟执行 horizon:snapshot),从而避免了 tick 串行阻塞问题。可以推断,当你在自己的调度表里同时挂了多个分钟级长命令时,->runInBackground() 就是恢复并发性的标准手段。

五、规则四:用 environments() 限制任务运行环境

原文档第四条规则:防止生产专用任务(计费、报表)在 staging 上被误触发,并给出示例:

Schedule::command('billing:charge')->monthly()->environments(['production']);

Coolify 处理环境差异的方式略有不同:它用 isDev() 条件分支直接拆分两套任务清单(app/Console/Kernel.php#L56-L98),例如 horizon:snapshot 开发环境每分钟、生产环境每五分钟。两种写法各有取舍——isDev() 分支能裁剪频率和任务集合,而 environments() 声明式地限制"任务只属于哪些环境",对 billing 这类绝对不能跨环境的任务更稳妥。此外,Coolify 还展示了第三种条件控制手法:->when() 闭包按运行时配置动态启停任务:

$this->scheduleInstance->call(fn () => app(CleanupStaleMultiplexedConnections::class)->handle())
    ->name('cleanup:ssh-mux')
    ->hourly()
    ->when(fn () => config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop'));

(见 app/Console/Kernel.php#L44-L47)——只有启用了 SSH 连接复用且不在 Windows Docker Desktop 上时才清理 SSH 复用连接,体现了"条件化调度"的第三种形态。

六、规则五:用 takeUntilTimeout() 约束无界处理时间

原文档第五条规则:一个每 15 分钟跑一次、处理无界游标(unbounded cursor)的任务,可能和下一次运行重叠;应当用 takeUntilTimeout() 给执行时间设上限。

这条规则对应的是"批处理 + 时间窗口"场景。Coolify 里与之精神一致的实现出现在 bootstrap/helpers/shared.php#L986-L1007shouldRunCronNow() 助手:用户自定义的备份/任务频率由数据库中的 cron 表达式驱动,派发判定采用"上次派发时间 vs 上一个到期时间点"的去重策略(getPreviousRunDate() + last-dispatch tracking),源码注释说明其设计目标是"对队列延迟保持弹性——即使任务晚了几分钟执行,仍能补上错过的 cron 窗口",去重键写入缓存后设置 30 天静态 TTL(2592000 秒)以便孤儿键自动清理。这展示了生产级调度中"时间约束"的另一个维度:不仅要限制单次执行时长,还要保证频率判定在时钟漂移和队列积压下依然正确。

七、规则六:用 Schedule Groups 共享重复配置

原文档第六条规则:避免在多个任务上重复 ->onOneServer()->timezone('America/New_York'),给出示例:

Schedule::daily()
    ->onOneServer()
    ->timezone('America/New_York')
    ->group(function () {
        Schedule::command('emails:send --force');
        Schedule::command('emails:prune');
    });

Coolify 的 Kernel 没有直接用 group(),但用"私有方法抽取共享调度参数"实现了同样的目的,且参数全部来自运行时配置:

$this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
if (validate_timezone($this->instanceTimezone) === false) {
    $this->instanceTimezone = config('app.timezone');
}

private function scheduleUpdates(): void
{
    $this->scheduleInstance->job(new CheckForUpdatesJob)
        ->cron($this->updateCheckFrequency)
        ->timezone($this->instanceTimezone)
        ->onOneServer();

    if ($this->settings->is_auto_update_enabled) {
        $autoUpdateFrequency = $this->settings->auto_update_frequency;
        $this->scheduleInstance->job(new UpdateCoolifyJob)
            ->cron($autoUpdateFrequency)
            ->timezone($this->instanceTimezone)
            ->onOneServer();
    }
}

(见 app/Console/Kernel.php#L38-L42app/Console/Kernel.php#L109-L123

这里 PullTemplatesFromCDNPullChangelogCheckHelperImageJobCheckForUpdatesJobUpdateCoolifyJob 五个任务共享同一个 cron 频率与时区,频率值来自实例设置(update_check_frequency 缺省为 '0 * * * *',见 app/Console/Kernel.php#L36),并在 app/Livewire/Settings/Updates.php 提供 UI 配置与 validate_cron_expression() 校验——空值会回退到 '0 0 * * *'(自动更新)或 '0 * * * *'(更新检查)。这是比 group() 更进一步的做法:共享的不只是静态链式配置,还包括用户可改的运行时频率

八、调度表全景:一条生产级 schedule() 应该长什么样

综合原文档六条规则与 Coolify 的实际代码,可以提炼出可直接套用的调度清单(均出自 app/Console/Kernel.php):

// 通用:按条件启停(对应 rules: environments/when 条件化)
->call(fn () => ...)->name('cleanup:ssh-mux')->hourly()->when(fn () => config('constants.ssh.mux_enabled'));

// 变长任务:锁 + 多节点去重组合(规则一 + 规则二)
->command('cleanup:stucked-resources')->daily()->onOneServer()->withoutOverlapping(60);

// 频率与时区全部来自实例设置(规则六的运行时版本)
->job(new PullChangelog)->cron($frequency)->timezone($tz)->onOneServer();

// 固定时间点任务(规则六:共享配置)
->job(new CheckTraefikVersionJob)->weekly()->sundays()->at('00:00')->timezone($tz)->onOneServer();

// 环境频率裁剪:dev 每 1 分钟,prod 每 2 分钟
if (isDev()) { ->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); }
else         { ->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); }

几点适用前提与限制需要强调:onOneServer() 依赖共享缓存,Coolify 默认走 Redis(config/cache.php#L18);withoutOverlapping 的锁可能因中间件重启残留而失效,需配套 TTL 过期与自愈逻辑(app/Jobs/ScheduledJobManager.php#L54-L92);schedule:work 进程本身可以通过 SCHEDULER_ENABLED=false 关闭,调试或灾备时可以整体停用(docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run)。

九、小结

scheduling.md 的六条规则对应调度器的四类典型事故:重叠执行withoutOverlapping())、多节点重复执行onOneServer() + 共享缓存)、串行阻塞runInBackground())、跨环境误触发environments()/when())、无界执行时间takeUntilTimeout() 与派发去重)、配置重复(Schedule Groups 或参数抽取)。Coolify 的 Console Kernel 是一份高完成度的参考答案:它把 onOneServer() 当作副作用任务的默认修饰符,用 when()isDev() 分支做条件化调度,并用私有方法 + 实例设置把 cron 频率与时区从硬编码中解放出来。对照 SKILL.md 第 14 节 的调度快速清单,这些模式可以直接迁移到任何生产级 Laravel 应用的定时任务设计中。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384