Skip to content
Published on

金融機関のバッチと日次締め(EOD) — 深夜を支配するアーキテクチャ

Authors

はじめに — 銀行の一日は午前0時に終わらない

銀行の営業店が閉まり、モバイルアプリのユーザーが眠りにつく深夜、銀行のデータセンターは一日で最も忙しい時間を迎えます。数千のバッチジョブが依存関係グラフに沿って流れ、利息を計算し、延滞を遷移させ、レポートを作成し、元帳を締めます。この一連の処理全体を日次締め(EOD、End of Day)と呼びます。

「このリアルタイム時代に、なぜいまだにバッチなのか」という質問をよく受けます。この記事ではその問いへの答えから始め、EOD処理の範囲、ジョブスケジューラと依存関係管理、大量処理の技法、オンラインとバッチの共存、障害復旧、そしてバッチのモダナイゼーションの流れまで、金融機関のバッチアーキテクチャの全体像を描いていきます。

この記事はシステムアーキテクチャの観点からの技術資料であり、特定機関の内部実装や規制解釈に関する助言ではありません。

金融機関でバッチが依然として中核である理由

バッチが消えないのは技術的負債のせいだけではありません。本質的にバッチでしかあり得ない業務があるのです。

  1. 基準時点が必要な業務: 利息計上、資産分類、レポートは「ある時点の状態」を基準に全口座へ一括適用する必要があります。基準時点なしに流れ続けるストリームでは、「本日残高基準の利息」を定義できません。
  2. 規制報告の単位が日次: 監督当局への業務報告、中央銀行への報告、預金保険関連の算出物の大半が、営業日単位のスナップショットを要求します。
  3. 照合と決済の構造: 他機関との決済(決済機関、カード会社、証券保管振替)は、ファイル交換と日次確定というプロトコル自体がバッチです。
  4. 処理効率: 1千万口座の利息計上を1件ずつオンライントランザクションで処理するより、セットベース(set-based)の一括処理の方が数十倍効率的です。

つまり「バッチ vs リアルタイム」は二者択一ではなく、基準時点が必要な業務はバッチ、即時性が必要な業務はオンラインという役割分担の問題です。

EOD処理の範囲 — 深夜に起きていること

日次締めウィンドウで処理される代表的な業務を整理すると次の通りです。

業務内容特徴
利息計上預金・貸出の口座別日割り利息計算と累積全口座対象、計算集約的
起算日処理締め後到着取引の営業日帰属の確定日付切替と連動
延滞遷移返済期日経過の貸出の延滞状態への遷移、延滞利息の起算状態マシン、規制依存
資産健全性分類正常/要注意/固定などの分類再算定与信全体の再評価
満期処理定期預金の満期解約・再預入、貸出期日処理商品ルールが多様
手数料・決済日次手数料集計、他機関向け決済ファイル生成対外インターフェース
総勘定元帳への転記取引元帳の集約とGL反映貸借平均ゲート
レポート・情報系ロード規制報告の算出、DW/情報系ETL締め確定後に実行

重要なのは、これらの業務が順序依存性を持つことです。利息計上の前に当日取引が確定していなければならず、資産分類は延滞遷移の後、GL転記はすべての元帳変動が終わった後でなければ実行できません。

バッチアーキテクチャ — スケジューラと依存関係DAG

数千のジョブを人間が順番に実行することはできないため、ジョブスケジューラが依存関係グラフ(DAG)に沿ってジョブを起動します。

[EOD依存関係DAG (簡略化)]

  オンライン締め宣言
  当日取引の確定 ──────┬─────────────┐
        │              │             │
        ▼              ▼             ▼
  利息計上         手数料集計     満期処理
        │              │             │
        ▼              │             │
  延滞遷移 ◀───────────┘             │
        │                            │
        ▼                            │
  資産健全性分類 ◀───────────────────┘
  元帳検証 (貸借平均) ── 失敗時は全体中断ゲート
  GL転記 ──▶ レポート生成 ──▶ 情報系ロード
  日付切替 ──▶ オンライン再開

伝統的に金融機関は商用エンタープライズスケジューラ(Control-M、TWS/IWS、AutoSys、JobScheduler系など)を使ってきましたが、最近はAirflowのようなオープンソースを導入するところも増えています。観点別の比較は次の通りです。

観点商用スケジューラAirflowなどオープンソース
依存関係モデルジョブ・カレンダー中心、運用者向けGUIコードベースDAG、開発者向け
営業日カレンダー休日・営業日カレンダーの組み込みが強力自前実装またはプラグインが必要
再起動・例外処理運用コンソールから即時介入が容易タスク単位retry、UI介入可能
監査・権限金融が求める水準の統制機能が成熟RBAC可能だが構成が必要
コスト・拡張ライセンス費用が高いライセンス無料、運用力が必要

どちらを選んでも、譲れない要求事項は同じです。

  • 営業日カレンダー: 休日、臨時休業、月末・四半期末の分岐をスケジュール定義で表現できること。
  • チェックポイントと再起動: ジョブが途中で失敗したら、最初からではなく失敗地点から再開できること。
  • 先行後続の強制: 先行ジョブの成功なしに後続ジョブが動かないこと。強制起動は権限統制と記録の下でのみ可能であること。

大量処理の技法 — パーティショニング、並列度、チャンク

1千万件以上を処理するバッチの基本は3つです。

  1. パーティショニング: 処理対象を重複しない区間(口座番号範囲、ハッシュ、支店)に分けて並列実行します。
  2. チャンク処理: 1件ずつコミットせず、N件単位で読み取り-処理-書き込みの後にコミットし、トランザクションコストを下げます。
  3. セットベース演算の優先: 行単位ループより、可能な限りSQLの一括演算(INSERT SELECT、一括UPDATE)を使います。

Spring Batchで利息計上ジョブの骨格を表現すると次のようになります。

@Configuration
public class InterestAccrualJobConfig {

    @Bean
    public Job interestAccrualJob(JobRepository jobRepository, Step partitionedStep) {
        return new JobBuilder("interestAccrualJob", jobRepository)
                .start(partitionedStep)
                .build();
    }

    // 口座番号範囲でパーティショニングし、グリッドサイズ分だけ並列実行
    @Bean
    public Step partitionedStep(JobRepository jobRepository,
                                Step accrualStep,
                                AccountRangePartitioner partitioner,
                                TaskExecutor taskExecutor) {
        return new StepBuilder("partitionedAccrualStep", jobRepository)
                .partitioner("accrualStep", partitioner)
                .step(accrualStep)
                .gridSize(8)
                .taskExecutor(taskExecutor)
                .build();
    }

    // チャンク単位処理: 1000件の読み取り-計算-書き込み後にコミット
    @Bean
    public Step accrualStep(JobRepository jobRepository,
                            PlatformTransactionManager txManager,
                            JdbcPagingItemReader<Account> reader,
                            InterestCalculator processor,
                            JdbcBatchItemWriter<AccrualResult> writer) {
        return new StepBuilder("accrualStep", jobRepository)
                .<Account, AccrualResult>chunk(1000, txManager)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .faultTolerant()
                .skipLimit(0)          // 金融バッチ: スキップではなく失敗させて調査
                .build();
    }
}
public class AccountRangePartitioner implements Partitioner {
    @Override
    public Map<String, ExecutionContext> partition(int gridSize) {
        Map<String, ExecutionContext> result = new HashMap<>();
        long min = 1_0000_0000L, max = 9_9999_9999L;
        long size = (max - min) / gridSize + 1;
        for (int i = 0; i < gridSize; i++) {
            ExecutionContext ctx = new ExecutionContext();
            ctx.putLong("minAccountNo", min + size * i);
            ctx.putLong("maxAccountNo", Math.min(min + size * (i + 1) - 1, max));
            result.put("partition" + i, ctx);
        }
        return result;
    }
}

設計のポイントです。

  • skipLimit(0): 一般的なデータパイプラインでは不良レコードをスキップしますが、利息計上で1口座をスキップすると、その口座の利息が消えます。失敗したら止めて調査するのが原則です。
  • JdbcPagingItemReader: カーソルベースのリーダーは再起動時の位置復元が難しい環境があり、キーベースのページングリーダーの方が再開の安全性で有利です。
  • パーティション境界はデータ分布を考慮: 口座番号が支店ごとに偏っていると、均等範囲分割は不均衡になります。ハッシュ分割または事前統計に基づく分割を検討します。

日次締めとオンラインの共存 — センターカットと日付切替

24時間バンキング時代の中心的な難題は、「締め処理中にも振込は届く」ことです。これを扱う仕組みがセンターカット(center-cut)と日付切替(cut-over)シーケンスです。

センターカットは、大量件数をオンライントランザクション経路へ流しつつ、バッチが主導する方式です。例えば給与振込10万件をバッチファイルとして受け取り、内部的にはオンライン振込トランザクションを高速で連射する構造です。オンライン経路を再利用するため、限度チェック・残高チェックのロジックが一元化されます。

日付切替はおおよそ次のシーケンスで進みます。

[日付切替 cut-over シーケンス (簡略化)]

  23:50  事前締め告知        : 一部チャネルの取引制限を案内
  23:55  新規取引のキューイング開始 : 以降の到着取引は翌日起算キューへ積載
  00:00  論理的日付切替      : システム営業日 D → D+1
         ├─ キューイングされた取引: D+1起算日でオンライン処理を再開
         │   (口座元帳は生き続ける — 無停止切替)
  00:05  D日付取引の確定スナップショット → EODバッチ起動
  02:00  元帳検証・GL転記完了
  04:00  レポート・情報系ロード完了、EOD終了宣言

設計上の重要な決定事項です。

  • 無停止切替の可否: かつては締め中にオンラインを停止しましたが、現在は大半が「起算日だけ切り替えてオンラインは維持」する無停止締めが標準です。そのためには元帳の参照・記帳が営業日基準で分離されている必要があります(前回の記事のbusiness_date設計がここで効いてきます)。
  • 締め時点の一貫性スナップショット: 利息計上はD日23:59:59時点の残高で計算する必要がありますが、オンラインは動き続けているため、「D日付のポスティングまでのみ合算」する論理スナップショットで解決します。
  • キューイング取引のSLA: 日付切替の間にキューへ積まれた取引は数分以内に処理されるべきで、キュー滞留の監視が必要です。

バッチ性能最適化 — インデックスとバルク演算

バッチ性能問題の大半は、「オンライン用に設計されたスキーマをバッチがフルスキャンする」ことから生じます。

  • 読み取り: バッチ専用のカバリングインデックスやパーティションプルーニングを活用します。日付パーティショニングされたポスティングテーブルなら、当日パーティションのみを読むようにします。
  • 書き込み: 1件ずつのINSERTではなくバッチINSERT(JDBC batch、COPY)を使います。大量UPDATEは、一時テーブルに結果を作ってMERGEする方が速い場合が多いです。
  • インデックス維持コスト: 数百万件をロードするテーブルの二次インデックスはロード速度を大きく落とします。夜間大量ロードのテーブルはインデックスを最小化するか、ロード後に再構築する戦略を検討します。
-- 行単位ループの代わりにセットベースの一括利息累積 (例)
INSERT INTO interest_accruals (account_id, business_date, accrual_amount)
SELECT b.account_id,
       DATE '2026-06-13',
       ROUND(b.balance * r.daily_rate, 0)
FROM eod_balance_snapshot b
JOIN product_rates r ON r.product_code = b.product_code
WHERE b.snapshot_date = DATE '2026-06-13'
  AND b.balance > 0;

ただしセットベースに寄せるほど、「どの口座でなぜこの金額になったのか」の追跡性が落ちるため、算出根拠(適用金利、基準残高)を結果テーブルに併せて保存しておくと監査対応に有利です。

障害処理 — 部分再処理と冪等設計

深夜2時にジョブが落ちたとき、運用者が安心して押せる「再起動ボタン」を作ることがバッチ設計の半分です。

原則は3つです。

  1. 再実行の冪等性: 同じジョブを同じパラメータで2回実行しても結果が同じでなければなりません。利息計上であれば、「当日計上分を削除して再計算」か「口座+日付のユニーク制約で重複遮断」のどちらかを明示的に選択します。
  2. チェックポイントによる部分再処理: チャンクのコミット地点がチェックポイントになるため、再起動時に完了済みチャンクはスキップされます。Spring BatchのJobRepositoryがこの状態を管理します。
  3. データ補正は補正ジョブで: 運用者がSQLで直接データを修正した瞬間、冪等性と監査証跡が壊れます。補正も記録が残るジョブとして実行します。
-- 冪等性パターン: 口座+日付のユニーク制約 + 再実行時の既存分クリーンアップ
ALTER TABLE interest_accruals
    ADD CONSTRAINT uq_accrual UNIQUE (account_id, business_date);

-- 再起動の前処理ステップ: 当日の未確定計上分を削除 (確定フラグは保護)
DELETE FROM interest_accruals
WHERE business_date = DATE '2026-06-13'
  AND finalized = false;

他機関インターフェースのジョブはさらに厄介です。決済ファイルをすでに送信したのにジョブが失敗として記録された場合、再実行でファイルが二重送信され得ます。送信ステップは「生成-検証-送信-確認」に分割し、送信ステップ専用の冪等性キー(ファイル名+日付)を設けます。

モニタリング — バッチSLAと遅延アラート

EODには「朝の営業開始前に完了」という絶対的なデッドラインがあります。モニタリングでは障害検知より遅延予測が重要です。

  • クリティカルパス管理: DAGの中で最も長い依存経路を特定し、経路上のジョブの開始・終了時刻をベースラインと比較します。
  • 段階別SLA: 「利息計上は01:30までに開始、02:30までに終了」のような区間SLAを設け、超過時にはオンコール通知を送ります。
  • スループットの傾向: 同じジョブが昨日より2倍遅ければ、データ増加、統計情報の陳腐化、実行計画の変更を疑います。ジョブごとの処理件数と所要時間を時系列で蓄積して傾向を見ます。
  • 先行データ検証: 外部ファイルの遅延や0件到着はよくある障害原因です。「ファイル到着+件数の妥当性」チェックをジョブの開始条件にします。

バッチのモダナイゼーション — 準リアルタイムとクラウド

バッチをすべてリアルタイムに変えることがモダナイゼーションではありません。現実的な方向性は次の通りです。

  1. 計算と確定の分離: 利息の「計算」はオンライン時間中にも増分で先行しておき(準リアルタイム集計)、締め時点では「確定」のみを行ってEODウィンドウを縮めます。
  2. イベントドリブンな遷移: 延滞遷移のようにトリガーが明確な業務は、日次1回のバッチからイベント(返済期日経過)ベースへ移せます。ただし規制報告の基準時点との整合には、依然として日次確定が必要です。
  3. クラウドでのバッチ: Kubernetes Job/CronJob、AWS Batch、マネージドAirflowなどへ実行基盤を移すと、弾力的な並列度(平常時4ノード、月末16ノード)が得られます。その代わり、営業日カレンダー、再起動統制、データ近接性(DBとのネットワーク遅延)は自分で設計する必要があります。
  4. 締めのない会計は存在しない: どんなアーキテクチャでも、「この時点を基準に数字を確定する」という会計上の要求は消えません。モダナイゼーションの目標は締めの排除ではなく、締めウィンドウの短縮と安定化です。

テスト — 大量データなしにバッチは信用できない

バッチは1万件では問題なく動き、1千万件で死にます。テスト戦略の核心は本番規模のデータです。

  • 合成データ生成器: 本番データの分布(口座数、商品比率、取引密度)を模した生成器を作り、性能テスト環境を満たします。個人情報をコピーする方式はマスキング・非識別化の統制が必要になるため、最初から合成生成がクリーンです。
  • 境界日テスト: 月末、四半期末、年末、うるう年の2月29日、連休の連続区間(連休明けの最初の営業日)は別シナリオで検証します。休日明けの利息計上は休日日数分が累積するため、平日と結果が異なります。
  • 再起動リハーサル: わざとジョブを途中で落として再起動し、結果が無欠か — 二重計上も欠落もないか — を定期的に検証します。

運用シナリオ — 締め遅延障害への対応

架空の、しかし典型的な障害シナリオで本論を締めくくります。

[シナリオ: 利息計上ジョブの遅延]

 01:40  利息計上ジョブ、通常40分 → 60分経過しても進捗55%
 01:45  遅延アラート発生。当直がクリティカルパスへの影響を評価
        → このままではGL転記が04:30、営業開始前の完了は不可能と判断
 01:50  原因分析: 前日の新商品リリースで対象口座が30%増加
        + 統計情報未更新で実行計画が変更(インデックス → フルスキャン)
 02:00  意思決定: ジョブ中断 → 統計更新 → チェックポイント再起動
        (並列度 8 → 12 に引き上げ、事前検証済みの上限内)
 02:10  再起動。完了済みパーティションはスキップされ残りのみ処理
 03:05  利息計上完了、後続ジョブが自動進行
 04:10  EOD正常終了。事後対応: 商品リリース時のバッチ影響評価を
        リリースチェックリストに追加、統計更新ジョブを先行段階に編入

このシナリオが示すのは、障害対応能力は結局のところ日頃の設計 — チェックポイント、冪等な再起動、並列度の調整可能性、クリティカルパスの可視性 — から生まれるということです。

バッチとオンラインの資源競合 — 分離戦略

無停止締めでは、バッチとオンラインが同じデータベースを奪い合います。分離戦略なしに大量バッチを回すと、オンラインの応答時間が跳ね、最悪の場合ロック待ちでチャネル取引がタイムアウトします。

実務で使う分離手段を段階別に整理すると次の通りです。

手段内容適用ポイント
時間分離オンラインピークを避けたウィンドウ割り当て最も基本、ただし24時間チャネルでは限界
資源分離バッチ専用コネクションプール・セッション数上限DBコネクション枯渇の防止
読み取り分離読み取り専用レプリカで参照系バッチを実行レポート・情報系ロード
ロック最小化短いトランザクション、チャンクコミット、楽観的処理元帳更新バッチ
優先度制御DBリソースマネージャでバッチセッションを降格ピーク時間に食い込んだ際の保護

特に注意すべきパターンが2つあります。

  1. バッチの長時間トランザクション: 100万件を1トランザクションで処理すると、undoの負担とロック保持時間が爆発します。チャンクコミットは性能技法である以前に、オンライン保護装置です。
  2. オンラインと同じ行を触るバッチ: 利息計上が更新する口座行をオンライン振込も更新するなら、行ロックの順序と待機時間の上限(lock timeout)を明示的に設計すべきです。バッチがロックを長く握る側にならないよう、バッチ側に短いタイムアウトとリトライを設けます。

ジョブ設計標準 — 運用可能なバッチの条件

数千のジョブを運用するには、個々のジョブの品質より標準の一貫性が重要です。組織に1つはあるべきジョブ設計標準の骨格です。

  • 命名規則: システム-業務-周期-連番の形式(例: DEP-INTACC-D-010)で、ジョブ名だけで所属と周期がわかるようにします。
  • パラメータ標準: すべてのジョブは基準日(business date)を明示的なパラメータとして受け取ります。「今日の日付をシステム時計から読む」ジョブは、再処理が不可能なジョブです。
  • 終了コード規約: 正常(0)、警告付き完了(4)、再起動可能な失敗(8)、介入が必要な失敗(16)のように、スケジューラが自動分岐できるコード体系を統一します。
  • ログ標準: 開始・終了時刻、入力件数、処理件数、スキップ件数、基準日を構造化ログとして残します。「処理件数 = 入力件数」の不一致はそれ自体がアラート対象です。
[ジョブ実行サマリーログの例 (構造化)]

  job=DEP-INTACC-D-010  business_date=2026-06-13
  status=COMPLETED      exit_code=0
  read_count=10482917   write_count=10482917  skip_count=0
  started=01:12:03      ended=01:54:41        duration=42m38s
  partition_count=8     restart=false

この5行が毎日蓄積されれば、スループット傾向分析と遅延予測の源泉データになります。逆にこの標準がなければ、障害のたびにジョブごとに異なるログフォーマットの解読に時間を費やすことになります。

日次締めの先へ — 月次締め、四半期締め、年次締め

EODの上には、より大きな周期の締めが重なっています。月次締め(EOM)は月単位の利息決算・手数料請求・月次レポートを、四半期締めは資産健全性の確定と引当金の算定を、年次締め(EOY)は年間決算と繰越処理を行います。

周期が重なる日 — 例えば12月31日はEOD、EOM、四半期締め、EOYがすべて重なります — の設計が厄介です。

  • ウィンドウの見積もり: 重なる日の総所要時間は平日の2~3倍になり得ます。最悪の組み合わせ日を基準にウィンドウを見積もるべきで、平日基準でSLAを約束してはいけません。
  • 周期間の依存関係: 月次締めはその月の最後のEODの正常完了が前提です。EOD失敗時に月次締めを自動保留するゲートが必要です。
  • 年末の凍結期間: 多くの機関が年次締めの前後でデプロイ凍結(freeze)を運用します。年次締めでのみ実行されるジョブは年に1回しか検証の機会がないため、年の途中にリハーサル環境で模擬年次締めを回しておくことが、事故を防ぐ唯一の方法です。
[締め周期の重なり (12月31日の例)]

  EOD (日次締め)
   └─ EOM (月次締め)    : 12月分の利息決算、月次レポート
       └─ EOQ (四半期締め): 資産分類の確定、引当金
           └─ EOY (年次締め): 年間決算、繰越、年次レポート

  実行順序: EOD → EOM → EOQ → EOY (各段階が次の先行条件)

落とし穴とアンチパターン

最後に、金融機関のバッチで繰り返し目撃されるアンチパターンを集めました。

  1. システム時計依存: ジョブ内部で現在時刻から基準日を決めるパターン。再処理時に昨日の日付で実行できなくなります。基準日は常にパラメータで。
  2. 暗黙的な依存関係: 「Aジョブは普段1時に終わるからBジョブは1時30分に開始」のような時間ベースの依存。Aが遅延した日、Bは空のデータを処理します。依存関係は必ず先行後続関係として宣言します。
  3. 再起動不可能な中間状態: 一時テーブルをTRUNCATEしてからロードするジョブが途中で落ちると、再起動時に半分空のテーブルから始まります。ステップ境界ごとに状態が自己完結しているか点検します。
  4. 無限リトライ: 外部機関の応答失敗に無限リトライを設定すると、相手機関の障害時にこちらのキューとスレッドも一緒に枯渇します。リトライ上限と手動介入への切替点を設けます。
  5. アラートの洪水: すべてのジョブ失敗を同じチャネルに送ると、本当にクリティカルなアラートが埋もれます。クリティカルパスのジョブと一般ジョブでアラート等級を分離します。
  6. 「一度だけ実行する」補正スクリプト: 記録のない一回限りのSQL補正は、次の締めの照合不一致として返ってきます。補正もジョブとして、記録とともに。
  7. テスト環境の小規模データ: 1万件で通った性能テストは何も保証しません。実行計画はデータ規模によって変わります。

設計チェックリスト

  • EOD全体の依存関係がDAGとして文書化・コード化されているか
  • 営業日カレンダー(休日、月末、四半期末)がスケジュール定義に反映されているか
  • すべてのジョブが同じパラメータで再実行しても安全(冪等)か
  • チェックポイント再起動時の完了分スキップが検証されているか
  • skipポリシーが業務特性に合わせて設定されているか(金融コアジョブはskip禁止)
  • パーティショニング境界がデータ分布を反映しているか
  • 日付切替中の取引キューイングと起算日分離が動作するか
  • 締め検証(貸借平均、件数照合)が後続ジョブ進行のゲートになっているか
  • 外部ファイルの到着・件数妥当性チェックがジョブの開始条件か
  • クリティカルパスと段階別SLA、遅延アラートが運用されているか
  • ジョブ別のスループット・所要時間が時系列で蓄積されているか
  • 本番規模の合成データで性能・再起動リハーサルをしているか
  • 境界日(月末・年末・うるう日・連休)シナリオがテストに含まれているか
  • 手動データ補正の代わりに記録が残る補正ジョブを使っているか

おわりに

金融機関のバッチは古い技術ではなく、「基準時点で数字を確定する」という会計上の要求を最も効率的に実装する形式です。良いバッチアーキテクチャの本質は華やかなフレームワークではなく — 依存関係の明示化、失敗を前提とした冪等な再起動、そして深夜2時の運用者が自信を持って押せる再起動ボタンです。EODウィンドウを縮めたいなら、バッチをなくそうとするのではなく、計算を前倒しして確定だけを締めに残す方向をまず検討してください。

参考資料