バッチ処理、ジョブ管理について書いてみる

僕はHive, Pythonバッチ処理を書いてAzkabanでジョブ管理するシステムを構築、運用した経験が2年ほどあるので今日はバッチ処理、ジョブ管理について書いてみようと思います。

僕の経験上Hadoop特有の部分、例えばテスト環境が作りづらいとかバッチサーバーはジョブをsubmitするだけなので負荷はそんなにかからないとか、はあるけれど割と汎用的なのではないかと思います。そもそもバッチ処理、ジョブ管理について書かれたものはほとんど見た事がないので参考になれば嬉しいし、こういう良い方法もあるよ!とかあれば是非ブログ等に書いてほしいと思っております。

最初に言っておくとバッチ処理、ジョブ管理において重要なのは障害時のリカバリのしやすさです。正常時はまあいいでしょ。
なので例えば引数に日付を持てないようなバッチ書いたら辛いですし、LL言語で書く方がコンパイル、パッケージングとか楽です。CPUパワーが求められるようなバッチなら話は別ですが。

で、ともあれ、どういう順番で何から書くかも難しいのでとりあえずリトライと冪等性について書こうかと思ったがまあ下記を読んでください。

リトライと冪等性のデザインパターン - Blog by Sadayuki Furuhashi
続・リトライと冪等性のデザインパターン - リトライはいつ成功するか - Blog by Sadayuki Furuhashi

で、済ますのもアレなので簡単に書く。

バッチ処理においてリトライはきわめて重要である。

バッチプログラムにバグがある場合はリトライしても意味無いわけだが、通信先のサーバーに障害が起きてバッチが失敗するようなケースは復旧後にリトライする必要がある。で、なるべく手動でリトライはさけたいわけである。面倒だから。

fluentdのようなストリーミングデータを扱うミドルウェアだと、一時的に通信先が調子悪いということはしょっちゅうあるのでリトライ処理は極めて重要でその辺はちゃんと実装されている。しかもexponential backoffつまり1秒、2秒、4秒、8秒のように通信先にあまり負荷をかけないようなリトライロジックが実装されている。

バッチ処理の場合はそこまで厳密にやる必要はないと思うがリトライは重要でかつ自前でやるのは面倒なのでAzkabanのようなジョブ管理ツールに任せるのが良い。ちなみにAzkabanはexponential backoffの機能は持ってなくて事前に指定されたリトライ回数、リトライ間隔をもとにリトライするだけである。ま、これで十分ではある。

Hadoop界隈だとHiveServerの機嫌が悪いのはよくあることなので1回目失敗しても5分後にリトライすると成功とかはあるのでリトライ設定しとくとよい。
ちなみに2回目に成功するのは1回目はジョブが集中してHiveServerに負荷が高まっているから失敗するけど2回目だとそんなに負荷が無いので成功するという背景もある。
なおリトライして成功したからといって1回目の失敗は無視しない方が良い。1回目失敗した原因を調べて解決した方が良い。例えば負荷が集中しているならずらすとか、あとそもそもジョブの依存関係の設定を間違えているというケースもある。つまり1回目は必要なデータが無いから失敗したけど、2回目は他のジョブによって必要なデータが出来てたから成功したとか。

なのでそのようなケースでもアラートは飛ばしておいた方が良い。僕のケースだとAzkabanでメールのエラー通知を行っている一方、バッチプログラム側でも失敗したらhipchatにメッセージとばすようにしている。Azkabanだとリトライして成功した場合に通知する手段は無いと思うのでパッチプログラムに手を入れている。

リトライ間隔、リトライ回数をどうするかはユースケース次第だが、僕のケースだと大半が5分後に1回リトライという設定にしている。ただし一部は10回リトライとか2時間後にリトライしているのもある。これは通信先システムの状況次第である。例えば他システムの1時間かかるバッチ処理の結果を取ってくるようなケースだと5分後にリトライしても意味が無い。他システムの復旧が5分で終わるわけないからね。

なお通信先システムが無いというようなケースでもリトライ処理があったほうがメンテナンスが楽に出来るというメリットがある。例えばHiveServer2にメモリーリークの疑惑があるから毎日定期的に再起動するというような場合にジョブが失敗してもリトライがあれば大丈夫。


リトライについて書いたなら冪等性についても触れないわけにはいかない。冪等性とはある操作を1回やっても複数回行っても結果が同じということで、数学の世界だとP*P=Pとか書く。英語だとidempotent
例えばecho a > a.txtは冪等だけどecho a >> a.txtは冪等じゃない。

リトライして結果が変わったらダメなので冪等性を持つようにバッチを書く必要がある。
Hiveの場合はinsert overwiteしとけば良いと思う。だいたい日付単位でパーティションきって処理することになるでしょう。
MySQLの場合はinsert into ... on duplicate key update ...で良さそうに見えるがdeleteしてinsertした方が安全である。
というのも前者の場合、プログラムのバグとかで不正なデータが残ってしまう可能性があるからである。keyが重複してなければupdateしないからね。deleteの方が安全。


冪等性と少し関連するかもしれないが次にジョブ設計の話をしよう。

ジョブの単位をどうするかという話があるが僕の場合はだいたいクエリ1つである。例えばテーブルA, Bをjoinして集計した結果をテーブルCにinsertするような処理が1つのジョブになる。
このジョブを依存関係を考えて組み合わせていくのがジョブフローである。

各ジョブは冪等性をもって作るのが良くてかつ、基本的には前回ジョブが失敗しても次回ジョブは気にせず実行したい。ま、これはケースバイケースかもしれないが僕のケースだとほとんどそう。
例えば前日のデータを集計する日次バッチがあったとして5/1のデータがおかしくて5/2のジョブが失敗したとしても、5/2のデータを処理する5/3のジョブはそれとは無関係なので実行したい。
前日のデータに依存するようなジョブ、例えば新規UUを取りたいとかだと、こういうわけにはいかなくて辛い。
ま、その辺の話はジョブ設計についてのあれやこれや - Togetter参照


長くてすでに読む気力を無くした人が大半だと思われるがw 次にジョブフローの話をしよう。
ジョブフローっていうのはジョブAの次にジョブBをその次にジョブCを実行する、A -> B -> C のようなやつである。このケースは分岐してないので話は単純。
普通はこんな感じに分岐しますよね。というか分岐しないならジョブ管理ツールは別に必要なくてcronで十分だと思います。

もちろんこれら全部まとめて1つのジョブとして実装する事も可能だけど他で再利用しづらいしメンテナンスも大変だよね。

で、まあジョブが肥大化していくわけですよ。そのときにどうするのがいいのか。

ざっくり3パターンあります。

まず1番目は1つのジョブフローでひたらすら頑張る。これは依存関係をきちんと設定できるというメリットがある反面ジョブが多くなるとメンテが辛くなります。

2番目はジョブフローをネストして依存関係を定義するパターン。AzkabanだとEmbedded Flowsというやつでこれが出来ます。これのメリットはメンテが多少楽になることですが、その代わり依存関係の設定が若干粗くなります。下記の例だと本当はジョブDはジョブB2にしか依存していないのにジョブCが終わるまでジョブDを実行できません。

3番目はジョブフローを完全に分割して時間で依存関係を定義するパターン。最初のジョブフローが午後2時頃に終わるから午後3時頃に次のジョブフローを開始すればいいよねってやつ。前段のジョブが遅延したら後続のジョブが失敗するのであんまやりたくないパターンですが、あまりにもジョブフローが肥大化した場合はこの方法の方が見通しがよくなります。その代わり後続のジョブはバリデーションをきちんとやる必要があります。

上記どのパターンにせよ場合によってはジョブを分割してフローを分離することによって、ジョブが失敗した場合でも依存関係にない後続ジョブを実行できるようにすることも重要です。


最後にジョブ管理ツールについて書きます。
僕はAzkabanを使っていますが、開発は停滞しており将来は暗いです。が、しかし、今のところ良い代替ソフトが無いので乗り換える予定は無いです。

僕が欲しいのは、ジョブフローの可視化、リトライ、スケジューリング、API、などでAzkabanはこれらをそなえています。
Azkabanは他にインストールが楽なのと安定して動いていること、ジョブ失敗時もボタンひとつで失敗したジョブの再実行が出来ることなど便利なんですよね。
ただしイマイチな点もあって例えばデフォルトだとジョブ失敗時に依存関係に無い後続ジョブがキャンセルされるという問題がありますが、これはAPI経由で操作するというworkaroundで僕は回避しています。
あとcron書式でジョブスケジューリング出来ないので1日2回実行したい場合なんかはちょっと設定が面倒になります。

ジョブ管理ツールをどうするかというのも一つの問題でジョブ数が少ないならcronで良いと思いますが、cronだとログの扱いとか面倒だしサーバーに入らないと設定できないので、モニタリングスクリプトを動かす用途以外で使うのは微妙かなとちょっと思ってます。ま、この辺はジョブ数次第でしょう。ジョブ数が少ないのにAzkabanのようなミドルウェアを入れるのはオーバースペックですし。

ただし、cronだと過去にジョブがどれぐらい時間かかっていたのかの履歴を追うのは困難なので、ある時からジョブの時間が長くなったとかのケースに気づくのが難しいかと思います。

では、長くなったのでこの辺で。enjoy batch!!!