进程、信号与优雅关机

专栏:TypeScript 与 Node 地基 · 第 10 / 13 篇
Node进程信号

:::info 学习目标 完成本篇后你能够:解释 SIGTERM/SIGINT 与退出码的语义;实现一个带超时的优雅关机;说明为什么 CI 场景必须正确处理退出码。 前置:第 8 篇完成。预计时长:45 分钟。 :::

第 2 篇(dsh 专栏)提过:SIGTERM → 优雅退出(码 0),SIGINT → 130,第二次信号强制走。这一篇把它的原理与最小实现写出来。

信号:操作系统发给进程的”通知”

信号谁发的含义Node 里的事件
SIGINTCtrl+C用户要求中断process.on('SIGINT')
SIGTERMkill / 容器停止请你正常退出process.on('SIGTERM')
SIGKILLkill -9无条件杀死(无法捕获

关键认知:信号是”请求”不是”命令”(SIGKILL 除外)。收到 SIGTERM 的进程有机会做完手头的事再退出——这就是”优雅关机”的窗口。

退出码:脚本世界的对话语言

process.exit(n) 的 n 就是退出码,惯例:0 = 成功,非 0 = 失败(具体码自定)。CI 和脚本靠它判断成败:agent-run "任务" 返回 1,CI 就标红。dsh 的约定(SIGTERM→0、SIGINT→130)与 Unix 传统一致。

优雅关机的四步

图表(ts-process-signals.md)

最小实现:带超时的优雅关机

最小实现:带超时的优雅关机

// graceful.ts —— 可复用的优雅关机
export function setupGracefulShutdown(server: { close(cb: () => void): void }) {
  let shuttingDown = false
  const pending = new Set<Function>()

  function track<T>(p: Promise<T>): Promise<T> {   // 登记进行中的任务
    pending.add(() => {})
    p.finally(() => pending.delete(() => {}))
    return p
  }

  async function shutdown(code: number) {
    if (shuttingDown) process.exit(1)              // 第二次信号:强制走
    shuttingDown = true
    console.log('正在优雅关机…')
    server.close(() => {                            // 停止接新请求
      console.log('已退出'); process.exit(code)
    })
    setTimeout(() => process.exit(code), 5000).unref()   // 5 秒超时兜底
  }

  process.on('SIGTERM', () => shutdown(0))
  process.on('SIGINT', () => shutdown(130))
  return { track, shutdown }
}

流程四步:停新请求 → 等在途任务(带超时兜底)→ 清理资源 → 退出unref() 让超时定时器不阻止进程退出。

与其他主题的联系

  • dsh 的 runProfile(第 2 篇时序)注册的就是这套逻辑:首次信号开始优雅 dispose(上限 5 秒)、第二次强制退出;dispose 的内容是 Cordis 插件树的级联卸载(第 4 篇);
  • headless 模式(第 13 篇)的退出码直接取自最终轮次的原因枚举——completed→0、aborted/error→1,CI 与脚本据此判断成败;
  • new-api 的多机部署(new-api 专栏第 10 篇)同样依赖优雅关机把看板缓存落库(SaveQuotaDataCache)。

常见踩坑

  1. 进程不退出——还有活跃的定时器/连接没清理(unref() 或显式 clear);
  2. 关机时限内干不完——超时兜底的时长要按最长的在途任务估算(dsh 给 SSE 留 120 秒);
  3. 第二次信号没处理——卡死的进程只能 kill -9,在途数据全丢;
  4. CI 里退出码永远是 0——脚本吞了错误码或没把失败映射到非零退出。

随堂练习(带验收标准)

  1. 跑通上面的服务,Ctrl+C 观察日志顺序(停止接新 → 退出)。验收:退出码正确(echo $? 显示 130);
  2. 发一个慢请求(10 秒),期间 kill <pid>。验收:请求完成后进程才退出;再发一次 kill -9 对比;
  3. codex-rs 或 dsh 里处理信号的那段源码(dsh 在 apps/cli/src/process-shutdown.ts),对照你的实现找差距。

← 返回文章列表