Laravel Horizonでジョブキュー監視・運用を強化する:導入から本番運用・障害対応まで

Laravel Horizonでジョブキュー監視・運用を強化する:導入から本番運用・障害対応まで

Laravel Horizonは、Redisベースのキューを「見える化」し、ワーカーのプロセス管理・スループット監視・失敗ジョブの追跡を一つにまとめる運用基盤。キュー処理が増えるほど、ログだけでは追いづらい「滞留」「再試行の嵐」「特定キューだけ遅い」「ワーカーが落ちている」などが発生しやすい。Horizonを入れると、待ち行列の状態・実行時間・失敗の傾向・プロセス構成を短時間で判断でき、復旧手順も定型化しやすくなる。

Horizonの前提:Redisキューが必須

Horizonは Redis ドライバ向けの監視・管理ツール。database/sqs 等を使っている場合、そのままではHorizonの恩恵が出ない。まずはキュー接続をRedisに寄せる。

# .env(例)
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis

発生条件(導入時につまずきやすい例):
・QUEUE_CONNECTION が database のまま → Horizon画面は出ても期待した監視にならない
・Redis拡張が無い/接続できない → ワーカーが起動しない、処理が流れない

インストールと初期セットアップ(最短手順)

基本は composer で追加し、設定ファイルを公開して管理する。

composer require laravel/horizon

php artisan horizon:install

php artisan migrate

※ migrate は Horizon用テーブルが必要な構成(例:failed_jobs等)を含む場合があるため、プロジェクトの状態に合わせて実行。

基本の起動:ローカルで動作確認する

まずはローカルで「ジョブを投げる→処理される→ダッシュボードで見える」を通す。

# Horizon起動(開発)
php artisan horizon

ブラウザでは通常 /horizon にアクセス(ルーティングはHorizonが提供)。

キュー・接続・リトライの基本:Horizonに載せる前に整える

ジョブの滞留や失敗は「timeout」「tries」「バックオフ」「例外設計」でかなり変わる。Horizonは監視・管理であって、ジョブ設計が弱いと結局荒れる。

// 例:ジョブ側でリトライ/タイムアウトを制御
class ImportUsers implements ShouldQueue
{
    public int $tries = 3;
    public int $timeout = 120;

    public function backoff(): array
    {
        return [10, 30, 60];
    }

    public function handle(): void
    {
        // 処理
    }
}

発生条件(失敗が増えやすい例):
・外部API呼び出しをタイムアウト無しで実行 → ワーカー占有→滞留
・リトライ間隔が短すぎる → 失敗の連打でスループット低下

horizon.phpの読み方:supervisor と balancing を理解する

config/horizon.php は Horizonの心臓部。基本は「どのキューを」「何プロセスで」「どの戦略で」回すかを決める。

# 設定を公開済みの場合
# config/horizon.php を編集

重要ポイント:
・supervisor:ワーカープロセス群の定義(キュー、プロセス数、メモリ制限など)
・balancing:キューの負荷に応じてプロセスを配分する考え方(環境による)
・timeout / tries / sleep:ワーカー側の動作パラメータ

環境別設定:local/staging/production でプロセス数を分ける

開発は少数、ステージングは検証用、本番は負荷に合わせて増やす。環境別に supervisor 設定を分けておくと、デプロイ後の調整が速い。

// config/horizon.php(概念例)
'environments' => [
    'local' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'simple',
            'processes' => 1,
            'tries' => 1,
        ],
    ],

    'production' => [
        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['default', 'emails'],
            'balance' => 'auto',
            'processes' => 10,
            'tries' => 3,
            'timeout' => 120,
        ],
        'supervisor-long' => [
            'connection' => 'redis',
            'queue' => ['long'],
            'balance' => 'simple',
            'processes' => 3,
            'tries' => 2,
            'timeout' => 600,
        ],
    ],
],

運用上のコツ:
・重いジョブ用キュー(long等)を分け、defaultの遅延を防ぐ
・emails/notifications なども分けると遅延切り分けが楽

ダッシュボードの見どころ:滞留・失敗・実行時間を短時間で判断する

Horizonでまず見るべきもの:
・Queues:キューごとの待ち数(Backlog)と処理速度
・Jobs:どの種類のジョブが多いか、どれが遅いか
・Failed Jobs:失敗が特定ジョブに偏っていないか
・Supervisors/Workers:プロセスが落ちていないか、メモリが伸びていないか
監視の軸を「キュー」「ジョブ種別」「プロセス」に分けておくと原因特定が速い。

アクセス制御:/horizon を本番で安全に公開する

Horizonは運用情報の塊なので、必ずアクセス制限する。基本は環境で切り替え、認証済みユーザーや特定IPのみ許可に寄せる。

// app/Providers/HorizonServiceProvider.php(概念例)
use Laravel\Horizon\Horizon;

public function boot(): void
{
    Horizon::auth(function ($request) {
        // 例:本番は管理者のみ
        return auth()->check() && auth()->user()->is_admin;
    });
}

発生条件(セキュリティ事故):
・/horizon を無制限公開 → キュー状況や内部情報が漏れる

本番常駐:Supervisor(systemd)でHorizonをプロセス管理する

本番では php artisan horizon を手動実行しない。プロセスマネージャで自動起動・自動再起動にする。

; /etc/supervisor/conf.d/horizon.conf(例)
[program:horizon]
process_name=%(program_name)s
command=php /var/www/html/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/horizon.log
stopwaitsecs=3600

デプロイ時は、コード更新後に Horizon を適切に再読み込みする(後述)。

デプロイ手順:設定変更を反映し、古いワーカーを安全に落とす

Horizonはプロセスを抱えるため、設定変更やコード更新を「安全に反映」する手順が必要。

# Horizonに再起動指示(ワーカーを段階的に入れ替える)
php artisan horizon:terminate

発生条件(デプロイ後に古いコードが動き続ける):
・terminate を打たず、常駐プロセスが旧コードのまま処理を継続
・config:cache を更新したのに supervisor 設定が反映されない

失敗ジョブの扱い:原因特定→再実行→再発防止をループで回す

失敗ジョブは「再試行」だけでは収束しない。
・例外の種類(認証切れ、外部API障害、データ不整合)を分類
・再試行して良い失敗/ダメな失敗を分ける
・冪等性(同じジョブが2回走っても壊れない)を担保する
・失敗を通知(Slack/メール)し、一定閾値で止血できるようにする

# failed_jobs の再実行(標準の仕組みを使う例)
php artisan queue:retry all

よくあるエラーと発生条件:Horizonが動かない/見えない/処理されない

・Horizonの画面は出るが処理されない
発生条件:ワーカー未起動、QUEUE_CONNECTIONがredisではない、supervisor設定ミス
・Redis接続エラー(Connection refused / timed out)
発生条件:REDIS_HOST/PORT/認証不整合、ネットワーク、Redis停止、TLS要否ズレ
・ジョブが途中で落ちる(timeout / memory)
発生条件:timeoutが短い、外部I/Oが遅い、巨大データをメモリに載せる
・失敗が連発してキューが増え続ける
発生条件:外部API障害、レート制限、データ不整合、リトライ間隔が短すぎる
・特定キューだけ詰まる
発生条件:重いジョブと軽いジョブを同じキューに混在、プロセス配分が不足

実務で効く設計:キュー分割・優先度・重い処理の隔離

おすすめの切り方:
・default:軽量(通知、更新、短い計算)
・emails:メール送信
・long:重い集計、外部連携、ファイル生成
・critical:即時性が高い処理(ただし濫用しない)
キューを分けるだけで「重いジョブが全体を止める」を回避でき、Horizonの監視も読みやすくなる。

サンプル:ジョブ投入→キュー分割→監視の流れ

コントローラ等からジョブを投入し、キュー名を明示する。

use App\Jobs\ImportUsers;

ImportUsers::dispatch($tenantId)->onQueue('long');

use App\Jobs\SendInvoiceMail;

SendInvoiceMail::dispatch($invoiceId)->onQueue('emails');

これにより、Horizon上で longemails の滞留や失敗を分離して追える。

まとめ:Horizonを運用に組み込むための要点

・Redisキュー前提を満たし、接続設定を固定する
・supervisorでキューとプロセス設計を分け、重い処理を隔離する
・/horizon は必ずアクセス制御し、本番はプロセスマネージャで常駐させる
・デプロイ時は horizon:terminate を手順化し、旧コード残留を防ぐ
・失敗ジョブは分類・冪等性・再試行設計までセットで回し、再発を潰す