首页
/ Coolify 的 Laravel Horizon Supervisor 配置详解:defaults/environments 合并机制、balance 策略与队列优先级

Coolify 的 Laravel Horizon Supervisor 配置详解:defaults/environments 合并机制、balance 策略与队列优先级

2026-09-05 15:34:38作者:伍霜盼Ellen

本文基于 Coolify 仓库中的 Horizon Supervisor 配置参考文档(.agents/skills/configuring-horizon/references/supervisors.md)展开,聚焦一个具体问题:在 config/horizon.php 中如何正确编写 supervisor 块,才能让 defaultsenvironments 按预期合并、让 balance 策略(auto / simple / false)匹配负载形态、以及如何在多队列场景下真正实现优先级处理。读完本文,你将能理解 Coolify 生产环境中 s6 supervisor 的完整参数来源,并能复制出一套可运行的 supervisor 配置。

Supervisor 配置在哪里定义

在 Laravel Horizon 体系中,所有队列 worker 的运行参数都集中在 config/horizon.phpdefaultsenvironments 两个数组里。Horizon 在启动时会把每个 supervisor 块展开为实际的 php artisan queue:work 进程组,minProcessesmaxProcesses 之间的进程数就是该 supervisor 的伸缩区间。

Coolify 中 Horizon 的完整生命周期技能文档见 .agents/skills/configuring-horizon/SKILL.md,其中明确提醒:在修改任何 supervisor 配置前,应先查阅当前版本 Horizon 的文档,因为选项名和默认值在不同 Horizon 版本之间会变化。参考文档给出的四个检索方向是:

  • "horizon supervisor configuration":完整的 supervisor 选项列表;
  • "horizon balancing strategies"autosimplefalse 三种 balance 模式;
  • "horizon autoscaling workers"autoScalingStrategy 的细节;
  • "horizon environment configuration"defaultsenvironments 的合并规则。

defaults 与 environments 是合并关系,不是替换关系

这是参考文档强调的第一个要点,也是最常见的配置误区:

  • defaults 数组定义了完整的基础 supervisor 配置(连接、队列、balance 策略、重试与超时等);
  • environments 数组按环境打补丁,只覆盖其中显式列出的键;
  • 因此不需要在每个环境块里重复所有键。

参考文档给出的通用模式是:在 defaults 中定义 connectionqueuebalanceautoScalingStrategytriestimeout,然后在 production 环境中只覆盖 maxProcessesbalanceMaxShiftbalanceCooldown

'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,
        ),
    ],
],

environmentsproductionlocal 两个环境只覆盖扩容相关参数

'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),
        ],
    ],
],

对照上面讲到的合并语义可以读出几个关键信息:

  1. connectionqueuetriestimeout 等基础键只在 defaults 出现一次,环境块无需重复;
  2. 所有与伸缩强度相关的参数(maxProcessesbalanceMaxShiftbalanceCooldownminProcesses)都通过环境变量暴露,可在不改代码的情况下调整容量;
  3. balance 默认值是字符串 'false'——即 Coolify 的 supervisor 默认不做自动伸缩maxProcesses(默认 4)就是固定的 worker 上限,这正好印证了参考文档中"固定 worker 数"场景的推荐做法。

balance 策略选型:auto、simple 与 false

参考文档给出了三种典型场景及其对应的 balance 取值。

场景一:变负载下用 balance: auto 自动伸缩

balance: auto 让 supervisor 根据队列积压量在 minProcessesmaxProcesses 之间自动伸缩 worker 数量,适合负载波动明显的场景。但 auto 模式在突发负载下可能在很短时间内连续上调再下调进程数,造成 worker 频繁启停。

参考文档的解法正是 Coolify 配置中已经采用的两个参数:

  • balanceCooldown:两次伸缩决策之间的最小间隔(秒),参考文档建议通常设为 3~5,用以平滑突发负载下的抖动;
  • balanceMaxShift:单个伸缩周期内允许增减的最大进程数,防止一次性拉起或杀掉大量 worker。

Coolify 中两者默认值都是 1HORIZON_BALANCE_COOLDOWNHORIZON_BALANCE_MAX_SHIFT),即最激进的伸缩节奏;若将 HORIZON_BALANCE 打开为 autosimple,建议同时把这两个值调大以贴合参考文档的建议。

场景二:专用队列用 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 选项(connectionqueuebalanceautoScalingStrategytriestimeoutmaxProcessesbalanceMaxShiftbalanceCooldown)在 Coolify 的 s6 supervisor 中基本都有对应实现,config/horizon.php 还额外配置了 maxTimemaxJobsmemorynicesleep 等 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.phpDEFAULT_TIMEOUT36000,即至少 36600 秒,保证一次带 10 分钟余量的卷备份任务不会被 supervisor 提前杀掉;
  • 默认值 HORIZON_TIMEOUT39600(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

了解"配置写在哪里"之后,还需要知道"配置如何被加载":

  1. 启动入口:Coolify 生产容器通过 s6-overlay 托管 Horizon,docker/production/etc/s6-overlay/s6-rc.d/horizon/run 的内容就是检查 .env 中是否有 HORIZON_ENABLED=false,否则 exec php artisan horizonphp artisan horizon 启动 master supervisor 时,才会按当前 APP_ENVenvironments 块合并进 defaults 并拉起对应数量的 worker。开发环境对应脚本为 docker/development/etc/s6-overlay/s6-rc.d/horizon/run
  2. Dashboard 授权app/Providers/HorizonServiceProvider.php 中定义了 viewHorizon Gate,只有 root 用户或配置在 horizon.allowed_emails(即 config/horizon.phpHORIZON_ALLOWED_EMAILS)中的邮箱能进入 Horizon 面板,可以在这里直观查看每个 supervisor 的进程数随负载的变化,验证 balancemaxProcesses 的实际效果。
  3. 运行时排障:仓库内置了交互式命令 horizon:manageapp/Console/Commands/HorizonManage.php),可查看 pending/running/failed 任务、当前 worker 列表、按队列 purge,以及"当前 worker 是否还有任务在跑"(用于安全重启判断)。调整 supervisor 参数后重启 worker 前,用它确认没有 in-progress 的部署任务是最稳妥的流程。
  4. 部署任务与 Horizon 的联动app/Providers/HorizonServiceProvider.php 监听了 JobReserved 事件,把 ApplicationDeploymentJob 的 Horizon job id 回写到 ApplicationDeploymentQueue 记录,这也是 supervisor 配置(尤其是 tries: 1 与超时链)对部署可靠性产生直接影响的原因。

小结:一套可复用的 supervisor 检查清单

综合参考文档与 Coolify 的实现,修改 supervisor 配置前可以按以下清单自检:

  1. 新键是否应放在 defaults、而环境差异是否只以最小补丁写入 environments?(合并而非替换)
  2. 该队列的负载形态是波动的还是必须固定并发?波动选 auto,固定并发(如受限的视频处理队列)选 false + 明确 maxProcesses
  3. 打开自动伸缩后,是否已设置 balanceCooldown(建议 3~5 秒)与 balanceMaxShift 抑制抖动?
  4. 存在优先级需求时,是否拆成了多个命名 supervisor,而不是依赖 queue 数组顺序?
  5. 超时链是否满足 job timeout < supervisor timeout < retry_after(Coolify 中即 3600 < 39600 < 86400)?
  6. 重启 worker 前,是否用 horizon:manage 确认没有任务在跑?

以上全部路径与取值均可在 Coolify 仓库内直接对照验证,核心文件为 config/horizon.phpconfig/queue.phpapp/Providers/HorizonServiceProvider.phpapp/Console/Commands/HorizonManage.php

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