Buenas prácticas de Cron: errores y tareas en producción

Utilidades

Expresiones Cron: 5 campos para controlar el tiempo

┌───────── Minuto (0-59)
│ ┌───────── Hora (0-23)
│ │ ┌───────── Día (1-31)
│ │ │ ┌───────── Mes (1-12)
│ │ │ │ ┌───────── Día de la semana (0-7, 0 y 7 son domingo)
│ │ │ │ │
* * * * *

I. Referencia rápida de sintaxis

Expresión Significado Frecuencia de ejecución
* * * * * Cada minuto 1440 veces/día
*/5 * * * * Cada 5 minutos 288 veces/día
0 * * * * Cada hora en punto 24 veces/día
0 9 * * * Todos los días a las 9:00 1 vez/día
0 9 * * 1-5 Días laborables a las 9:00 5 veces/semana
30 2 1 * * Día 1 de cada mes a las 2:30 1 vez/mes
0 0 1 1 * 1 de enero a las 0:00 1 vez/año

Caracteres especiales

Carácter Significado Ejemplo
* Cualquier valor * * * * *
, Enumeración 0 9,12,18 * * * (9/12/18 horas)
- Rango 0 9-17 * * 1-5 (días laborables 9-17 horas)
/ Paso */15 * * * * (cada 15 minutos)
L Último 0 0 L * * (último día de cada mes)
W Día laborable más cercano 0 0 1W * * (día laborable más cercano al 1)
# Enésimo día de la semana 0 0 * * 5#3 (tercer viernes)

II. Trampas comunes

Trampa 1: Especificar día y día de la semana simultáneamente

# Comprensión errónea: el día 15 de cada mes Y que sea viernes
0 0 15 * 5

# Significado real: el día 15 de cada mes O cada viernes (¡unión!)
# La forma correcta requiere lógica adicional

Regla: Cuando se especifican día y día de la semana simultáneamente, se toma la unión en lugar de la intersección.

Trampa 2: Problemas de zona horaria

# Se espera ejecución a las 9:00 hora de Beijing
0 9 * * *

# Servidor en zona UTC → ¡ejecución real a las 17:00 hora de Beijing!
# Solución: especificar la zona horaria explícitamente
// node-cron especificar zona horaria
cron.schedule('0 9 * * *', task, {
  scheduled: true,
  timezone: 'Asia/Shanghai',
});

Trampa 3: Error de escritura de 60 segundos

# Incorrecto: el campo de minutos no tiene 60
0 0 60 * * *

# Correcto: ejecutar cada hora
0 * * * *

Trampa 4: 30 de febrero

# Febrero no tiene día 30, esta expresión no se ejecutará en febrero
0 0 30 2 *

Trampa 5: Horario de verano

Durante el cambio de horario de verano, ciertas horas pueden no existir o repetirse:

Primavera adelantar: 2:00 → 3:00 (2:30 no existe)
Otoño retrasar: 2:00 → 1:00 (1:30 se ejecuta dos veces)

Recomendación: Usar zona horaria UTC para evitar problemas de horario de verano.


III. Verificar expresiones Cron con ToolsKu

Pasos de operación

  1. Abrir la herramienta de expresiones Cron
  2. Ingresar la expresión Cron
  3. Ver la interpretación en español:
    • 0 9 * * 1-5 → "Lunes a viernes a las 09:00"
    • */30 8-18 * * * → "Cada 30 minutos de 08:00 a 18:00 cada día"
  4. Ver la lista de próximas ejecuciones (5-10 próximas)
  5. Verificar si la expresión es válida

IV. Diseño de tareas programadas de nivel productivo

Control de concurrencia

import { CronJob } from 'cron';

let isRunning = false;

const job = new CronJob('0 */5 * * * *', async () => {
  if (isRunning) {
    console.log('La ronda anterior aún no ha terminado, se omite esta ejecución');
    return;
  }

  isRunning = true;
  try {
    await processData();
  } finally {
    isRunning = false;
  }
});

Bloqueo distribuido (despliegue de múltiples instancias)

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('Otra instancia ya adquirió el bloqueo, se omite');
    return;
  }

  try {
    await generateDailyReport();
  } finally {
    await redis.del(lockKey);
  }
});

Reintento ante fallos

async function runWithRetry(task, maxRetries = 3, delay = 5000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await task();
    } catch (error) {
      console.error(`Intento ${attempt} fallido:`, error.message);
      if (attempt < maxRetries) {
        await sleep(delay * attempt); // Retraso incremental
      } else {
        throw error;
      }
    }
  }
}

const job = new CronJob('0 */10 * * * *', () => {
  runWithRetry(syncData, 3, 5000);
});

Control de tiempo de espera

async function runWithTimeout(task, timeoutMs = 60000) {
  return Promise.race([
    task(),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Tarea excedió el tiempo de espera')), timeoutMs)
    ),
  ]);
}

V. Monitoreo de tareas

Registro de ejecución

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); // Alerta
  } finally {
    context.duration = Date.now() - startTime;
    await logExecution(context); // Registrar log
  }
});

Verificación de salud

// Registrar el último tiempo de ejecución exitosa
await redis.set('cron:last-success:daily-sync', Date.now());

// Verificación de salud: si no se ha ejecutado en más de 25 horas, activar alerta
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. Patrones comunes de tareas programadas

Necesidad Expresión Cron Descripción
Sincronización de datos cada hora 0 * * * * En punto
Reporte matutino de días laborables 0 9 * * 1-5 Lunes a viernes a las 9:00
Respaldo diario 0 2 * * * A las 2:00 a.m.
Limpieza semanal 0 3 * * 0 Domingos a las 3:00
Conciliación mensual 0 6 1 * * Día 1 de cada mes a las 6:00
Heartbeat cada 15 minutos */15 * * * * Verificación de alta frecuencia
Reporte trimestral 0 8 1 1,4,7,10 * Primer día del primer mes del trimestre

VII. Lista de mejores prácticas

  1. Siempre usar zona horaria UTC, a menos que el negocio dependa fuertemente de la hora local
  2. No especificar día y día de la semana simultáneamente, evitar la trampa de la unión
  3. Control de concurrencia: Solo una instancia ejecutándose en un momento dado
  4. Bloqueo distribuido: Prevenir ejecución duplicada en despliegues de múltiples instancias
  5. Reintento ante fallos + alertas: No fallar silenciosamente
  6. Control de tiempo de espera: Evitar que las tareas se cuelguen indefinidamente
  7. Diseño idempotente: Resultados consistentes en ejecuciones repetidas
  8. Registro de ejecución: Registrar tiempo, duración y estado de cada ejecución
  9. Verificar expresiones con herramientas: Confirmar la semántica correcta antes del despliegue
#Cron#定时任务#crontab#时区#运维