文章目录

引言
事情是这样的:我用 SSH 连上阿里云服务器,跑了一个简单的 Node.js 小项目。Express 起的 HTTP 服务,监听 3000 端口,代码在本地测得好好的,推到服务器上用 node app.js 一跑,也正常响应。但过了两天发现接口调不通了------登上服务器一看,进程没了。
重启,又过了一天,又没了。我开始觉得这不是偶然。
如果你也遇到过类似的情况------SSH 连着的时候一切正常,一旦断开终端或者网络闪断,服务就莫名其妙地挂掉------这篇文章就是写给你的。我们将从现象出发,一步步定位根因,理解 SIGHUP 信号的工作机制,最后给出四种解决方案,并重点展开 PM2 的用法,让你的 Node 服务真正"稳"在服务器上。
配置环境:Linux 全发行版(CentOS / Ubuntu / Debian 等),文中命令和配置在 Ubuntu 22.04 及 CentOS 7 上均可运行。Node.js 版本不限,以下示例基于 Node.js 18 LTS。
定位过程
第一步:排除代码问题
遇到服务挂掉,第一反应往往是代码有 bug。我把错误日志翻了一遍------什么都没有 。没有 uncaughtException,没有 process.on('error') 的输出,进程没有留下任何遗言就消失了。如果是代码抛异常,至少会有一个错误堆栈,但日志干净得像什么都没发生过。
这让我意识到:进程不是自己崩溃的,是被外部力量杀死的。
第二步:看进程还在不在
SSH 连上服务器后,用 ps 查看进程:
bash
ps aux | grep node
# 空空如也------进程确实没了
正常退出的进程会留下退出码,系统日志里也可能有记录。但这里的情况是进程直接消失,说明它不是"主动退出",而是被终止信号干掉的。
第三步:复现------锁定触发条件
回想服务挂掉的时间点,都是在我合上笔记本盖、或者网络切换之后。那就主动复现一次:
bash
# 终端 A:SSH 登录服务器,启动服务
ssh user@my-server
node app.js
# 输出:Server running on port 3000
# 然后直接关掉终端 A
# 终端 B:另一台机器立刻测试
curl http://my-server:3000/
# 结果:Connection refused ------ 服务已经挂了
复现成功。 只要 SSH 断开,进程立刻消失。触发条件锁定了。
第四步:理解信号
在 Linux 中,每个进程都从属于一个会话(Session)和一个进程组(Process Group) 。当你通过 SSH 登录时,你会得到一个终端(TTY),所有在这个终端里启动的进程都属于同一个会话。当你关闭终端或 SSH 断连时,内核会向这个会话中的所有进程发送一个信号------SIGHUP(Hangup,挂断信号)。
node app.js 直接在前台运行,它是 Shell 会话的子进程。会话结束 → 收到 SIGHUP → 默认行为退出。这就是根因。

根因分析
SIGHUP 是什么
SIGHUP(Signal Hangup)是 Unix/Linux 系统中最古老的信号之一,编号为 1。它最初的设计目的是在调制解调器(modem)连接的电话线路挂断时通知进程。虽然现在没人再用拨号上网了,但这个信号被保留了下来,并且被赋予了新的职责:在终端关闭时通知该终端下的所有进程。
当一个终端(TTY)关闭时,内核会遍历这个终端上的所有进程,向它们发送 SIGHUP。进程收到 SIGHUP 后的默认行为 是立即终止 。你的 node app.js 没有显式捕获这个信号,那就走默认行为------退出。
为什么不是 Node.js 的问题
有人可能会怀疑是 Node.js 的事件循环或异步处理导致的问题。实际上,这和 Node 一点关系都没有。可以试试其他环境,比如python:
bash
# 开一个终端 SSH 进去,运行
python3 -m http.server 3000
# 断开 SSH,服务同样会挂
# 换成 Go 写的二进制也一样
./my-go-server
# 断开 SSH,一样挂
任何程序直接在终端前台运行,都会因为终端关闭而被 SIGHUP 杀掉。 这是 Linux 进程模型决定的,不挑语言。
📌 核心结论: Node 进程和 SSH 会话是父子关系。父进程(Shell)死的时候,子进程(Node)会收到 SIGHUP 信号,默认行为就是退出。你把 SSH 终端一关,等于亲手给 Node 进程发了一张"死亡通知单"。
解决方案
知道原因就好办了。解决思路很明确:让进程脱离 SSH 会话独立运行,并且最好能挂了自动重启。
下面按推荐程度从低到高介绍四种方案。
方案一:nohup------最简单的一行命令
nohup(no hangup)的作用就是让进程忽略 SIGHUP 信号:
bash
nohup node app.js > app.log 2>&1 &
各参数含义:
| 部分 | 含义 |
|---|---|
nohup |
让进程忽略 SIGHUP 信号,终端关了也不退出 |
> app.log |
将标准输出重定向到 app.log 文件 |
2>&1 |
将标准错误也合并到标准输出,一起写入 app.log |
& |
将进程放到后台运行,不占用终端 |
运行后会输出一个进程 ID(PID),可以这样查看:
bash
# 查看进程是否在运行
ps -p [PID]
# 查看日志
tail -f app.log
优点 :一条命令搞定,所有 Linux 发行版自带,无需安装任何东西。
缺点:进程崩溃了不会自动重启;重启服务器后需要手动再跑一遍;管理多个进程时完全靠脑子记。适合"临时跑一会儿看看效果",不适合长期运行。
方案二:screen / tmux------虚拟终端保活
screen 和 tmux 创建了一个独立于 SSH 会话的虚拟终端。即使你的 SSH 连接断了,这个虚拟终端还在运行,里面的进程也安然无恙。
以 tmux 为例(Ubuntu 下 sudo apt install tmux 安装):
bash
# 创建一个名为 myapp 的会话
tmux new -s myapp
# 在 tmux 窗口里启动服务
node app.js
# 按 Ctrl+B,再按 D ------ 从 tmux 会话中"分离"(detach),回到普通终端
# 现在可以安全退出 SSH 了,服务继续跑
# 下次 SSH 登录后,重新连接 tmux 会话
tmux attach -t myapp
# 又看到了熟悉的 Node 输出,就像从未离开过
screen 的用法类似:
bash
screen -S myapp # 创建会话
node app.js # 启动服务
# 按 Ctrl+A,再按 D ------ 分离
screen -r myapp # 重新连接
screen -ls # 查看所有会话
优点 :可以随时 attach 回去看实时输出,方便调试;同时管理多个窗口。
缺点:依旧不会自动重启崩溃的进程;服务器重启后需要手动重建会话。
适合开发调试阶段,想随时看到程序输出了什么。
方案三:PM2------Node.js 生产环境首选 ⭐
PM2(Process Manager 2)是 Node.js 生态中最成熟的进程管理器,也是本文的核心推荐方案。它同时解决了进程守护、日志管理、自动重启、开机自启等多个生产环境的刚需问题。详细内容可参考【PM2 教程】从零开始的 PM2 进程管理实战:让你的 Node.js 应用永不掉线
安装与基础使用
bash
# 全局安装
npm install -g pm2
# 启动应用并命名为 my-app(--name 便于后续管理)
pm2 start app.js --name my-app
参数说明:
start:启动一个应用app.js:入口文件路径--name my-app:给应用起一个名字,后续操作靠这个名字来定位,比记 PID 方便得多
运行后会输出一个进程表格,类似这样:
plain
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ my-app │ default │ 1.0.0 │ fork │ 12345 │ 0s │ 0 │ online │ 0% │ 25.3mb │ user │ disabled │
└─────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

PM2 核心能力拆解
① 进程守护与自动重启
PM2 启动的应用是 PM2 守护进程(daemon)的子进程。SSH 断开时,PM2 守护进程本身不受影响(它以 daemon 方式运行,不依附于任何终端),所以它管理的 Node 进程也不会收到 SIGHUP。
更关键的是,如果 Node 进程因为代码异常而崩溃,PM2 会自动重启它。默认情况下,PM2 会立即重启,如果在短时间内多次崩溃,它会指数退避(exponential backoff),避免死循环式的重启风暴。
② 日志管理
不用再手动重定向日志了:
bash
# 实时查看日志(所有应用)
pm2 logs
# 只看 my-app 的日志
pm2 logs my-app
# 清空日志
pm2 flush
PM2 自动收集 stdout 和 stderr,存储在 ~/.pm2/logs/ 目录下,按应用名称分文件,而且会自动处理日志轮转。
③ 开机自启
服务器重启后,PM2 可以自动恢复之前运行的所有应用:
bash
# 生成开机启动脚本(根据你的 init 系统自动选择 systemd/upstart 等)
pm2 startup
# 保存当前进程列表,下次开机自动恢复
pm2 save
pm2 startup 的输出会告诉你接下来需要执行什么命令(通常是一条 sudo 命令来注册 systemd 服务),照着做就行。
④ 零停机重载(Graceful Reload)
代码更新后要重启服务,直接 kill 进程会导致正在处理的请求中断。PM2 提供了热重载:
bash
# 零停机重载(逐个 worker 重启,始终有进程在服务)
pm2 reload my-app
# 注意:pm2 restart 会先停再启,有短暂的服务中断
# 生产环境应该用 reload 而非 restart
⚠️ 风险提示 :
pm2 restart会先停止进程再启动,造成短暂的服务不可用。在生产环境中重启服务时,请使用pm2 reload来实现零停机。另外,pm2 save会保存当前进程快照------如果某次调试时临时起了一个进程并执行了pm2 save,这个临时进程也会在下次重启时被恢复。记得定期检查pm2 list确认只保存了需要持久化的应用。
常用命令速查
| 命令 | 作用 |
|---|---|
pm2 list |
查看所有应用状态 |
pm2 logs [name] |
查看日志 |
pm2 restart [name] |
重启应用(有中断) |
pm2 reload [name] |
零停机重载 |
pm2 stop [name] |
停止应用 |
pm2 delete [name] |
从 PM2 中删除应用 |
pm2 monit |
终端内的实时监控面板 |
pm2 save |
保存当前进程列表 |
pm2 startup |
生成开机自启脚本 |
pm2 info [name] |
查看单个应用的详细信息 |
方案四:systemd------Linux 原生方案
如果你不想在服务器上装 Node 生态的工具,或者需要管理的不只是 Node 应用,systemd 是 Linux 系统自带的服务管理器,同样可以实现进程守护和开机自启。
创建服务配置文件:
bash
sudo vim /etc/systemd/system/my-app.service
写入以下内容:
properties
[Unit]
Description=My Node.js App
After=network.target
[Service]
Type=simple
User=www
WorkingDirectory=/opt/my-app
ExecStart=/usr/bin/node /opt/my-app/app.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
关键参数解释:
| 参数 | 含义 |
|---|---|
After=network.target |
在网络就绪之后再启动服务,避免启动时网络不可达 |
Type=simple |
最简单的启动类型,systemd 认为 ExecStart 启动的进程就是主服务进程 |
User=www |
以 www 用户身份运行(不要用 root,降低安全风险) |
WorkingDirectory |
进程的工作目录,确保相对路径引用正确 |
ExecStart |
启动命令,必须使用绝对路径 ,which node 可以查到 Node 的安装路径 |
Restart=always |
无论什么原因退出都自动重启------这就是进程守护的核心 |
RestartSec=5 |
重启前等待 5 秒,避免崩溃后立刻重启陷入死循环 |
Environment |
设置环境变量,相当于 export NODE_ENV=production |
部署与验证:
bash
# 重新加载 systemd 配置
sudo systemctl daemon-reload
# 设为开机自启
sudo systemctl enable my-app
# 启动服务
sudo systemctl start my-app
# 查看状态
sudo systemctl status my-app
# 查看日志
sudo journalctl -u my-app -f
优点 :系统自带、零依赖、统一管理各种语言的服务。
缺点 :配置比 PM2 稍微繁琐;日志用 journalctl 查看,没有 PM2 的 pm2 logs 那么直观。
方案对比与选择建议
| 方案 | 自动重启 | 开机自启 | 日志管理 | 上手难度 | 适用场景 |
|---|---|---|---|---|---|
nohup |
❌ | ❌ | ❌ | ⭐ | 临时跑一下,几分钟的事 |
screen/tmux |
❌ | ❌ | ❌ | ⭐⭐ | 开发调试,随时想看输出 |
| PM2 | ✅ | ✅ | ✅ | ⭐⭐ | Node 生产环境首选(推荐) |
systemd |
✅ | ✅ | ✅ | ⭐⭐⭐ | 不想装额外依赖、多语言混合管理 |
决策建议:
- 只是在服务器上临时测试一下 →
nohup,一行命令的事 - 开发阶段,想随时 attach 回去看
console.log→tmux - 你的 Node 项目要正式部署了 → PM2,十分钟配置好,省下未来无数次排查"服务怎么又挂了"的时间
- 团队有运维规范要求用 systemd 统一管理所有服务 →
systemd
如果你和我一样了解过前端/Node,我建议直接上 PM2。它专为 Node 设计,配置文件简单,常用命令不超过 10 条,而且自带 pm2 monit 监控面板非常直观。
总结
回顾整个排查过程,我们从一个看似"随机"的服务挂掉现象出发,通过逐步复现锁定了 SSH 断连这个触发条件,找到了幕后真凶 SIGHUP 信号,最终理解了 Linux 终端会话与进程组的关系。
这个问题的本质教训是:别在 SSH 终端里裸跑服务。 任何需要长期运行的后台服务,都应该通过进程管理器来守护------无论是 PM2、systemd 还是 Docker,它们的核心能力之一就是帮你的进程扛住 SIGHUP,并且在崩溃时自动拉起来。
本文重点推荐的 PM2,用三条命令就能让服务稳稳地跑起来:
bash
npm install -g pm2
pm2 start app.js --name my-app
pm2 save && pm2 startup
从此 SSH 随便断,网络随便闪,你的 Node 服务都不会莫名其妙地消失了。
适用边界说明 :本文讨论的场景是 Linux 服务器上通过 SSH 直接运行 Node 应用的情况。如果你使用的是 Docker 容器部署,容器内的进程管理逻辑不同(PID 1 问题),应优先通过 Docker 的
restart policy来保障服务可用性。Windows Server 上的 IISNode 或 Windows Service 部署也不在本文讨论范围。
参考
- PM2 官方文档:pm2.keymetrics.io