Лучшие практики 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
Шаги использования
- Откройте инструмент выражений Cron
- Введите выражение Cron
- Просмотрите интерпретацию на русском:
0 9 * * 1-5→ "Каждый понедельник-пятницу в 09:00"*/30 8-18 * * *→ "Каждые 30 минут с 08:00 до 18:00 ежедневно"
- Просмотрите список следующих выполнений (5-10 ближайших)
- Проверьте корректность выражения
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. Список лучших практик
- Всегда использовать часовой пояс UTC, если бизнес не требует локального времени
- Не указывать день и день недели одновременно, избегать ловушки объединения
- Контроль параллелизма: Только один экземпляр выполняется в данный момент
- Распределённая блокировка: Предотвращать дублирование при развёртывании нескольких экземпляров
- Повторные попытки + оповещения при ошибках: Не игнорировать ошибки молча
- Контроль тайм-аута: Предотвращать бесконечное зависание задач
- Идемпотентный дизайн: Повторное выполнение даёт одинаковый результат
- Журнал выполнения: Записывать время, длительность и статус каждого выполнения
- Проверять выражения инструментами: Подтверждать корректную семантику перед развёртыванием
#Cron#定时任务#crontab#时区#运维