Лучшие практики Cron: ловушки и продакшн-дизайн задач

Утилиты

Выражения Cron: 5 полей для управления временем

┌───────── Минута (0-59)
│ ┌───────── Час (0-23)
│ │ ┌───────── День (1-31)
│ │ │ ┌───────── Месяц (1-12)
│ │ │ │ ┌───────── День недели (0-7, 0 и 7 — воскресенье)
│ │ │ │ │
* * * * *

I. Краткий справочник по синтаксису

Выражение Значение Частота выполнения
* * * * * Каждую минуту 1440 раз/день
*/5 * * * * Каждые 5 минут 288 раз/день
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 января в 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 (третья пятница)

II. Распространённые ловушки

Ловушка 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: 30 февраля

# В феврале нет 30-го числа, это выражение не выполнится в феврале
0 0 30 2 *

Ловушка 5: Летнее время

При переходе на летнее время некоторые моменты могут не существовать или повторяться:

Весна вперёд: 2:00 → 3:00 (2:30 не существует)
Осень назад: 2:00 → 1:00 (1:30 выполняется дважды)

Рекомендация: Использовать часовой пояс UTC, чтобы избежать проблем с летним временем.


III. Проверка выражений Cron с помощью ToolsKu

Шаги использования

  1. Откройте инструмент выражений Cron
  2. Введите выражение Cron
  3. Просмотрите интерпретацию на русском:
    • 0 9 * * 1-5 → "Каждый понедельник-пятницу в 09:00"
    • */30 8-18 * * * → "Каждые 30 минут с 08:00 до 18:00 ежедневно"
  4. Просмотрите список следующих выполнений (5-10 ближайших)
  5. Проверьте корректность выражения

IV. Продакшн-дизайн запланированных задач

Контроль параллелизма

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)
    ),
  ]);
}

V. Мониторинг задач

Журнал выполнения

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' });
}

VI. Распространённые паттерны запланированных задач

Потребность Выражение Cron Описание
Синхронизация данных каждый час 0 * * * * В точку
Утренний отчёт по рабочим дням 0 9 * * 1-5 Понедельник-пятница в 9:00
Ежедневный бэкап 0 2 * * * В 2:00 ночи
Еженедельная очистка 0 3 * * 0 Воскресенье в 3:00
Ежемесячная сверка 0 6 1 * * 1-го числа каждого месяца в 6:00
Heartbeat каждые 15 минут */15 * * * * Высокочастотная проверка
Квартальный отчёт 0 8 1 1,4,7,10 * Первый день квартального месяца

VII. Список лучших практик

  1. Всегда использовать часовой пояс UTC, если бизнес не требует локального времени
  2. Не указывать день и день недели одновременно, избегать ловушки объединения
  3. Контроль параллелизма: Только один экземпляр выполняется в данный момент
  4. Распределённая блокировка: Предотвращать дублирование при развёртывании нескольких экземпляров
  5. Повторные попытки + оповещения при ошибках: Не игнорировать ошибки молча
  6. Контроль тайм-аута: Предотвращать бесконечное зависание задач
  7. Идемпотентный дизайн: Повторное выполнение даёт одинаковый результат
  8. Журнал выполнения: Записывать время, длительность и статус каждого выполнения
  9. Проверять выражения инструментами: Подтверждать корректную семантику перед развёртыванием
#Cron#定时任务#crontab#时区#运维