Perl Hackers Hub

第25回cron周りのベストプラクティス(2)

前回の(1)こちらから。

プロジェクトでcronを利用する

筆者は普段ゲーム開発のサーバサイドを担当していますが、プロジェクトによってはバッチサーバのcrontabが100行を超えることもあります。イベント、ランキング処理、監視、集計、バックアップ、リカバリ処理などをしっかりやろうとすると、どうしてもそれくらいになってしまいます。

100行とはいかなくても、プロジェクトで使うcrontabの行数が膨らんでくると、サーバで直接crontabを編集することは管理上現実的ではありません。

crontabの記述とリポジトリ管理

では実際のプロジェクトでcrontabをどのように管理していけばよいのでしょうか。筆者は次の方針を立てています。

  • crontabの記述にゆるやかな規約を設け、リポジトリ管理する
  • crontabの自動テストを行う
  • crontabの反映方法をなるべく自動化する
  • crontabの反映漏れを検知する

crontabの記述にはRubyのwheneverのようなDSLDomain Specific Languageドメイン特化言語)を使うアプローチもあります。これはちょっとした用途には便利かもしれませんが、かっちりとcron運用する中ではあまりお勧めできません。DSLレイヤが増える分、結果的に管理が煩雑になり、crontabのテストも書きづらくなるからです。それよりもcrontabを記述するうえでのゆるやかな規約を設け、わかりやすく記述できるように工夫することが大事です。

crontabの例

では実際にはどのようなcrontabを書けばよいのでしょうか? リスト1リスト2に悪い例と良い例を示します。

リスト1 悪いcrontabの例
37 2 * * * /opt/perl-5.18/bin/perl /home/app/proj/script/daily_partitition.pl > /dev/null 2>&1
7 2 * * * /opt/perl-5.18/bin/perl /home/app/proj/script/event_partition.pl > /dev/null 2>&1

リスト1には次の問題点があります。

  • レイアウトがバラバラで読みづらい
  • 重複した記述が多い
  • 出力を/dev/nullに捨ててしまっている
リスト2 良いcrontabの例
PATH=/usr/local/bin:/usr/bin:/bin
MYPROJ_RUN="/home/app/myproj/env-exec runcron"
LOGGER="/path/to/fluent-agent-lite"

### db partitioning
37 2 * * * $MYPROJ_RUN -- script/batch.pl DailyPartition 2>&1 | $LOGGER cron.daily_partition -
7 2 * * * $MYPROJ_RUN -- script/batch.pl EventPartition 2>&1 | $LOGGER cron.event_partition -

筆者が担当しているプロジェクトではリスト2のようにcrontabを記述し、リポジトリにコミットしています。これには次のような工夫が見られます。

  • レイアウトの工夫
  • 環境変数の活用
  • env-execとruncronを指定した起動方法の統一
  • perlの場合はbatch.plランチャをさらに経由
  • 出力先の統一とfluent-agent-liteの活用

それぞれに関して以降で説明していきます。

レイアウトの工夫

当たり前ですが、見やすく書くことは大事です。次のことに気をつけています。

  • ジョブの内容などで分類してコメントを入れ、セクション分けする
  • セクションごとでよいので縦の位置をそろえて書く

環境変数の活用

環境変数は/usr/local/bin$PATHが通っていないと不便なこともあるので、通すようにしています。

また、crontab内では同じような記述が重複することも多いので、そういったものはcrontab内で環境変数を設定してまとめるとよいでしょう。リスト2では、$MYPROJ_RUN$LOGGERという変数を定義しています。

env-execとruncronを指定した起動方法の統一

$MYPROJ_RUNを前置して各種バッチを起動するように統一しています。$MYPROJ_RUNの中にはenv-execruncronが指定されています。

env-execはプロジェクト用にカスタマイズされたラッパ用のシェルスクリプトです。

env-execの内容
#!/bin/sh
set -e
export USER=app
export HOME=/home/$USER
cd $(dirname $0)
export PATH="local/bin:/opt/perl-5.18/bin:$PATH"
export PERL5LIB="lib:local/lib/perl5"
export PLACK_ENV=production
exec "$@"

このシェルスクリプトでは次のことを行っています。

  • appというアプリケーション実行ユーザの指定
  • プロジェクトディレクトリへcd
  • perl-buildでビルドしたPerlへの$PATHを通す
  • 各種環境変数の設定
  • 引数で指定したコマンドをexecで実行

このラッパシェルは、cronに限らずアプリケーションサーバを含めた各種デーモンの起動においても利用しています。これはPerlプロジェクトの例ですが、同じようなアプローチはほかの言語でも可能でしょう。

runcronについては次回(3)で詳しく取り上げます。

perlの場合はbatch.plランチャをさらに経由

上記の例ではさらに、script/batch.plというファイルを指定しています。内容は以下のとおりです。

batch.plの内容
#!/usr/bin/env perl
use strict;
use warnings;
use Module::Load;
my $name = shift @ARGV;
my $module = "MyApp::Batch::$name";
load $module;
$module->new_with_options->run;

リスト2ではDailyPartitionという引数を受け取っていますが、この場合はMyApp::Batch::DailyPartitionというモジュールが読み込まれ、そのコンストラクタとrunメソッドが呼ばれるようになっています。MouseX::Getoptを統一的に使っているため、コンストラクタはnew_with_optionsになっています。

このようにバッチ処理の実体をモジュール形式で書くことで、プロジェクトリポジトリ内にplファイルが氾濫せず、バッチ処理のテストが書きやすくなるというメリットもあります。

出力先の統一とfluent-agent-liteの活用

cronにおいてジョブの出力をどのように扱うかは重要です。 /dev/nullに捨ててしまう例を時折見かけますが、これは完全なる悪手です。$MAILTOの適切な設定を行う、loggerコマンドでsyslogに投げる、ファイルに追記保存するなどいろいろやり方はありますが、筆者の最近のお気に入りはfluent-agent-liteを使う方法です。リスト2でも、$LOGGERという出力コマンド格納用の変数にfluent-agent-liteを代入しています。

fluent-agent-liteは田籠聡さんが作成したFluentdプロトコル互換のツールです。Perlで書かれており、送信に特化しているため軽量です。

fluent-agent-liteは標準入力を受け取り、その内容を{"message":"hello"}といったJSONJavaScript Object Notation形式に自動変換して、指定したタグとともにFluentdに送信してくれます。ログファイルなどをtailした出力と連携して使われることが多いですが、このように単発の出力を扱う際にも利用できます。

loggerコマンドの代替としてFluentdがカジュアルに使え、Fluentdに投げてしまえば、あとはさまざまな処理を行うことができる点が魅力です。


このような規約を設けることで、crontabをわかりやすく記述できます。ほかに、crontab自体のテストを書きやすくできるというメリットもあります。次項でcrontabのテストについて解説します。

crontabをテストする

cronはサービスの一部であり、crontabの内容も日々変化していくものです。指定の誤りがあった場合に大事故になる可能性があります。そういった性質上、crontabをテストすることは重要です。

テスト項目には次のようなものが挙げられます。

  • a.crontabにシンタックスエラーがないか
  • b.意図しない危険な指定がされていないか(うっかり毎分指定、日付と曜日の重複指定など)
  • c.ログを捨てていないか
  • d.規約に従って記述をしているか
  • e.コマンドに実行パーミッションが付いているか
  • f.指定コマンドやファイル名をtypoしていないか
  • g.実行時間のチェック(意図した時間に実行されるか、実行されたくない時間に誤って実行されないか)

これだけのテスト要求項目があり、それらを通さずに本番に反映してしまうことは危険です。

Parse::Crontabを使ったcrontabのテスト

crontabのテストには拙作のParse::Crontabが便利です。先ほど取り上げたテスト項目を、次のようなテストコードで一通りチェックできます。

use strict;
use warnings;
use Test::More;
use File::Which qw/which/;
use Parse::Crontab;
# 11 2 * * * $MYPROJ_RUN script/batch.pl \
# DailyPartition 2>&1 | $LOGGER cron.daily_partition -
# 上記のように指定されているか確認する

my $crontab = Parse::Crontab->new(
  file => 'data/crontab.txt'
);
# a crontab のシンタックスに誤りがないか
ok $crontab->is_valid
    or diag $crontab->error_messages;

# b 危険な指定の確認
ok !$crontab->warning_messages
  or diag $crontab->warning_messages;

# ジョブ一覧を取得する
for my $job ($crontab->jobs) {
  my $command = $job->command;
  # c $LOGGER でログを出力しているか
  like $command, qr!2>&1\s+\|\s+\$LOGGER!, 'logger ok';

  # d $MYPROJ_RUN を使う規約を守っているか
  ok $command =~ m!^\$MYPROJ_RUN!;
  my ($opt, $rest_command) =
    $command =~
    m!^\$MYPROJ_RUN\s*(*?)?\s*--\s*(.*)!;
  my $cmd = +(split /\s+/, $rest_command)[0];

  # e 実行権がちゃんと付与されているか
  ok -e $cmd or $cmd = which($cmd);
  ok -x $cmd;
  if (my ($module) =
    $rest_command =~
      m!^script/batch\.pl\s+([A-Za-z0-9]+)!
  ) {
    # f batch.pl への指定モジュールが存在するか
    my $module_file = "lib/MyApp/Batch/$module.pm";
    ok -e $module_file;
  }
  # g スケジュールオブジェクトを取り出して
  # 必要に応じて実行時間のテストをする
  my $schedule = $job->schedule;
  ok $schedule->match(hour => 2, minutes => 15 ...);
}
done_testing;

aでcrontabのシンタックス確認、bで危険な指定の確認、cでログ出力の確認、dで規約に従った記述をしているかの確認、eで実行権の確認、fで指定コマンドをtypoしていないかの確認、gで実行時間のチェックを行っています。先ほどの項目を一通り網羅していることがわかるでしょう。

crontabの本番環境

本番反映の方法

crontabコマンドは引数にファイルを指定すると、その内容をcrontabに反映してくれます。

次のようにしてcrontabを反映すればよいでしょう。筆者のプロジェクトではcronやワーカ類を動かす専用のサーバを立てているので、デプロイ後にsshでバッチサーバに入り、反映を行っています。

% ssh myproj-batch01
# 差分確認
% diff -u <(crontab -l) data/crontab.txt
# 反映
% crontab data/crontab.txt

上記はデプロイ時に自動反映してもよいとは思いますが、危険な動作をしたバッチを緊急的に止めるなど一時的にcrontabを直接編集していることもあり得るので、念のため上記の手順を踏むようにしています。

crontabの本番反映漏れを防ぐTips

さて、上記のように反映手順が増えると、逆にcrontabの反映漏れが心配になります。一時的に本番でコメントアウトしていたバッチの戻し忘れなども心配です。

そういうミスを防ぐために、デプロイ時にバッチサーバのcrontabの内容を確認して差分がないかを検査し、差分があった場合は目立つように警告を赤字で出して、crontabの同期を促すようにしています。

<続きの(3)こちらから。>

おすすめ記事

記事・ニュース一覧