はじめに
こんにちは。ソーイ株式会社の工藤です。
今回、当社が担当した受託開発案件でバッチの監視・テストを担当する中で、バッチが「実行されたか」だけでなく、「正常に動き続けているか」「正常終了したか」「異常から復旧したか」まで確認する必要があることを学びました。
本記事では、バッチ処理の基本に触れつつ、実務でどのようにバッチの状態を監視したのかを初心者向けにまとめます。
結論
バッチ処理を安定して運用するためには、次の4つの状態を確認できる仕組みが重要です。
- 起動したか
- 正常に動き続けているか
- 時間内に終了したか
- 異常から復旧できたか
今回の案件では、CloudWatch Logsへのログ出力、Laravel Cacheを使ったheartbeat、監視API、外形監視を組み合わせて確認しました。
想定読者
- これからバッチ処理の実装や監視に関わる初心者エンジニア
- バッチ処理とWeb APIの違いを整理したい方
- 監視設計・異常系テストの考え方を知りたい方
この記事でわかること
- バッチ処理とWeb APIの違い
- バッチで起こりうる異常のパターン
- heartbeatによる生存確認の考え方
- 異常系テストで確認すべき観点
バッチ処理とは?
バッチ処理とは、決められた処理をまとめて自動実行する仕組みです。
Webアプリでは、ユーザーがボタンを押したことをきっかけに処理が実行されることが多いですが、バッチ処理はユーザー操作とは関係なく、決められた時刻や条件によって実行されます。
例えば、以下のような処理があります。
- 毎日深夜に不要なデータを削除する
- 1時間ごとにデータを集計する
- 定期的にレポートを作成する
- 一定期間を過ぎたログを削除する
流れを簡単にすると、次のようになります。
Laravelでは、Artisanコマンドとして処理を作成し、Schedulerから定期的に実行する方法などがあります。
Web APIとの違い
バッチ処理とWeb APIの大きな違いは、処理が実行されるきっかけです。
| Web API | バッチ処理 | |
|---|---|---|
| 実行のきっかけ | ユーザー操作や外部システムからのリクエスト | 時刻・スケジュールなど |
| 実行時間 | 比較的短いことが多い | 長時間になる場合もある |
| ユーザー操作 | 操作をきっかけにすることが多い | 基本的に不要 |
| 例 | ログイン、一覧取得 | 集計、削除、定期処理 |
バッチは裏側で自動実行されるため、途中で停止してもユーザーがすぐに気付けない場合があります。
そのため、「実行したか」だけでなく、実行中や終了後の状態を監視することが重要になります。
action:start / action:end を監視APIで判定する
今回のバッチでは、処理開始時に action:start、正常終了時に action:end をCloudWatch Logsへ出力していました。
正常な場合は次のようになります。
action:start
↓
処理
↓
action:end
また、それぞれの実行には BATCH_ID を付け、同じバッチの start と end を紐付けられるようにしています。
BATCH_ID: abc123 action:start
↓
処理
↓
BATCH_ID: abc123 action:end
action:start がない場合の検知
正常終了だけでなく、「そもそも予定時刻にバッチが起動したか」も監視しています。
今回のバッチは毎時00分に実行されるため、実行予定時刻から5分間を猶予時間とし、それを過ぎてもCloudWatch Logsに action:start が確認できない場合は異常と判定します。
この判定も監視APIで行っています。
// 実際の実装を簡略化した例
$start = findScheduledBatchStartFromCloudWatch();
if (now()->minute >= 5 && !$start) {
return response()->json([
'status' => 'error',
'reason' => 'batch_not_started',
], 500);
}
これにより、予定時刻から5分以内は起動待ちとして扱い、5分を過ぎても action:start がない場合は「バッチが起動していない」と判断できます。
action:end がない場合の検知
監視APIからCloudWatch Logsを確認し、開始したバッチと同じ BATCH_ID の action:end が存在するかを確認していました。
開始から1時間を経過しても action:end が確認できない場合は、処理が正常終了していないと判断し、監視APIが異常を返します。
イメージとしては次のような判定です。
// 実際の実装を簡略化した例
$start = findLatestBatchStartFromCloudWatch();
if ($start && $start->startedAt->lte(now()->subHour())) {
$end = findBatchEndFromCloudWatch($start->batchId);
if (!$end) {
return response()->json([
'status' => 'error',
'message' => 'Batch process is delayed',
], 500);
}
}
return response()->json([
'status' => 'ok',
], 200);
単に action:start が出ているだけでは、「起動したこと」しか分かりません。
action:end まで確認することで、正常に処理を完了できたことを確認できます。
heartbeatで「処理が進んでいるか」を確認する
start と end だけでは、処理の途中で止まっている状態をすぐに検知できません。
そこで利用しているのが heartbeat です。
heartbeatとは、バッチが「まだ処理を続けています」という情報を継続的に記録する、生存確認の仕組みです。
今回の実装では、heartbeatをLaravel Cacheに保存しています。
実際の実装を簡略化すると、次のような形です。
Cache::put('batch:running', [
'batch_id' => $batchId,
'status' => 'running',
'heartbeat_at' => now()->toIso8601String(),
'phase' => $phase,
]);
heartbeatは固定で「○秒ごと」に更新する方式ではなく、各処理フェーズの開始・終了や、時間のかかる処理の途中などで更新しています。
監視APIでは、最後のheartbeatから一定時間が経過していないかを確認します。
今回の監視では、15分以上heartbeatが更新されなかった場合を異常としています。
$running = Cache::get('batch:running');
if ($running) {
$heartbeatAt = Carbon::parse($running['heartbeat_at']);
if ($heartbeatAt->lt(now()->subMinutes(15))) {
return response()->json([
'status' => 'error',
'reason' => 'heartbeat_timeout',
], 500);
}
}
return response()->json([
'status' => 'ok',
], 200);
例えば、
10:00 バッチ開始
10:03 heartbeat更新
10:06 heartbeat更新
10:09 heartbeat更新
のように更新されていれば、処理が継続していることが分かります。
一方、
10:00 バッチ開始
10:03 heartbeat更新
10:06 heartbeat更新
↓
15分以上更新なし
となった場合は、バッチが途中で停止している可能性があると判断します。
このようにheartbeatを使うことで、「プロセスが存在するか」だけでなく、「実際に処理が進んでいるか」を確認できます。
異常系のテストも重要
監視機能を作っても、正常にバッチを実行するだけでは異常検知が正しく動くか確認できません。
今回のテストでは、実際に実行中のバッチプロセスを途中で終了させ、意図的に異常状態を作りました。
そのうえで、
- heartbeatの更新が止まった場合に異常を検知できるか
- バッチが予定時刻に起動しなかった場合に検知できるか
-
action:startのあとにaction:endが出なかった場合に検知できるか - 強制終了後も次回の定期バッチが正常に起動するか
- 異常状態を解除すると正常状態へ戻るか
を確認しました。
特に印象に残ったのは、異常を検知するだけでなく、復旧まで確認することが重要という点です。
「500を返せたからOK」ではなく、その後正常なバッチが実行された際に監視APIが再び200へ戻るところまで確認しました。
UptimeRobotを使った外形監視で気付いたこと
外形監視とは、システムの外部から実際にURLへアクセスし、正常に応答しているかを確認する監視方法です。
今回は、UptimeRobotを使って監視APIの外形監視を行いました。
その際、最初に次のような結果を確認しました。
GET → 500
HEAD → 200
当初は「監視サービスがHEADリクエストを送っていることが原因ではないか」と考えました。
Laravelでは通常、HEADリクエストもGETと同じルートで処理されるため、HTTPメソッドの違いだけでステータスコードが変わるのは不自然です。
そこで監視APIの状態を調査したところ、最初のGETリクエストによって監視状態が変化しており、これが結果に影響していたことが分かりました。
監視APIの状態変化が結果に影響していた
今回の監視APIでは、異常を検知した際に、その異常を通知済みとして管理するための alerted_at を記録していました。
処理の流れは以下の通りです。
GET
↓
heartbeat異常を検知
↓
HTTP 500
↓
alerted_at を記録
その後GETを再度実行すると200になることも確認できたため、GET → 500 / HEAD → 200 という結果だけでは、HTTPメソッドの違いが原因とは判断できませんでした。
調査すると、最初のGETで alerted_at が記録され、監視APIの状態が変化していました。
今回はこの仕組み自体は変更せず、異常状態を作り直したあと手元から先にアクセスせず、UptimeRobotから最初の監視リクエストを送ることでDOWN判定を確認しました。
そのため、監視APIへのアクセスによって状態が変化する点は、運用上の注意点として残っています。
外形監視ではどう確認したか
今回利用したUptimeRobotのHTTP Monitorでは、期待した異常検知を確認できませんでした。
HTTP MonitorではHEADリクエストが使われていた一方、手動で行ったGETによって alerted_at が記録され、監視APIの状態も変化していたため、同じ条件で比較できていませんでした。
そのため、テストではUptimeRobotのKeyword Monitorを利用しました。
heartbeat異常時に監視APIが返すエラー識別子を監視対象としました。
本記事では、説明を簡単にするため heartbeat_timeout と表記します。
heartbeat_timeout
という文字列がレスポンスに含まれた場合にDOWNと判定するよう設定しました。
また、テスト用の異常状態を作り直したあと、手元から先に監視APIへアクセスせず、外形監視サービス自身に最初のリクエストを行わせました。
これにより、
異常発生
↓
監視APIが500を返す
↓
外形監視がDOWNを検知
↓
メール通知
↓
異常状態を解除
↓
監視APIが200へ復旧
↓
UPの復旧通知
まで確認できました。
今回の経験から、外形監視ではHTTPメソッドだけでなく、監視APIを呼び出すことで状態が変化しないかも確認する必要があると学びました。
特にヘルスチェック用のAPIについては、参照しただけで状態が変化しない設計にすることも重要だと感じました。
まとめ
今回の業務を通して、バッチ監視では単に「最後に成功したか」だけを見るのではなく、
起動したか
↓
処理が進んでいるか
↓
正常に終了したか
↓
異常から復旧したか
という一連の状態を確認することが重要だと学びました。
今回それぞれを確認するために利用したのが、
-
action:start/action:endによる開始・終了の確認 - heartbeatによる実行中の状態確認
- 監視APIによる異常判定
- 外形監視によるシステム外部からの確認
です。
また、実際に異常系テストを行ったことで、正常系だけでは気付けなかった監視APIの状態変化についても確認できました。
今後バッチ処理を実装・運用する際は、処理そのものだけでなく、「起動しなかったらどう気付くか」「途中で止まったらどう気付くか」「その後正常に復旧できたか」まで考えることを意識したいと思います。
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii


