Cron式ベストプラクティス:初心者の落とし穴から本番レベルの定期実行ジョブ設計まで
ユーティリティ伊藤 結衣
Cron式:5つのフィールドで時間を制御
┌───────── 分 (0-59)
│ ┌───────── 時 (0-23)
│ │ ┌───────── 日 (1-31)
│ │ │ ┌───────── 月 (1-12)
│ │ │ │ ┌───────── 曜日 (0-7, 0と7はどちらも日曜日)
│ │ │ │ │
* * * * *
一、構文早見表
| 式 | 意味 | 実行頻度 |
|---|---|---|
* * * * * |
毎分 | 1440回/日 |
*/5 * * * * |
5分ごと | 288回/日 |
0 * * * * |
毎時0分 | 24回/日 |
0 9 * * * |
毎日9:00 | 1回/日 |
0 9 * * 1-5 |
平日9:00 | 5回/週 |
30 2 1 * * |
毎月1日 2:30 | 1回/月 |
0 0 1 1 * |
毎年1月1日 0:00 | 1回/年 |
特殊文字
| 文字 | 意味 | 例 |
|---|---|---|
* |
任意の値 | * * * * * |
, |
列挙 | 0 9,12,18 * * *(9時/12時/18時) |
- |
範囲 | 0 9-17 * * 1-5(平日9〜17時) |
/ |
ステップ | */15 * * * *(15分ごと) |
L |
最後 | 0 0 L * *(毎月最終日) |
W |
最も近い平日 | 0 0 1W * *(1日に最も近い平日) |
# |
第n曜日 | 0 0 * * 5#3(第3金曜日) |
二、よくある落とし穴
落とし穴1:日と曜日を同時に指定する
# 誤った理解:毎月15日かつ金曜日
0 0 15 * 5
# 実際の意味:毎月15日 または 毎週金曜日(和集合!)
# 正しく書くには追加のロジック判定が必要
ルール:日と曜日を同時に指定した場合、積集合ではなく和集合になります。
落とし穴2:タイムゾーン問題
# 北京時間9:00に実行したい
0 9 * * *
# サーバーがUTCタイムゾーン → 実際には北京時間17:00に実行される!
# 解決策:タイムゾーンを明示的に指定する
// node-cron でタイムゾーンを指定
cron.schedule('0 9 * * *', task, {
scheduled: true,
timezone: 'Asia/Shanghai',
});
落とし穴3:60秒の書き間違い
# 間違い:分フィールドに60は存在しない
0 0 60 * * *
# 正しい:毎時実行
0 * * * *
落とし穴4:2月30日
# 2月に30日は存在しないため、この式は2月には実行されない
0 0 30 2 *
落とし穴5:サマータイム(DST)
サマータイムの切り替え時、存在しない時刻や重複する時刻が発生します:
春の切り替え:2:00 → 3:00(2:30は存在しない)
秋の切り替え:2:00 → 1:00(1:30が2回実行される)
推奨:サマータイム問題を避けるため、UTCタイムゾーンを使用しましょう。
三、ToolsKuでCron式を検証する
操作手順
- Cron式ツールを開く
- Cron式を入力
- 人間が読める解釈を確認:
0 9 * * 1-5→ 「毎週月曜〜金曜 09:00」*/30 8-18 * * *→ 「毎日 08:00-18:00、30分ごと」
- 次回実行時刻のリスト(今後5〜10回分)を確認
- 式が正しいか検証
四、本番レベルの定期実行ジョブ設計
同時実行制御
import { CronJob } from 'cron';
let isRunning = false;
const job = new CronJob('0 */5 * * * *', async () => {
if (isRunning) {
console.log('前回の実行がまだ完了していないため、今回はスキップ');
return;
}
isRunning = true;
try {
await processData();
} finally {
isRunning = false;
}
});
分散ロック(複数インスタンスでのデプロイ)
import { Redis } from 'ioredis';
const redis = new Redis();
async function acquireLock(key, ttl = 300) {
const acquired = await redis.set(key, '1', 'EX', ttl, 'NX');
return acquired === 'OK';
}
const job = new CronJob('0 2 * * *', async () => {
const lockKey = `cron:daily-report:${new Date().toISOString().slice(0, 10)}`;
if (!(await acquireLock(lockKey))) {
console.log('他のインスタンスがロックを取得済みのため、スキップ');
return;
}
try {
await generateDailyReport();
} finally {
await redis.del(lockKey);
}
});
失敗時のリトライ
async function runWithRetry(task, maxRetries = 3, delay = 5000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await task();
} catch (error) {
console.error(`${attempt}回目の実行が失敗:`, error.message);
if (attempt < maxRetries) {
await sleep(delay * attempt); // 遅延を増加
} else {
throw error;
}
}
}
}
const job = new CronJob('0 */10 * * * *', () => {
runWithRetry(syncData, 3, 5000);
});
タイムアウト制御
async function runWithTimeout(task, timeoutMs = 60000) {
return Promise.race([
task(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('タスクがタイムアウトしました')), timeoutMs)
),
]);
}
五、ジョブのモニタリング
実行ログ
const job = new CronJob('0 2 * * *', async () => {
const startTime = Date.now();
const context = { job: 'daily-sync', startedAt: new Date().toISOString() };
try {
await runWithRetry(syncData);
context.status = 'success';
} catch (error) {
context.status = 'failed';
context.error = error.message;
await sendAlert(context); // アラート
} finally {
context.duration = Date.now() - startTime;
await logExecution(context); // ログを記録
}
});
ヘルスチェック
// 最終成功実行時刻を記録
await redis.set('cron:last-success:daily-sync', Date.now());
// ヘルスチェック:25時間以上実行されていない場合にアラート
const lastSuccess = await redis.get('cron:last-success:daily-sync');
if (Date.now() - lastSuccess > 25 * 3600 * 1000) {
await sendAlert({ type: 'cron_stale', job: 'daily-sync' });
}
六、よくある定期実行パターン
| 要件 | Cron式 | 説明 |
|---|---|---|
| 毎時データ同期 | 0 * * * * |
毎時0分に実行 |
| 平日の朝レポート | 0 9 * * 1-5 |
月〜金曜 9:00 |
| 毎日バックアップ | 0 2 * * * |
深夜2:00 |
| 毎週クリーンアップ | 0 3 * * 0 |
日曜3:00 |
| 毎月の照合 | 0 6 1 * * |
毎月1日 6:00 |
| 15分ごとのハートビート | */15 * * * * |
高頻度チェック |
| 四半期レポート | 0 8 1 1,4,7,10 * |
四半期初月の1日 |
七、ベストプラクティスチェックリスト
- 常にUTCタイムゾーンを使用(業務がローカル時刻に強く依存する場合を除く)
- 日と曜日を同時に指定しない(和集合の罠を回避)
- 同時実行制御:同時に実行されるインスタンスは1つだけ
- 分散ロック:複数インスタンスでのデプロイ時に重複実行を防止
- リトライ+アラート:サイレントに失敗させない
- タイムアウト制御:タスクの無限ハングを防止
- 冪等性のある設計:繰り返し実行しても同じ結果になる
- 実行ログ:毎回の実行時刻、所要時間、ステータスを記録
- ツールで式を検証:デプロイ前に意味が正しいことを確認
#Cron#定时任务#crontab#时区#运维