Coolify 的 Laravel Horizon Supervisor 配置详解:defaults/environments 合并机制、balance 策略与队列优先级
本文基于 Coolify 仓库中的 Horizon Supervisor 配置参考文档(.agents/skills/configuring-horizon/references/supervisors.md)展开,聚焦一个具体问题:在 config/horizon.php 中如何正确编写 supervisor 块,才能让 defaults 与 environments 按预期合并、让 balance 策略(auto / simple / false)匹配负载形态、以及如何在多队列场景下真正实现优先级处理。读完本文,你将能理解 Coolify 生产环境中 s6 supervisor 的完整参数来源,并能复制出一套可运行的 supervisor 配置。
Supervisor 配置在哪里定义
在 Laravel Horizon 体系中,所有队列 worker 的运行参数都集中在 config/horizon.php 的 defaults 与 environments 两个数组里。Horizon 在启动时会把每个 supervisor 块展开为实际的 php artisan queue:work 进程组,minProcesses 到 maxProcesses 之间的进程数就是该 supervisor 的伸缩区间。
Coolify 中 Horizon 的完整生命周期技能文档见 .agents/skills/configuring-horizon/SKILL.md,其中明确提醒:在修改任何 supervisor 配置前,应先查阅当前版本 Horizon 的文档,因为选项名和默认值在不同 Horizon 版本之间会变化。参考文档给出的四个检索方向是:
"horizon supervisor configuration":完整的 supervisor 选项列表;"horizon balancing strategies":auto、simple、false三种 balance 模式;"horizon autoscaling workers":autoScalingStrategy的细节;"horizon environment configuration":defaults与environments的合并规则。
defaults 与 environments 是合并关系,不是替换关系
这是参考文档强调的第一个要点,也是最常见的配置误区:
defaults数组定义了完整的基础 supervisor 配置(连接、队列、balance 策略、重试与超时等);environments数组按环境打补丁,只覆盖其中显式列出的键;- 因此不需要在每个环境块里重复所有键。
参考文档给出的通用模式是:在 defaults 中定义 connection、queue、balance、autoScalingStrategy、tries、timeout,然后在 production 环境中只覆盖 maxProcesses、balanceMaxShift、balanceCooldown:
'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 中的环境名对应 Laravel 的 APP_ENV,supervisor 名必须与 defaults 中的键一致才会被合并。
Coolify 的真实配置
Coolify 仓库中的 config/horizon.php 正是这种"defaults 全量 + environments 打补丁"写法的实例。其 defaults 中只定义了一个名为 s6 的 supervisor(名字对应容器内 s6-overlay 进程管理器的服务命名习惯):
'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 与 local 两个环境只覆盖扩容相关参数:
'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' => [
's6' => [
// 与 production 相同的覆盖方式
'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),
],
],
],
对照上面讲到的合并语义可以读出几个关键信息:
connection、queue、tries、timeout等基础键只在defaults出现一次,环境块无需重复;- 所有与伸缩强度相关的参数(
maxProcesses、balanceMaxShift、balanceCooldown、minProcesses)都通过环境变量暴露,可在不改代码的情况下调整容量; balance默认值是字符串'false'——即 Coolify 的 supervisor 默认不做自动伸缩,maxProcesses(默认 4)就是固定的 worker 上限,这正好印证了参考文档中"固定 worker 数"场景的推荐做法。
balance 策略选型:auto、simple 与 false
参考文档给出了三种典型场景及其对应的 balance 取值。
场景一:变负载下用 balance: auto 自动伸缩
balance: auto 让 supervisor 根据队列积压量在 minProcesses 与 maxProcesses 之间自动伸缩 worker 数量,适合负载波动明显的场景。但 auto 模式在突发负载下可能在很短时间内连续上调再下调进程数,造成 worker 频繁启停。
参考文档的解法正是 Coolify 配置中已经采用的两个参数:
balanceCooldown:两次伸缩决策之间的最小间隔(秒),参考文档建议通常设为 3~5,用以平滑突发负载下的抖动;balanceMaxShift:单个伸缩周期内允许增减的最大进程数,防止一次性拉起或杀掉大量 worker。
Coolify 中两者默认值都是 1(HORIZON_BALANCE_COOLDOWN、HORIZON_BALANCE_MAX_SHIFT),即最激进的伸缩节奏;若将 HORIZON_BALANCE 打开为 auto 或 simple,建议同时把这两个值调大以贴合参考文档的建议。
场景二:专用队列用 balance: false 固定 worker 数
参考文档指出的第二种场景:如果某个队列必须始终保持恰好 N 个 worker——例如一个视频处理队列被硬件/许可限制在 2 个并发——就不该用自动伸缩,因为 auto 模式会在流量高峰把进程数拉高,超出限制。正确做法是:
'supervisor-video' => [
'connection' => 'redis',
'queue' => 'video',
'balance' => false,
'maxProcesses' => 2,
],
balance: false 时 supervisor 直接以 maxProcesses 为固定进程数运行。Coolify 的 HORIZON_BALANCE 默认 'false' 就是这种保守取向:部署任务(见 app/Jobs/ApplicationDeploymentJob.php)本身执行时间长、资源消耗大,固定进程数比随时扩缩更容易控制并发。
场景三:auto 与 simple 的差别体现在 autoScalingStrategy
当需要自动伸缩但希望控制伸缩"依据什么"时,autoScalingStrategy 起作用。Coolify 在两个环境块中都将其固定为 'size'。从源码结构看,该值决定 supervisor 在多队列场景下按何种策略分配进程(例如按队列积压量分配,或在多个队列间均匀分配);参考文档也建议用 "horizon autoscaling workers" 检索确认当前版本的 autoScalingStrategy 具体取值语义,因为它属于版本间容易变化的选项。
用多个命名 supervisor 强制队列优先级
这是参考文档中非常关键、且容易被忽略的一条:当单个 supervisor 使用 balance: auto 时,Horizon 不会强制队列处理顺序,queue 数组的书写顺序对负载均衡是无效的。
也就是说,下面的写法不能保证 notifications 先于 default 被处理:
// 错误示例:顺序在这里不起作用
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['notifications', 'default'],
'balance' => 'auto',
],
参考文档给出的正确方案是拆成两个独立命名的 supervisor,用不同的 maxProcesses 上限表达优先级:
// 高优先级队列:给更高的并发上限
'notifications' => [
'connection' => 'redis',
'queue' => 'notifications',
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 8,
],
// 低优先级队列:给更低的并发上限
'default' => [
'connection' => 'redis',
'queue' => 'default',
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 2,
],
这样在总容量受限时,高优先级 supervisor 能占据更多 worker。Coolify 自身目前是单 supervisor 消费 high,default 两个队列(HORIZON_QUEUES 默认值 'high,default'),如果未来要为部署类任务与通知类任务建立优先级隔离,参照文档的做法就是拆分为两个命名 supervisor,而不是调整 queue 字符串顺序。
supervisor 参数与超时链:结合 Coolify 源码的纵深解读
参考文档列出的 supervisor 选项(connection、queue、balance、autoScalingStrategy、tries、timeout、maxProcesses、balanceMaxShift、balanceCooldown)在 Coolify 的 s6 supervisor 中基本都有对应实现,config/horizon.php 还额外配置了 maxTime、maxJobs、memory、nice、sleep 等 worker 生命周期参数。结合仓库中的实际取值,可以梳理出一组相互关联的超时约束:
| 参数 | Coolify 取值 | 来源 |
|---|---|---|
balance |
HORIZON_BALANCE,默认 'false' |
config/horizon.php |
queue |
HORIZON_QUEUES,默认 'high,default' |
config/horizon.php |
maxJobs |
400(单个 worker 处理 400 个任务后重启) |
config/horizon.php |
memory |
128(MB,worker 内存上限) |
config/horizon.php |
tries |
1(任务不自动重试) |
config/horizon.php |
sleep |
3(秒,队列空闲时 worker 休眠时长) |
config/horizon.php |
supervisor timeout |
min(max(HORIZON_TIMEOUT, 36600), 85800) |
config/horizon.php |
redis 连接 retry_after |
86400(24 小时) |
config/queue.php |
其中 supervisor timeout 的表达式值得展开:
- 下限是
ScheduledVolumeBackup::DEFAULT_TIMEOUT + 600,而 app/Models/ScheduledVolumeBackup.php 中DEFAULT_TIMEOUT为36000,即至少36600秒,保证一次带 10 分钟余量的卷备份任务不会被 supervisor 提前杀掉; - 默认值
HORIZON_TIMEOUT为39600(11 小时); - 上限被硬性钳制在
85800秒(约 23.8 小时)。
这条超时链与 redis 连接配置的 retry_after(86400 秒)共同保证了同一 skill 目录下 SKILL.md 提到的顺序约束:job timeout < supervisor timeout < retry_after。以 app/Jobs/ApplicationDeploymentJob.php 为例,部署任务自身声明 $timeout = 3600(1 小时),小于 supervisor 的 39600 秒;supervisor 上限 85800 秒又小于 retry_after 的 86400 秒。这个顺序一旦写反,就会出现任务还没被 Horizon 判超时、Redis 侧却先释放了锁导致重复执行的问题。
配置如何生效:从 s6 服务到 horizon:manage
了解"配置写在哪里"之后,还需要知道"配置如何被加载":
- 启动入口:Coolify 生产容器通过 s6-overlay 托管 Horizon,docker/production/etc/s6-overlay/s6-rc.d/horizon/run 的内容就是检查
.env中是否有HORIZON_ENABLED=false,否则exec php artisan horizon。php artisan horizon启动 master supervisor 时,才会按当前APP_ENV把environments块合并进defaults并拉起对应数量的 worker。开发环境对应脚本为 docker/development/etc/s6-overlay/s6-rc.d/horizon/run。 - Dashboard 授权:app/Providers/HorizonServiceProvider.php 中定义了
viewHorizonGate,只有 root 用户或配置在horizon.allowed_emails(即 config/horizon.php 的HORIZON_ALLOWED_EMAILS)中的邮箱能进入 Horizon 面板,可以在这里直观查看每个 supervisor 的进程数随负载的变化,验证balance与maxProcesses的实际效果。 - 运行时排障:仓库内置了交互式命令
horizon:manage(app/Console/Commands/HorizonManage.php),可查看 pending/running/failed 任务、当前 worker 列表、按队列 purge,以及"当前 worker 是否还有任务在跑"(用于安全重启判断)。调整 supervisor 参数后重启 worker 前,用它确认没有 in-progress 的部署任务是最稳妥的流程。 - 部署任务与 Horizon 的联动:app/Providers/HorizonServiceProvider.php 监听了
JobReserved事件,把ApplicationDeploymentJob的 Horizon job id 回写到ApplicationDeploymentQueue记录,这也是 supervisor 配置(尤其是tries: 1与超时链)对部署可靠性产生直接影响的原因。
小结:一套可复用的 supervisor 检查清单
综合参考文档与 Coolify 的实现,修改 supervisor 配置前可以按以下清单自检:
- 新键是否应放在
defaults、而环境差异是否只以最小补丁写入environments?(合并而非替换) - 该队列的负载形态是波动的还是必须固定并发?波动选
auto,固定并发(如受限的视频处理队列)选false+ 明确maxProcesses; - 打开自动伸缩后,是否已设置
balanceCooldown(建议 3~5 秒)与balanceMaxShift抑制抖动? - 存在优先级需求时,是否拆成了多个命名 supervisor,而不是依赖
queue数组顺序? - 超时链是否满足 job
timeout< supervisortimeout<retry_after(Coolify 中即 3600 < 39600 < 86400)? - 重启 worker 前,是否用
horizon:manage确认没有任务在跑?
以上全部路径与取值均可在 Coolify 仓库内直接对照验证,核心文件为 config/horizon.php、config/queue.php、app/Providers/HorizonServiceProvider.php 与 app/Console/Commands/HorizonManage.php。
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