Web系の自分が想像と障害で学んだバッチ処理・設計の基本

バッチ処理というのはそれ単体で勉強しようとするとなかなか何を勉強したらいいのかわからないことが多い。 特に経験がWeb系ばっかりだと、いざバッチ処理を実装しようとした時に基本的なノウハウを知らないままに書いてしまうことが多い。

バッチ処理というのは実態を整理すると「何らかのトリガーを期に起動し、データをロード・加工・変換・集計してから、出力する」という事になる。 まぁ、INがあって処理してOUTがあるという点では関数だと考えてもいいだろう。 システムの利用者(人に限らない)のアクションとは直接関係ない処理であったり、利用者のアクションをトリガーとしていても、即時にレスポンスがいらないor返せない場合に バッチ処理を選択する事が多い。 実現方式はシェルスクリプトLL言語、実行可能バイナリだったりするし、デーモンとして立ち上げる場合もある。

利用者の操作に対して対話的・同期的な処理はオンライン処理、そうでない処理はバッチ処理と言われることが多い気がする。

バッチ処理のフェイズ

では「何らかのトリガーを機に起動し、データをロード・加工・変換・集計してから、出力する」について細かく考えていこう。 このあたりを本当は細かく説明したほうがいいのかもしれないけれど、今は気力がない。

起動タイミングについての適当な説明

起動タイミングについては大きく二つ種類がある。 定時起動とトリガー起動だ。

  • 定時起動
    • 毎時N分
    • 毎日N時
    • 毎週N曜日
    • 毎月N日
  • トリガー
    • WebUIの操作
    • WebAPIへのリクエスト
    • scpでファイルが置かれる
    • メッセージキュー

ざっくりこんな感じだ。

データをロード

システム内部・外部のデータを特定期間・範囲でロードする。 ロード元はいろいろあるけどだいたいこのあたりだろう。

  • CSVファイル
  • ログファイル
  • 画像ファイル
  • RDBMS
  • 外部Webサービス
  • SCPからファイル取得or受信
  • メッセージキュー

加工・変換・集計する

ロードしたデータを処理していく。 処理内容はいろいろあるけど大体このあたりの処理をする。

  • クリーニング・ベリファイ
  • 加工
  • 変換
  • 集計

データの出力

さて、データを加工・集計しても、それをどこかに出力しないと意味がない。 ので、出力する。出力先はまぁいろいろある。ここが外部へ影響を与える瞬間なので一番怖い。

  • RDBMSへ保存・更新
  • CSVへ保存
  • メール・メッセージを送る
  • 外部Webサービスに送信
  • 次のバッチを起動する

テクニック・運用のためにやっておきたいこと

さて、ここからがこの記事のメインだ。バッチ処理を実装する際には、結構いろいろなテクニックがある。 これらすべてを対応する必要はないかもしれないけど、実装する際に一度は考えておいたほうがいいことばかりだと思っている。

データの更新はギリギリまで避ける

RDBMSなどのばあい、トランザクション複数のデータを更新する際に早い段階からレコードを一行ずつ更新なんかしたりしていると、 ロック範囲が広がるばっかりでデッドロックの温床になったりして何もいいことがない。 一時テーブルなどに変更結果やインサートデータを投入しておいて、更新クエリ一発で対象データをガッツリ書き換える様な実装をするほうがよいだろう。 ファイルなどの場合も、書き込み途中で次のジョブに参照されるみたいな不幸を避けるために一時テーブルに書き込んでおいて最後にリネームするとかしたほうがよい。

可能であれば冪等に実装する

冪等についてはいろいろなところでかなり触れられているので、特に言うことは無い。 これをきちんとやっておかないと、集計にバグがあったりデータが届いてないとかで再集計を強いられた時に「じゃぁ、この日次バッチを今年の頭から再集計やっていきますか 」というつらいことになる。 ただ、これは設計センスがかなり問われるし、自分も失敗した経験があるので「可能な限りがんばっていきましょう!」みたいな気持ちがある。

並列化可能なポイントを抑えておく

バッチ処理は大体時間がかかるからこそバッチ処理になってるのだけど、一連のバッチ処理をナイーブに直列に実行していたらいつまで立っても処理が終わらないということも ままある。 バッチ処理のフェイズ毎だとかユーザー毎に、影響範囲が重なっていなければ並列で実行することで処理が早く終わることもあるので抑えておきましょう。 ただ、並列処理すると当然負荷も上がる場合が多いので、主目的のサービスが死なないよう最大同時スレッド数など、実行負荷の調整ができる機構を備えておきましょう。

時刻の記録

バッチの対象とするデータの範囲・開始時刻・終了時刻は必ずログやテーブルに記録しよう。 ログにはかかった時間も出力するとよい。パフォーマンスの劣化を監視するための指標にできる。 問題が起きた時に開始時刻・終了時刻から計算するのは面倒なので計算してログに出しておこう。 RRDToolsなどの運用ツールに放り込みやすいようになってるとよりよい。 RDBMSに投入するときは、バッチ処理そのもののトランザクションと実行記録を残すためのトランザクションを同一にしていると、処理失敗時に記録用レコードもロールバックされてしまうので、実行記録はログに出力しておくほうがよいだろう。

処理対象範囲を引数で指定可能にする

不幸なことに障害が数日続くという時があるかもしれない、問題解消後に日次バッチを手動集計します。みたいなときに助かる。 指定しないときは動かないようにするか適切なデフォルト値を自分で判定するかについては、実装者の好みで決めればよい。

ロックファイルによる多重起動禁止

たとえば /var/lock/hogehoge~batch~.pid みたいなファイルがあれば、既にバッチが起動しているので新たに起動したバッチは処理を続行しない。 みたいな制御を入れておくと多重起動してはいけない処理を重複して実行されずに済む。

ロックファイルではなくて、DBの設定レコードを見るとかでもよいと思う。 むしろ、バッチサーバーが複数台にまたがっている場合などは一台のDBでロックをとるほうが安心だ。

停止ファイルによる起動禁止

たとえば /var/lock/maintenance みたいなファイルがあれば、メンテナンスモードに入っているため新たに起動したバッチは処理を続行しない。 みたいな制御を入れておくとリリースや障害対応の際に crontab のコメントアウトし忘れたり、外部からのトリガーが不意に来たりすることがないのでよい。 停止ファイルではなくて、DBの設定レコードを見るとかでもよいと思う。

バッチ処理の実行、停止はコントローラブルにしておこう。

データの保持期限・削除基準の決定

バッチ処理の処理結果や処理の副産物として生成されたファイルとかテーブルの後始末について、カウボーイコーディングをしていたり仕様の検討から漏れてたりで、考慮が抜 けてしまう事が多い。

そのまま残しておくとディスク容量を圧迫するけれど、システムからはほぼ利用される事が無い。 そういうデータは定期的に消すような処理(これもバッチ処理だ)を忘れずに入れておきましょう。

法律的な理由で一定期間残さなければならないだとか、実装後半年たったら本当に消してもいいデータなのかどうかよくわからなくなった。 というケースも多い。最初からデータのライフサイクルは意識しておこう。

デーモンの採用基準

バッチの実行プロセスをデーモンにする理由は、単にカッコイイからだとちょっと弱い。感覚の話でアーキテクチャを決めてはいけない。

  • 起動処理が遅く、期待するレスポンスタイムを満たせない
  • トリガーの頻度が多く、常時起動のほうが負荷を制御しやすい
  • トリガーとなるアクションを常に監視しなければいけない

みたいな理由をでっちあげて君だけのかっこいいデーモンを作り上げよう。 面倒なら delayed~job~ や webアプリサーバーにエンドポイント作る方法でもいい。 webアプリサーバーに組み込んでしまうと複数バッチ処理をひとつのインスタンスで実行するということになるので、再起動のタイミングなどが少しシビアになってしまうかもしれない。 このあたりはトレードオフをきちんと考えておこう。

以上、ざっくり踏みまくった地雷から得た知見をまとめたので、各位には「お前の書いていることはインチキだ」だとか「これを読めば全部わかる」だとか「これは間違っている」などどんどん殴って来て欲しい。俺はもうこれ以上障害を起こしたくないんだ!

追記

はてぶで意見をいっぱいもらったので続きも書きました。 mitomasan.hatenablog.com