TL;DR
- ひとくちにバッチといっても色々ある
- 夜間バッチをもう作るな
- オンラインバッチはSQL以前にDB設計がんばれ
はじめに
Twitterのタイムラインで以下のようなツイートが回ってきました。
バッチ処理をみんな舐めてかかったり、ショボイとか思ってる人多い印象なんだけれども、数十万~数千万件規模のデータを処理したことあるのかな。テンプレ通りのコードじゃ動かないよ?ネットに本にも答え載ってないよ?低レイヤも意識しないと動かないよ?
2020年1月10日
ツイートされたわだっしーさんの意図がどこにあるかは確認してないですが、極限の世界でテンプレート的な処理では対応出来ないのはあるよな、と思いつつもある程度はバッチの作法としての書き方があると思っています。
このツイートとその関連ツイートを読みながら、そういえばバッチ処理に関して書いてある記事はあまり見ないなぁ、とおもったので他のネットや本に書かれてるレベルの話かもしれないですが、基本的な考え方をまとめてみました。
前半は四方山話なので、Tipsのみが知りたい人は後半にジャンプ!
そもそもバッチ処理って何?
バッチ処理とはなんでしょうか? Wikipediaによると下記の通りになります。
バッチ処理(バッチしょり)とは、コンピュータで1つの流れのプログラム群(ジョブ)を順次に実行すること。あらかじめ定めた処理を一度に行うことを示すコンピュータ用語。反対語は対話処理・インタラクティブ処理またはリアルタイム処理。引用: [Wikipedia - バッチ処理](https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%83%E3%83%81%E5%87%A6%E7%90%86)
つまりWebアクセスのようなリアルタイム処理ではなく、毎時とか日次とか月次とかでまとめて処理を行う方式です。
コンピュータが登場した当時、プログラムとはすなわちバッチ処理でした。パンチカードに書いて計算を一括しておこなっていたらしいです。
これが超初期のバッチ処理です。
つづいてメインフレーム時代から続く所謂「夜間バッチ」です。おそらくエンプラ系でバッチと聞くと思い浮かべるのはこれでしょう。コンピュータ黎明期には24時間稼働するリアルタイムシステムは稀でほとんどが業務時間が存在していました。
そのため、業務時間中はリクエストを貯めて業務後(=夜間)に日中のリクエストをまとめて処理する方式が良く採用されていたようです。
これは当時のユースケースならリソース効率化の観点で非常に優秀です。夜はリアルタイム処理が来ないからバッチ処理にコンピュータリソースをまわせたわけです。
これはメインフレームからオープン系になっても変わらず大量の夜間バッチが作られてきました。
SOAの時代以来、バッチよりもリアルタイム/セミリアルタイムの仕組みの方が良いという風潮になってきたとは思いますがまだまだ現役です。
なお、運用系のスクリプトとしてキャッシュのクリアとかゴミファイルの削除とかJenkinsやcronで実行されるような軽量のバッチも多く実運用では使われてると思いますが、今回は扱いません。
オフラインバッチとオンラインバッチ
バッチには大別すると純バッチとも呼ばれるオフラインバッチとオンラインバッチがあります。
オフラインバッチはデータベースなどトランザクション系のシステムから切り離されたバッチの事です。現代だとETLのような一旦ファイルに落として、処理をして最終的に書き戻すような処理ですね。
オンラインバッチはデータベースと繋がっているバッチの事です。オンラインバッチを「オンライン」と略すことも現場によっては稀によくあるので、リアルタイム処理の話なのかバッチの話なのか空気を読みましょう。
その性質上オフラインバッチはトランザクションがありませんが性能が出しやすいです。ファイルやインメモリ処理なので爆速ですし並列化のペナルティも少ないです。DB性能に不安があったレガシーから続くバッチやビッグデータに代表されるような大規模データ処理はオフラインバッチで組まれてる事が多いと思います。Hadoop/HDFSなんかは典型的なオフラインバッチ環境です。
オンラインバッチはSQLなどを使用してRDBに直接読み書きをします。
その最大の特徴はトランザクション配下だということです。オフラインバッチは性質上データを閉めるタイミングというかウインドウを適切に組まないとシステムでデータ不整合が発生します。システム全体のデータアクセス特性を把握して結果整合性が保たれるように作る必要があるわけです。
オンラインバッチはトランザクション配下なのでそのような考慮は本来的には不要です。ただしDB毎の分離レベルの挙動は把握しておきましょう。
便利なのですが、上手に作らないとRDBのコネクションやメモリを大量に消費したり、長時間または広範囲のロックをかけてしまいシステム全体の性能ペナルティを発生しやすいという課題もあります。
ちなみに同じバッチの中で、RDBからデータを取得して一時的にメモリ等に格納し、処理を行い最後にバルクインサートするようなETLをPG内で完結する処理もトランザクションを切って実行されてると思うので特性上はオフラインバッチになります。
バッチ処理とジョブスケジューラ
バッチ処理でアプリケーションと同じくらい重要なものがジョブスケジューラです。ワークフローエンジンとかパイプラインと呼ばれる事もあります。
ほとんどのケースで「バッチ処理」というのは単独のロジックで完結しません。例えば「顧客テーブルをCSVに抽出 -> 特定条件でメールアドレスを抽出 -> メールを送信」のように複数の処理の組み合わせになります。
また、システム間連携(たとえば業務系システムからBIにデータを渡すとか)が発生するのでごく小規模かメインフレームでもない限りは複数のシステムやコンピュータをまたがった処理をすることになります。
ジョブスケジューラとはそのようなバッチを管理するためのツールです。主に以下の機能を備えます。
- 任意のイベント(時間/日付/曜日/ファイルの更新等)でジョブをキックするスケジューリング
- ジョブの依存(先行ジョブや分岐/待ち合わせ等)を管理/可視化するワークフロー/パイプライン
- どのサーバで実行するかや同時実行数を管理するリソースマネージメント
cronやWindows タスクスケジューラ等は単独マシンへのスケジューリングしか持ってないため大規模バッチにはそれだけでは不足します。
そのためエンプラではすべてを備えた統合ジョブ管理ツールがよく使われます。JP1とかTivoliや千手やA-AUTO、OSSだとJobSchedulerやHinemosが有名ですね。
これらのトラディショナルなジョブスケジューラは上記の3つの機能に加えて監視やバージョン管理など全てをこなします。この辺はバッチこそがITシステム基盤だった頃の名残かと思います。
一方で、大規模分散処理の台頭もあって最近は高度なリソーススケジューラとスケジューリング/パイプラインのツールをそれぞれ組み合わせる事も多いです。ビッグデータ処理基板はこのような構成の方が多いでしょう。
リソーススケジューラとして有名なのはHadoopのYARNやMesosあるいはKubernetesです。スケジューラとしてはAirFlowやAzkaban、Oozieあたりですね。
これらの多くは統合管理という点ではトラディショナルなツールに劣る事もありますが、既存の監視やリリースの仕組みと統合しやすいですし、従来ではなし得なかったキメ細やかなリソース管理が可能です。また、ETLを強く意識して作られているのでData lineageをサポートするものもあります。
夜間バッチとマイクロバッチ
歴史的にオンラインのリアルタイムトランザクションを夜間に集計なりする夜間バッチが作られてきたという話をしました。
これは当時としては正しいのですがサービスが24時間稼働し大量のコンピュータを運用する現代ではアンチパターンです。
そもそも登場した背景としてメインフレームのような高価な単独のマシンでリアルタイム処理もバッチ処理も両方賄っていた事情があります。リクエストが少ない夜間にコンピュータを遊ばせないための方法だったのです。
一方で、現代ではリアルタイム系のサーバと大規模バッチを同じマシンで動かす構成なんてありえません。バッチサーバありますよね? NFSやDBが共有リソースになるケースもたしかにありますが、当時とは対照的に夜間バッチはコンピュータを遊ばせてしまうのです。
他にも「突き抜け」が起こる可能性もあります。これはバッチが予定時間つまり業務の始まる日中までに終わらない障害を指します。
コンピュータは速くなっていますがそれ以上にデータが増えています。そのため夜間に一気にデータを処理するのは時間的制限がキツイのです。夜は短し。
そこで現代に向いたバッチの考え方がマイクロバッチです。
これは1件ないしは少量のデータをイベントが発生しだい逐次処理する方式です。非同期処理とか遅延実行とかいろんな呼び方がありますね。
これは一見すると「無限に続くデータを逐次処理するストリーム処理」のようですが、それもそのはずそもそもバッチとはストリーム処理の特殊な状態です。有限のデータのストリーム処理をバッチ処理と呼ぶならば、バッチを小さくして連続実行すればストリーム処理のようになるのは当然です。
実行形態としてはAWS LambdaでもKafkaでもJMSでもcron+DBでもなんでも良いのですが注意点としてはウインドウの作り方です。分析系のサマリデータをリアルタイムに反映したいとかだと多少の取りこぼしやズレがあっても良いですが、請求処理とかだとそうはいきません。業務的に妥当なウォーターマーカを設けて到着順の問題等に対応する必要があります。秒でも分でも時でも必要なだけ間を置けばいいと思います。
マイクロバッチであればコンピュータを24時間使い切れますし、一つ一つの処理は十分に小さいので突き抜けのリスクも低いです。また、リアルタイム系と同様にスケールアウトで性能を伸ばす事が可能でありクラウドであればオンデマンドでリソースを取得することも可能です。
ちなみにHadoopやSparkを始めとした分散基盤というのは内部的にはマイクロバッチを大量に実行させています。小さく区切ってるから同時にたくさん投げれるわけです。
バッチ処理と再実行
バッチでは再実行がとても重要です。なぜならバッチはアベンド(=異常終了)するのです。転けたら再実行する必要があります。
アベンドの理由はリソース枯渇から想定外のデータあるいはPGのバグに原因不明のアベンドなど多岐にわたるので割愛します。
ともかく再実行が必要なのですがこれが割りと曲者です。単純に考えると失敗したところから再開したいのですが、その場合はどこまで処理をしたか、ということを判断できるようにしておく必要がります。ではロールバックさせてしまえば良いと思いますがデータ量が多いと一度もコミットしないというのはリソース的に無理でしょう。また、ファイルはロールバックできません。
そもそも実行時間の長いジョブを最初からやり直すのはペナルティが大きいです。
基本的な戦略としては以下のいずれかまたは組み合わせとなります。
- 可能な限りDBで完結させてトランザクションを作る
- 一時テーブルや一時ファイルを作ってやりなおすポイントを作る
- ジョブを高速化してやり直しのペナルティを小さくする
特に最後が重要です。そのため先程話したマイクロバッチはもちろんですが、トラディショナルなバッチでもジョブを機能毎に分割しさらにデータを分割して並列度を上げることで、1つ1つのジョブのリカバリポイントをシンプルにする必要があります。
Tips
基本戦略
まずは可能であればオフラインバッチを選びましょう。さらに完全新規であればマイクロバッチを検討してください。
オフラインバッチは結果整合性を保てる必要がありますし、マイクロバッチもウインドウの設計が必要です。ただしそのコストに見合う価値があり性能問題をかなりの部分解決できます。
特に完全なオンラインバッチで性能を出すにはストアドプロシージャをガンガン使うしか無いので高価なDBをまずは用意してください。
以下では、大規模バッチでの主に性能面にフォーカスした内容を書いています。
オフラインバッチ
現代のシステムでDBを持ってないシステムは考えられないので、オフラインバッチは複数のジョブの組み合わせまたは単独のPGでETLを実現しているはずです。そのため純バッチであるTの部分だけではなくEやLの部分もまとめて記載します。
以下のいずれかあるいは組み合わせを意識する必要があります。
- Load時にコミットはなるべくしない。間違っても1レコードずつしない。
- LoadはBulk InsertやSelect Insertを使う。あとRDBのインポートコマンドが使えるならそれを使う。
- Load処理はLoadプロセスはむやみに増やさずにパラレルDMLやインポートコマンドの並列オプションを活用してなるべくRDBにマネージさせる
- Oracleならダイレクト・パス使え
- JavaならResultSetのキャッシュサイズをちゃんと確認する
- ファイルシステムのマウントオプションを確認する。特にNFS。
- Extract/Transform部分はなるべく並列度を上げる、Loadは並列度をコントロール
- 一時テーブルを作ったりしてDB内で完結させてI/Oコストを下げる
- ファイルにして大規模並列で実行する
- 過剰にインデックスを貼らずにフルスキャンでも目的の性能が出ないかは検証
- DBからは必要なカラムとレコードだけとる。インデックス無くてもWHERE句で処理できるものはWHERE句。
- 使えるならSparkとかAsakusaFWとか並列フレームワークを導入
色々書いてますが基本的には多くのシステムで利用されているRDBというやつは大量のInsert特に並列でのInsertが苦手なアーキテクチャなのでLoad処理に工夫が必要です。
RDBにはバルクインサートやパラレルDMLなどRDB側でマネージ出来る機能を使うのが基本です。
1件1件Insertしてコミットとかは典型的なミスですね。
同時にTransformの部分は共有リソースであるDBから切り離されているのでガンガン並列で実行することが可能ですのでそこを最大化する戦略が良いです。
オンラインバッチ
オンラインバッチはETLを伴わないでトランザクション配下で行うDB処理です。トランザクション配下にあるので整合性の観点で運用は楽なのですが、性能面で様々なペナルティを持つので注意が必要です。
実際は内部的にETLをしているバッチは実質オフラインバッチでありオンラインバッチの恩恵が受けれないのでチューニング方法を間違えないようにしましょう。
以下のいずれかあるいは組み合わせを意識する必要があります。
- 長過ぎるトランザクションは避ける
- 複数のSQLを使わないSELECT-INSERTやUPDATEなどを駆使して単独のSQLで処理を完結させる
- SELECT FOR UPDATEに頼らない
- 多少複雑でも良いのでSQLで可能な限り処理をする
- RDBの分離レベルを確認。同じ分離レベルでもDB毎に挙動が違う
- ストアドプロシージャを活用する
- 再実行のためのリカバリ向け情報をファイル等に格納する
- Insertコストを極小化するためにインデックスはなるべく貼らない
- 並列実行の性能をマシにするためにPKはシーケンスを使わないUUIDか業務上意味のある値
- どうしてもシーケンス使うならキャッシュは必須で有効にしてシーケンスがInsert順であることは絶対に期待しない
- 同じテーブルに書き込むジョブがぶつからないようにジョブスケジューラで調整する
- ハッシュパーティーションとかでインサートコストを減らす
- 別テーブルに書いてリネームとかオフラインバッチ化出来ないかを考える
- シャーディングしてリソース競合の範囲を減らして並列度を高める。RDBを辞めるのも手
オンラインバッチでの基本はトランザクションを小さく短くすることです。
バッチなので必然実行時間が長くなりますがトランザクションを長時間確保すると他のバッチやリアルタイム系と競合してロック待ちやリソース枯渇などシステム全体が遅くなります。そのためどのようにトランザクションを小さく短くするかが鍵になります。
他にも並列で大量にインサートしようとするとインデックス周りを中心にリソースの競合やロックが発生します。これらを避けるためにはそもそもDB/テーブルの設計が重要になってくるので要注意です。
また、分離レベルの理解も重要です。Selectを同一トランザクションで複数回実行した時の結果が同一かどうかとかを意識する必要があります。
Select結果を一回変数に入れて加工してUpdateやインサートしてコミットするという問題なさそうな処理をしても意図した挙動にならないことが良く有ります。
ロックも悲観ロックだけでは性能ペナルティが大きいので楽観ロックで作れないかの考慮も必要です。
まとめ
なんか最初はhow-toだけ書くつもりだったのですが、興が乗ってバッチ処理の基本的な解説から書いてしまいましたが如何でしたでしょうか?
個人的にはすべての夜間バッチには滅んでほしいと思ってる派です。マイクロバッチでがんばりたい。
そうはいってもバッチの運用は日常です。これをいかに良く作り良く運用するかは重要になると思います。
今回は書かなかったですが、バッチはシステム関連系やデータ依存が秘伝のタレになりやすいので適切なツールや仕組みをいれてそこを可視化したりするのも運用負荷を大きく下げるポイントだと思います。
運用系は小さなことだとサービスカタログ書いて全ジョブのリカバリ手順書こうよ、から始まるのですが。。。
あと、大量の紙帳票があり電子帳票に変えれないならメインフレームのがセンタープリントは高速な環境が揃ってるから諦めてそっち使うのが良いんじゃないですかね。
Happy Hacking!