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 连接
退出进程
如果先关数据库,再等待请求完成,请求可能执行到一半发现连接不可用。 顺序错了,优雅停机就会变成优雅地制造错误。
五、信号和退出码要处理清楚
常见信号包括 SIGTERM 和 SIGINT。
容器平台通常先发 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 新连接,再处理进行中请求和队列任务,最后关连接池,并用超时兜底避免卡死。