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式を検証する

操作手順

  1. Cron式ツールを開く
  2. Cron式を入力
  3. 人間が読める解釈を確認:
    • 0 9 * * 1-5 → 「毎週月曜〜金曜 09:00」
    • */30 8-18 * * * → 「毎日 08:00-18:00、30分ごと」
  4. 次回実行時刻のリスト(今後5〜10回分)を確認
  5. 式が正しいか検証

四、本番レベルの定期実行ジョブ設計

同時実行制御

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日

七、ベストプラクティスチェックリスト

  1. 常にUTCタイムゾーンを使用(業務がローカル時刻に強く依存する場合を除く)
  2. 日と曜日を同時に指定しない(和集合の罠を回避)
  3. 同時実行制御:同時に実行されるインスタンスは1つだけ
  4. 分散ロック:複数インスタンスでのデプロイ時に重複実行を防止
  5. リトライ+アラート:サイレントに失敗させない
  6. タイムアウト制御:タスクの無限ハングを防止
  7. 冪等性のある設計:繰り返し実行しても同じ結果になる
  8. 実行ログ:毎回の実行時刻、所要時間、ステータスを記録
  9. ツールで式を検証:デプロイ前に意味が正しいことを確認
#Cron#定时任务#crontab#时区#运维