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

Node.js热重载残留僵尸进程占用端口问题求助

问题解决:Node.js热重载后旧进程残留占用端口

核心问题定位

你遇到的问题确实是Bree的Worker Threads未彻底终止导致旧进程无法退出,进而占用端口。bree.stop()只是触发停止指令,但内部Worker可能还在执行异步任务,你的Promise.race加1秒超时会提前结束等待,导致Worker残留;另外手动调用process.exit(0)可能跳过Node.js的自然退出流程,残留的句柄会让进程保持存活。

针对性解决方案

1. 确保Bree工作线程彻底终止

替换原来的Bree停止逻辑,不要用超时跳过等待,而是等待所有Worker真正退出:

// 替换shutdown函数中Bree相关的代码
await bree.stop();
// 等待所有Worker线程完全终止
await Promise.all(
  Array.from(bree.workers.values()).map(worker => 
    new Promise((resolve) => {
      // 如果Worker已经断开,直接resolve
      if (worker.exitedAfterDisconnect) return resolve(true);
      // 监听exit和error事件,确保Worker终止
      worker.once('exit', resolve);
      worker.once('error', resolve);
    })
  )
);
consola.success('Bree workers fully terminated');

同时,给Bree初始化时加上workerExit事件监听,排查是否有Worker异常退出:

bree.on('workerExit', (worker, workerId) => {
  consola.debug(`Worker ${workerId} exited with code ${worker.exitCode}`);
});

2. 修复进程退出逻辑

  • 移除手动的process.exit(0),Node.js会在所有事件循环任务完成后自动退出,手动exit可能跳过Worker的清理流程。
  • 添加防重复触发的标记,避免信号多次触发shutdown:
let isShuttingDown = false;

async function shutdown(signal: string) {
  if (isShuttingDown) return;
  isShuttingDown = true;
  
  // 保留超时逻辑但延长时间,给Worker足够清理时间,不要unref
  const timeout = setTimeout(() => {
    consola.error('Timeout dépassé, force exit');
    process.exit(1);
  }, 5000);

  try {
    // ... 原有资源清理逻辑(销毁连接、关闭服务器、Redis、SQLite)
    
    // 等待Bree Worker完全终止(上面修改后的代码)
    
    consola.success('Shutdown complete');
    clearTimeout(timeout); // 清理超时器,让进程自然退出
  } catch (err) {
    consola.error('Erreur pendant shutdown:', err);
    clearTimeout(timeout);
    process.exit(1);
  }
}

3. 排查残留活跃句柄

在shutdown的最后(退出前),打印所有活跃句柄和请求,找出残留资源:

console.log('=== Active Handles ===');
process._getActiveHandles().forEach(handle => {
  console.log(handle.constructor.name, handle);
});
console.log('=== Active Requests ===');
process._getActiveRequests().forEach(req => {
  console.log(req.constructor.name, req);
});

如果输出里还有Worker实例,说明还有线程没终止,需要进一步排查Worker内部的代码是否有未处理的异步任务(比如定时器、未关闭的数据库连接)。

4. 调整热重载启动时机

方案A:给热重载工具加延迟

对于nodemon,修改nodemon.json添加启动延迟:

{
  "delay": 3000,
  "watch": ["src/**/*.ts"],
  "exec": "node --import tsx app.ts"
}

对于原生node --watch,可以写一个简单的启动脚本,等待端口释放后再启动新进程:

// start.js
const net = require('net');
const { spawn } = require('child_process');

function waitForPortFree(port) {
  return new Promise(resolve => {
    const check = () => {
      const socket = new net.Socket();
      socket.on('connect', () => {
        socket.destroy();
        setTimeout(check, 500);
      });
      socket.on('error', () => {
        resolve();
      });
      socket.connect(port, 'localhost');
    };
    check();
  });
}

async function startServer() {
  await waitForPortFree(3001);
  spawn('node', ['--watch', '--import', 'tsx', 'app.ts'], { stdio: 'inherit' });
}

startServer();

启动命令改为node start.js,脚本会自动等待3001端口释放后再启动新进程。

方案B:用IPC通信通知热重载工具

让旧进程在shutdown完成后发送信号给热重载工具,确认可以启动新进程。比如自定义nodemon的exit钩子,但实现起来稍复杂,适合需要精确控制的场景。

额外注意事项

  • 检查Worker线程内部的代码:如果Worker里有未处理的Promise、定时器或者持久化连接,即使调用bree.stop()也无法终止,需要在Worker内部监听message事件,收到停止信号时主动清理资源并退出。
  • 测试时不要快速连续保存文件:频繁触发热重载会增加进程残留的概率,建议每次保存后等待旧进程完全退出再进行下一次保存。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.11 12:44:50