← 返回题目列表

Node.js 服务如何做优雅停机?

中等 第 19 / 27 题 更新于 2026/07/29
Node.js优雅停机进程信号部署

简化版

优雅停机是收到 SIGTERM 等信号后,停止接收新请求,等待正在处理的请求完成,关闭数据库、队列、定时器等资源,最后在超时时间内退出。它能减少发布、扩缩容和容器重启时的请求中断。

详细版

Node.js 服务不能只依赖 process.exit() 粗暴退出。 常见流程是监听信号,先 server.close() 停止接收新连接,再清理资源。

const server = app.listen(3000)

process.on('SIGTERM', async () => {
  server.close(async () => {
    await db.close()
    process.exit(0)
  })
})

真实项目还要设置超时兜底、处理 keep-alive 连接、停止消费队列、关闭定时任务,并配合负载均衡摘流。

完整版教学

一、为什么不能直接杀进程

服务发布、容器扩缩容、机器下线时,进程通常会收到终止信号。 如果立刻退出,正在处理的请求会断开,数据库事务可能半途失败,队列消息可能重复消费。 优雅停机就是让服务有一个收尾窗口。

粗暴退出:
收到 SIGTERM -> 进程立即退出 -> 请求断开

优雅停机:
收到 SIGTERM -> 停止接新请求 -> 等旧请求完成 -> 清理资源 -> 退出

假设一次订单接口平均 300ms,发布时直接杀进程可能让几十个请求失败。 给 10 秒收尾时间,大多数请求都能自然结束。

二、第一步是停止接收新请求

HTTP 服务需要先从入口层摘流。 应用进程收到信号后调用 server.close(),它会停止接受新连接,并等待已有连接关闭。 同时负载均衡或 Kubernetes readiness 也应把实例标记为不可接流。

let shuttingDown = false

app.use((req, res, next) => {
  if (shuttingDown) {
    res.status(503).send('Server shutting down')
    return
  }
  next()
})

process.on('SIGTERM', () => {
  shuttingDown = true
  server.close()
})
动作目的
readiness 失败让负载均衡停止转发
server.close()Node 停止接受新连接
返回 503拒绝收尾期新请求
超时退出防止进程永远不退出

入口摘流要先于资源关闭。 否则新请求进来时数据库已经关了,错误会更混乱。

三、等待正在处理的请求完成

server.close() 会等待已有连接结束,但 keep-alive 和长连接会让等待变复杂。 对于普通 HTTP 请求,可以维护一个 active request 计数。 当计数归零或到达超时,就继续退出流程。

let active = 0

app.use((req, res, next) => {
  active++
  res.on('finish', () => active--)
  res.on('close', () => active--)
  next()
})

如果当前有 20 个请求,平均 200ms 完成,可以等它们自然结束。 但如果有一个请求卡 2 分钟,不能无限等。 所以优雅停机必须配超时,例如 10 到 30 秒。

四、资源清理要按依赖顺序

服务不只是 HTTP 端口。 还可能有数据库连接池、Redis、消息队列消费者、定时任务、文件句柄和后台任务。 停机时要先停止产生新任务,再等待旧任务,再关闭底层资源。

推荐顺序:
停止接流
停止队列消费
停止定时任务
等待进行中任务
关闭 DB/Redis/MQ 连接
退出进程

如果先关数据库,再等待请求完成,请求可能执行到一半发现连接不可用。 顺序错了,优雅停机就会变成优雅地制造错误。

五、信号和退出码要处理清楚

常见信号包括 SIGTERMSIGINT。 容器平台通常先发 SIGTERM,本地 Ctrl+C 是 SIGINT。 清理成功后用退出码 0,异常退出用非 0。

async function shutdown(signal: string) {
  console.log(`received ${signal}`)
  // cleanup...
  process.exit(0)
}

process.once('SIGTERM', () => shutdown('SIGTERM'))
process.once('SIGINT', () => shutdown('SIGINT'))

使用 once 能避免重复进入停机流程。 如果连续收到信号,也可以第二次直接强退。 这类细节在生产环境很实际。

六、必须有强制超时兜底

优雅停机不能无限等待。 长轮询、WebSocket、卡死请求、未关闭句柄都可能让进程挂住。 要设置最大等待时间,到点后记录日志并强制退出。

const timer = setTimeout(() => {
  console.error('shutdown timeout')
  process.exit(1)
}, 30_000)

timer.unref()

记忆钩子:优雅停机不是慢慢死,是先关门、送完客、收拾桌子、到点打烊。

回答时把“停止新流量、等待旧任务、关闭资源、超时兜底”串起来。 这比只写一个 SIGTERM 监听更像真实工程。

七、常见误区与追问

  • 误区:监听 SIGTERM 后直接 process.exit 就是优雅停机。 直接退出没有等待请求和资源清理,仍然可能中断业务。
  • 误区:只关闭 HTTP server 就够了。 数据库、Redis、队列消费者、定时任务和后台任务也要处理。
  • 误区:优雅停机可以无限等待。 必须设置超时兜底,否则卡住的请求会让发布或缩容阻塞。
  • 追问:Kubernetes 里怎么配合? readiness 先失败摘流,收到 SIGTERM 后应用执行优雅停机,并在 terminationGracePeriodSeconds 内退出。
  • 追问:keep-alive 连接怎么办? 停止接收新请求,必要时设置连接超时或主动销毁空闲连接。
  • 追问:队列消费者怎么停? 先暂停拉取新消息,再等待正在处理的消息 ack 或重试策略完成。

八、加强记忆

优雅停机记四步:摘流、等待、清理、超时。Node 收到信号后不要立刻 exit,先停 HTTP 新连接,再处理进行中请求和队列任务,最后关连接池,并用超时兜底避免卡死。