云服务器部署 web 项目的进程常驻记录

云服务器部署 web 项目,最容易返工的地方不在代码,在进程怎么活下来。文件传上去了,端口也通了,SSH 一断,服务跟着一起没。

这次的过程可以拆成两层:进程层决定服务活不活,入口层决定外面能不能进来。本文记录的是进程层这一遍,以及中间卡住的三个位置。

一、背景

一台新加坡区域的实例,系统是 Ubuntu 22.04,跑一个 Node 服务,监听 3000。

项目的启动方式是 node server.js,本地开发时一直这么起,没有任何进程管理。部署的目标只有两条:机器重启之后服务自己起来,异常退出之后服务自己回来。

二、现象:SSH 断开,服务就没了

第一次登上去,进目录,执行启动命令,前台开始打日志,浏览器也能打开。

关掉终端窗口,再刷新页面,连接被拒绝。

重新登上去看,进程列表里已经没有 node。端口上没有监听,报的是 connection refused,不是超时。

这两种表现要分开看。超时是包被丢在机器外面,拒绝是机器上没人听这个端口。

一个指向网络,一个指向本机。

三、排查

按进程、端口、日志三层的顺序走下来。前一层没有结论之前不动下一层。

第一步:确认进程还在不在

断开连接之后再执行一次:

复制代码
ps -ef | grep server.js | grep -v grep

输出是空的。

空输出就是第一个结论:进程不在了。不是端口的问题,也不是网络的问题。

第二步:看端口当时被谁占着

进程还在的时候,端口是有人听的。

复制代码
ss -lntp | grep :3000

-l 只看监听状态,-n 不做域名解析,-t 限 TCP,-p 带出进程。

四个参数合起来,一次看清端口归谁。这一步的作用是排除端口冲突。

端口被别的进程占着时,表现同样是打不开,但进程列表里会出现另一个名字。

第三步:看服务是怎么被拉起来的

ps -ef 的输出里有一列父进程号,这时候它是某个 bash,不是 1。

父进程是 bash,说明进程挂在当前的登录会话下面。会话结束,内核会给这个会话里的进程发挂断信号,进程跟着退出。

这一层看的是血缘,不是启动命令本身。命令写得再对,挂在错误的父进程下也活不过一次断线。

四、根因

两个根因都在默认状态上:进程挂在前台会话里,服务没有交给 init 系统托管。

前台启动的进程属于当前会话,会话是 SSH 连接的一部分。终端关掉,会话结束,进程收到挂断信号,默认行为是退出。

机器重启之后,也没有任何东西会把它再拉起来。

这一点在当时看不出来,要等到下一次断线或者下一次重启才暴露。

五、方案:交给 systemd

systemd 是 Ubuntu 22.04 默认的 init 系统,负责开机拉起服务,也负责进程退出之后的重启。写一个 unit 文件,把启动方式交给它托管。

第四步:写 unit 文件

复制代码
[Unit]
Description=app server
After=network.target

[Service]
User=app
WorkingDirectory=/srv/app
EnvironmentFile=/etc/app.env
ExecStart=/usr/bin/node /srv/app/server.js
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

几个字段各管一件事。

User 决定进程以谁的身份运行。用 root 跑,等于把整个账号的权限面交给这个进程。

WorkingDirectory 决定相对路径的基准。缺了它,程序按当前目录找配置文件会找不到。

EnvironmentFile 把密钥和连接串从 unit 文件里拿出来,unit 文件本身就不用限制权限了。

Restart=always 让异常退出后自动重启,RestartSec 决定重启间隔。

After=network.target 只保证网络栈就绪之后启动,不保证公网可达。这一点在排查时容易搞混,把启动延迟当成网络问题。

第五步:环境变量文件的归属

环境变量文件要归服务用户所有,权限收到 600。

复制代码
sudo chown app:app /etc/app.env
sudo chmod 600 /etc/app.env

两条的用途是让服务读得到文件,同时不让其他用户读到内容。

这一步做错的表现很典型。服务起不来,日志里只有一句权限被拒绝,看不出是哪个文件。

第六步:重载、自启、看日志

改过 unit 文件之后先重载,再开机自启,最后看状态与日志。

复制代码
sudo systemctl daemon-reload
sudo systemctl enable --now app
systemctl status app --no-pager
journalctl -u app -n 100 --no-pager

daemon-reload 不能漏。systemd 读的是已经加载的配置副本,文件改了不会自动重读。

enable 管开机,start 管当前,--now 是把两个动作合成一条命令。

journalctl -u app 看的是这个 unit 的输出,比翻 /var/log 下的文件准。

日志里的时间戳和系统时区一致,不用换算。这一条在跨时区协作时省下的时间比想象中多。

六、复盘

进程活不活,看的不是启动命令对不对,是它归属于谁。

交互式启动的进程天然属于会话,会话一断就结束。交给 init 系统托管之后,进程归属于系统,断线、重启都不影响它。

入口层这次没碰。进程正常而外面打不开,问题在放行规则和反向代理上,不在 systemd。

七、常见问题

status 显示 active,页面还是打不开? active 只说明进程在跑。要看 ss -lntp 有没有监听,再看放行规则有没有开对应端口。

改完 unit 文件重启服务没生效? daemon-reload 漏了。systemd 读的是已经加载的配置副本。

日志里只有一行退出码怎么办? 137 是收到强制信号,1 是程序自己退出。前者查内存和 OOM,后者直接看程序输出。

相关推荐
曹牧1 小时前
VS: Visual Studio 里把未包含的文件加进项目2
linux·运维·算法
IT_陈寒1 小时前
一文搞懂React的核心概念
前端·人工智能·后端
微软技术分享1 小时前
NexShell 1.0|大模型驱动的智能 SSH 运维终端,永久免费,赋能 Linux 服务器管理
运维·服务器·ssh
薛一半1 小时前
Redux实战:组件状态管理全解析
开发语言·前端·javascript·react.js
风骏时光牛马2 小时前
功能模块性能测试用例验证
前端
ha_lydms2 小时前
String Functions(字符串函数)
大数据·hadoop·阿里云·spark·数据处理·dataworks·maxcompute
Chenyu_3102 小时前
NUMA 基础:从 CPU、内存到 Node 的整体理解
linux·服务器·网络·缓存
对空六课3 小时前
两个统计平台数据不同:用同一时间窗和事件样本做差异定位
大数据·前端·网络·数据库
小白的码BUG之路10 小时前
linux -- ssh两台机器相连
运维·服务器·ssh