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

如何在Node.js主进程结束后自动清理阻塞的子进程资源?

在Node.js中自动安全退出重用的长期运行子进程

完全可以实现你的需求,核心思路是跟踪调用生命周期+监听进程终止信号,既处理正常流程下所有调用完成后的自动退出,也覆盖强制终止场景的安全清理。以下是两种可行方案:

方案1:调用计数+信号监听(推荐,简单易维护)

通过统计活跃调用的数量,当所有调用完成后自动触发子进程安全退出;同时监听系统终止信号,确保异常终止时也能清理子进程。

import * as child_process from "node:child_process"

const reusedProc = child_process.spawn("long-running", [], {
  stdio: ['pipe', 'pipe', 'pipe'] // 根据通信需求配置stdio
})
let activeCallCount = 0
let isExiting = false

// 封装业务调用函数,自动跟踪调用状态
async function call() {
  if (isExiting) {
    throw new Error('子进程正在退出,无法处理新请求')
  }
  activeCallCount++
  try {
    // 这里编写与子进程通信的逻辑,示例:发送命令并等待响应
    reusedProc.stdin.write('business-command\n')
    const response = await new Promise(resolve => {
      reusedProc.stdout.once('data', data => resolve(data.toString().trim()))
    })
    return response
  } finally {
    activeCallCount--
    // 所有调用完成且未处于退出流程时,触发安全退出
    if (activeCallCount === 0 && !isExiting) {
      triggerSafeExit()
    }
  }
}

// 子进程安全退出逻辑
function triggerSafeExit() {
  isExiting = true
  // 向子进程发送安全退出指令(根据子进程协议调整)
  reusedProc.stdin.write('exit\n')
  
  // 等待子进程正常退出,避免僵尸进程
  reusedProc.once('exit', (code, signal) => {
    console.log(`子进程已退出,退出码:${code},终止信号:${signal}`)
    process.exit(0)
  })
  
  // 设置超时兜底,防止子进程无响应导致主进程挂起
  setTimeout(() => {
    console.warn('子进程未及时响应,强制终止')
    reusedProc.kill('SIGTERM')
    process.exit(1)
  }, 5000)
}

// 监听系统终止信号,触发安全退出
process.on('SIGINT', () => {
  if (!isExiting) {
    console.log('收到终止信号,开始安全退出子进程')
    triggerSafeExit()
  }
})

process.on('SIGTERM', () => {
  if (!isExiting) {
    console.log('收到终止信号,开始安全退出子进程')
    triggerSafeExit()
  }
})

// 兜底:主进程意外退出时强制清理子进程
process.on('exit', () => {
  if (!isExiting) {
    reusedProc.kill('SIGTERM')
  }
})

// 使用示例(无需显式调用exitLongRunning)
// call().then(console.log)
// call().then(console.log)

方案2:Async Hooks跟踪异步资源(进阶,复杂场景)

如果业务调用的异步逻辑复杂,无法通过简单计数跟踪,可以用Node.js的async_hooks模块跟踪所有关联的异步资源,当所有资源释放后触发子进程退出:

import * as child_process from "node:child_process"
import { AsyncResource, executionAsyncId } from "node:async_hooks"

const reusedProc = child_process.spawn("long-running")
const resourceMap = new Map()
let isExiting = false

// 自定义异步资源,跟踪单个调用的生命周期
class CallResource extends AsyncResource {
  constructor() {
    super('CallResource')
    const id = executionAsyncId()
    resourceMap.set(id, this)
  }

  done() {
    const id = executionAsyncId()
    resourceMap.delete(id)
    this.emitDestroy()
    checkForExit()
  }
}

// 检查是否所有异步资源已释放
function checkForExit() {
  if (resourceMap.size === 0 && !isExiting) {
    triggerSafeExit()
  }
}

// 子进程安全退出逻辑(同方案1)
function triggerSafeExit() {
  isExiting = true
  reusedProc.stdin.write('exit\n')
  reusedProc.once('exit', () => process.exit(0))
  setTimeout(() => {
    reusedProc.kill('SIGTERM')
    process.exit(1)
  }, 5000)
}

async function call() {
  if (isExiting) throw new Error('子进程正在退出')
  const resource = new CallResource()
  try {
    // 异步通信逻辑
    reusedProc.stdin.write('business-command\n')
    const resp = await new Promise(resolve => reusedProc.stdout.once('data', resolve))
    return resp.toString()
  } finally {
    resource.done()
  }
}

// 信号监听同方案1
process.on('SIGINT', triggerSafeExit)
process.on('SIGTERM', triggerSafeExit)

关键说明

  • 为什么beforeExit/ node-cleanup无效?
    • beforeExit仅在事件循环为空且无其他退出触发条件时触发,若子进程的stdio流存在未处理事件(如stdout监听),事件循环不会为空,因此不会触发。
    • node-cleanup依赖进程进入退出流程,若主进程被子进程的事件阻塞(如持续输出),则无法触发清理逻辑。
  • 安全退出核心:必须主动向子进程发送退出指令(而非直接kill),让子进程完成自身资源清理;同时设置超时兜底,避免子进程无响应导致主进程挂起。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 17:18:13