Timee Product Team Blog

タイミー開発者ブログ

バッチ処理の改善 〜冪等性の設計導入〜

前編(トランザクション範囲の最小化)へ

はじめに

こんにちは。タイミーのバックエンドエンジニア中野です。

前編では締めのバッチ処理におけるトランザクションの範囲を最小化した技術的改善をご紹介しました。トランザクションの範囲をバッチ処理全体から最小限の範囲に変更したことにより、バッチ処理が失敗した場合に請求レコードの処理が途中まで完了している状態が発生するようになりました。後編では、処理対象の請求レコードに対し状態を持たせることでバッチ処理全体での冪等性を担保し、バッチ処理が途中で失敗した場合でも安全に処理を再開できるようにした取り組みをご紹介します。

締めのバッチ処理とは

まずは前編のおさらいになりますが、弊社の締めのバッチ処理に関して説明します。締めのバッチ処理とは、月初に定期的に実行されるオンラインバッチであり企業への先月の請求を確定させる処理になります。

具体的な締め処理までのプロセスを記述すると以下の通りになります。

  1. 企業への請求が発生した段階で「請求テーブル」にレコードが作成されます
  2. 毎月月初の「締め」のバッチ処理により、「請求テーブル」のレコードを「確定請求テーブル」に複製することで企業への請求を確定させていきます

つまり、締めのバッチ処理とは月初時点の請求テーブルのスナップショットを撮る行為であるとみなすことができます。

現状の課題認識

前編の技術的改善によりトランザクションの範囲をバッチ処理全体から最小限の範囲(レコード単位)に変更しました。これまではバッチ処理の途中で処理が失敗した場合にはトランザクションロールバックにより次の2つの状態が担保されてきました。

  • 確定請求レコードが全て作成される状態
    トランザクションが成功した場合に遷移する状態。「請求テーブル」のレコードがすべて処理され「確定請求テーブル」に過不足なくレコードが作成されている。この状態に遷移した時に確定請求テーブルに依存している利用明細画面の表示が一斉に更新される。

  • 確定請求レコードが1つも作成されていない状態
    バッチ処理開始前または処理が失敗したに遷移する状態。バッチ処理が途中で失敗した場合にはトランザクションロールバックにより処理開始前の状態のままとなる。確定請求テーブルに依存している利用明細画面の表示はバッチ処理実行前の表示のままとなる。

トランザクションをレコード単位に変更することで、バッチ処理全体での原子性が担保されなくなる状況が発生しました。下図右側の失敗時の通り、途中までレコードが作成されている状態が存在するようになりました。

トランザクションを最小化したことによる影響

途中まで「確定請求テーブル」のレコードが作成される状態が出現したことにより生じうる課題は下記の通りです。

  • 処理再開時に重複して確定請求レコードが作成される問題
    これまではバッチ処理が失敗した際に、トランザクションロールバック機構によりバッチ処理開始前の状態に戻るため失敗の原因を排除後に再度最初からバッチ処理を再開することできました。しかし、バッチ処理全体でのトランザクションを廃止し、途中まで「確定請求レコード」が作成された状態が発生したことで、再度処理を実行すると重複した確定請求レコードが作成される可能性がでてきました。

  • 利用明細への影響
    企業管理画面では、翌月の請求予定額及び月ごとの確定した請求額を閲覧する機能を提供しています。この翌月の請求予定額が「請求テーブル」に依存しており、締め処理がされていない請求テーブルのレコード合計を表示するようになっています。一方で、月ごとの確定した請求額は「確定請求テーブル」に依存しています。バッチ処理全体においてトランザクションが適用されていた場合は、トランザクションがコミットされたタイミングで処理結果がデータベースに反映されるため、バッチ処理完了時に利用明細の表示が一斉に更新されてきました。しかし、バッチ処理全体のトランザクションを廃止し逐次的に処理結果をデータベースに反映するようになったため、企業が利用明細を開いたタイミングによって翌月の請求予定額が変動する問題が発生しました。

締め処理と利用明細画面のテーブルへの依存関係

このように処理が途中で失敗するケースはバッチ処理だけに限った話ではなく、アプリケーションの開発・運用していく中で様々な原因に起因し発生します。例えば、プログラムのバグ、データ欠損や異常値、メモリ不足などが挙げられます。そのため処理が失敗した場合に備えてアプリケーション設計を考えていく必要があります。

実施した施策

上記の課題を解決すべく、締めのバッチ処理を再設計するにあたり冪等性を意識しました。

冪等性とは

冪等性とは「ある操作を1度実行しても、複数回実行しても同じ結果になる」性質のことを言います。冪等性が担保されていると、仮に処理が途中で失敗したときに処理をリトライしても最終的な結果が1回で処理が成功した場合と変わらないことになります。今回扱う締めのバッチ処理では、リトライ時に処理済みの請求データに基づく確定請求レコードが重複して作成される可能性があります。そのため、バッチ処理に冪等性を持たせることが非常に重要となってきます。バッチ処理において冪等性を実現する方法を次に2点紹介します。

冪等性を実現する方法

  1. すでに処理が完了した対象をリトライ時に処理しない方法
    バッチ処理の対象に未処理もしくは処理済みの状態をもたさせることで、リトライ時に処理済みの対象をスキップさせる方法があります。リトライ時に未処理のみを対象に処理を開始できるため、バッチ処理全体の完了時間を早められる反面、状態を管理する必要があるため条件分岐のロジックが入るなどコードの複雑さが増す傾向にあります。

  2. すでに処理が完了した対象も含めてリトライする方法
    1度目のバッチ処理によって処理された対象も含めて処理する方法です。例えば、処理した結果をデータベースに永続化している場合はすでに存在しているレコードを削除し新規でレコードを作成し直す処理を1つのトランザクション内で実行します。そうすることで重複したレコードは作成されず冪等性が担保されます。留意すべき点としては1つひとつの処理対象レコード自体が冪等性を有しており外部サービスに依存していないことです。1と比較し分岐処理によるコードの複雑性は回避できる反面、リトライ時に最初から処理の実行を再開する必要があるため完了時間が遅延する傾向にあります。

バッチ処理への適用

今回は1の方法で冪等性を実現しました。具体的には、請求レコードに「締め前」「締め中」「締め後」の状態を持たせることで締め処理済みかどうかの判定を行うことができ、たとえ途中でバッチ処理が失敗した場合でもリトライ する際に締め処理済みのレコードをスキップすれば確定請求レコードの重複作成を回避できます。また、請求テーブルに状態を持つことで「締め後」の状態に遷移した際に依存先の利用明細画面の表示を切り替えるだけで、締め処理が途中で失敗した場合の締め途中の中途半端な状態の金額が閲覧される問題を回避できるようになりました。

達成できたこと

締めのバッチ処理に冪等性を考慮した設計改善を行うことで次の成果が得られました。

  • 処理が失敗した場合にリトライしても重複した請求が発生することを防止できる
  • 請求レコードに状態を持たせることでバッチ処理の処理状態に依存している利用明細画面の表示を制御できるようになった
  • 副次的な効果としてバッチ処理全体の完了時間の短期化を実現できました。バッチ処理が失敗した際に、処理が失敗した原因を排除後に単にリトライすれば処理を再開できるようになり、結果的にバッチ処理完了までの時間の短縮化につながりました。

今後の課題

最後に、これから改善を考えている課題を提示した上でバッチ処理の改善に関する記事の「締め」とさせていただきたいと思います。

スループット向上とリソース最適化

バッチ処理は通常処理負荷が高いためサーバーのCPUやメモリなどのリソース消費が高くなる傾向になります。無限にリソースが利用できれば良いですが、通常はコスト制約が存在するため利用できるリソースにも制約が生まれます。そこで処理時間とリソースの制約条件に基づきバッチ処理のパフォーマンスをチューニングする必要があります。よくあるバッチ処理の事例として、以下の処理を考えます。

  1. データベース からデータを読み取る
  2. アプリケーションのインメモリでデータを加工処理する
  3. データベースへ加工処理したデータを挿入する

上記の場合に、データベースのレコード1件ごとにRead/Writeクエリを走らせると、大量のデータ量を扱う場合に処理時間の致命的な遅延を招きます。そのため通常はバッチ数を設定し一定件数ごとに処理を行います。バッチ数を大きく設定した場合は一度にインメモリで処理できる件数が増加するためデータベースとのIOを削減でき処理のスループットが向上しますが、メモリのリソース消費が大きくなります。バッチ処理の処理時間とメモリのリソース制約を鑑み最適なバッチ数を設定していく必要があります。

まとめ

バッチ処理の一連の技術的な改善において以下の学びを得ました。

  • 一般的なシステム開発においてトランザクションの範囲は最小限に限定する必要がある。今回の事例のようにレコードに広範囲のロックがとられる可能性があり、オンライン処理へ影響を与える。
  • バッチ処理全体におけるトランザクションを廃止した結果、バッチ処理全体の原子性が担保されなくなった。原子性が担保されなくなったことにより、依存先の利用明細画面の表示に一貫性のない状態が発生した。そこで、処理対象レコードに処理済みの状態を持たせることでバッチ処理全体の冪等性を担保できるようになった。結果的に、処理失敗時のリトライが容易になり異常状態からの回復が迅速に行えるようになった。

今回はバッチ処理におけるトランザクションの改善及び冪等性の設計に関してご紹介しましたが、バッチ処理にはここで紹介しきれなかった様々な論点があります。

僕たちのバッチ処理改善の戦いはこれからだ(完)連載打ち切り

tech.timee.co.jp