実際の本番環境で使われる 5 つの Cron 式パターン
Cron 式は単純に見えます — 5 つのフィールドと一握りの演算子 — しかし本番環境のチームは同じ失敗を繰り返します: 特定の月で静かにスキップされるジョブ、共有状態を破壊する重複実行、すべて同じ秒に目覚めてデータベースを圧倒する何百ものジョブ。式自体が問題であることはまれです。問題は、エッジケースを十分に考えないことにあります。
このガイドでは、エンジニアリングチームが実際に本番環境で使用する 5 つの cron パターンと、それぞれの背景にある理由および回避すべき罠を取り上げます。デプロイする前に Toova Cron パーサーを使って任意の式を検証し、次の 10 回の実行時刻を確認してください。
Cron 式の構造
標準的な cron 式は、スペースで区切られた 5 つのフィールドを持ちます:
*/15 * * * *
| | | | |
| | | | +---- 曜日 (0-6, 0=日曜)
| | | +------- 月 (1-12)
| | +---------- 日 (1-31)
| +------------- 時 (0-23)
+----------------- 分: 15 分ごと (0, 15, 30, 45)
各フィールドは次のものを受け入れます: 特定の値 (5)、ワイルドカード (*)、範囲 (1-5)、リスト (1,3,5)、ステップ値 (*/15)。一部の実装は秒のための 6 番目のフィールドを追加します。他(GitHub Actions など)は正確に 5 つを期待します。複雑な式を書く前に、スケジューラーが使用している方言を必ず確認してください。
パターン 1: 15 分ごと — */15 * * * *
*/15 * * * *
| | | | |
| | | | +---- 曜日 (0-6, 0=日曜)
| | | +------- 月 (1-12)
| | +---------- 日 (1-31)
| +------------- 時 (0-23)
+----------------- 分: 15 分ごと (0, 15, 30, 45) キュープロセッサー、ハートビートチェック、頻繁に実行する必要があるが連続実行は避けたいポーリングジョブの定番表現です。毎時 :00、:15、:30、:45 に毎日発火します。
crontab では:
# crontab エントリ — バッチ処理を 15 分ごとに実行
*/15 * * * * /opt/app/bin/process-queue.sh >> /var/log/queue.log 2>&1 Node.js では:
// node-cron を使った Node.js
import cron from 'node-cron';
cron.schedule('*/15 * * * *', async () => {
const batch = await db.query(
'SELECT * FROM jobs WHERE status = $1 LIMIT 100',
['pending']
);
await processBatch(batch);
}); サンダリングハード問題。同じ */15 式を持つ数百のワーカーを実行すると、すべて :00、:15、:30、:45 に同時に発火します。各ワーカーが共有データベースを叩くと、滑らかな負荷ではなく時間あたり 4 つのスパイクが発生します。修正方法は、各実行前にランダムなジッターを追加することです:
// 分散システムでのサンダリングハード問題を避けるためにジッターを追加
cron.schedule('*/15 * * * *', async () => {
const jitterMs = Math.floor(Math.random() * 30_000); // 最大 30 秒
await sleep(jitterMs);
await processBatch();
}); 最大 30 秒のジッターは、予想される 15 分のサイクル内にジョブを保ちながら、スパイクを 30 秒の窓に分散します。より高度な分散については、パターン 5(Jenkins H 構文)を見てください。
ジョブのタイミングをデバッグする際は、タイムスタンプ変換ツールを使って次のスケジュール実行時刻を Unix エポックから人間が読める時刻に変換できます。
パターン 2: 平日業務時間 — 0 9 * * 1-5
0 9 * * 1-5
| | | | |
| | | | +-- 曜日: 月曜-金曜 (1-5)
| | | +------ 月: 毎月
| | +---------- 日: 毎日
| +-------------- 時: 9
+------------------ 分: 0 (正時) この式は月曜から金曜まで、正確に 9 時 0 分に 1 回発火します。次のような場合の標準パターンです: 日次ダイジェストメールの送信、業務時間レポートの実行、業務日にヒューマンレビューが必要なワークフローのトリガー、エンジニアリング日の開始時にステージング環境にデプロイ。
Kubernetes CronJob では:
# Kubernetes CronJob — 平日 9 時 (UTC) のダイジェストメール
apiVersion: batch/v1
kind: CronJob
metadata:
name: digest-email
spec:
schedule: "0 9 * * 1-5"
jobTemplate:
spec:
template:
spec:
containers:
- name: mailer
image: myapp/mailer:latest
command: ["/app/send-digest"]
restartPolicy: OnFailure タイムゾーンの罠。Kubernetes CronJob スケジュールはクラスターのタイムゾーンで実行され、デフォルトは UTC です。「UTC の 9 時」は冬の米国東部時間では 5 時、米国太平洋時間では 4 時です。ユーザーは「朝のダイジェスト」を真夜中に受け取ることになります。IANA タイムゾーン文字列を受け入れる cron ライブラリを使用します:
// タイムゾーン処理 — 「UTC の 9 時」はユーザーが期待するものとは異なる
// IANA タイムゾーンを理解するライブラリを使用する
import { CronJob } from 'cron';
const job = new CronJob(
'0 9 * * 1-5',
() => sendDailyDigest(),
null,
true,
'America/New_York' // UTC の 9 時ではなく、米国東部時間の 9 時に実行
); タイムゾーン変換ツールは、任意の IANA タイムゾーン全体で指定したローカル時刻の UTC 相当を見つけるのに役立ちます。
曜日の番号付け。標準 cron 定義では日曜に 0 または 7 を使います。一部の実装(Quartz、cron4j)は月曜に 0 ではなく 1 を使います。スケジューラーのドキュメントを確認し、デプロイする前に Cron パーサーでテストしてください。
パターン 3: 夜間バッチ — 0 22 * * *
0 22 * * *
| | | | |
| | | | +-- 曜日: 任意
| | | +------ 月: 任意
| | +---------- 日: 任意
| +-------------- 時: 22 (午後 10 時)
+------------------- 分: 0 1 日 1 回、午後 10 時に実行します。夜間バッチは、データエンジニアリングで最も一般的なパターンの 1 つです: その日のイベントを集計、検索インデックスを再構築、レポートを生成、ログをアーカイブ、終業時通知を送信。
なぜ深夜ではなく午後 10 時なのか?2 つの理由があります。第一に、午後 10 時は日が変わる前のバッファを提供し、遅れて到着するイベントを拾う可能性を減らします。第二に、深夜に重いジョブを実行すると、毎月 1 日に月末請求(パターン 4)と競合することを意味します。
日次ジョブを書く 3 つの最も一般的な方法:
# 「毎日午後 10 時」と書く 3 つの方法:
0 22 * * * # 標準 cron
@daily # 深夜 — 午後 10 時とは異なる
0 22 * * * # 深夜以外の時間には常に @macros よりも明示的な時刻を優先 重複防止。通常 45 分かかる夜間ジョブが、たまに 2 時間かかることがあります — そして次の夜のトリガーが発火しているときにまだ実行中だと、両方のジョブが同じデータを修正します。分散ロックを使用します:
// 冪等性チェック — ジョブが重複する場合の二重処理を防ぐ
async function runNightlyBatch() {
const lock = await redis.set(
'nightly-batch-lock',
process.pid,
'EX', 3600, // TTL: 1 時間
'NX' // 存在しない場合のみセット
);
if (!lock) {
console.log('バッチが既に実行中、スキップします。');
return;
}
try {
await processNightlyData();
} finally {
await redis.del('nightly-batch-lock');
}
}
Redis の NX フラグは、一度に 1 つのプロセスのみがロックを保持できることを保証します。TTL は、クラッシュしたジョブがロックを永遠に保持することを防ぎます。
パターン 4: 月次請求 — 0 0 1 * *
0 0 1 * *
| | | | |
| | | | +-- 曜日: 任意
| | | +------ 月: 任意
| | +---------- 日: 1 (毎月 1 日)
| +-------------- 時: 0 (深夜)
+------------------ 分: 0 毎月 1 日の深夜に発火します。次の典型的なパターンです: 月次請求書の生成、使用量クォータのリセット、月末調整の実行、前月のデータのアーカイブ。
月次ジョブの cron の罠:
# 毎月 1 日の UTC 深夜
0 0 1 * *
# ---- 避けるべき罠 ----
# これは「毎月」確実に実行されない:
0 0 29-31 * * # 日数の少ない月は静かにスキップ
# 四半期請求 (1 月、4 月、7 月、10 月):
0 0 1 1,4,7,10 *
# 月末は厄介 — 標準 cron に「最終日」はない
# @reboot とアプリケーションロジック、または毎日実行する cron で
# コード内で今日が月末かどうかをチェックする方法を使う。 月末請求。各月の最終日(最初の日ではなく)に請求する必要がある場合、クリーンな cron 式はありません。回避策:
// アプリケーションレベルの「月末日」チェック
cron.schedule('0 23 28-31 * *', async () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// 明日が 1 日なら、今日が月末日
if (tomorrow.getDate() === 1) {
await runEndOfMonthBilling();
}
}); これは毎月 28〜31 日に発火し、コードで今日が実際に最終日かどうかをチェックします。31 日未満の月では最大 4 回不要に実行されますが、チェックは安価で、ロジックは明確です。
四半期初の請求には、月フィールドでリスト演算子を使用します: 0 0 1 1,4,7,10 *(1 月、4 月、7 月、10 月)。
パターン 5: ハッシュ化スケジュール (Jenkins H 構文) — H/30 * * * *
H/30 * * * *
|
+--- H は Jenkins がジョブごとにランダムな分を選びます。
H/30 = H から始まる 30 分ごと。
ジョブごとに H が異なり、負荷を分散します。 H トークンは、cron 構文に対する Jenkins 特有の拡張です。H をジョブ名から派生した安定した擬似乱数で置き換えます。分フィールドの H/30 は「ジョブ名のハッシュによって決定された分から始まる 30 分ごと」を意味します。
Jenkinsfile では:
// Jenkinsfile — 30 分ごとにトリガー、負荷分散済み
pipeline {
triggers {
cron('H/30 * * * *')
}
stages {
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
} なぜこれが重要なのか。Jenkins のインストールには、しばしば数百のパイプラインがあります。すべてのパイプラインが */30 * * * * を使用すると、すべてが :00 と :30 にトリガーされ、エージェントプールと下流システムに巨大な負荷スパイクを生み出します。H/30 では、各ジョブが異なるオフセットを取得します — 1 つは :03/:33、別のは :17/:47 — 時間全体にわたって負荷を均等に分散します。
GitHub Actions は H をサポートしません。複数の Actions ワークフローを分散させるには、異なる固定分オフセットを手動で選びます:
// GitHub Actions — H はサポートされていない; 手動でオフセット付きスケジュールを使用
// 2 つのジョブを分散させる: 異なる固定分を選択
name: Job A
on:
schedule:
- cron: '5 * * * *' # 毎時 :05 に実行
---
name: Job B
on:
schedule:
- cron: '35 * * * *' # :35 に実行 — Job A の 30 分後 これは Jenkins H ほどエレガントではありませんが、少数のワークフローでは同じ効果を達成します。多数のワークフローには、分散スケジューリングをネイティブにサポートする外部オーケストレーターを検討してください。
横断的な懸念事項
冪等性は譲れない
すべての cron ジョブは 2 回実行しても安全であるべきです。インフラストラクチャは故障し、スケジューラーは再起動し、分散ロックは時々タイムアウトします。同じ入力で同じ時刻に 2 回目を実行しても同じ結果を生成するようにジョブを設計します。insert の代わりに upsert を使用し、check-before-act パターンを使用し、外部 API 呼び出しに冪等性キーを使用します。
デッドレターアラート
静かに失敗する cron ジョブは、実行されないジョブよりも悪いです。ジョブが失敗したり、予想される時間枠内に完了しなかった場合、デッドレターキューや監視システムにアラートを送ります。Sentry Crons、Healthchecks.io、Cronitor のようなツールはハートビートチェックを追加します: ジョブが開始時と終了時に URL を ping します。終了の ping が期限内に到着しないと、アラートが発火します。
スケジュールをログに記録する
すべての cron 実行の開始時と終了時に、式、トリガー時刻のタイムスタンプ(現在時刻ではなく、ジッターによりわずかに異なる場合があります)、所要時間を含む構造化ログ行を出力します。これにより、下流の事故 —「データベースが午後 10 時に遅くなった」— を特定のバッチジョブ実行と関連付けるのが容易になります。
クイックリファレンス
Cron パーサーで任意の式を解析し、検証します。タイムゾーン変換ツールでスケジュールされた時刻をタイムゾーン間で変換します。ジョブが Unix タイムスタンプをログに記録する場合、タイムスタンプ変換ツールがすぐに読める日付に変換します。
cron 構文と一般的な例の包括的なリファレンスとして、crontab.guru が標準的なクイックリファレンスです。Jenkins H 構文とその完全仕様については、Jenkins Pipeline cron 構文ドキュメントを参照してください。