先记住这个答案
在 Node.js 中,SIGINT(终端 Ctrl+C)、SIGTERM(kill 默认信号)和 SIGHUP(终端挂断)若没有注册监听,默认动作是立即退出进程。但它们属于可捕获信号,可以通过 process.on('SIGINT', handler) 注册回调,从而阻止默认退出并执行清理。而 SIGKILL 和 SIGSTOP 是不可捕获的:SIGKILL 强制杀死进程,SIGSTOP 将进程暂停,均无法拦截或忽略。只有前一类信号适合做优雅停机。
- SIGINT/SIGTERM/SIGHUP默认终止,可监听改变
- SIGKILL/SIGSTOP无法捕获,只能被动接受
- 监听信号后进程不会自动退出,需手动收尾
三种信号为何可优雅处理
POSIX 信号各自有预设动作,Node.js 在启动时对 SIGINT、SIGTERM、SIGHUP 都采用默认终止。当 shell 发送 SIGINT 表示用户中断,SIGTERM 用于进程管理器通知退出,SIGHUP 多代表控制终端关闭。一旦注册了任意监听,默认动作就被覆盖,信号到达时会触发回调,进程不会自动退出,必须靠代码安排退出路径。
SIGKILL 和 SIGSTOP 的作用是安全网:SIGKILL 让内核立即回收进程所有资源,SIGSTOP 将进程挂起。它们没有向用户态回调的入口,Node.js 无法为它们注册有效监听,因为信号处理机制被内核禁止。因此不能依赖它们做任何业务收尾。
Kubernetes 滚动更新中的优雅退出
容器编排平台默认向应用发 SIGTERM 并等待宽限期,超时才发 SIGKILL。假设 Node.js 服务收到 SIGTERM 后要零丢请求退出,正确做法是注册 process.on('SIGTERM', handler),在回调里调用 server.close() 停止接纳新连接,再用 Promise 追踪在途请求,等它们完成后再 process.exit(0)。
如果直接忽略 SIGTERM,编排器会在宽限期后发 SIGKILL,SIGKILL 不会触发任何回调,未完成的连接被强制断开,可能丢数据。若回调里只调用 process.exit() 也不含等待,同样达不到优雅。只有把关闭服务器和等待存量请求纳入同一清理流程,才能完成稳定切换。
信号监听何时失效
第一限制是信号只能在主线程收到,Worker 线程没有 signal 事件,需由主线程统一转发。第二限制是如果主线程被 CPU 密集型任务阻塞,事件循环无法转动,即使注册了 SIGTERM,回调也不会执行,此时 SIGKILL 成了唯一强制手段。
另一个易错点是重复注册多个监听器,Node.js 会依次执行而不覆盖,多个回调可能重复清理。若要恢复默认行为,需移除监听;生产上建议只注册一次,并将退出决策收敛到单一函数,避免互相争抢导致幂等问题。
容易答错的地方
- 认为 SIGHUP 无法监听
- 实际 SIGHUP 默认动作是终止,但同样可以注册
process.on('SIGHUP')来处理终端断开时的任务,比如重新加载配置。只有 SIGKILL 与 SIGSTOP 才真正不能被进程注册处理。 - 把 process.kill 发送的信号都当作不可捕获
process.kill(pid)只是发送信号,能否被捕获取决于目标进程是否注册了监听。SIGTERM 可捕获,SIGKILL 不可捕获。生产环境常用 SIGTERM 做优雅停机,只有必要时才用 SIGKILL。
面试官还会怎么问?
进程收到 SIGTERM 后不退出会怎样?
进程会继续运行,除非你显式 process.exit 或再次收到不可忽略信号。但编排系统等待宽限期后通常会补发 SIGKILL,届时进程被强制终止,上下文清理由系统回收。
如何为 SIGINT 与 SIGTERM 编写同一套清理函数?
将清理逻辑提取为 async 函数,在多个信号监听中调用它,并设置一次性标志防止并发执行,最后在回调里调用 process.exit 结束。这样保持逻辑单点可控。
SIGTERM 默认的进程退出码是多少?
信号终止时,Shell 或父进程通过 wait 系统调用得到退出原因是信号,常规显示为 128+信号编号(SIGTERM=143)。Node.js 内可用 process.exitCode 设定自己的码,但被信号杀时通常视为非零。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。