404 Not Found

404 Not Found


nginx

Laravelキューと非同期タスク

キューはLaravelの「バックグラウンドチーム」です—時間のかかるタスクをキューに回して非同期処理することで, ユーザーは待機する必要がなく, リクエストが即座に返されます。

1. 学ぶこと


2. ユーザーのストーリー

(1) 痛み:レポートのエクスポートがシステム全体をフリーズさせる

BobがShopMetrics管理パネルで「月次レポートをエクスポート」をクリックしました—10万件の注文レコードを含むCSVファイルの生成に30秒かかり, その間ページはずっとロード中のまま, Aliceも同時にダッシュボードにアクセスして動作が遅くなりました。さらに悪いことに, Bobが5つのレポートエクスポートを同時にトリガーし, PHPプロセスプールを使い果たしてサイト全体が502エラーを返しました。

(2) キューによる解決策

キューは時間のかかるタスクをバックグラウンドに送ります—ユーザーが「エクスポート」をクリックすると「レポートを生成中」というメッセージがすぐに表示され, ジョブはバックグラウンドでゆっくり実行されます。完了するとダウンロードリンク付きのメールが送信されます。

PHP
// 同期 — 30秒ブロック
$csv = ReportService::generateMonthlyReport($tenant);

// 非同期 — 即座に返却, ジョブはバックグラウンドで実行
GenerateReportJob::dispatch($tenant, 'monthly');
// ユーザーに表示:「レポートを生成中です。準備ができたらメールでお知らせします。」

(3) 成果

Bobがキューを実装した後, レポートエクスポートリクエストは100ミリ秒以内に返されるようになり, バックグラウンドジョブはスムーズに実行され, Aliceのダッシュボードもフリーズしなくなりました。


3. キュードライバー設定

(1) ドライバーの比較

ドライバー 永続性 パフォーマンス 適用用途 コスト
sync ❌ 即時実行 最速 開発/テスト 無料
database ✅ DBテーブル 遅い 小規模プロジェクト 無料
redis ✅ メモリ 高速 本番環境 Redis
sqs ✅ AWS 高い 大規模 従量課金
beanstalkd ✅ 専用 高速 中規模 無料

(2) 設定

BASH
# .env
QUEUE_CONNECTION=redis

# Redis接続
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PHP
// config/queue.php
'connections' => [
    'database' => [
        'driver' => 'database',
        'table' => 'jobs',
        'queue' => 'default',
        'retry_after' => 90,
    ],
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
        'queue' => '{default}',
        'retry_after' => 90,
        'block_for' => null,
    ],
],

(3) チームリストの作成

BASH
# databaseドライバーの場合
php artisan queue:table
php artisan queue:failed-table
php artisan migrate

# 作成されるもの:jobsテーブル + failed_jobsテーブル

(1) ▶ サンプル:ShopMetricsキュー設定

BASH
# .env — 開発環境
QUEUE_CONNECTION=database

# .env — 本番環境
QUEUE_CONNECTION=redis
REDIS_HOST=redis.shopmetrics.internal
REDIS_PORT=6379

# キューテーブルの作成 (databaseドライバー)
php artisan queue:table
php artisan queue:failed-table
php artisan queue:batches-table
php artisan migrate

出力:

TEXT
# コマンド実行成功

4. ジョブの作成とディスパッチ

(1) ジョブの作成

BASH
php artisan make:job GenerateReportJob
# 作成される:app/Jobs/GenerateReportJob.php

(2) ジョブの定義

PHP
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public bool $deleteWhenMissingModels = true;

    public function __construct(
        public Tenant $tenant,
        public string $reportType,
        public string $format = 'csv',
    ) {}

    public function handle(
        ReportService $reportService,
        Mailer $mailer,
    ): void {
        $path = $reportService->generate(
            $this->tenant,
            $this->reportType,
            $this->format,
        );

        $url = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));

        $this->tenant->users->each(function ($user) use ($url) {
            $mailer->to($user)->send(new ReportReadyNotification($url, $this->reportType));
        });
    }

    public function failed(\Throwable $exception): void
    {
        Log::error('Report generation failed', [
            'tenant_id' => $this->tenant->id,
            'report_type' => $this->reportType,
            'error' => $exception->getMessage(),
        ]);
    }
}

(3) ジョブのディスパッチ

PHP
// 基本的なディスパッチ
GenerateReportJob::dispatch($tenant, 'monthly');

// 遅延ディスパッチ
GenerateReportJob::dispatch($tenant, 'monthly')
    ->delay(now()->addMinutes(5));

// 特定のキューにディスパッチ
GenerateReportJob::dispatch($tenant, 'monthly')
    ->onQueue('reports');

// 条件を満たす場合のみディスパッチ
GenerateReportJob::dispatchIf($tenant->subscription?->isActive(), $tenant, 'monthly');

// レスポンス送信後にディスパッチ
GenerateReportJob::dispatchAfterResponse($tenant, 'monthly');
ディスパッチ方法 説明 適用シナリオ
dispatch() キューに登録 一般的な非同期タスク
dispatchSync() 同期実行 即座に完了する必要がある場合
dispatchAfterResponse() レスポンス後実行 軽量タスク
delay() 遅延実行 スケジュールタスク
onQueue() 指定キュー 優先度別

(1) ▶ サンプル:ShopMetrics注文処理ジョブ

PHP
// app/Jobs/ProcessOrderJob.php
class ProcessOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = [30, 60, 120];

    public function __construct(public Order $order) {}

    public function handle(
        OrderService $orderService,
        PaymentGateway $payment,
    ): void {
        // 支払いの引き落とし
        $payment->charge($this->order);

        // 在庫の更新
        $orderService->deductInventory($this->order);

        // イベントのブロードキャスト
        event(new OrderPlaced($this->order));

        // 確認メールの送信
        $this->order->user->notify(new OrderConfirmationNotification($this->order));
    }

    public function failed(\Throwable $exception): void
    {
        $this->order->update(['status' => 'failed']);
        $this->order->user->notify(new OrderFailedNotification($this->order));
    }
}

出力:

TEXT
// 実行成功

5. 失敗時のリトライ機構

(1) リトライ設定

PHP
// ジョブクラス内
public int $tries = 3;           // 最大リトライ回数
public int $backoff = 60;        // リトライ間隔 (秒)
public int $timeout = 120;       // 1回あたりの最大秒数

// または指数バックオフ
public array $backoff = [30, 60, 120]; // 30秒, 60秒, 120秒

// または動的バックオフ
public function backoff(): int
{
    return 30 * $this->attempts();
}

(2) エラー処理

BASH
# 失敗したジョブの確認
php artisan queue:failed

# 特定の失敗ジョブのリトライ
php artisan queue:retry 5

# すべての失敗ジョブのリトライ
php artisan queue:retry all

# 失敗ジョブの削除
php artisan queue:forget 5

# すべての失敗ジョブのクリア
php artisan queue:flush

(3) キュータスクのライフサイクル

100%
flowchart LR
    A[ジョブディスパッチ] --> B[キュー]
    B --> C[ワーカーが取得]
    C --> D{成功?}
    D -->|はい| E[ジョブ完了]
    D -->|いいえ| F{attempts < tries?}
    F -->|はい| G[バックオフ + リトライ]
    G --> B
    F -->|いいえ| H[failed_jobsテーブル]
    H --> I[手動リトライ / クリア]

(1) ▶ サンプル:ShopMetrics失敗リトライ設定

PHP
// app/Jobs/SendWebhookJob.php
class SendWebhookJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120, 300];
    public int $timeout = 30;

    public function __construct(
        public Shop $shop,
        public array $payload,
    ) {}

    public function handle(): void
    {
        $response = Http::timeout($this->timeout)
            ->post($this->shop->webhook_url, $this->payload);

        if (!$response->successful()) {
            $this->release($this->backoff[$this->attempts() - 1] ?? 60);
        }
    }

    public function failed(\Throwable $exception): void
    {
        $this->shop->tenant->users->each(function ($user) {
            $user->notify(new WebhookFailedNotification($this->shop));
        });
    }
}

出力:

TEXT
// 実行成功

6. バッチタスク

(1) バッチの作成

BASH
php artisan queue:batches-table
php artisan migrate
PHP
// app/Jobs/ProcessTenantAnalyticsJob.php
class ProcessTenantAnalyticsJob implements ShouldQueue
{
    use Batchable;

    public function __construct(public Tenant $tenant) {}

    public function handle(): void
    {
        if ($this->batch()->cancelled()) {
            return;
        }

        AnalyticsService::computeForTenant($this->tenant);
    }
}

(2) バッチのディスパッチ

PHP
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch(
    Tenant::active()->get()->map(
        fn ($tenant) => new ProcessTenantAnalyticsJob($tenant)
    )
)->then(function (Batch $batch) {
    // すべてのジョブが正常に完了
    Log::info("Batch {$batch->id} completed: {$batch->totalJobs} tenants processed.");
})->catch(function (Batch $batch, \Throwable $e) {
    // 最初のジョブ失敗
    Log::error("Batch {$batch->id} failed: {$e->getMessage()}");
})->finally(function (Batch $batch) {
    // 常に実行 (成功・失敗に関わらず)
    Cache::forget('analytics:computing');
})->name('Process Monthly Analytics')
  ->onQueue('analytics')
  ->dispatch();

(3) バッチ管理

PHP
// バッチの進捗確認
$batch = Bus::findBatch($batchId);
$batch->progress();     // 0-100
$batch->processedJobs();
$batch->totalJobs();
$batch->failedJobs();
$batch->finished();

// バッチのキャンセル
$batch->cancel();
メソッド 説明
then() 全コールバック成功
catch() 最初の失敗時コールバック
finally() 完了時コールバック (成功・失敗に関わらず)
progress() 完了率
cancel() 残りのタスクをキャンセル

(1) ▶ サンプル:ShopMetrics月次分析バッチタスク

PHP
// app/Console/Commands/ProcessMonthlyAnalytics.php
class ProcessMonthlyAnalytics extends Command
{
    protected $signature = 'analytics:process-monthly';

    public function handle(): int
    {
        $tenants = Tenant::active()->get();
        $this->info("Processing analytics for {$tenants->count()} tenants...");

        Bus::batch(
            $tenants->map(fn ($t) => new ProcessTenantAnalyticsJob($t))
        )->then(function (Batch $batch) {
            $this->info("All {$batch->totalJobs} tenants processed.");
        })->catch(function (Batch $batch, \Throwable $e) {
            $this->error("Batch failed: {$e->getMessage()}");
        })->name('Monthly Analytics')
          ->onQueue('analytics')
          ->allowFailures()
          ->dispatch();

        return self::SUCCESS;
    }
}

出力:

TEXT
// 実行成功

7. Supervisorデーモン

(1) Supervisorのインストール

BASH
# Ubuntu/Debian
sudo apt-get install supervisor

# 設定ファイルの作成
sudo nano /etc/supervisor/conf.d/shopmetrics-worker.conf

(2) Supervisor設定

INI
[program:shopmetrics-worker-default]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-default.log
stopwaitsecs=3600

[program:shopmetrics-worker-reports]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=reports --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=1
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-reports.log

(3) supervisorコマンド

BASH
# 新しい設定を読み込む
sudo supervisorctl reread
sudo supervisorctl update

# ワーカーの開始/停止/再起動
sudo supervisorctl start shopmetrics-worker-default:*
sudo supervisorctl stop shopmetrics-worker-default:*
sudo supervisorctl restart shopmetrics-worker-default:*

# ステータス確認
sudo supervisorctl status
プロセス数 キュー CPU メモリ 説明
2 default 一般タスク
1 reports レポート生成
1 analytics データ分析
1 notifications メール/プッシュ

(1) ▶ サンプル:ShopMetricsマルチキューワーカーの起動

BASH
# 開発 — 単一ワーカー, 全キュー
php artisan queue:work --queue=default,reports,notifications

# 本番 — キュー優先度ごとに別々のワーカー
# 優先度:high > default > low
php artisan queue:work redis --queue=high,default
php artisan queue:work redis --queue=reports,low

# タイムリミット付きでジョブを処理 (メモリリーク対策の自動再起動)
php artisan queue:work --max-time=3600 --max-jobs=1000

# キューの監視
php artisan queue:monitor redis:default,redis:reports

出力:

TEXT
# コマンド実行成功

8. 総合サンプル:ShopMetrics非同期レポートシステム

PHP
// ============================================
// 総合:ShopMetrics非同期レポートシステム
// 対象:jobs, batches, retries, supervisor, notifications
// ============================================

// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public array $backoff = [60, 180, 600];
    public int $timeout = 600;
    public bool $deleteWhenMissingModels = true;

    public function __construct(
        public Tenant $tenant,
        public string $reportType,
        public string $format = 'csv',
        public ?int $userId = null,
    ) {
        $this->onQueue('reports');
    }

    public function handle(ReportService $reportService): void
    {
        $path = $reportService->generate(
            $this->tenant,
            $this->reportType,
            $this->format,
        );

        $downloadUrl = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));

        $user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
        $user?->notify(new ReportReadyNotification(
            downloadUrl: $downloadUrl,
            reportType: $this->reportType,
            expiresAt: now()->addDays(7),
        ));
    }

    public function failed(\Throwable $exception): void
    {
        $user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
        $user?->notify(new ReportFailedNotification(
            reportType: $this->reportType,
            error: $exception->getMessage(),
        ));
    }
}

// コントローラーでの使用
class ReportController extends Controller
{
    public function generate(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'report_type' => 'required|in:monthly,weekly,custom',
            'format' => 'sometimes|in:csv,xlsx,pdf',
            'date_from' => 'sometimes|date',
            'date_to' => 'sometimes|date|after:date_from',
        ]);

        GenerateReportJob::dispatch(
            tenant(),
            $validated['report_type'],
            $validated['format'] ?? 'csv',
            auth()->id(),
        );

        return response()->json([
            'message' => 'Report generation started. You will receive an email when ready.',
            'estimated_time' => '5-15 minutes',
        ], 202);
    }
}

❓ よくある質問

Q queue:workqueue:listenの違いは何ですか?
A queue:workはフレームワークを再起動せずにメモリ上で実行します (高速ですが, コード変更後は手動再起動が必要)。queue:listenはタスクごとにフレームワークを再起動します (低速ですが, 新しいコードを自動的に読み込みます)。本番ではqueue:work + Supervisorを使用し, 開発ではqueue:listenまたはqueue:work --onceを使用してください。
Q ジョブ内でEloquentモデルを使用できますか?
A はい, SerializesModelsトレイトがモデルIDを自動的にシリアライズし, デシリアライズ時にデータベースから再取得します。利点はジョブのデータフットプリントが小さくなること, リスクはモデルが削除されるとジョブが失敗することです (deleteWhenMissingModelsで無視できます)。
Q キューが溜まるのを防ぐにはどうすればよいですか?
A ワーカー数 (numprocs)を増やす, 高性能ドライバー (Redis)を使用する, ジョブのタイムアウトを設定してデッドロックを防ぐ, キュー長を監視するアラートを設定してください。
Q バッチ内の1つのタスクが失敗した場合はどうすればよいですか?
A デフォルトでは, バッチは最初の失敗で残りのタスクをキャンセルします。allowFailures()を設定すると, 一部が失敗してもバッチの実行を継続できます。Bus::findBatch()で失敗の詳細を確認し, 失敗したタスクを手動でリトライしてください。
Q ジョブをデバッグするにはどうすればよいですか?
A 開発時はQUEUE_CONNECTION=syncでジョブを同期的に実行します (エラーがすぐに表示されます)。本番ではphp artisan queue:failedで失敗ジョブのエラーメッセージを確認します。ジョブのfailed()メソッドで詳細情報をログに記録することもできます。
Q Supervisorとsystemdの違いは何ですか?
A SupervisorはPython製のプロセスマネージャーで設定が簡単, Laravel公式推奨です。systemdはLinuxネイティブのサービスマネージャーでパフォーマンスは優れていますが設定が複雑です。どちらもqueue:workプロセスをデーモンとして実行できます。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐):GenerateReportJobを作成し, コントローラーでCSVレポートを非同期生成するリクエストをディスパッチし, databaseドライバーでテストし, queue:workを実行してジョブが実行されることを確認してください。

  2. 応用問題 (⭐⭐):Redisキュードライバーを設定し, 指数バックオフ (30秒/60秒/120秒)で3回リトライを実装し, ジョブ失敗時にユーザーに通知を送信し, 手動リトライ (queue:retry)をテストしてください。

  3. チャレンジ (⭐⭐⭐):Bus::batch()を使用して月次マルチテナント分析のバッチタスクを実装してください—すべてのアクティブなテナントを反復処理し, テナントごとに1つのジョブを作成し, 進捗率を監視し, 完了時にキャッシュをクリアし, Supervisorを設定してワーカーを監視してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%