Coolify 队列监控实践:Laravel Horizon 指标快照(horizon:snapshot)与快照保留期配置
Coolify 基于 Laravel Horizon 管理其全部后台任务(部署、备份、通知等)的队列工作进程。然而,即使 php artisan horizon 正常启动,Horizon 控制台的 Metrics 面板也可能长时间一片空白——原因是指标曲线完全由 horizon:snapshot 生成的快照数据驱动,而快照本身必须显式注册到 Laravel 任务调度器中才会周期性产生。本文以 Coolify 仓库中 .claude/skills/configuring-horizon/references/metrics.md 参考文档为主线,结合 config/horizon.php 与 app/Console/Kernel.php 的真实实现,讲清 Horizon 指标"为什么是空的"、"快照该如何注册"以及"快照保留期参数到底代表什么"这三个核心问题。读完本文,你可以直接定位并修复 Horizon 指标面板空白、快照调度缺失、保留期配置误解这三类典型问题。
指标面板为空的根因:horizon 命令不产生快照数据
参考文档给出的第一个关键结论是:运行 horizon artisan 命令并不会自动填充指标数据。
Horizon 控制台的指标图(Jobs/Queues 处理速率、失败率等曲线)并不是实时从队列读取的,而是从"快照(snapshot)"数据构建出来的。快照由独立的 horizon:snapshot 命令生成,且必须通过 Laravel 调度器每 5 分钟执行一次,指标曲线才会有持续的数据点:
// 快照必须注册到调度器,而不是手动执行
$schedule->command('horizon:snapshot')->everyFiveMinutes();
也就是说,php artisan horizon 只负责启动 supervisor 进程消费队列,它不触碰快照存储。如果你只在启动 Horizon 后打开 /horizon 页面,Metrics 图表自然没有任何数据点。这也是参考文档中 "Metrics dashboard stays blank until horizon:snapshot is scheduled" 这一条目的完整含义:快照调度是指标可视化的前置依赖,而非可选增强。
Coolify 中的真实调度注册
在 Coolify 仓库中,快照的调度注册位于 app/Console/Kernel.php,且按开发/生产环境采用了不同频率:
// app/Console/Kernel.php
if (isDev()) {
// Instance Jobs
$this->scheduleInstance->command('horizon:snapshot')->everyMinute();
...
} else {
// Instance Jobs
$this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
...
}
可以看到:
- 开发环境(
isDev()):horizon:snapshot每分钟执行一次,便于本地快速看到指标曲线变化; - 生产环境:每 5 分钟执行一次,这是参考文档中推荐的标准节奏("must be scheduled to run every 5 minutes via Laravel's scheduler")。
而调度器本身的运行由容器内的 s6-overlay 服务承载:docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run 与 docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run 均执行 php artisan schedule:work。从源码结构看,这意味着 horizon:snapshot 并非独立 cron,而是随 schedule:work 常驻进程按上表频率触发——排查"生产环境快照缺失"时,应当先确认 scheduler-worker 服务是否存活,而不是只盯着 horizon 进程。
用调度器注册快照,而不是手动执行
参考文档的第二条告诫是:手动执行一次快照只能让面板短暂有数据,无法维持更新。
- 手动
php artisan horizon:snapshot一次 → 只写入一个时间点的快照,面板短暂出现一个数据点; - 调度器周期性执行 → 指标曲线随时间连续生长,且与保留策略(见下一节)配合形成滚动窗口。
参考文档同时提醒:调度器注册语法在 Laravel 10 与 Laravel 11+ 之间存在差异。这一点在 Coolify 中可以直接确认:composer.json 中声明了 "laravel/framework": "^12.65.0" 与 "laravel/horizon": "^5.48.2",即项目运行于 Laravel 12 之上,因此 app/Console/Kernel.php 使用的是 Laravel 11+ 风格:
// Laravel 11+(Coolify 当前实际写法)
$this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
而 Laravel 10 及以下项目的等价写法是传统的调度器闭包:
// Laravel 10 及以下
protected function schedule(Schedule $schedule): void
{
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
两者的语义一致(每 5 分钟触发一次 horizon:snapshot),差异在于调度器的注册入口与文件组织方式。如果你的代码库从旧版 Laravel 升级而来,应检查 schedule 定义位置是否已迁移,这正是参考文档要求先搜索 "horizon metrics snapshot" 确认语法的原因——用错版本语法会导致调度静默失效,指标面板依旧空白。
metrics.trim_snapshots 是快照条数,不是时间单位
参考文档指出的第三个易错点极具误导性风险:config/horizon.php 中 metrics.trim_snapshots 的 job 与 queue 值表示"保留多少条快照",而不是分钟或小时数。
Coolify 仓库中的实际配置(config/horizon.php)如下:
// config/horizon.php
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
其上方注释也明确写道:该值与 horizon:snapshot 的调度周期结合使用,共同决定指标保留时长("This will get used in combination with Horizon's horizon:snapshot schedule to define how long to retain metrics")。
按参考文档的算法:
| 参数 | 值 | 快照频率 | 等效保留时长 |
|---|---|---|---|
trim_snapshots.job |
24 | 每 5 分钟 1 次 | 24 × 5 min = 2 小时 |
trim_snapshots.queue |
24 | 每 5 分钟 1 次 | 24 × 5 min = 2 小时 |
即默认配置下,Job 与 Queue 的指标曲线只展示最近 2 小时的历史。需要更长历史时,应增大这两个数值,代价是 Redis 内存占用上升——每个快照都会以字符串形式写入 Horizon 前缀下的 Redis 键空间。
这里有一个必须区分的易混淆配置:同一文件中的 trim 段(config/horizon.php)里的 recent、pending、completed、recent_failed、failed、monitored 才是分钟数:
// config/horizon.php —— 注意:这里的值单位是分钟
'trim' => [
'recent' => 60, // 分钟
'pending' => 60, // 分钟
'completed' => 60, // 分钟
'recent_failed' => 10080, // 1 周(分钟)
'failed' => 10080,
'monitored' => 10080,
],
trim 控制的是任务条目(recent/pending/completed/failed 列表)的持久化时长,metrics.trim_snapshots 控制的是指标快照条数——两者单位不同、用途不同,混用会导致对"指标为什么只显示 2 小时"产生错误判断。
前提条件与排查清单
结合参考文档与仓库实现,Horizon 指标要正常工作需满足以下前提(均可在仓库中验证):
- 队列驱动必须是 Redis。config/queue.php 中
'default' => env('QUEUE_CONNECTION', 'redis'),默认即 Redis 连接;config/horizon.php 中 Horizon 自身元数据存储也使用defaultRedis 连接。参考文档所属的技能指南(SKILL.md)亦明确:Horizon 仅支持 Redis 队列驱动,database、SQS 均不受支持。 horizon:snapshot必须存在于调度器中,且调度器进程(schedule:work)在运行。Coolify 中对应 app/Console/Kernel.php(开发,每分钟)与 app/Console/Kernel.php(生产,每 5 分钟)。- 保留窗口 = 快照条数 × 快照间隔。默认 24 条 × 5 分钟 = 2 小时;调大
metrics.trim_snapshots.job/queue可延长曲线历史,需权衡 Redis 内存。 - 快照写入是滚动覆盖的,旧快照超出保留条数即被修剪,因此"面板空白"与"曲线只有近期数据"是两种不同现象:前者通常缺调度,后者是保留策略生效。
小结
围绕 metrics.md 参考文档的三个要点,可以在 Coolify 仓库中找到完整落点:指标面板空白源于 horizon:snapshot 未入调度器(对照 app/Console/Kernel.php 的真实注册方式);快照必须靠周期性调度而非手动单次执行,且注册语法需匹配 Laravel 版本(Coolify 为 Laravel 12,采用 11+ 写法);metrics.trim_snapshots 的 job/queue(默认各 24 条)是快照条数,与 5 分钟快照间隔相乘得到约 2 小时的曲线保留窗口,与以分钟为单位的 trim 段不可混淆。掌握这三点后,任何 Horizon 指标相关的"空白面板、数据断档、窗口偏短"问题都可以按"调度是否存在 → 调度器是否存活 → 保留条数是否合理"的顺序快速定位。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00