Help us understand the problem. What is going on with this article?

Laravel Job Queueで バッチ運用した話

やりたい事

  • 毎日、スクレイピング処理を実行
  • スクレイピングした結果をGoogleスプレッドシートに書き込む
  • 大量の処理が走るので、PHPメモリエラーになる可能性が高い
  • スプレッドシートの結果を毎日、バックアップする
  • スプレッドシートの結果を毎日、メールで送る

やった事

  • ①スケジューラの設定
  • ②バッチコマンドクラスの作成
  • ③ジョブキューの仕組みを実装
  • ④GASでスプレッドシート書き込みの処理を実装
  • ⑤supervisorの設定

0.各キーワード説明

Job Queueとは?

ジョブキューとはジョブをキューで管理するものです。
キューとはFIFO(First In First Out)を実現するデータ構造です。
キューに登録されたモノは、キューに登録した順に処理されます。
スクリーンショット 2019-12-24 21.53.56.png

参考:http://tech.voyagegroup.com/archives/495474.html

全体構成

スクリーンショット 2019-12-24 21.52.52.png

①スケジューラの設定

Kernel.phpに朝9時でバッチを動くように設定

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // バッチを実行
        $schedule->command('batch:daily')->dailyAt('09:00');
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

②バッチコマンドクラスの作成

コマンドでの処理
- 本日分のジョブキューを詰める
- 前日分のジョブの実行結果をメールで送る

<?php

namespace App\Console\Commands;

use App\Entities\JobStatus;
use App\Entities\Project;
use App\Jobs\ProcessGoogleSearch;
use App\Mail\DailyBatchResultMail;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Mail;

class dailyBatchCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'batch:daily';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '1日1回実行されるバッチ用コマンド';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     * @throws \Exception
     */
    public function handle()
    {
        $error = $this->SendJobStatusesByEmail();

        if ($error !== null) {
            throw new \Exception($error);
        }

        $error = $this->updateIsActive();

        if ($error !== null) {
            throw new \Exception($error);
        }

        $this->refreshQueue();
        $projects = Project::where('is_active','=', 1)->get();
        $this->registeredJobStatuses($projects);
        $this->pushQueue($projects);
    }

    // 以下、省略

}

③ジョブキューの仕組みを実装

※サンプルコードなので、細かい部分は省略。

<?php

namespace App\Jobs;

use App\Entities\JobStatus;
use App\Entities\Project;
use App\Enums\JobState;
use App\Services\CustomSearchService;
use App\Services\SearchService;
use App\Services\SpreadsheetService;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Log;

class ProcessSearch implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * 最大試行回数
     *
     * @var int
     */
    public $tries = 0;

    /**
     * 詳細検索するかどうか(TRUE)
     */
    const IS_DETAIL_SEARCH_TRUE = 1;

    /**
     * 詳細検索するかどうか(FALSE)
     */
    const IS_DETAIL_SEARCH_FALSE = 0;

    private $project;
    private $jobStatus;
    private $searchService;
    private $customSearchService;
    private $spreadsheetService;

    /**
     * Create a new job instance.
     *
     * @param Project $project
     * @param JobStatus $jobStatus
     */
    public function __construct(Project $project, JobStatus $jobStatus)
    {
        $this->project = $project;
        $this->jobStatus = $jobStatus;

        $this->searchService = new SearchService();
        $this->customSearchService = new CustomSearchService();
        $this->spreadsheetService = new SpreadsheetService();
    }

    /**
     * Execute the job.
     *
     * @return void
     * @throws \Exception
     */
    public function handle()
    {
        // 一部、処理を省略
        // JobStatusの更新
        $jobStatus = $this->jobStatus;
        $jobStatus->status = \App\Enums\JobState::RUNNING;
        $jobStatus->started_at = Carbon::now();
        $jobStatus->save();

        $parameter = [];

        $keyword = $jobStatus->keyword;

        if (!empty($keyword)) {
            $searchCount = $this->project->search_count;

            // 検索を実行
            // サービスクラスの具体的な処理は省略
            $parameter['searchResults'][] = $this->customSearchService
                ->execute($keyword, $searchCount, self::IS_DETAIL_SEARCH_TRUE, $jobStatus);

            sleep(mt_rand(config('variables.google_search_min_wait'), config('variables.google_search_max_wait')));
        }

        // GAS側で使う
        $parameter['sheetName'] = $this->project->name;
        $parameter['searchCount'] = $searchCount;

        // スプレッドシート書き出し
        [$spreadSheetUrl, $error] = $this->spreadsheetService->outputSpreadsheet($parameter);

        // JobStatusの更新
        if (empty($error)) {
            $jobStatus->status = JobState::SUCCESS;
            $jobStatus->spreadsheet_url = $spreadSheetUrl;
        } else {
            $jobStatus->status = JobState::FAILED;
        }
        $jobStatus->finished_at = Carbon::now();
        $jobStatus->save();
    }

    /**
     * 失敗したジョブの処理
     *
     * @param \Exception $exception
     * @return void
     */
    public function failed(\Exception $exception)
    {
        // JobStatusの更新
        $jobStatus = JobStatus::where('project_id', $this->project->id)->first();
        $jobStatus->status = \App\Enums\JobState::FAILED;
        $jobStatus->finished_at = Carbon::now();
        $jobStatus->save();

        // 失敗の通知をユーザーへ送るなど…
        \Log::error($exception->getMessage());
    }

}


④GASでスプレッドシート書き込みの処理を実装

  • PHPからパラメータを取得
    スクリーンショット 2019-12-24 23.38.05.png

  • Excelファイルを作成して、メール送信
    スクリーンショット 2019-12-24 22.26.38.png

  • Excelファイルを作成して、Googleドライブにバックアップ
    スクリーンショット 2019-12-24 22.27.24.png

⑤supervisorの設定

スクリーンショット 2019-12-24 22.28.02.png
スクリーンショット 2019-12-24 22.28.08.png

まとめ

  • 大量の処理が走るような機能があった際に、Job Queueを使うとPHPメモリエラーを防げる可能性がある。
  • メールの大量送信やスクレイピング、高速でAPIを叩くような処理には向いてそう。

参考記事

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした