見出し画像

バッチ処理をコード化するときに意識していること

最近、同僚の@sunakujira(F.Shibusawa)と共にバッチ処理を書くことが多いので備忘録も兼ねてバッチ処理を書く上で意識していることを紹介したいと思います。

大きくは以下の3点を意識してます。

1. 再実行(リカバリ)する前提で考える
2. 処理の実行時間は問題ないか
3. サーバー負荷は問題ないか

今回は再実行(リカバリ)する前提で考えるに絞って紹介しようと思います。

内的要因、外的要因問わずエラーは必ず起こるものだと想定し、再実行(リカバリ)できるようにしておくことはバッチ処理の最たる要件の一つと言えるでしょう。具体的には以下を意識しています。

冪等性を考慮した設計

冪等性とは、何度実行しても同じ結果が得られることを指します。例えば、デイリーの売上を計算しDBに結果を保存するようなバッチ処理を仮定すると、冪等性を担保するためには、対象データが存在する場合に削除する処理を仕込むようにします。処理によっては削除せずに更新したいケースもあるでしょう。

処理対象とするデータの条件を指定して実行できるようにする

サービスがローンチしたばかりでデータ量が少ないうちは全データを対象とすると割り切ってもいいかもしれないですが、データ量が多くなると処理時間やサーバ負荷の観点はもちろん、即時性を求められるデータはビジネスに影響を及ぼすので流暢なことを言ってられなくなります。なので、例えばデイリーの売上を計算するバッチ処理であれば処理対象の日付をオプション(実行時に環境変数など)で指定できるようにしておきます。

適切なログ出力

処理が正常終了したのか、エラーなどで処理が異常終了してしまったのか。特にエラーが出てしまった場合はその要因をクリアにする必要があるため解析の材料としてログを残すことはとても重要です。具体的には最低限以下のログを出力するようにしています。

・開始, 終了, 開始から終了までの処理時間
・エラーのスタックトレース
・処理対象件数, 完了件数
・処理進行状況

異常終了時にエラーのスタックトレースをSlackに通知することはよくある作法かと思いますが、最近はバッチ終了時に処理対象件数と完了件数もSlackに通知するようにしています。特に課金やユーザ情報を扱うバッチは欠損が命取りになり得るので、処理対象件数と完了件数を確認することで正常終了したかどうかをSlack上で判断できるのは楽ですし、精神衛生にも効果的です。

外部API呼び出し時はエラー発生を考慮して複数回のリトライ処理

「再実行する前提で考える」とはちょっと逸れますが、極力正常終了するよう(再実行しないよう)にするテクニックの1つとして、指数関数的に処理のリトライ間隔を増やしリトライするいわゆるExponential Backoffを採用します。自前で実装しても良いですが、各言語でライブラリが公開されているのでライブラリを頼るのも良いでしょう。個人的にはRubyであればkamui/retriable、Nodeであればtim-kos/node-retryを利用しています。

dry runの機能を備える

こちらも極力正常終了するよう(再実行しないよう)にするテクニックの1つです。単体テストをしっかり書いたとしてもいざ本番環境で実行すると環境依存やデータ依存でエラーとなることがあります。エラーの弊害として例えば、DBのデータを登録/削除/更新をするようなものは、仮に冪等性を担保したバッチで再実行が可能だとしてもリカバリまでに時間を要し、扱うデータによってはアプリケーションに影響が出てしまう可能性があります。これを避けるべく異常終了を事前に検知するためにdry run(リハーサル)の機能を備え、まずはDB上のデータの登録/削除/更新以外の部分が想定通りに動くかどうかを確認するようにしています。また、意図しない実行を避けるためにデフォルト実行をdry runとしておくことも有効です。Rails製でrailsコンソール上で実行してみるのであればsandboxオプションも便利です。

おわりに

再実行(リカバリ)する前提で考えるに絞って意識していることを紹介しましたが、これ以外にも知見やテクニックがありましたらぜひ教えてください!

弊noteをよりよいサービスにするために、技術書購入や勉強会・セミナー参加の費用にあてたいと思います🙏