バッチ処理を設計することになったとき、具体的なやり方が分からないかもしれません。そもそもバッチ処理とは何か、あいまいになっているかもしれません。私はこれまでウェブサービスやアプリを開発してきて、その中でバッチ処理も設計や実装をしてきました。
この経験を元に、この記事ではバッチ処理とは何か、どう設計すればいいか、そのやり方を解説します。バッチ処理を設計する上で押さえるべきポイントをまとめているので、参考になればと思います。
この記事はエンジニアとしての実務経験が1〜3年目くらいの方を想定して書いています。また、ソフトウェアの種類としてはウェブアプリケーションやモバイルアプリを想定しています。
テクニカルライター。元エンジニア。共著で「現場で使えるRuby on Rails 5」を書きました。プログラミング教室を作るのが目標です。
バッチ処理とは
まず、この記事におけるバッチ処理の定義について示します。バッチ処理とは「入力があり、その入力を加工し出力するプログラム」をいいます。たとえばデータベースから処理対象となるデータを取得し、データを変更した上で更新するプログラムがバッチ処理にあたります。
この処理はなんらかのトリガーによって起動します。たとえば時間がトリガーとなって起動します。
バッチ処理はプログラムの実行方式のひとつで、まとまった処理を一度に行います。時間がかかる処理や定時処理などに有用で、ビジネス側の要件を満たすために重要な役割をもちます。
バッチ処理が満たすべき要件
次にバッチ処理を設計する上で満たすべき要件を定めます。この記事で示す各ポイントは、次の要件を満たすことを目的とします。
- ビジネス側の要件を満たすこと
- 問題をできるだけ起こさないこと
- 処理の状況を把握できること
- 異常時にリカバリできること
- リソースの消費を抑えること
バッチ処理を設計するやり方
上記の要件を満たすために、バッチ処理を設計する上で抑えるべきポイントは次のとおりです。各ポイントごとに書いていきます。
順番 | 項目 |
---|---|
1 | ビジネス側の要件を明確にする |
2 | Dry Runできるようにする |
3 | APIを叩く処理はリトライする |
4 | 外的要因に依存しないようにする |
5 | テストコードを書く |
6 | ログを出力する |
7 | コミュニケーションツールに通知する |
8 | 冪等性をもたせる |
9 | 処理対象を引数で指定できるようにする |
10 | リソースの消費を減らす |
1. ビジネス側の要件を明確にする
バッチ処理にはビジネス側の要件があります。いつはじまっていつ終わるのか、どの処理の後にはじめるのか。処理対象はなにか。どういう処理をし、どう出力するのか。こういった要件を明確にした上で設計に入ります。
2. Dry Runできるようにする
バッチ処理を実行する際のオペレーションミスを防ぐため、Dry Runの仕組みをとりいれます。バッチ処理はビジネス上重要な処理であることが多いです。処理対象を間違えるとリカバリをする必要があります。
場合によってはメンテナンスモードに入ることになります。これはユーザーのUXを低下させてしまいます。このような問題を防ぐため、処理を実行する前にふるまいをチェックする仕組みをつくります。
正しい処理対象に正しい処理が行えるか。これを標準出力をとおして確認できるようにします。より安全な運用をするために、バッチ処理のデフォルトのふるまいをDry Runにすることも検討します。
3. APIを叩く処理はリトライする
APIの利用制限にひっかかるとバッチ処理全体が失敗してしまいます。これを防ぐため、APIを叩く処理はリトライするようにします。APIによってはRate Limitなどのリクエストに関する制限があります。あるいはAPIが一時的にダウンしているかもしれません。
リトライを行うことでことで、処理全体が成功する可能性を高めることができます。リトライの時間間隔はExponential Backoffという手法が有用です。
4. 外的要因に依存しないようにする
処理が外的要因に依存すると、それが利用不能になった場合にバッチ処理全体が失敗してしまいます。可能な限り依存しないようにします。たとえばバッチ処理内で運用者にSlackで通知したとします。この通知を同期的に行うと、エラーハンドリングしない限りバッチ処理全体が失敗します。
通知の失敗が許容できるケースでは失敗してもいいようにします。具体的には非同期での実行などの方法をとります。
5. テストコードを書く
バッチ処理も、アプリケーションのコードと同じくテストコードを書きます。このことが問題の防止につながります。
テストコードは処理のふるまいを定義し、テストによって正しくふるまっているかを検証します。これを継続的にテストすることで、バッチ処理の品質を保つことができます。
6. ログを出力する
バッチ処理に問題が発生したとき、その調査やリカバリにログを利用します。このためログはバッチ処理において重要な役割を果たします。
ログを設計する際は5W1Hを意識すると効果的です。バッチ処理をいつ開始したか、いつ終了したか。処理対象はなにか。どういう処理を行ったか、行わなかったか。なぜその対象なのか。どの処理か──これは一意の処理IDをログに付与すると、ログ監視ツールで調査しやすくなります。
また、エラーが発生したときはスタックトレースをログに記録します。
7. コミュニケーションツールに通知する
バッチ処理の状況を把握するために、処理内の主要なポイントでSlackなどのコミュニケーションツールに通知します。これによりバッチ処理が正しく動作していることを確認できます。問題が発生したときにはすぐ気づくことができ、リカバリ作業に移行できます。
この通知はエンジニア以外のメンバーにもわかりやすい形で通知します。こうすることで、ユーザーへのフォローまで含めたリカバリを行うことができます。
8. 冪等性をもたせる
バッチ処理は、何度実行しても一度処理した対象を二重に処理しないようにします。「何度実行しても同じ結果が得られる性質」を冪等性といいます。冪等性をもたせることで、処理が失敗したときに再度処理すれば未処理のもののみ処理にかけることができます。
9. 処理対象を引数で指定できるようにする
未処理のデータを指定して処理にかけられるよう、処理対象を引数で指定できるようにします。バッチ処理が失敗したとき、処理されたものと未処理のものが出てきます。未処理のもののみ再実行できるとリカバリしやすくなります。
これは冪等性をもたせることが難しいバッチ処理に対して特に効果的です。また、Dry Runと組み合わせることも有用です。今後実施予定のバッチ処理を、処理対象を指定した上で事前に確認できるようになります。
10. リソースの消費を減らす
バッチ処理は、その性質上コンピューティングリソースを消費しやすいです。できるだけ消費を減らせるよう、実装上の工夫をします。
たとえばデータベースにある大量のレコードを扱うとき、全件をメモリ上に展開すると多くのリソースを消費してしまいます。この場合、分割して処理することを検討します。
またトランザクションも有用ですが、大量のデータに対するトランザクションはデータベースリソースの消費につながります。適切な粒度で行う必要があります。
おわりに
以上のポイントを意識してバッチ処理を設計することで、ビジネス側の要件を満たしつつ、問題が起きづらいバッチ処理を運用することができます。問題が起きたときにも対処しやすいため、可能な限りポイントを押さえるようにしたいところです。