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 07 の両方が日曜

行は左から右へ読みます。30 2 1 * * は「分 30、時 2、日 1、すべての月、 すべての曜日」、つまり毎月 1 日の 02:30 です。* のフィールドは「その範囲の すべての値」を意味します。

曜日の範囲が最初の罠です。Vixie cron は日曜に 07 の両方を受け入れる ので、0-61-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 の癖において、展開されたスケジュールが和集合の動作をすぐに目に見える形に してくれます。