cron 式の落とし穴: フィールド・範囲・Vixie vs POSIX
cron のフィールドと特殊文字、人々を悩ませる日 (日付) vs 曜日の OR の癖、タイムゾーンと DST の危険、そして秒フィールドの移植性の罠を整理します。
cron 式は、作業がいつ実行されるかを一緒に記述する 5 つの空白区切りの フィールドです。文法は些細に見え、実際おおむねそうですが、いくつかの細部が、 間違った時刻に実行されるほぼすべてのスケジュール作業の原因になります。 論理的な癖が 1 つ、実装の分岐が 2 つ、タイムゾーンの危険が 1 つです。この 記事では、フィールドと特殊文字、そしてコードレビューを通り抜けて本番で 問題を起こす落とし穴を取り上げます。
5 つのフィールド
| フィールド | 位置 | 許可される値 | 備考 |
|---|---|---|---|
| 分 | 1 | 0-59 |
|
| 時 | 2 | 0-23 |
24 時間制 |
| 日 (日付) | 3 | 1-31 |
|
| 月 | 4 | 1-12 または JAN-DEC |
名前は Vixie、大文字小文字を区別しない |
| 曜日 | 5 | 0-7 または SUN-SAT |
0 と 7 の両方が日曜 |
行は左から右へ読みます。30 2 1 * * は「分 30、時 2、日 1、すべての月、
すべての曜日」、つまり毎月 1 日の 02:30 です。* のフィールドは「その範囲の
すべての値」を意味します。
曜日の範囲が最初の罠です。Vixie cron は日曜に 0 と 7 の両方を受け入れる
ので、0-6 と 1-7 のどちらも一週間全体を覆います。1-7 を「月曜から
日曜まで」の意味で書く人は、日曜が 2 度マッチする形になって意外に感じます。
0-6 を使うか、名前を使ってください。
特殊文字
4 つの演算子がフィールド内の値を構成します。
*— フィールドの範囲のすべての値。,— リスト。0,15,30,45は 4 つの個別の分です。-— 包含範囲。9-17は 9 から 17 までのすべての値です。/— ステップ。分フィールドの*/15は「15 分ごと」です (0, 15, 30, 45)。 ステップは範囲の上にも乗せられます。0-30/10は 0, 10, 20, 30 です。
実際のスケジュールの大半を覆う例をいくつか挙げます。
*/15 * * * * # 15 分ごと
0 9-17 * * 1-5 # 毎時の正時、午前 9 時〜午後 5 時、月〜金
0 0 * * 0 # 毎週日曜の深夜 0 時
0 2 1 * * # 毎月 1 日の 02:00
30 3 * * 6 # 毎週土曜の 03:30
*/15 はもう少し詳しく見る価値があります。ステップが「今」ではなく、
フィールドの範囲全体を基準に計算されるからです。*/15 は常に 0, 15, 30, 45
に実行されます。「デーモンが起動してから 15 分」を意味しません。分フィールドの
*/40 のようなステップは 0 と 40 に実行され、その後、次の時が 0 に切り替わる
まで 20 分待ちます。80 分というものは存在しないので、分 80 には実行されません。
範囲を均等に割れないステップはばらついた間隔を生み、これは */40 を「40 分
ごと」と期待する人の予想を裏切ります。
日 (日付) と曜日の OR の癖
これは経験豊富なエンジニアを最もよく捕まえる落とし穴です。
通常 cron はすべてのフィールドがマッチすることを要求します。ところが、日
(日付) フィールド (3) と曜日フィールド (5) の 両方 が制限されたとき、
つまりどちらも * でないとき、Vixie cron とそこから派生した Linux の cron
群は、その 2 つのフィールドの間で OR 論理に切り替わります。作業は日付が
合うか、または 曜日が合うときに実行されます。
13 日の金曜日の深夜 0 時に実行しようとしたこの行を見てみましょう。
0 0 13 * 5 # 「13 日の金曜日」ではない
そうは動きません。2 つの日付フィールドが両方とも制限されているため、cron は 毎月 13 日の深夜 0 時に、そして 毎週金曜の深夜 0 時に作業を実行します。 意図よりはるかに頻繁です。13 日の金曜日は 2 つの条件の 積集合 ですが、cron は 和集合 を計算します。
この動作は Vixie crontab の man ページに文書化されており、1987 年から安定して
いるので、バグではなく仕様です。ただ、十分に直感に反するので crontab.guru が
これに関するページを別に設けているほどです。実用的なルールとして、2 つの
日付フィールドを両方とも制限していると気づいたら、止まって AND を意図したのか
確認してください。そうであれば、単一の標準 cron 行では表現できません。作業の
中に日付ガード ([ "$(date +\%a)" = Fri ] || exit 0) を入れるか、ロジックを
cron の外に出すことになります。
2 つの日付フィールドのうち一方が * であれば OR ルールは適用されず、すべてが
当たり前どおりに動きます。
マクロは Vixie の拡張
Vixie cron は、5 フィールドの式全体を置き換えるニックネームマクロの一式を 追加しました。
@reboot # デーモン起動時に 1 回
@yearly # 0 0 1 1 * (@annually も同じ)
@monthly # 0 0 1 * *
@weekly # 0 0 * * 0
@daily # 0 0 * * * (@midnight も同じ)
@hourly # 0 * * * *
これらは便利で Linux で広くサポートされていますが、POSIX ではありません。
crontab の POSIX 仕様は 5 つの数値フィールドだけを定義します。@daily の入った
crontab を厳格な POSIX cron や組み込みの busybox ビルド、一部の BSD 構成で
走らせると、拒否されるか黙って無視されることがあります。@reboot がその中で
最も移植性がありません。多くのコンテナの init システムでは意味を持たず、cron
デーモンが「再起動」しない環境では発火しません。移植性が重要なら、5 つの
フィールドを書き下してください。
タイムゾーンと DST
cron は式をデーモンのタイムゾーンで評価します。これはシステムが UTC に
設定されていない限り、通常はシステムのタイムゾーンであって UTC では
ありません。0 9 * * * として書いた作業は、デーモンがあるマシンがどこで
あれ、その場所の午前 9 時のローカル時刻に実行されます。ワークロードを別の
リージョンのホストに移すと、スケジュールは黙ってずれます。
サマータイムの切り替えが鋭い角です。春に時刻を進める夜、ローカル時刻は 01:59 から 03:00 へ飛びます。02:30 にスケジュールされた作業は、02:30 が存在しないので その日は実行されません。秋に戻す夜には 02:30 が 2 度発生し、cron の実装に よって作業が 2 度実行されるか、重複排除されることがあります。Vixie cron は 春に飛ばされた作業を実行し、秋にその区間の作業が二重実行されないようにする 特別なロジックを持っていますが、その動作は実装ごとに異なり、金融や請求の 作業で頼れるものではありません。
防御的な姿勢は、cron デーモンを UTC で走らせ、ローカル時刻への変換は作業の
中で行うことです。UTC でスケジュールされた作業は、UTC に DST がないので、
飛ばされたり重複したりする時刻に決して出くわしません。ほとんどの現代の
スケジューラ (systemd タイマーの OnCalendar、Kubernetes の CronJob) は
タイムゾーンを明示的に設定させてくれます。標準の crond はシステムの TZ を
継承するので、OS レベルで UTC に固定するのが最良です。
秒フィールドの罠
標準の cron には 秒フィールドがありません。スケジュールする最小単位は 1 分です。いくつかの人気の非 Unix スケジューラが秒のための先頭 6 番目の フィールドを追加するため、別の生態系から来た人をこれが捕まえます。
- Quartz (Java) は 6 フィールドまたは 7 フィールド形式を使います。
秒 分 時 日 月 曜日 [年]です。 - node-cron や複数の言語ライブラリは、省略可能な先頭の秒フィールドを 受け入れ、5 フィールドと 6 フィールドの式の両方を取ります。
- Spring の
@Scheduled(cron=...)は Quartz スタイルの 6 フィールド文法を 使います。
移植性の罠は、Quartz の例からコピーした 6 フィールドの式を Unix の crontab に
貼り付けると、すべてのフィールドが左に 1 つずれることです。0 0 12 * * ?
(Quartz の毎日正午) を Linux の crontab に貼り付けると、分 0、時 0、日 12、
月 *、曜日 * と読まれ、標準 cron が理解すらしない ? がぽつんと残ります。
Quartz は標準 cron に概念すらない ? と L/W/# の修飾子も使います。
システム間で式をコピーする前に、対象のランタイムが 5 フィールドを期待するか
6 フィールドを期待するかを必ず確認してください。
式のサニティチェック
cron 行をコミットする前に、3 つを確認してください。フィールド数がランタイムに 合っているか (Unix cron は 5、Quartz/node-cron は 6)、AND を意図したのに 2 つの 日付フィールドが誤って制限されていないか、そして次の数回の発火時刻が期待した ところに落ちるか、です。次の実行時刻を声に出して読み上げると、たいていの ミスは捕まえられます。「毎日に見える」式が実は毎時発火するなら、スケジュールを 展開した瞬間に明らかになります。
私たちの Cron パーサ は、式を平易な説明に展開し、今後の 実行時刻を一覧にするので、行が出荷される前に検証する最速の方法です。とくに OR の癖において、展開されたスケジュールが和集合の動作をすぐに目に見える形に してくれます。