クックパッドのジョブ管理システム kuroko2 の紹介

こんにちは。技術部 開発基盤グループの大石です。

今回はクックパッドで利用されているRuby製のジョブ管理ツールkuroko2について紹介したいと思います。

f:id:eisuke-oishi:20151207174217p:plain

kuroko2 とは

クックパッドでは2011年頃より、kurokoというジョブ管理ツールがありました。 そして現在、kurokoの後継のジョブ管理ツールとしてkuroko2が2014年にkurokoの開発者でもある当時の技術部長 takai によって開発され現在に至っています。

商用のジョブ管理ツールは昔からありますし、ここ最近はAirflowやAzkabanなどのOSSのツールが存在しており、わざわざ自社でジョブ管理ツールを作る必要は無いのかもしれません。 しかし、kurokoが開発された当時はそこまでの完成度のOSSのジョブ管理ツールは存在していなかったということと、過去のバッチ資産も問題なく動作させることでき、機能追加など自分たちの望む形に素早く進化させやすいため、自社製のジョブ管理ツールを使っています。

アーキテクチャ

f:id:eisuke-oishi:20151207174236p:plain

Web UI

Web UIの主な機能は以下のようなものになります。

  • ジョブの定義 (後述) と、スケジュールの定義、管理者、エラーの通知方法などの設定
  • ジョブの手動実行
  • 実行中のジョブの確認、実行中のジョブの強制停止などのオペレーション
  • 実行履歴、実行時のログ確認

job-scheduler

cronスタイルで設定されたスケジュールに合わせて実行するべきジョブをworkflow-processorへ伝えます。

workflow-processor

ジョブの定義から実際に実行されるコマンドやワークフローをcommand-executorへ伝えます。 また、実行中のジョブに対するkill signalやエラー時のリトライなどの操作もWeb UIを通じてworkflow-processor経由で行われます。

command-executor

command-executorという実際のコマンド実行を責務としたワーカーが複数台のEC2 インスタンス上で動作し、実行可能なジョブを逐次実行します。 command-executorは同時に、実行状態を確認するcommand-monitorと呼ばれるワーカーと、Web UI経由で送信される kill Signal を受け取り実行中のバッチを停止させるcommand-killerと呼ばれるワーカーも起動させます。

また、1つのマシン上で複数のcommand-executorが動作してるので、大量のメモリやCPUを消費するようなバッチの場合は、それに合わせた性能の専用のインスタンスを立て、それに向けてジョブを実行することが可能です。

機能

ジョブの定義方法

メンテナンスや障害時にはインフラチームが確認することもあるため、他の部署が見たときのために、どのようなジョブなのか、どのような影響があるのか、障害時にどのような対応をするべきかを把握できることが重要です。 kuroko2では、設定時に入力するジョブの内容説明のテキストフィールドに、プレースホルダーとしてテンプレートが用意されており、Markdown記法を採用することで、これらの情報を記述、管理しやすいように工夫がされています。

f:id:eisuke-oishi:20151207174251p:plain

kuroko2 script

具体的なジョブの定義は kuroko2 script と呼ばれるDSLを記述していきます。

最も基本的な記述例は下記のようになります。

execute: echo "Hello, world"

execute: の後に書かれたものは、command-executorからシェルにそのまま渡される単なるシェルコマンドになります。

クックパッドでは、多くのアプリケーションでRailsを採用しています。 Railsアプリケーションのバッチについては app/batches 以下に管理し、 rails runner コマンドで実行できるようにしています。

kuroko2ではそのようなRailsアプリケーション用に、RubyやRailsに関する環境変数を自動で設定する execute を拡張したDSLを用意して、それを経由してバッチを実行しています。

もちろん、Rails以外にも対応しています。 具体的な例として、aamine のSQLバッチフレームワークである bricolage も kuroko2 から実行されています。 詳細な bricolage についての説明は、「巨大なバッチを分割して構成する 〜SQLバッチフレームワークBricolage〜」 を参照ください。

ワークフロー管理

上記のジョブ定義DSLは、1つのバッチ実行を定義するだけでなく、依存関係にあるような複数のバッチを1つのジョブとして定義することができます。

execute: command1
execute: command2

上記はcommand1、command2の順番で逐次実行をするだけですが、さらに

execute: command1
fork:
  execute: command2-a
  execute: command2-b
execute: command3

のように、command2-aとcommand2-bは並列実行され、2つの実行が終了したところで、command3が実行されるようなワークフローも定義することができます。 また、例えばcommand2-bがエラーになった場合、そこでワークフローは一時停止し、実行をリトライあるいはスキップさせることもできます。

これらはほんの一例ですが、その他にも、

  • batch_runner - railsアプリケーション用のバッチ実行コマンド
  • env - バッチに渡す環境変数を設定できる
  • sub_process - 他のジョブを実行する
  • timeout - 設定された時間を過ぎた場合はジョブを途中で終了させる
  • expected_time - 想定実行時間を超えたらジョブの管理者にアラートを送る

上記のようなDSLがあります。 さらにDSLの追加が簡単に行えるようになっており、今後の環境変化などに柔軟に対応できるように設計されています。

ジョブ実行の管理

エラーや状態の管理について

ジョブ実行中に何らかの原因でエラーが起きたときのkuroko2の挙動について説明します。

f:id:eisuke-oishi:20151207174313p:plain

kuroko2 はジョブに、WORKING, ERROR, SUCCESS, CANCEL ステータスを持っており上記のような流れになります。

終端ステータスは、SUCCESSとCANCELになり、ERRORの場合はジョブ内のエラーが起きたバッチをリトライやスキップ、あるいはジョブ自体をキャンセルします。

ERRORが終端ステータスではない理由は、ERRORになったということは途中までしか実行できず完了していないという意味でWORKINGと同じ扱い、という理由です。さらにERRORの状態からリトライが行えるような機能となっています。

また、エラーが発生したときは、メールやチャットで管理者に通知し、エラーのまま放置されている場合にもエラーの状態が解決されるまでアラートメールを送信します。 これらの機能によって、エラーが発生した場合には管理者は素早く気づいて対応が行うことができ、ジョブがエラーのまま放置されるような事態も防げるようになっています。

多重実行について

バッチ実行の際に気をつけるべきものとして、同じバッチが多重に実行されてしまうことです。 バッチの実行が想定外に時間がかかりスケジューリングされた次の実行の時間になってしまったり、意図せず手動で多重に実行してまい事故に繋がることもあります。

多重実行を避けるコードを、すべてのバッチに個別に実装していくのは骨が折れるため、kuroko2はジョブの定義として、多重実行の可否を定義できるようになっています。

エラーが起きたときにスケジュールされたジョブの実行を止める

例えば、1日1回必ず実行されることが前提で、1日でも歯抜けになってしまうとデータに齟齬が生じてしまうため、日付の順番通りに実行しなければいけないバッチがあったとします。

そういったバッチがエラーになった場合、その日のうちに担当者がリカバリできれば問題ないですが、それを絶対に行えるという保証はありません。

kuroko2ではジョブがエラーになり上記のERRORステータスのままの履歴が存在している場合、それ以降のジョブのスケジュールを自動でキャンセルする機能があります。すべてリカバリを行い、エラーが解決されたところでスケジュール通りに実行するように再開されます。

これは特に休日や連休などすぐに対応できない場合に、最悪の事態が起きないような防御策として有効です。

ジョブの一覧管理

kuroko2では大量にジョブを管理しているため、ジョブを素早く探せることや、管理しているジョブの状態を素早く知れることは非常に重要です。 kuroko2のWeb UIには検索機能やダッシュボードがあり以下のような機能が提供されています。

  • 自分が管理しているジョブやお気に入りに登録したジョブが一覧で確認できる
  • タグで絞り込みができる
  • 次回実行予定時間が確認でき、その時間でソートすることでこれからの実行予定がわかる
  • 検索機能
  • 実行中のジョブの一覧

f:id:eisuke-oishi:20151207174329p:plain

Dockerコンテナの活用

kuroko2の欠点として、新規のプロジェクトで新たにアプリケーションがバッチを実行したいときには、そのアプリケーションが動作できるように環境構築の作業が発生してしまうことです。

社内では、eagletmt によるDocker環境の整備が進められており、バッチ実行に関してもDockerの恩恵が受けられるようになっています。 具体的には、Dockerイメージをジョブ実行の際にpull、コンテナを立ち上げ、Dockerコンテナ内でコマンドを実行できるようにしたものです。

Dockerを利用することで、アプリケーションサーバーにも使えるDockerイメージを1つ作るだけでジョブ管理システム上での環境構築を行う必要なく、ジョブ管理システム上でバッチが実行できるようになりました。

最後に

クックパッドで利用されているジョブ管理システムとその周辺について簡単に紹介しました。

残念ながらkuroko2はオープンソース化の予定はありませんが、今回紹介した運用方法や機能などの知見は過去の経験や失敗を元にしたものが多く、ジョブ管理やバッチ実行の管理を行うエンジニアにとっては、同じような悩みや問題を抱えていたりするのではないかと思います。 そういった方々にとって、今回の記事が参考になれば幸いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/