Spring Batch アーキテクチャ

Spring Batch は、拡張性と多様なエンドユーザーグループを考慮して設計されています。次の図は、エンドユーザー開発者の拡張性と使いやすさをサポートする階層化された アーキテクチャを示しています。

Figure 1.1: Spring Batch Layered Architecture
図 1: Spring Batch 階層化アーキテクチャ

この階層化されたアーキテクチャは、アプリケーション、コア、インフラストラクチャという 3 つの主要な高レベルコンポーネントを強調しています。このアプリケーションには、開発者が Spring Batch を使用して作成したすべてのバッチジョブとカスタムコードが含まれています。バッチコアには、バッチジョブの起動と制御に必要なコアランタイムクラスが含まれています。JobLauncherJobStep の実装が含まれています。アプリケーションとコアの両方が、共通のインフラストラクチャ上に構築されています。このインフラストラクチャには、アプリケーション開発者 ( ItemReader や ItemWriter などのリーダーとライター) とコアフレームワーク自体 (独自のライブラリである再試行) の両方が使用する一般的なリーダーとライター、およびサービス ( RetryTemplate など) が含まれています。

一般的なバッチの原則とガイドライン

バッチソリューションを構築するときは、次の主要な原則、ガイドライン、一般的な考慮事項を考慮する必要があります。

  • 通常、バッチアーキテクチャはオンラインアーキテクチャに影響し、その逆も同様であることに注意してください。可能な場合は共通のビルドブロックを使用して、アーキテクチャと環境の両方を念頭に置いて設計します。

  • 可能な限り簡素化し、単一バッチアプリケーションで複雑な論理構造を構築することを避けます。

  • データの処理とストレージを物理的に密接に保ちます(言い換えると、処理が行われる場所でデータを保持します)。

  • システムリソースの使用、特に I/O を最小限に抑えます。内部メモリで可能な限り多くの操作を実行します。

  • アプリケーション I/O(SQL ステートメントの分析)を確認して、不必要な物理 I/O が回避されていることを確認します。特に、次の 4 つの一般的な欠陥を探す必要があります。

    • データが 1 回読み取られ、作業用ストレージにキャッシュまたは保持される場合に、すべてのトランザクションのデータを読み取ります。

    • 同じトランザクションで以前にデータが読み取られたトランザクションのデータを再読み取りします。

    • 不要なテーブルまたはインデックスのスキャンの原因。

    • SQL ステートメントの WHERE 節でキー値を指定しない。

  • バッチ実行で 2 回実行しないでください。たとえば、レポート目的でデータの要約が必要な場合、データが最初に処理されているときに(可能であれば)保存された合計をインクリメントする必要があります。これにより、レポートアプリケーションで同じデータを再処理する必要がなくなります。

  • バッチアプリケーションの開始時に十分なメモリを割り当てて、プロセス中の時間のかかる再割り当てを回避します。

  • データの整合性に関しては、常に最悪の事態を想定してください。適切なチェックを挿入し、検証を記録して、データの整合性を維持します。

  • 可能な場合、内部検証用のチェックサムを実装します。例: フラットファイルには、ファイル内のレコードの合計とキーフィールドの集約を示すトレーラーレコードが必要です。

  • 現実的なデータ量を備えた本番環境のような環境で、できるだけ早期にストレステストを計画および実行します。

  • 大規模なバッチシステムでは、特にシステムが 24-7 ベースでオンラインアプリケーションと同時に実行されている場合は、バックアップが困難になる可能性があります。通常、データベースのバックアップはオンライン設計で適切に処理されますが、ファイルのバックアップも同様に重要であると考える必要があります。システムがフラットファイルに依存している場合は、ファイルのバックアップ手順を整備してドキュメント化するだけでなく、定期的にテストする必要があります。

バッチ処理戦略

バッチシステムの設計と実装を支援するために、基本的なバッチアプリケーションのビルドブロックとパターンを、サンプルの構造ビューとコードシェルの形式で設計者とプログラマーに提供する必要があります。バッチジョブの設計を開始するときは、ビジネスロジックを次の標準的な構成要素を使用して実装できる一連の手順に分解する必要があります。

  • 変換アプリケーション : 外部システムによって提供または生成されたファイルの種類ごとに、変換アプリケーションを作成して、提供されたトランザクションレコードを処理に必要な標準形式に変換する必要があります。この型のバッチアプリケーションは、部分的または全体的に変換ユーティリティモジュールで構成できます (「基本的なバッチサービス」を参照)。

  • 検証アプリケーション : 検証アプリケーションは、すべての入力および出力レコードが正しく、一貫していることを保証します。検証は通常、ファイルヘッダーとトレーラー、チェックサムと検証アルゴリズム、レコードレベルのクロスチェックに基づいています。

  • 抽出アプリケーション : 抽出アプリケーションは、データベースまたは入力ファイルから一連のレコードを読み取り、定義済みのルールに基づいてレコードを選択し、そのレコードを出力ファイルに書き込みます。

  • 抽出 / 更新アプリケーション : 抽出 / 更新アプリケーションは、データベースまたは入力ファイルからレコードを読み取り、各入力レコードで見つかったデータに基づいて、データベースまたは出力ファイルに変更を加えます。

  • 処理と更新アプリケーション : 処理および更新アプリケーションは、抽出または検証アプリケーションからの入力トランザクションに対して処理を実行します。通常、処理には、処理に必要なデータを取得するためのデータベースの読み取り、データベースの更新、出力処理用のレコードの作成が含まれます。

  • 出力 / フォーマットアプリケーション : 出力 / フォーマットアプリケーションは、入力ファイルを読み取り、標準フォーマットに従ってこのレコードからデータを再構築し、出力または別のプログラムまたはシステムへの送信用の出力ファイルを生成します。

さらに、前述のビルドブロックを使用して構築できないビジネスロジック用に、基本的なアプリケーションシェルを提供する必要があります。

主なビルドブロックに加えて、各アプリケーションは、次のような 1 つ以上の標準的なユーティリティステップを使用する場合があります。

  • ソート: 入力ファイルを読み取り、レコード内のソートキーフィールドに従ってレコードが再配列された出力ファイルを生成するプログラム。ソートは通常、標準のシステムユーティリティによって実行されます。

  • スプリット: 単一の入力ファイルを読み取り、フィールド値に基づいて複数の出力ファイルのいずれかに各レコードを書き込むプログラム。分割は、パラメーター駆動の標準システムユーティリティによって調整または実行できます。

  • マージ: 複数の入力ファイルからレコードを読み取り、入力ファイルのデータを組み合わせて 1 つの出力ファイルを生成するプログラム。マージは、パラメーター駆動の標準システムユーティリティによって調整または実行できます。

バッチアプリケーションは、入力ソースによってさらに分類できます。

  • データベース駆動型アプリケーションは、データベースから取得した行または値によって駆動されます。

  • ファイル駆動型アプリケーションは、ファイルから取得したレコードまたは値によって駆動されます。

  • メッセージ駆動型アプリケーションは、メッセージキューから取得したメッセージによって駆動されます。

バッチシステムの基礎は処理戦略です。戦略の選択に影響を与える要因には、推定バッチシステムボリューム、オンラインシステムまたは他のバッチシステムとの同時実行性、利用可能なバッチウィンドウなどがあります。(24 時間 365 日稼働を望む企業が増えているため、明確なバッチウィンドウが消えつつあることに注意してください)。

バッチの一般的な処理オプションは次のとおりです(実装の複雑さの順)。

  • オフラインモードでのバッチウィンドウ中の通常の処理。

  • 同時バッチまたはオンライン処理。

  • 多くの異なるバッチ実行またはジョブの同時並行処理。

  • パーティション分割(同じジョブの多くのインスタンスを同時に処理)。

  • 上記のオプションの組み合わせ。

これらのオプションの一部またはすべては、有償スケジューラによってサポートされている場合があります。

このセクションの残りの部分では、これらの処理オプションについて詳しく説明します。経験則として、バッチプロセスで採用されるコミットおよびロック戦略は、実行される処理の型に依存し、オンラインロック戦略も同じ原則を使用する必要があることに注意してください。アーキテクチャ全体を設計する際に、バッチアーキテクチャを単に後付けすることはできません。

ロック戦略は、通常のデータベースロックのみを使用することも、アーキテクチャに追加のカスタムロックサービスを実装することもできます。ロッキングサービスは、データベースのロックを追跡し (たとえば、必要な情報を専用のデータベーステーブルに格納することによって)、データベース操作をリクエストするアプリケーションプログラムにアクセス許可を付与または拒否します。ロック状態の場合にバッチジョブの中止を回避するために、このアーキテクチャによって再試行ロジックを実装することもできます。

1. バッチウィンドウでの通常の処理オンラインユーザーまたは他のバッチプロセスが更新中のデータを必要としない別のバッチウィンドウで実行される単純なバッチプロセスの場合、同時実行性は課題にならず、バッチ実行の最後に単一のコミットを実行できます。

ほとんどの場合、より堅牢なアプローチがより適切です。バッチシステムは、複雑さと処理するデータ量の両方の点で、時間が経つにつれて大きくなる傾向があることに注意してください。ロック戦略が設定されておらず、システムがまだ単一のコミットポイントに依存している場合、バッチプログラムの変更は苦痛を伴う場合があります。最も単純なバッチシステムであっても、再起動リカバリオプションのコミットロジックの必要性と、このセクションで後述するより複雑なケースに関する情報を考慮してください。

2. 同時バッチ処理またはオンライン処理オンラインユーザーが同時に更新できるデータを処理するバッチアプリケーションは、オンラインユーザーが数秒以上必要とする可能性のあるデータ (データベースまたはファイルのいずれか) をロックしてはなりません。また、いくつかのトランザクションごとに更新をデータベースにコミットする必要があります。そうすることで、他のプロセスで使用できないデータの部分と、データが使用できなくなる経過時間を最小限に抑えることができます。

物理的なロックを最小限に抑えるもう 1 つのオプションは、論理的な行レベルのロックを楽観的なロックパターンまたは悲観的なロックパターンのいずれかで実装することです。

  • 楽観的ロックは、レコード競合の可能性が低いことを前提としています。これは通常、バッチ処理とオンライン処理の両方で同時に使用される各データベーステーブルにタイムスタンプ列を挿入することを意味します。アプリケーションは、処理のために行をフェッチするときに、タイムスタンプもフェッチします。その後、アプリケーションは処理された行を更新しようとするため、更新では WHERE 句で元のタイムスタンプが使用されます。タイムスタンプが一致すると、データとタイムスタンプが更新されます。タイムスタンプが一致しない場合、フェッチと更新の試行の間に別のアプリケーションが同じ行を更新したことを示します。そのため、アップデートを実行できません。

  • 悲観的ロックは、レコードの競合が発生する可能性が高いことを前提とするロック戦略であり、そのため、検索時に物理ロックまたは論理ロックを取得する必要があります。悲観的論理ロックの 1 つの型は、データベーステーブルで専用のロック列を使用します。アプリケーションが更新のために行を取得すると、ロック列にフラグが設定されます。フラグが設定されていると、同じ行を取得しようとする他のアプリケーションは論理的に失敗します。フラグを設定するアプリケーションが行を更新すると、フラグもクリアされ、他のアプリケーションが行を取得できるようになります。初期フェッチとフラグの設定の間でも、データの整合性を維持する必要があることに注意してください。たとえば、データベースロック ( SELECT FOR UPDATE など) を使用します。また、この方法には物理的なロックと同じ欠点があることにも注意してください。ただし、レコードがロックされている間にユーザーが昼食に出かけた場合にロックを解除するタイムアウトメカニズムの構築を管理する方がやや簡単です。

これらのパターンは必ずしもバッチ処理に適しているとは限りませんが、同時バッチ処理とオンライン処理 (データベースが行レベルのロックをサポートしていない場合など) に使用される場合があります。原則として、楽観的ロックはオンラインアプリケーションにより適していますが、悲観的ロックはバッチアプリケーションにより適しています。論理ロックを使用する場合は常に、論理ロックによって保護されたデータエンティティにアクセスするすべてのアプリケーションに対して同じスキームを使用する必要があります。

これらのソリューションは両方とも、1 つのレコードのロックのみに対応していることに注意してください。多くの場合、論理的に関連するレコードのグループをロックする必要があります。物理ロックでは、潜在的なデッドロックを回避するために、これらを非常に慎重に管理する必要があります。論理ロックを使用する場合、通常は、保護する論理レコードグループを理解し、ロックが一貫性がありデッドロックしないことを保証できる論理ロックマネージャーを構築するのが最善です。通常、この論理ロックマネージャーは、ロック管理、競合レポート、タイムアウトメカニズム、およびその他の問題のために独自のテーブルを使用します。

3. 並列処理並列処理では、複数のバッチ実行またはジョブを並列に実行して、バッチ処理の合計経過時間を最小限に抑えることができます。ジョブが同じファイル、データベーステーブル、またはインデックススペースを共有していない限り、これは問題ではありません。その場合、このサービスは、分割されたデータを使用して実装する必要があります。もう 1 つのオプションは、制御テーブルを使用して相互依存性を維持するためのアーキテクチャモジュールを構築することです。コントロールテーブルには、各共有リソースの行と、それがアプリケーションによって使用されているかどうかが含まれている必要があります。バッチアーキテクチャまたは並列ジョブのアプリケーションは、そのテーブルから情報を取得して、必要なリソースにアクセスできるかどうかを判断します。

データアクセスが問題にならない場合は、追加のスレッドを使用して並列処理を実行することにより、並列処理を実装できます。メインフレーム環境では、すべてのプロセスに十分な CPU 時間を確保するために、従来から並列ジョブクラスが使用されてきました。ソリューションは、実行中のすべてのプロセスのタイムスライスを確保するのに十分堅牢でなければなりません。

並列処理におけるその他の重要な課題には、ロードバランシングと、ファイルやデータベースバッファプールなどの一般的なシステムリソースの可用性が含まれます。また、制御テーブル自体が重要なリソースになりやすいことにも注意してください。

4. パーティショニングパーティショニングを使用すると、大規模なバッチアプリケーションの複数のバージョンを同時に実行できます。これの目的は、長いバッチジョブの処理に必要な経過時間を短縮することです。正常に分割できるプロセスは、入力ファイルを分割できるプロセス、またはアプリケーションをさまざまなデータセットに対して実行できるようにメインデータベーステーブルを分割できるプロセスです。

さらに、分割されたプロセスは、割り当てられたデータセットのみを処理するように設計する必要があります。パーティショニングアーキテクチャは、データベースの設計とデータベースのパーティショニング戦略に密接に結び付ける必要があります。データベースのパーティション化は、必ずしもデータベースの物理的なパーティション化を意味するわけではないことに注意してください (ただし、ほとんどの場合、これは推奨されます)。次の図は、パーティショニングアプローチを示しています。

Figure 1.2: Partitioned Process
図 2: 分割プロセス

アーキテクチャは、パーティション数を動的に構成できるほど柔軟でなければなりません。自動構成とユーザー制御構成の両方を検討する必要があります。自動構成は、入力ファイルのサイズや入力レコードの数などのパラメーターに基づいている場合があります。

4.1 分割アプローチパーティション化アプローチの選択は、ケースバイケースで行う必要があります。以下のリストは、可能なパーティション分割アプローチのいくつかを説明しています。

1. レコードセットの修正済みおよび均等な分割

これには、入力レコードセットを偶数の部分に分割することが含まれます(たとえば、各部分がレコードセット全体のちょうど 1/10 である 10 個)。各部分は、バッチ / 抽出アプリケーションの 1 つのインスタンスによって処理されます。

このアプローチを使用するには、レコード設定を分割するための前処理が必要です。この分割の結果は、バッチ / 抽出アプリケーションへの入力として使用して、その処理をその部分のみに制限できる下限および上限の配置数です。

前処理は、レコードセットの各部分の境界を計算および決定する必要があるため、大きなオーバーヘッドになる可能性があります。

2. キー列で分割する

これには、場所コードなどのキー列によって設定された入力レコードを分割し、各キーからバッチインスタンスにデータを割り当てることが含まれます。これを実現するために、列の値は次のいずれかになります。

  • パーティションテーブル(このセクションで後述)によってバッチインスタンスに割り当てられます。

  • 値の一部(0000-0999, 1000 - 1999 など)によってバッチインスタンスに割り当てられます。

オプション 1 では、新しい値を追加するということは、バッチまたは抽出を手動で再構成して、新しい値が特定のインスタンスに確実に追加されるようにすることを意味します。

オプション 2 では、これにより、すべての値がバッチジョブのインスタンスによってカバーされることが保証されます。ただし、1 つのインスタンスによって処理される値の数は、列値の分布に依存します (0000-0999 範囲に多数の場所があり、1000-1999 範囲には少数の場所がある場合があります)。このオプションでは、パーティション分割を考慮してデータ範囲を設計する必要があります。

両方のオプションでは、バッチインスタンスへのレコードの最適な均等分散を実現できません。使用されるバッチインスタンスの数の動的な構成はありません。

3. ビュー別

このアプローチは基本的にキー列による分割ですが、データベースレベルで行われます。レコードセットをビューに分割する必要があります。これらのビューは、処理中にバッチアプリケーションの各インスタンスによって使用されます。分割は、データをグループ化することにより行われます。

このオプションでは、バッチアプリケーションの各インスタンスを、(メインテーブルではなく)特定のビューにヒットするように構成する必要があります。また、新しいデータ値を追加すると、この新しいデータグループをビューに含める必要があります。インスタンス数を変更するとビューが変更されるため、動的構成機能はありません。

4. 処理インジケータの追加

これには、インジケータとして機能する入力テーブルへの新しい列の追加が含まれます。前処理ステップとして、すべてのインジケーターが未処理としてマークされます。バッチアプリケーションのレコードフェッチステージでは、個々のレコードが未処理としてマークされているという条件でレコードが読み取られ、(ロック付きで) 読み取られると、処理中としてマークされます。そのレコードが完了すると、インジケータは完了またはエラーに更新されます。追加の列によってレコードが 1 回だけ処理されることが保証されるため、バッチアプリケーションの多くのインスタンスを変更せずに開始できます。

このオプションを使用すると、テーブルの I/O が動的に増加します。バッチアプリケーションを更新する場合、書き込みは必ず発生するため、この影響は軽減されます。

5. テーブルをフラットファイルに抽出する

このアプローチには、フラットファイルへのテーブルの抽出が含まれます。次に、このファイルを複数のセグメントに分割し、バッチインスタンスへの入力として使用できます。

このオプションを使用すると、テーブルをファイルに抽出して分割する追加のオーバーヘッドにより、マルチパーティションの効果が相殺される場合があります。動的構成は、ファイル分割スクリプトを変更することで実現できます。

6. ハッシュカラムの使用

このスキームには、ドライバーレコードの取得に使用されるデータベーステーブルへのハッシュ列 (キーまたはインデックス) の追加が含まれます。このハッシュ列には、バッチアプリケーションのどのインスタンスがこの特定の行を処理するかを決定するインジケーターがあります。例: 開始するバッチインスタンスが 3 つある場合、'A' のインジケーターはインスタンス 1 で処理する行をマークし、'B' のインジケーターはインスタンス 2 で処理する行をマークし、'C' のインジケーターはマークを付けます。インスタンス 3 で処理する行。

レコードの取得に使用されるプロシージャには、特定のインジケータでマークされたすべての行を選択するための追加の WHERE 句があります。このテーブルへの挿入には、マーカーフィールドの追加が含まれます。マーカーフィールドは、デフォルトでインスタンスの 1 つ( "A" など)になります。

単純なバッチアプリケーションを使用して、異なるインスタンス間で負荷を再分配するなど、インジケーターを更新します。十分な数の新しい行が追加されたら、このバッチを実行して(バッチウィンドウを除くいつでも)、新しい行を他のインスタンスに再配布できます。

バッチアプリケーションの追加のインスタンスは、バッチアプリケーションを実行するだけで (前の段落で説明したように)、新しい数のインスタンスで動作するようにインジケーターを再配布します。

4.2 データベースとアプリケーションの設計原則

パーティション化されたデータベーステーブルに対して実行され、キー列アプローチを使用するマルチパーティション化されたアプリケーションをサポートするアーキテクチャには、パーティションパラメーターを格納するための主要パーティションリポジトリが含まれている必要があります。これにより、柔軟性が提供され、保守性が確保されます。通常、リポジトリは、パーティションテーブルと呼ばれる単一のテーブルで構成されます。

パーティションテーブルに格納される情報は静的であり、一般に DBA が管理する必要があります。テーブルは、マルチパーティションアプリケーションのパーティションごとに 1 行の情報で構成されている必要があります。テーブルには、プログラム ID コード、パーティション番号 (パーティションの論理 ID)、このパーティションのデータベースキー列の下限値、およびこのパーティションのデータベースキー列の上限値の列が必要です。

プログラムの起動時に、プログラム id とパーティション番号がアーキテクチャ (具体的には制御処理タスクレット) からアプリケーションに渡されます。キー列アプローチが使用されている場合、これらの変数を使用してパーティションテーブルが読み取られ、アプリケーションが処理するデータの範囲が決定されます。さらに、次の目的で、処理全体でパーティション番号を使用する必要があります。

  • マージプロセスが正しく機能するように、出力ファイルまたはデータベースの更新に追加します。

  • 通常の処理をバッチログに、エラーをアーキテクチャエラーハンドラーに報告します。

4.3 デッドロックの最小化

アプリケーションが並列で実行されているか、分割されている場合、データベースリソースの競合やデッドロックが発生する可能性があります。データベース設計チームは、データベース設計の一環として、潜在的な競合状況を可能な限り排除することが重要です。

また、開発者は、デッドロック防止とパフォーマンスを念頭に置いて、データベースインデックステーブルが設計されていることを確認する必要があります。

デッドロックまたはホットスポットは、ログテーブル、コントロールテーブル、ロックテーブルなどの管理テーブルまたはアーキテクチャテーブルで発生することがよくあります。これらの影響も考慮に入れる必要があります。アーキテクチャのボトルネックの可能性を特定するには、現実的なストレステストが不可欠です。

データに対する競合の影響を最小限に抑えるために、アーキテクチャは、データベースへのアタッチ時またはデッドロックの発生時にサービス (待機と再試行の間隔など) を提供する必要があります。これは、特定のデータベースリターンコードに反応し、すぐにエラーを発行する代わりに、所定の時間待機してデータベース操作を再試行する組み込みメカニズムを意味します。

4.4 パラメーターの受け渡しと検証

パーティションアーキテクチャは、アプリケーション開発者に対して比較的透過的でなければなりません。アーキテクチャは、次のようなパーティションモードでのアプリケーションの実行に関連するすべてのタスクを実行する必要があります。

  • アプリケーションの起動前にパーティションパラメーターを取得します。

  • アプリケーションの起動前にパーティションパラメーターを検証します。

  • 起動時にアプリケーションにパラメーターを渡します。

検証には、次のことを確認するチェックを含める必要があります。

  • アプリケーションには、データ範囲全体をカバーするのに十分なパーティションがあります。

  • パーティション間にギャップはありません。

データベースがパーティション化されている場合、単一のパーティションがデータベースパーティションにまたがっていないことを確認するために、追加の検証が必要になる場合があります。

また、アーキテクチャはパーティションの統合を考慮する必要があります。主な質問は次のとおりです。

  • 次のジョブステップに進む前に、すべてのパーティションを終了する必要がありますか?

  • パーティションの 1 つが異常終了するとどうなるでしょうか?