基于DigitalOcean Droplet的Node.js定时任务:PM2重载中断与集群重复执行问题
解决方案:pm2重载不中断定时任务 + 集群下避免重复执行
一、解决pm2 reload中断定时任务的问题
方案1:将定时任务与业务服务解耦(最可靠)
把定时任务单独抽成一个独立的Node.js进程,用pm2单独管理。这样业务服务执行pm2 reload my-app时,定时任务进程不受任何影响,完全避免中断问题。
- 步骤:
- 创建独立的定时任务文件(比如
cron-service.js),仅包含定时任务逻辑。 - 用pm2启动这个独立进程:
pm2 start cron-service.js --name my-cron-jobs - 后续业务服务的重载、重启操作都不会影响
my-cron-jobs进程。
- 创建独立的定时任务文件(比如
方案2:优化优雅重启逻辑(不拆分进程时使用)
通过监听pm2发送的终止信号,让进程等待当前正在执行的定时任务完成后再退出,同时停止新任务的调度。
- 在你的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); });
- 修改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实现分布式锁为例:
- 安装Redis客户端:
npm install redis - 编写带锁的定时任务逻辑:
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
相关产品推荐
相关产品推荐

