Skip to content

Laravel Horizon

简介

NOTE

在深入了解 Laravel Horizon 之前,你应该先熟悉 Laravel 的基础队列服务。Horizon 通过额外的功能增强了 Laravel 的队列,如果你还不熟悉 Laravel 提供的基本队列功能,这些额外功能可能会让人感到困惑。

Laravel Horizon 为你的 Laravel 驱动的 Redis 队列提供了一个美观的仪表盘和代码驱动的配置。Horizon 让你可以轻松监控队列系统的关键指标,如任务吞吐量、运行时间和任务失败情况。

使用 Horizon 时,所有队列 worker 配置都存储在一个简单的配置文件中。通过在版本控制文件中定义应用程序的 worker 配置,你可以在部署应用程序时轻松扩展或修改队列 worker。

安装

WARNING

Laravel Horizon 要求你使用 Redis 来驱动队列。因此,你应该确保在应用程序的 config/queue.php 配置文件中将队列连接设置为 redis。Horizon 目前不兼容 Redis Cluster。

你可以使用 Composer 包管理器将 Horizon 安装到项目中:

shell
composer require laravel/horizon

安装 Horizon 后,使用 horizon:install Artisan 命令发布其资源:

shell
php artisan horizon:install

配置

发布 Horizon 的资源后,其主要配置文件将位于 config/horizon.php。此配置文件允许你配置应用程序的队列 worker 选项。每个配置选项都包含其用途的描述,因此请务必仔细浏览此文件。

WARNING

Horizon 在内部使用名为 horizon 的 Redis 连接。此 Redis 连接名称是保留的,不应在 database.php 配置文件中分配给另一个 Redis 连接,也不应作为 horizon.php 配置文件中 use 选项的值。

环境

安装后,你应该熟悉的主要 Horizon 配置选项是 environments 配置选项。此配置选项是应用程序运行的环境数组,并为每个环境定义 worker 进程选项。默认情况下,此条目包含 productionlocal 环境。但是,你可以根据需要自由添加更多环境:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'maxProcesses' => 10,
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
        ],
    ],

    'local' => [
        'supervisor-1' => [
            'maxProcesses' => 3,
        ],
    ],
],

你还可以定义一个通配符环境(*),当没有找到其他匹配的环境时将使用它:

php
'environments' => [
    // ...

    '*' => [
        'supervisor-1' => [
            'maxProcesses' => 3,
        ],
    ],
],

启动 Horizon 时,它将使用应用程序正在运行的环境的 worker 进程配置选项。通常,环境由 APP_ENV 环境变量的值决定。例如,默认的 local Horizon 环境配置为启动三个 worker 进程,并自动平衡分配给每个队列的 worker 进程数量。默认的 production 环境配置为启动最多 10 个 worker 进程,并自动平衡分配给每个队列的 worker 进程数量。

WARNING

你应该确保 horizon 配置文件的 environments 部分包含你计划运行 Horizon 的每个环境的条目。

Supervisors

如你在 Horizon 的默认配置文件中所见,每个环境可以包含一个或多个「supervisors」。默认情况下,配置文件将此 supervisor 定义为 supervisor-1;但是,你可以自由地为 supervisor 命名。每个 supervisor 本质上负责「监督」一组 worker 进程,并负责在队列之间平衡 worker 进程。

如果你想定义一组应该在该环境中运行的新 worker 进程,你可以向给定环境添加额外的 supervisor。如果你想为应用程序使用的给定队列定义不同的平衡策略或 worker 进程数量,你可以选择这样做。

维护模式

当你的应用程序处于维护模式时,除非 Horizon 配置文件中 supervisor 的 force 选项被定义为 true,否则队列任务将不会被 Horizon 处理:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'force' => true,
        ],
    ],
],

默认值

在 Horizon 的默认配置文件中,你会注意到一个 defaults 配置选项。此配置选项指定应用程序 supervisors 的默认值。supervisor 的默认配置值将合并到每个环境的 supervisor 配置中,让你在定义 supervisor 时避免不必要的重复。

仪表盘授权

Horizon 仪表盘可以通过 /horizon 路由访问。默认情况下,你只能在 local 环境中访问此仪表盘。但是,在你的 app/Providers/HorizonServiceProvider.php 文件中,有一个授权门定义。此授权门控制在非本地环境中对 Horizon 的访问。你可以根据需要修改此门来限制对 Horizon 安装的访问:

php
/**
 * 注册 Horizon 门。
 *
 * 此门确定谁可以在非本地环境中访问 Horizon。
 */
protected function gate(): void
{
    Gate::define('viewHorizon', function (User $user) {
        return in_array($user->email, [
            'taylor@laravel.com',
        ]);
    });
}

替代身份验证策略

请记住,Laravel 会自动将经过身份验证的用户注入到门闭包中。如果你的应用程序通过其他方法(如 IP 限制)提供 Horizon 安全性,那么你的 Horizon 用户可能不需要「登录」。因此,你需要将上面的 function (User $user) 闭包签名更改为 function (User $user = null),以强制 Laravel 不要求身份验证。

最大任务尝试次数

NOTE

在调整这些选项之前,请确保你熟悉 Laravel 的默认队列服务和「尝试」的概念。

你可以在 supervisor 的配置中定义任务可以消耗的最大尝试次数:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'tries' => 10,
        ],
    ],
],

NOTE

此选项类似于使用 Artisan 命令处理队列时的 --tries 选项。

在使用 WithoutOverlappingRateLimited 等中间件时,调整 tries 选项至关重要,因为它们会消耗尝试次数。要处理此问题,请在 supervisor 级别或通过在任务类上定义 $tries 属性来调整 tries 配置值。

如果你不设置 tries 选项,Horizon 默认为单次尝试,除非任务类定义了 $tries,它优先于 Horizon 配置。

tries$tries 设置为 0 允许无限次尝试,这在尝试次数不确定时非常理想。为防止无尽的失败,你可以通过在任务类上设置 $maxExceptions 属性来限制允许的异常数量。

任务超时

同样,你可以在 supervisor 级别设置 timeout 值,该值指定 worker 进程在强制终止之前可以运行任务的秒数。终止后,任务将根据你的队列配置被重试或标记为失败:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'timeout' => 60,
        ],
    ],
],

WARNING

使用 auto 平衡策略时,Horizon 会在缩减期间将正在进行的 worker 视为「挂起」并在 Horizon 超时后强制终止它们。始终确保 Horizon 超时大于任何任务级别的超时,否则任务可能在执行过程中被终止。此外,timeout 值应始终比 config/queue.php 配置文件中定义的 retry_after 值至少短几秒。否则,你的任务可能会被处理两次。

任务退避

你可以在 supervisor 级别定义 backoff 值,以指定 Horizon 在重试遇到未处理异常的任务之前应等待多长时间:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'backoff' => 10,
        ],
    ],
],

你还可以通过使用数组作为 backoff 值来配置「指数」退避。在此示例中,第一次重试的延迟为 1 秒,第二次重试为 5 秒,第三次重试为 10 秒,如果还有更多剩余尝试,则每次后续重试为 10 秒:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'backoff' => [1, 5, 10],
        ],
    ],
],

静默任务

有时,你可能对查看应用程序或第三方包分发的某些任务不感兴趣。与其让这些任务占用「已完成任务」列表中的空间,你可以将它们静默。首先,将任务的类名添加到应用程序 horizon 配置文件中的 silenced 配置选项中:

php
'silenced' => [
    App\Jobs\ProcessPodcast::class,
],

除了静默单个任务类外,Horizon 还支持根据标签静默任务。如果你想隐藏共享相同标签的多个任务,这会很有用:

php
'silenced_tags' => [
    'notifications'
],

或者,你希望静默的任务可以实现 Laravel\Horizon\Contracts\Silenced 接口。如果任务实现了此接口,即使它不在 silenced 配置数组中,也会自动被静默:

php
use Laravel\Horizon\Contracts\Silenced;

class ProcessPodcast implements ShouldQueue, Silenced
{
    use Queueable;

    // ...
}

平衡策略

每个 supervisor 可以处理一个或多个队列,但与 Laravel 的默认队列系统不同,Horizon 允许你从三种 worker 平衡策略中选择:autosimplefalse

自动平衡

auto 策略是默认策略,它根据队列的当前工作负载调整每个队列的 worker 进程数量。例如,如果你的 notifications 队列有 1,000 个待处理任务,而你的 default 队列为空,Horizon 将为 notifications 队列分配更多 worker,直到队列为空。

使用 auto 策略时,你还可以配置 minProcessesmaxProcesses 配置选项:

  • minProcesses 定义每个队列的最小 worker 进程数。此值必须大于或等于 1。
  • maxProcesses 定义 Horizon 可以在所有队列中扩展到的最大 worker 进程总数。此值通常应大于队列数量乘以 minProcesses 值。要防止 supervisor 生成任何进程,你可以将此值设置为 0。

例如,你可以配置 Horizon 为每个队列维护至少一个进程,并扩展到总共 10 个 worker 进程:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['default', 'notifications'],
            'balance' => 'auto',
            'autoScalingStrategy' => 'time',
            'minProcesses' => 1,
            'maxProcesses' => 10,
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
        ],
    ],
],

autoScalingStrategy 配置选项决定 Horizon 如何为队列分配更多 worker 进程。你可以在两种策略之间选择:

  • time 策略将根据清空队列所需的总估计时间分配 worker。
  • size 策略将根据队列上的任务总数分配 worker。

balanceMaxShiftbalanceCooldown 配置值决定 Horizon 扩展以满足 worker 需求的速度。在上面的示例中,每三秒最多创建或销毁一个新进程。你可以根据应用程序的需要自由调整这些值。

队列优先级和自动平衡

使用 auto 平衡策略时,Horizon 不会在队列之间强制执行严格的优先级。supervisor 配置中队列的顺序不会影响 worker 进程的分配方式。相反,Horizon 依赖所选的 autoScalingStrategy 根据队列负载动态分配 worker 进程。

例如,在以下配置中,尽管 high 队列出现在列表的第一位,但它并不比 default 队列优先:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'queue' => ['high', 'default'],
            'minProcesses' => 1,
            'maxProcesses' => 10,
        ],
    ],
],

如果你需要在队列之间强制执行相对优先级,你可以定义多个 supervisor 并显式分配处理资源:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'queue' => ['default'],
            'minProcesses' => 1,
            'maxProcesses' => 10,
        ],
        'supervisor-2' => [
            // ...
            'queue' => ['images'],
            'minProcesses' => 1,
            'maxProcesses' => 1,
        ],
    ],
],

在此示例中,default 队列可以扩展到 10 个进程,而 images 队列限制为一个进程。此配置确保你的队列可以独立扩展。

NOTE

分发资源密集型任务时,有时最好将它们分配到一个具有有限 maxProcesses 值的专用队列。否则,这些任务可能会消耗过多的 CPU 资源并使你的系统过载。

简单平衡

simple 策略在指定的队列之间均匀分配 worker 进程。使用此策略时,Horizon 不会自动扩展 worker 进程数量。相反,它使用固定数量的进程:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'queue' => ['default', 'notifications'],
            'balance' => 'simple',
            'processes' => 10,
        ],
    ],
],

在上面的示例中,Horizon 将为每个队列分配 5 个进程,将总共 10 个进程均匀分配。

如果你想单独控制分配给每个队列的 worker 进程数量,你可以定义多个 supervisor:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'queue' => ['default'],
            'balance' => 'simple',
            'processes' => 10,
        ],
        'supervisor-notifications' => [
            // ...
            'queue' => ['notifications'],
            'balance' => 'simple',
            'processes' => 2,
        ],
    ],
],

使用此配置,Horizon 将为 default 队列分配 10 个进程,为 notifications 队列分配 2 个进程。

无平衡

balance 选项设置为 false 时,Horizon 严格按照队列列出的顺序处理它们,类似于 Laravel 的默认队列系统。但是,如果任务开始积累,它仍然会扩展 worker 进程的数量:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            // ...
            'queue' => ['default', 'notifications'],
            'balance' => false,
            'minProcesses' => 1,
            'maxProcesses' => 10,
        ],
    ],
],

在上面的示例中,default 队列中的任务始终优先于 notifications 队列中的任务。例如,如果 default 中有 1,000 个任务而 notifications 中只有 10 个,Horizon 将在处理 notifications 中的任何任务之前完全处理所有 default 任务。

你可以使用 minProcessesmaxProcesses 选项控制 Horizon 扩展 worker 进程的能力:

  • minProcesses 定义 worker 进程的最小总数。此值必须大于或等于 1。
  • maxProcesses 定义 Horizon 可以扩展到的最大 worker 进程总数。

升级 Horizon

升级到 Horizon 的新主要版本时,务必仔细查看升级指南

运行 Horizon

在应用程序的 config/horizon.php 配置文件中配置 supervisor 和 worker 后,你可以使用 horizon Artisan 命令启动 Horizon。此单个命令将为当前环境启动所有已配置的 worker 进程:

shell
php artisan horizon

你可以使用 horizon:pausehorizon:continue Artisan 命令暂停 Horizon 进程并指示其继续处理任务:

shell
php artisan horizon:pause

php artisan horizon:continue

你还可以使用 horizon:pause-supervisorhorizon:continue-supervisor Artisan 命令暂停和继续特定的 Horizon supervisors

shell
php artisan horizon:pause-supervisor supervisor-1

php artisan horizon:continue-supervisor supervisor-1

你可以使用 horizon:status Artisan 命令检查 Horizon 进程的当前状态:

shell
php artisan horizon:status

你可以使用 horizon:supervisor-status Artisan 命令检查特定 Horizon supervisor 的当前状态:

shell
php artisan horizon:supervisor-status supervisor-1

你可以使用 horizon:terminate Artisan 命令优雅地终止 Horizon 进程。当前正在处理的任何任务都将完成,然后 Horizon 将停止执行:

shell
php artisan horizon:terminate

自动重启 Horizon

在本地开发期间,你可以运行 horizon:listen 命令。使用 horizon:listen 命令时,当你想要重新加载更新后的代码时,不必手动重启 Horizon。使用此功能之前,你应该确保在本地开发环境中安装了 Node。此外,你应该在项目中安装 Chokidar 文件监视库:

shell
npm install --save-dev chokidar

安装 Chokidar 后,你可以使用 horizon:listen 命令启动 Horizon:

shell
php artisan horizon:listen

在 Docker 或 Vagrant 中运行时,你应该使用 --poll 选项:

shell
php artisan horizon:listen --poll

你可以使用应用程序 config/horizon.php 配置文件中的 watch 配置选项来配置应该被监视的目录和文件:

php
'watch' => [
    'app',
    'bootstrap',
    'config',
    'database',
    'public/**/*.php',
    'resources/**/*.php',
    'routes',
    'composer.lock',
    '.env',
],

部署 Horizon

当你准备将 Horizon 部署到应用程序的实际服务器时,你应该配置一个进程监控器来监控 php artisan horizon 命令,并在它意外退出时重新启动它。别担心,我们将在下面讨论如何安装进程监控器。

在应用程序的部署过程中,你应该指示 Horizon 进程终止,以便它将被进程监控器重新启动并接收你的代码更改:

shell
php artisan horizon:terminate

安装 Supervisor

Supervisor 是 Linux 操作系统的进程监控器,如果 horizon 进程停止执行,它将自动重新启动。要在 Ubuntu 上安装 Supervisor,你可以使用以下命令。如果你没有使用 Ubuntu,你可以使用操作系统的包管理器来安装 Supervisor:

shell
sudo apt-get install supervisor

NOTE

如果自行配置 Supervisor 听起来很复杂,可以考虑使用 Laravel Cloud,它可以管理你的 Laravel 应用程序的后台进程。

Supervisor 配置

Supervisor 配置文件通常存储在服务器的 /etc/supervisor/conf.d 目录中。在此目录中,你可以创建任意数量的配置文件来指示 Supervisor 如何监控你的进程。例如,让我们创建一个启动和监控 horizon 进程的 horizon.conf 文件:

ini
[program:horizon]
process_name=%(program_name)s
command=php /home/forge/example.com/artisan horizon
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/home/forge/example.com/horizon.log
stopwaitsecs=3600

定义 Supervisor 配置时,你应该确保 stopwaitsecs 的值大于运行时间最长的任务所消耗的秒数。否则,Supervisor 可能会在任务完成处理之前将其终止。

WARNING

虽然上面的示例对基于 Ubuntu 的服务器有效,但 Supervisor 配置文件的位置和文件扩展名在其他服务器操作系统之间可能有所不同。请查阅你的服务器文档以获取更多信息。

启动 Supervisor

创建配置文件后,你可以使用以下命令更新 Supervisor 配置并启动被监控的进程:

shell
sudo supervisorctl reread

sudo supervisorctl update

sudo supervisorctl start horizon

NOTE

有关运行 Supervisor 的更多信息,请查阅 Supervisor 文档

标签

Horizon 允许你为任务分配「标签」,包括 mailables、广播事件、通知和队列事件监听器。事实上,Horizon 会根据附加到任务的 Eloquent 模型智能地自动标记大多数任务。例如,看看以下任务:

php
<?php

namespace App\Jobs;

use App\Models\Video;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class RenderVideo implements ShouldQueue
{
    use Queueable;

    /**
     * 创建新的任务实例。
     */
    public function __construct(
        public Video $video,
    ) {}

    /**
     * 执行任务。
     */
    public function handle(): void
    {
        // ...
    }
}

如果此任务使用 id 属性为 1App\Models\Video 实例入队,它将自动接收标签 App\Models\Video:1。这是因为 Horizon 会搜索任务的属性以查找任何 Eloquent 模型。如果找到 Eloquent 模型,Horizon 将使用模型的类名和主键智能地标记任务:

php
use App\Jobs\RenderVideo;
use App\Models\Video;

$video = Video::find(1);

RenderVideo::dispatch($video);

手动标记任务

如果你想手动定义可队列对象的标签,你可以在类上定义一个 tags 方法:

php
class RenderVideo implements ShouldQueue
{
    /**
     * 获取应分配给任务的标签。
     *
     * @return array<int, string>
     */
    public function tags(): array
    {
        return ['render', 'video:'.$this->video->id];
    }
}

手动标记事件监听器

获取队列事件监听器的标签时,Horizon 会自动将事件实例传递给 tags 方法,允许你将事件数据添加到标签中:

php
class SendRenderNotifications implements ShouldQueue
{
    /**
     * 获取应分配给监听器的标签。
     *
     * @return array<int, string>
     */
    public function tags(VideoRendered $event): array
    {
        return ['video:'.$event->video->id];
    }
}

通知

WARNING

配置 Horizon 发送 Slack 或 SMS 通知时,你应该查看相关通知渠道的先决条件

如果你希望在某个队列的等待时间过长时收到通知,你可以使用 Horizon::routeMailNotificationsToHorizon::routeSlackNotificationsToHorizon::routeSmsNotificationsTo 方法。你可以在应用程序的 App\Providers\HorizonServiceProviderboot 方法中调用这些方法:

php
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    parent::boot();

    Horizon::routeSmsNotificationsTo('15556667777');
    Horizon::routeMailNotificationsTo('example@example.com');
    Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
}

配置通知等待时间阈值

你可以在应用程序的 config/horizon.php 配置文件中配置多少秒被视为「长时间等待」。此文件中的 waits 配置选项允许你控制每个连接/队列组合的长等待阈值。任何未定义的连接/队列组合将默认为 60 秒的长等待阈值:

php
'waits' => [
    'redis:critical' => 30,
    'redis:default' => 60,
    'redis:batch' => 120,
],

将队列的阈值设置为 0 将禁用该队列的长等待通知。

指标

Horizon 包含一个指标仪表盘,提供有关你的任务和队列等待时间和吞吐量的信息。为了填充此仪表盘,你应该在应用程序的 routes/console.php 文件中配置 Horizon 的 snapshot Artisan 命令每五分钟运行一次:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('horizon:snapshot')->everyFiveMinutes();

如果你想删除所有指标数据,你可以调用 horizon:clear-metrics Artisan 命令:

shell
php artisan horizon:clear-metrics

删除失败的任务

如果你想删除一个失败的任务,你可以使用 horizon:forget 命令。horizon:forget 命令接受失败任务的 ID 或 UUID 作为唯一参数:

shell
php artisan horizon:forget 5

如果你想删除所有失败的任务,你可以向 horizon:forget 命令提供 --all 选项:

shell
php artisan horizon:forget --all

从队列中清除任务

如果你想从应用程序的默认队列中删除所有任务,你可以使用 horizon:clear Artisan 命令:

shell
php artisan horizon:clear

你可以提供 queue 选项从特定队列中删除任务:

shell
php artisan horizon:clear --queue=emails