Coolify 队列运维深度指南:Laravel Horizon 的 Supervisor 配置、面板授权与指标排障实战
本篇围绕 Coolify 仓库中的 Horizon 配置技能文档(.agents/skills/configuring-horizon/SKILL.md)及其配套参考资料展开,系统讲解 Laravel Horizon 的完整生命周期:从安装(horizon:install)、config/horizon.php 的 supervisor 与 environments 合并规则、负载均衡策略,到仪表盘授权(viewHorizon Gate)、指标快照调度(horizon:snapshot)、LongWaitDetected 告警阈值(waits)、任务标签与静默机制,并结合 Coolify 仓库中真实的 config/horizon.php 与 HorizonServiceProvider 源码印证每一步的实际落地方式。读完后,你应当能够在任何使用 Redis 队列的 Laravel 项目中正确配置 Horizon 自动扩缩容、打通指标面板,并快速定位生产环境的常见故障。
需要强调的前提:Horizon 仅支持 Redis 队列驱动,database、SQS 等其他驱动均不受支持;同时 Horizon 不支持 Redis Cluster,它要求一个独立的 Redis 连接。这两条是全部配置讨论的边界条件。
安装与入口
安装只需一条命令:
php artisan horizon:install
该命令会生成 config/horizon.php 配置文件。运行 php artisan horizon 启动 Horizon 主进程,仪表盘则部署在应用的 horizon 路径下(路径可通过配置中的 path 项修改)。Coolify 的 config/horizon.php 中即有如下定义:
'domain' => env('HORIZON_DOMAIN'),
'path' => env('HORIZON_PATH', 'horizon'),
其中 domain 允许把 Horizon 挂到独立子域名;若为 null,则与应用共用同一域名。
启动后请完成三个基本验证:
- 运行
php artisan horizon并访问/horizon; - 确认仪表盘访问权限按预期受限(未授权用户被拒绝);
- 确认调度了
horizon:snapshot之后指标能够填充——仅运行php artisan horizon本身不会生成任何指标数据。
Supervisor 配置:defaults 与 environments 的合并规则
Supervisor 是 Horizon 的工作进程管理单元,定义在 config/horizon.php 的 defaults 数组中。技能文档给出的典型示例如下:
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
'environments' => [
'production' => [
'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3],
],
'local' => [
'supervisor-1' => ['maxProcesses' => 2],
],
],
这里最关键、也最容易被误解的一条规则是:environments 数组是按 key 合并(merge)进 defaults 的,而不是整体替换 supervisor 块。也就是说,环境块中只需写要覆盖的 key,其余配置自动继承 defaults。这是 supervisors 参考文档中明确指出的重点,常见写法是:在 defaults 中定义 connection、queue、balance、autoScalingStrategy、tries、timeout 等完整基线,然后在 production 中仅覆盖 maxProcesses、balanceMaxShift、balanceCooldown。
Coolify 的真实配置:一个生产级 supervisor 样本
Coolify 的 config/horizon.php 提供了一个信息量很大的生产级配置,值得逐项解读:
'defaults' => [
'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' => [
's6' => [
'autoScalingStrategy' => 'size',
'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
],
// local 与 production 的 supervisor 覆盖项相同
],
几个值得注意的实现细节:
timeout的下限保护:Coolify 用max()保证 worker 超时不低于ScheduledVolumeBackup::DEFAULT_TIMEOUT + 600,因为该项目的卷备份任务是最长耗时的队列作业,worker 超时若小于作业自身超时,会导致作业被 worker 提前杀掉。再用min(..., 85800)封顶,避免环境变量误配置出极端值。这是"timeout 链条"(后文详述)在真实项目中的防御式写法。- 扩缩容参数全部走环境变量:
HORIZON_MIN_PROCESSES/HORIZON_MAX_PROCESSES默认 1~4,balanceMaxShift限制每个周期最多增减 1 个进程,balanceCooldown设为 1 秒,配合autoScalingStrategy => 'size'(按队列中等待作业数量决定进程数)实现温和的自动扩缩容。 balance默认为false:Coolify 默认关闭负载均衡,由HORIZON_BALANCE环境变量显式开启。
队列优先级:单个 supervisor + balance: auto 不保证顺序
Supervisors 参考文档指出两个实操要点:
queue数组的顺序对负载均衡是无效的。当你给单个 supervisor 配置balance: auto并传入多个队列时,Horizon 不会按数组顺序处理队列。若业务上要求notifications先于default被消费,正确做法是拆成两个命名 supervisor:高优先级队列的 supervisor 设置更高的maxProcesses,低优先级队列设置更低的上限。- 需要恒定 N 个 worker 的队列请用
balance: false+maxProcesses: N。例如一个视频转码队列限制为 2 个并发,自动均衡会在流量突发时把进程数往上抬,这恰恰是这类资源受限队列不想要的。若使用balance: auto,建议同时设置balanceCooldown(参考文档建议 3~5 秒)来平滑突发负载下的频繁扩缩。
仪表盘授权:viewHorizon Gate
Horizon 仪表盘默认受 viewHorizon Gate 保护,自定义授权逻辑写在 App\Providers\HorizonServiceProvider 的 gate() 方法中。技能文档给出的通用范式:
protected function gate(): void
{
Gate::define('viewHorizon', function (User $user) {
return $user->is_admin;
});
}
Coolify 的实际实现(见 HorizonServiceProvider)则展示了另一种按白名单放行的模式,并与 config/horizon.php 中的 allowed_emails 配置联动:
protected function gate(): void
{
Gate::define('viewHorizon', function (User $user) {
if ($user->id === 0) {
return true;
}
return str(config()->string('horizon.allowed_emails'))
->lower()
->explode(',')
->map(fn (string $email) => trim($email))
->contains($user->email);
});
}
allowed_emails 在 config/horizon.php 中定义为逗号分隔的字符串:
'allowed_emails' => env('HORIZON_ALLOWED_EMAILS', ''),
即:id 为 0 的 root 用户始终可访问,其余用户必须命中 HORIZON_ALLOWED_EMAILS 环境变量中的邮箱白名单(比较时统一小写并 trim)。修改该环境变量即可调整可见仪表盘的账号集合。
此外,Coolify 在同一 Provider 中做了两件与 Horizon 数据流相关的定制,体现了技能文档之外源码级的真实用法:
- 通过
Event::listen(JobReserved)监听作业被领取事件:当被领取的是App\Jobs\ApplicationDeploymentJob时,从 payload 的tags中解析出App\Models\ApplicationDeploymentQueue:{id}形式的标签,把horizon_job_id回写到部署队列表。这使得部署记录与 Horizon 中的具体作业一一对应,运维脚本(如 bootstrap/helpers/shared.php 中按horizon_job_id查询作业状态的逻辑)可以据此向 Horizon 实时拉取每个部署的进度。 - 在
register()中将JobRepository单例绑定为自定义的 CustomJobRepository(继承自RedisJobRepository并实现CustomJobRepositoryInterface),替换 Horizon 默认的 Redis 作业仓库实现。
指标面板与快照调度:空白指标的第一嫌疑
指标仪表盘空白是 Horizon 最常见的"伪故障"。参考文档 metrics.md 给出了明确结论:
- 运行
horizon命令本身不会填充指标。指标图完全由快照构建,必须在 Laravel 调度器中注册horizon:snapshot每 5 分钟执行一次。 - 手动跑一次 snapshot 只能临时点亮面板,面板不会保持更新;必须注册到调度器(注意 Laravel 10 与 11+ 的注册语法有差异,以所用版本的队列文档为准)。
metrics.trim_snapshots的取值是"快照个数",不是时间。Coolify 的 config/horizon.php 中:
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
按每 5 分钟一次快照计算,保留 24 个即保留 2 小时的历史指标。调大该值可以换取更长的历史窗口,代价是 Redis 内存占用上升。
超时链条:timeout、supervisor timeout 与 retry_after 的排序
技能文档"Common Pitfalls"中一条极易踩中的规则是超时链条必须严格有序:
作业
timeout< supervisortimeout<retry_after
顺序颠倒的后果是:作业还没被 Horizon 判定超时,retry_after 就先到期把作业视为"失联"重新投递,造成同一作业被并发重复执行。Coolify 的 supervisor timeout 配置(上文 max(..., 85800) 封顶)正是在保证链条中 supervisor 层足够宽裕,让 ScheduledVolumeBackup 这类长作业有机会在作业自身超时之前完成。修改任何一层超时时,都应把整条链条放在一起核对。
告警:waits 阈值与通知路由
config/horizon.php 中的 waits 数组控制 LongWaitDetected 事件的触发阈值——即作业在队列中等待多少秒后告警:
'waits' => [
'redis:default' => 60,
],
按 notifications 参考文档 的说法:
- 每个"连接/队列"组合都可以有独立阈值(键形如
redis:default),Coolify 的默认值为 60 秒; - 告警触发太频繁或太迟时,应调整
waits,而不是去改通知路由配置; - 通知路由应使用 Horizon 内置 API,在
App\Providers\HorizonServiceProvider::boot()中调用Horizon::routeMailNotificationsTo()、Horizon::routeSlackNotificationsTo()或Horizon::routeSmsNotificationsTo()。Horizon 已把LongWaitDetected事件接到其通知发送器,无需手动注册监听器; - 失败作业(failed job)的告警不属于 Horizon 文档化的通知路由范畴,应视为自定义的队列事件处理,参考队列文档而非 Horizon 的 routing API。
标签、静默与数据保留
自动标签
若作业的构造函数接收 Eloquent 模型实例,Horizon 会自动为作业打上 ModelClass:id 形式的标签(如 App\Models\User:42),无需在作业类中写任何代码,且这些标签在仪表盘里可直接用于筛选。只有需要超出自动标签的自定义标签时,才实现 tags() 方法。Coolify 前文提到的 ApplicationDeploymentQueue:{id} 标签即利用了标签机制做部署追溯。
静默(silenced)
silenced 参考文档 澄清了两个概念的边界:
- 把作业类加入 config/horizon.php 的
silenced数组,只是把它从仪表盘"已完成作业"视图中隐藏,作业本身照常运行。它是降噪工具,不是禁用手段:
'silenced' => [
// App\Jobs\ExampleJob::class,
],
silenced_tags则按标签隐藏:携带匹配标签字符串的作业全部不出现在已完成列表中,适合静默一整类作业(如所有打了notifications标签的作业),而不是逐个屏蔽类。
作业记录裁剪(trim)
trim 配置控制各类作业记录在 Redis 中的保留时长(单位:分钟)。Coolify 的配置:
'trim' => [
'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080,
'failed' => 10080,
'monitored' => 10080,
],
即近期/待处理/已完成作业保留 1 小时,失败与受监控作业保留 10080 分钟(7 天)。调整时应注意:失败作业保留越久,事故回溯窗口越大,但 Redis 占用也越高。
生产运维细节:Redis 连接、前缀与快速终止
config/horizon.php 还有几项与部署运维直接相关的配置,Coolify 中均保留默认值:
'use' => 'default', // Horizon 元数据使用的 Redis 连接
'prefix' => env('HORIZON_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'),
'fast_termination' => false,
'memory_limit' => 64, // master supervisor 的内存上限(MB)
use与prefix:Horizon 把 supervisor 列表、失败作业、指标等元数据都存在这个 Redis 连接下,并以prefix为键前缀。同一 Redis 上跑多套 Horizon 实例时,务必用HORIZON_PREFIX区分前缀,避免元数据互相污染。fast_termination:开启后horizon:terminate命令不会等待所有 worker 退出,允许新实例在旧实例收尾期间启动,从而缩短部署停机时间;不想要这种"交叠"语义时保持false即可。memory_limit:限制 Horizon master supervisor 本身可占用的内存(worker 的内存限制由 supervisor 的memory项控制,Coolify 设为 128 MB,超过即重启 worker)。
验证清单与常见陷阱汇总
完成配置后,按技能文档的验证清单逐项确认:
php artisan horizon能启动,/horizon可访问;- 未授权账号访问仪表盘被拒(
viewHorizonGate 生效); - 调度
horizon:snapshot后指标正常填充。
最后汇总技能文档的常见陷阱清单,作为交付前的自检项:
- Horizon 只支持 Redis 队列驱动;
database、SQS等驱动不受支持; - 不支持 Redis Cluster,需要独立 Redis 连接;
- 改动前先读一遍
config/horizon.php,弄清当前 supervisor 与环境配置再下手; environments只做按键合并覆盖,不会整体替换defaults;- 超时链条排序:job
timeout< supervisortimeout<retry_after,顺序错误会导致作业在 Horizon 判定超时前就被重试; - 指标空白直到
horizon:snapshot被调度——单独运行php artisan horizon不会生成指标。
延伸阅读:技能包内的参考资料
本仓库在 .agents/skills/configuring-horizon/ 下还维护了四个专题参考文档,可作为 Horizon 相关排查时的分主题入口:
- supervisors.md:supervisor 块、balance 的 auto/simple/false 模式、多队列与自动扩缩容;
- notifications.md:
LongWaitDetected告警、通知路由与waits配置; - tags.md:作业标签、仪表盘筛选与噪音作业静默;
- metrics.md:空白指标面板、快照调度与保留期配置。
这些文档与 config/horizon.php、app/Providers/HorizonServiceProvider.php 中的真实实现相互印证,是理解 Coolify 队列体系(尤其是部署作业经 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 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