メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

イマドキのジョブスケジューラについて考える

こんにちは。Ruby化をすすめるメドピアをお手伝いしている@willnetといいます。

メドピアではPHPからRubyに移行するにあたり、単純に言語を置き換えるだけではなく、言語以外の仕組みについても適宜見直しを行っています。今回はそのうちジョブスケジューラを見直した件について書いていきます。

言語を置き換えた話はこちらを参考にしてください。

レガシーな独自フレームワークから脱却してRailsへ徐々に移行している話 - メドピア開発者ブログ

そもそもジョブスケジューラってなに

「毎日1時になったら前日のアクセスログを集計して統計データとしてまとめる」などといった定期的に実行するジョブを登録するためのものです。

ウェブサービスを作るときのジョブスケジューラといったらやっぱりcronですよね。メドピアでもこれまでcronを活用していました。しかしサービスが小さいうちはcronでもそれほど問題ないのですが、サービスが育つにつれだんだん問題が顕になってきます。

cronの問題ってなに

以下列挙します

スケジュールをバージョン管理できない

crontab -e などで直接crontabファイルを編集した場合、変更の履歴が残りません。つまり以前のバージョンに切り戻したり、過去の履歴を振り返ったりすることができません。

余談ですがcrontab -eを打とうとしてeの隣のrを押してしまい、crontab -rでcrontabの設定を消去してしまった経験のある人はチームに一人くらいいるんじゃないでしょうか。

間違えやすい記法

crontabの記法は一見してわかりづらく、設定ミスをしやすいものになっています。

例えば次のようなcronを設定したとします。

5 2 1 * * /your/batch/command

これは毎月1日の2時5分に/your/batch/commandを実行するcronジョブですが、ぱっと見ただけで理解するのは難しくはないでしょうか。あとは*/10とか1-5,10-20とかの特殊記号を使ったときも、本当にこの書き方で想定通りの時間に実行できるのか不安になります。

cronサーバを分散できない

ウェブサービスで負荷が高まったときには、仮にアプリケーションサーバがボトルネックであれば同じサーバを追加(スケールアウト)することで負荷を分散することができます。しかしcronサーバで同じことをすると複数のサーバで同じジョブが同時に実行されることになってしまいます。そのため大抵の環境においてcronサーバは1台だけで運用されているはずです。

開発が進みcronジョブが増えてくると、時間帯によっては複数のジョブが1台のcronサーバで並列に実行されて負荷が異常に高まり、予想しない挙動や障害につながっていきます。

デバッグしづらい

cronの環境と、普段使っているシェルの環境との違いでジョブが失敗することはよくありますが、特にそれを示唆するような出力はないことが多いです。そのためこの手の経験がないと「手元で実行すると動くのになぜかcronだと動かない!なぜ!?」のようにハマります。

ログを追いづらい

cronを実行した際の出力はメールで送信されますが、2017年の現在その機能を使っている方はあまりいないのではないでしょうか。普段は自前でログを頑張って出力したものを保持しておき、エラーが起きたときにはAirbrakeなどのエラー管理システムと連携して通知、ログを漁って原因を究明する…というケースが多そうです。

ジョブごとに成功/失敗のステータスやログがまとまっていると便利ですが、そこまで自前で実装するのは大変ですね。

どうやって問題を解決するか

cronの問題についていろいろ書きましたが、どのようにしたらこれらの問題を解決できるのでしょうか。cronそのものを改善させる方向と、cronをやめる方向で考えてみます。

whenever で cron を改善させる

これまで挙げたcronの問題のうちいくつかはwheneverというgemを利用することで改善可能です。使っている方も多いのではないでしょうか。

wheneverを使うと次のような記法でジョブをファイルに定義し、crontab用の記法に変換して登録することができます。

every 1.day, :at => '4:30 am' do
  runner "MyModel.task_to_run_at_four_thirty_in_the_morning"
end

このファイルをバージョン管理しておき、デプロイ時に自動でcrontabを更新するようにします。これで、バージョン管理の問題と、記法の問題を解決できました。

しかし、他の問題は変わらず残っています。

cron をやめる

調べると、cronを置き換えようとするプロジェクトはたくさんあることがわかります。

上記はなるべくRubyで作られたツールの中から選びました。つまりRubyにこだわらなければもっとたくさんの選択肢があるわけです。悩みますね><。

全ての問題を完全に解決するツールは存在しなかったので、次のような観点でツールを選定しました。

  • 適度に問題を解決していること
  • Ruby を使ったプロジェクトであること
  • メンテナンスが続いていること
  • 最悪メンテナンスが停滞しても自分たちでなんとかできること
  • 導入のしやすさ

結果として、sidekiq-cronを採用することにしました。

sidekiq-cron

sidekiq-cron はバックグラウンドジョブを扱うgemであるsidekiqを拡張し、cron的な機能を追加してくれるgemです。

主なメリットとしては、

  • sidekiqに相乗りする形で利用できるので導入が楽
  • ジョブ失敗時にリトライさせることができる
  • sidekiqのワーカーを増やすことで負荷分散できる
  • yamlでスケジューリングを定義するのでスケジュールのバージョン管理ができる
  • sidekiqがweb uiを用意しているので、ジョブの状態を確認できる*1

などがあります。慣れているsidekiqをそのまま使う感覚でいけるのがいいですね。

時間を指定する記法はcrontabと同じだったり、詳細なログ出力は独自で頑張る必要がありますが、cron単体の時よりはだいぶ前進できた気がします。

将来的にどうするか

sidekiq-cronは本番導入されており、今の段階では特に問題はないのですが、遅くても数年したら乗り換えを検討する必要がありそうです。

例えば複数のRailsアプリを管理するようになった場合に、複数アプリを横断したジョブスケジューラを管理したいという要望にはsidekiq-cronでは応えられません。また、ジョブの数が数十〜百になった場合にジョブをどうやって管理するかも悩ましいところですし、ログの出力が弱いことが問題となるケースも今後出てくるでしょう。

と、そんな折に昨年OSS化されたkuroko2を軽く触ってみたところ、なかなか良さそうでした*2。次回乗り換えを検討する際の有力な候補となりそうです。

kuroko2の詳細は以下のリンクを参考にどうぞ。

まとめ

開発しているアプリケーションの規模によって適切なツールは変わってきます。小規模なアプリケーションのジョブスケジューラであればcronはまったく問題ないと思いますが、アプリケーションが成長していくにつれてより適したツールに乗り換えていく必要があるでしょう。このエントリが次のツール選定の参考になれば幸いです。

*1:sidekiqの仕様上、WebUI上にログはほぼ残らないので、sidekiq-failuresを利用して失敗したジョブのエラーを閲覧できるようにしています。

*2:ツール選定当時はOSS化されていなかった