Coolify 的 Horizon 指标与快照:让 Metrics 面板不空白的 horizon:snapshot 与 trim_snapshots 配置
本文以 Coolify 仓库中关于 Laravel Horizon 指标(Metrics & Snapshots)的参考文档为主体,讲清楚三件事:为什么 Horizon 的指标图表默认是空白的、horizon:snapshot 必须通过 Laravel Scheduler 注册而非手动执行、以及 config/horizon.php 中 metrics.trim_snapshots 的真实语义(快照个数而非时间长度)。读完本文,你可以直接在 Coolify 或任何 Horizon 项目中定位指标面板空白的根因、复用 Coolify 的调度注册写法,并按需调整指标历史保留时长与 Redis 内存开销之间的平衡。
一、指标图表为空是"设计使然":Horizon 不会自动产出指标
参考文档给出的第一个关键事实是:运行 horizon artisan 命令本身并不会自动填充指标数据。Horizon 的 metrics 图表并不是由 worker 在消费任务时实时写入的,而是完全由快照(snapshot)构建的——只有 horizon:snapshot 命令执行后,才会生成一条用于绘图的数据点。
这意味着一个常见误区:只要 php artisan horizon 起来了,dashboard 上就"应该"有曲线。实际上如果调度器从未触发 horizon:snapshot,Metrics 面板会一直保持空白,且不会报任何错误。
Coolify 项目完整集成了 Horizon(composer.json 中依赖 laravel/horizon: ^5.48.2),其生产镜像通过 s6-overlay 服务启动 Horizon:docker/production/etc/s6-overlay/s6-rc.d/horizon/run 中最终执行 exec php artisan horizon。可以注意到,这个脚本只负责"启动 Horizon 主进程",整个仓库里没有任何地方在进程启动时顺手做快照——这正是文档强调"metrics 与 horizon 进程解耦"的实际体现:指标数据是调度侧的职责,不是 worker 侧的副作用。
二、正确做法:把 horizon:snapshot 注册进 Scheduler,而不是手动跑
文档第二个要点:手动执行一次 horizon:snapshot 只会让 dashboard 短暂出现一个数据点,随后立刻"过期",因为它不会持续更新。正确做法是把快照命令注册进 Laravel 调度器,每 5 分钟执行一次,与 trim_snapshots 的计数配合形成连续的时间序列。
Coolify 仓库中恰好给出了该命令的真实注册位置——应用 Console Kernel 的 schedule(Schedule $schedule) 方法,并且按环境区分了两种频率:
if (isDev()) {
// 开发环境:每分钟一次,便于调试时快速看到曲线变化
$this->scheduleInstance->command('horizon:snapshot')->everyMinute();
// ...
} else {
// 生产环境:标准做法,每 5 分钟一次
$this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
// ...
}
两个细节值得注意:
- 调度器必须真正在运行。在 Coolify 的容器化部署里,
schedule:work由独立的 s6 服务进程承担,见 scheduler-worker 服务脚本(开发环境见 docker/development 下的同名脚本),其最终执行php artisan schedule:work。如果只启动了horizon服务而漏掉scheduler-worker,horizon:snapshot永远不会触发,症状就是"进程都活着,但指标图是空的"——排查时应优先确认调度器进程存在,而不是怀疑 Horizon 本身。 - 参考文档还提醒:调度注册的写法在 Laravel 10 与 11+ 之间存在差异。上例中 Coolify 采用的是新版写法(
schedule(Schedule $schedule)直接接收 Schedule 实例)。如果你要在旧版 Laravel 10 的项目里复制这段逻辑,请对照对应版本的调度器文档确认注册语法,不要跨版本盲抄。
补充一个 Coolify 特有的开关:config/constants.php 中定义了 is_horizon_enabled => env('HORIZON_ENABLED', true)(config/constants.php),而生产 s6 脚本会在启动前检查 .env 是否包含 HORIZON_ENABLED=false,若是则让服务睡眠不启动 Horizon。也就是说在 Coolify 中,"指标图空白"还有一种前置原因:Horizon 整体被环境变量关掉了。
三、trim_snapshots 是"快照个数",不是"时长"——保留历史的标准算法
文档第三个要点,也是最容易误读的参数:config/horizon.php 中 metrics.trim_snapshots 下的 job 与 queue 值是要保留的快照数量,而不是分钟数或小时数。
Coolify 仓库的实际配置位于 config/horizon.php 的 Metrics 段落:
'metrics' => [
'trim_snapshots' => [
'job' => 24, // 保留 24 个 job 维度快照
'queue' => 24, // 保留 24 个 queue 维度快照
],
],
其注释原文也点明了语义:这个配置"将和 horizon:snapshot 的调度一起使用,决定指标保留多久"。由此可以推出保留历史的标准计算公式:
历史时长 = 快照个数 × 快照间隔
= 24 个 × 5 分钟
= 2 小时
即 Coolify 默认配置下,Metrics 面板上的 job/queue 曲线最多展示最近 2 小时的数据。要延长曲线跨度,唯一正确的方式是调大 trim_snapshots 的数值(例如 48 个 → 4 小时),而不是去改 horizon:snapshot 之外的任何"时间"参数。
代价是 Redis 内存:每个快照都会写入 Horizon 使用的 Redis 连接(Coolify 中为 'use' => 'default',数据键统一带前缀,见 config/horizon.php 的 prefix 配置,默认形如 {APP_NAME}_horizon:)。保留的快照越多,累积的 key 越多,因此调大该值前应在 Redis 容量上留有余量。文档的结论可以概括为一条权衡规则:在 Redis 内存可接受的前提下增大 trim_snapshots,换取更长的指标历史;反之维持 24 的默认值以节省内存。
顺带一提,同一份配置中的 waits 段('redis:default' => 60,config/horizon.php)定义的是队列等待阈值(秒),与快照保留无关,注意不要混淆这两个"时间型"参数。
四、Coolify 的配套:s6 监督器配置如何影响指标曲线的内容
理解 trim_snapshots 的作用对象(job 与 queue 两个维度)后,再结合 Coolify 的监督器(supervisor)配置,就能读懂指标图里"曲线"的来源。Coolify 在 config/horizon.php 的 defaults 段 只定义了一个名为 s6 的监督器:
's6' => [
'connection' => 'redis',
'balance' => env('HORIZON_BALANCE', 'false'),
'queue' => env('HORIZON_QUEUES', 'high,default'),
'maxTime' => env('HORIZON_MAX_TIME', 0),
'maxJobs' => 400,
'memory' => 128,
'tries' => 1,
'nice' => 0,
'sleep' => 3,
'timeout' => min(
max((int) env('HORIZON_TIMEOUT', 39600), ScheduledVolumeBackup::DEFAULT_TIMEOUT + 600),
85800,
),
],
其中 environments 段为 production/local 两个环境补充了自动扩缩参数(autoScalingStrategy => 'size'、minProcesses/maxProcesses 默认 1~4、balanceMaxShift 与 balanceCooldown 默认均为 1)。从源码结构看,队列维度默认覆盖 high 和 default 两条队列,可用环境变量 HORIZON_QUEUES 调整——当你新增自定义队列后,需要确认它被纳入了某个监督器,否则该队列的任务不会出现在 queue 维度的指标快照中(快照只统计被 Horizon 监督的工作负载)。
五、排查清单:Metrics 面板空白时的检查顺序
综合参考文档与 Coolify 仓库的实际实现,按以下顺序排查可以覆盖绝大多数"指标不显示"场景:
horizon:snapshot是否已注册进调度器:在schedule()方法中搜索horizon:snapshot(Coolify 位于 app/Console/Kernel.php 与 #L72),且间隔应 ≤ 5 分钟;- 调度器进程是否存活:Coolify 容器化部署中即
scheduler-worker服务(php artisan schedule:work),手动执行快照无法替代它; - 间隔与保留数是否匹配:快照间隔 ×
trim_snapshots个数 = 可见历史长度(默认 24 × 5 分钟 = 2 小时); - Horizon 是否被整体关闭:检查
.env中HORIZON_ENABLED=false及 config/constants.php 的默认值; - Redis 容量:调大
trim_snapshots前先评估prefix命名空间下的键增长。
最后需要强调文档反复提示的一条限制:metrics.trim_snapshots 的语义是计数,不是时长;任何把它当"保留分钟数"来调的写法都是错误的,这也是本文与参考文档最想让读者记住的一点。
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 StartedRust0623
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