You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于DigitalOcean Droplet的Node.js定时任务:PM2重载中断与集群重复执行问题

解决方案:pm2重载不中断定时任务 + 集群下避免重复执行

一、解决pm2 reload中断定时任务的问题

方案1:将定时任务与业务服务解耦(最可靠)

把定时任务单独抽成一个独立的Node.js进程,用pm2单独管理。这样业务服务执行pm2 reload my-app时,定时任务进程不受任何影响,完全避免中断问题。

  • 步骤:
    1. 创建独立的定时任务文件(比如cron-service.js),仅包含定时任务逻辑。
    2. 用pm2启动这个独立进程:pm2 start cron-service.js --name my-cron-jobs
    3. 后续业务服务的重载、重启操作都不会影响my-cron-jobs进程。

方案2:优化优雅重启逻辑(不拆分进程时使用)

通过监听pm2发送的终止信号,让进程等待当前正在执行的定时任务完成后再退出,同时停止新任务的调度。

  1. 在你的Node.js代码中添加信号监听与任务控制逻辑:
const cron = require('node-cron');

// 存储当前正在执行的异步任务
let activeTask = null;

// 定义定时任务
const cronJob = cron.schedule('0 3 * * *', async () => {
  activeTask = executeImportantProcess(); // 替换为你的定时任务函数
  await activeTask;
  activeTask = null;
});

// 监听pm2的SIGTERM终止信号
process.on('SIGTERM', async () => {
  // 停止新的定时任务调度
  cronJob.stop();
  
  // 等待当前正在执行的任务完成
  if (activeTask) {
    await activeTask;
  }
  
  // 进程正常退出
  process.exit(0);
});
  1. 修改pm2配置文件(ecosystem.config.js),增加优雅重启相关配置:
module.exports = {
  apps: [{
    name: 'my-app',
    script: 'app.js',
    instances: 'max',
    exec_mode: 'cluster',
    wait_ready: true,
    listen_timeout: 5000, // 等待服务就绪的超时时间
    kill_timeout: 30000, // 给进程足够时间处理完当前任务(根据你的任务时长调整)
  }]
};

之后执行pm2 reload ecosystem.config.js,pm2会逐个向实例发送SIGTERM信号,等待进程处理完任务后再重启。

二、解决集群下定时任务重复执行的问题

仅依赖process.env.NODE_APP_INSTANCE === 0的方式不够可靠(比如实例重启时编号可能变化,或者主实例临时下线),推荐使用分布式锁确保同一时间只有一个实例执行定时任务。

以Redis实现分布式锁为例:

  1. 安装Redis客户端:npm install redis
  2. 编写带锁的定时任务逻辑:
const cron = require('node-cron');
const redis = require('redis');

// 初始化Redis客户端
const redisClient = redis.createClient({
  // 配置你的Redis连接信息,比如host、port等
});
redisClient.connect();

// 获取分布式锁
async function getLock(jobKey, lockExpire = 60000) {
  const lockKey = `cron:lock:${jobKey}`;
  // 设置锁,仅当键不存在时生效(NX),并设置过期时间(PX)
  const result = await redisClient.set(lockKey, process.pid, {
    NX: true,
    PX: lockExpire
  });
  return result === 'OK';
}

// 释放分布式锁
async function releaseLock(jobKey) {
  const lockKey = `cron:lock:${jobKey}`;
  await redisClient.del(lockKey);
}

// 带锁的定时任务
cron.schedule('0 3 * * *', async () => {
  const hasLock = await getLock('important-process');
  if (!hasLock) {
    console.log('其他实例正在执行该任务,本次跳过');
    return;
  }

  try {
    // 执行你的核心任务逻辑
    await executeImportantProcess();
  } finally {
    // 无论任务成功失败,都释放锁
    await releaseLock('important-process');
  }
});

这种方式下,不管集群有多少个实例,只有成功获取锁的实例会执行任务,彻底避免重复执行问题,同时在pm2重载时,旧实例释放锁后,新实例可以正常获取锁继续执行任务。

内容的提问来源于stack exchange,提问作者Keval Rakholiya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 07:10:55