跳转至内容

Laravel Horizon

简介

在深入研究 Laravel Horizon 之前,您应该先熟悉 Laravel 的基础 队列服务。Horizon 在 Laravel 队列的基础上增加了一些额外功能,如果您还不熟悉 Laravel 提供的基础队列功能,可能会感到困惑。

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

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

安装

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

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

1composer require laravel/horizon

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

1php artisan horizon:install

配置

发布 Horizon 资源后,其主要配置文件将位于 config/horizon.php。此配置文件允许您为应用程序配置队列工作进程选项。每个配置选项都包含了其用途说明,因此请务必仔细阅读该文件。

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

环境

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

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 'maxProcesses' => 10,
5 'balanceMaxShift' => 1,
6 'balanceCooldown' => 3,
7 ],
8 ],
9 
10 'local' => [
11 'supervisor-1' => [
12 'maxProcesses' => 3,
13 ],
14 ],
15],

您还可以定义一个通配符环境 (*),它将在找不到其他匹配环境时被使用。

1'environments' => [
2 // ...
3 
4 '*' => [
5 'supervisor-1' => [
6 'maxProcesses' => 3,
7 ],
8 ],
9],

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

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

Supervisor(监督者)

正如您在 Horizon 默认配置文件中看到的那样,每个环境可以包含一个或多个“supervisor”。默认情况下,配置文件将此 supervisor 定义为 supervisor-1;不过,您可以随意命名您的 supervisor。每个 supervisor 本质上负责“监督”一组工作进程,并处理跨队列的工作进程均衡。

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

维护模式

当您的应用程序处于 维护模式 时,除非在 Horizon 配置文件中将 supervisor 的 force 选项设置为 true,否则排队任务将不会被 Horizon 处理。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'force' => true,
6 ],
7 ],
8],

默认值

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

仪表盘授权

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

1/**
2 * Register the Horizon gate.
3 *
4 * This gate determines who can access Horizon in non-local environments.
5 */
6protected function gate(): void
7{
8 Gate::define('viewHorizon', function (User $user) {
9 return in_array($user->email, [
11 ]);
12 });
13}

替代认证策略

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

最大任务尝试次数

在优化这些选项之前,请确保您熟悉 Laravel 默认的 队列服务 和“尝试次数(attempts)”的概念。

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

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'tries' => 10,
6 ],
7 ],
8],

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

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

如果您未设置 tries 选项,Horizon 默认只会尝试一次,除非任务类定义了 $tries,后者优先于 Horizon 配置。

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

任务超时

同样,您可以在 supervisor 级别设置 timeout 值,它指定了工作进程在强制终止前可以运行任务的秒数。一旦终止,任务将根据您的队列配置进行重试或标记为失败。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...¨
5 'timeout' => 60,
6 ],
7 ],
8],

使用 auto 均衡策略时,Horizon 会将处理中的工作进程视为“挂起”,并在缩容期间超过 Horizon 超时时间后强制杀死它们。请始终确保 Horizon 超时时间大于任何任务级的超时时间,否则任务可能会在执行过程中被终止。此外,timeout 值应始终比 config/queue.php 配置文件中定义的 retry_after 值短几秒。否则,您的任务可能会被处理两次。

任务退避(延迟重试)

您可以在 supervisor 级别定义 backoff 值,以指定 Horizon 在重试遇到未捕获异常的任务之前应等待的时间。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'backoff' => 10,
6 ],
7 ],
8],

您还可以通过使用数组来为 backoff 值配置“指数”退避。在此示例中,第一次重试将延迟 1 秒,第二次重试延迟 5 秒,第三次重试延迟 10 秒,如果还有剩余尝试次数,则随后的每次重试均延迟 10 秒。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'backoff' => [1, 5, 10],
6 ],
7 ],
8],

静默任务

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

1'silenced' => [
2 App\Jobs\ProcessPodcast::class,
3],

除了静默单个任务类外,Horizon 还支持基于 标签 静默任务。如果您想隐藏共享同一标签的多个任务,这将非常有用。

1'silenced_tags' => [
2 'notifications'
3],

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

1use Laravel\Horizon\Contracts\Silenced;
2 
3class ProcessPodcast implements ShouldQueue, Silenced
4{
5 use Queueable;
6 
7 // ...
8}

均衡策略

每个 supervisor 可以处理一个或多个队列,但与 Laravel 的默认队列系统不同,Horizon 允许您在三种工作进程均衡策略中进行选择:autosimplefalse

自动均衡

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

使用 auto 策略时,您还可以配置 minProcessesmaxProcesses 配置选项。

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

例如,您可以将 Horizon 配置为每个队列至少保持一个进程,并最多扩展到总共 10 个工作进程。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 'connection' => 'redis',
5 'queue' => ['default', 'notifications'],
6 'balance' => 'auto',
7 'autoScalingStrategy' => 'time',
8 'minProcesses' => 1,
9 'maxProcesses' => 10,
10 'balanceMaxShift' => 1,
11 'balanceCooldown' => 3,
12 ],
13 ],
14],

autoScalingStrategy 配置选项决定了 Horizon 将如何为队列分配更多工作进程。您可以在两种策略之间进行选择:

  • time 策略将根据清除队列所需的预计总时间来分配工作进程。
  • size 策略将根据队列中的任务总数来分配工作进程。

balanceMaxShiftbalanceCooldown 配置值决定了 Horizon 适应工作负载需求的响应速度。在上面的示例中,每三秒钟最多创建一个或销毁一个新进程。您可以根据应用程序的需求自由调整这些值。

队列优先级与自动均衡

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

例如,在以下配置中,尽管 high 队列在列表中排在首位,但它并不优先于 default 队列。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'queue' => ['high', 'default'],
6 'minProcesses' => 1,
7 'maxProcesses' => 10,
8 ],
9 ],
10],

如果您需要强制执行队列之间的相对优先级,可以定义多个 supervisor 并明确分配处理资源。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'queue' => ['default'],
6 'minProcesses' => 1,
7 'maxProcesses' => 10,
8 ],
9 'supervisor-2' => [
10 // ...
11 'queue' => ['images'],
12 'minProcesses' => 1,
13 'maxProcesses' => 1,
14 ],
15 ],
16],

在此示例中,默认的 queue 最多可扩展到 10 个进程,而 images 队列被限制为一个进程。此配置确保了您的队列可以独立扩展。

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

简单均衡

simple 策略将工作进程均匀分布在指定的队列中。使用此策略,Horizon 不会自动缩放工作进程的数量。相反,它使用固定数量的进程。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'queue' => ['default', 'notifications'],
6 'balance' => 'simple',
7 'processes' => 10,
8 ],
9 ],
10],

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

如果您想单独控制分配给每个队列的工作进程数量,可以定义多个 supervisor。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'queue' => ['default'],
6 'balance' => 'simple',
7 'processes' => 10,
8 ],
9 'supervisor-notifications' => [
10 // ...
11 'queue' => ['notifications'],
12 'balance' => 'simple',
13 'processes' => 2,
14 ],
15 ],
16],

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

不均衡

balance 选项设置为 false 时,Horizon 将严格按照队列在配置中列出的顺序处理队列,类似于 Laravel 的默认队列系统。但是,如果任务开始积压,它仍然会缩放工作进程的数量。

1'environments' => [
2 'production' => [
3 'supervisor-1' => [
4 // ...
5 'queue' => ['default', 'notifications'],
6 'balance' => false,
7 'minProcesses' => 1,
8 'maxProcesses' => 10,
9 ],
10 ],
11],

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

您可以使用 minProcessesmaxProcesses 选项来控制 Horizon 缩放工作进程的能力。

  • minProcesses 定义了总的最少工作进程数。该值必须大于或等于 1。
  • maxProcesses 定义了 Horizon 可以缩放到的最大总工作进程数。

升级 Horizon

升级到 Horizon 的新大版本时,务必仔细阅读 升级指南

运行 Horizon

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

1php artisan horizon

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

1php artisan horizon:pause
2 
3php artisan horizon:continue

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

1php artisan horizon:pause-supervisor supervisor-1
2 
3php artisan horizon:continue-supervisor supervisor-1

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

1php artisan horizon:status

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

1php artisan horizon:supervisor-status supervisor-1

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

1php artisan horizon:terminate

自动重启 Horizon

在本地开发过程中,您可以运行 horizon:listen 命令。使用 horizon:listen 命令时,您无需在想要重新加载更新后的代码时手动重启 Horizon。在使用此功能之前,请确保您的本地开发环境中已安装 Node。此外,您还应该在项目中安装 Chokidar 文件监视库。

1npm install --save-dev chokidar

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

1php artisan horizon:listen

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

1php artisan horizon:listen --poll

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

1'watch' => [
2 'app',
3 'bootstrap',
4 'config',
5 'database',
6 'public/**/*.php',
7 'resources/**/*.php',
8 'routes',
9 'composer.lock',
10 '.env',
11],

部署 Horizon

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

在应用程序的部署过程中,您应该指示 Horizon 进程终止,以便它能被进程监视器重启并接收您的代码更改。

1php artisan horizon:terminate

安装 Supervisor

Supervisor 是 Linux 操作系统的进程监视器,如果 horizon 进程停止执行,它将自动重启该进程。要在 Ubuntu 上安装 Supervisor,您可以使用以下命令。如果您不使用 Ubuntu,通常可以使用操作系统的包管理器来安装 Supervisor。

1sudo apt-get install supervisor

如果您觉得自行配置 Supervisor 有困难,可以考虑使用 Laravel Cloud,它可以为您管理 Laravel 应用程序的后台进程。

Supervisor 配置

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

1[program:horizon]
2process_name=%(program_name)s
3command=php /home/forge/example.com/artisan horizon
4autostart=true
5autorestart=true
6user=forge
7redirect_stderr=true
8stdout_logfile=/home/forge/example.com/horizon.log
9stopwaitsecs=3600

定义 Supervisor 配置时,请确保 stopwaitsecs 的值大于您运行时间最长的任务所花费的秒数。否则,Supervisor 可能会在任务处理完成之前将其杀死。

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

启动 Supervisor

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

1sudo supervisorctl reread
2 
3sudo supervisorctl update
4 
5sudo supervisorctl start horizon

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

标签

Horizon 允许您为任务分配“标签”,包括可邮件对象、广播事件、通知和排队事件监听器。事实上,Horizon 会根据附加到任务的 Eloquent 模型智能且自动地为大多数任务添加标签。例如,看看下面这个任务。

1<?php
2 
3namespace App\Jobs;
4 
5use App\Models\Video;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Foundation\Queue\Queueable;
8 
9class RenderVideo implements ShouldQueue
10{
11 use Queueable;
12 
13 /**
14 * Create a new job instance.
15 */
16 public function __construct(
17 public Video $video,
18 ) {}
19 
20 /**
21 * Execute the job.
22 */
23 public function handle(): void
24 {
25 // ...
26 }
27}

如果此任务与一个 id 属性为 1App\Models\Video 实例一起排队,它将自动获得 App\Models\Video:1 标签。这是因为 Horizon 会在任务属性中搜索任何 Eloquent 模型。如果找到 Eloquent 模型,Horizon 将使用模型的类名和主键智能地为任务添加标签。

1use App\Jobs\RenderVideo;
2use App\Models\Video;
3 
4$video = Video::find(1);
5 
6RenderVideo::dispatch($video);

手动为任务添加标签

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

1class RenderVideo implements ShouldQueue
2{
3 /**
4 * Get the tags that should be assigned to the job.
5 *
6 * @return array<int, string>
7 */
8 public function tags(): array
9 {
10 return ['render', 'video:'.$this->video->id];
11 }
12}

手动为事件监听器添加标签

检索排队事件监听器的标签时,Horizon 会自动将事件实例传递给 tags 方法,允许您将事件数据添加到标签中。

1class SendRenderNotifications implements ShouldQueue
2{
3 /**
4 * Get the tags that should be assigned to the listener.
5 *
6 * @return array<int, string>
7 */
8 public function tags(VideoRendered $event): array
9 {
10 return ['video:'.$event->video->id];
11 }
12}

通知

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

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

1/**
2 * Bootstrap any application services.
3 */
4public function boot(): void
5{
6 parent::boot();
7 
8 Horizon::routeSmsNotificationsTo('15556667777');
9 Horizon::routeMailNotificationsTo('[email protected]');
10 Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
11}

配置通知等待时间阈值

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

1'waits' => [
2 'redis:critical' => 30,
3 'redis:default' => 60,
4 'redis:batch' => 120,
5],

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

指标

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

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('horizon:snapshot')->everyFiveMinutes();

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

1php artisan horizon:clear-metrics

删除失败任务

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

1php artisan horizon:forget 5

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

1php artisan horizon:forget --all

从队列中清除任务

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

1php artisan horizon:clear

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

1php artisan horizon:clear --queue=emails