第 30 章 · systemd 与进程管理
一句话:systemd 是 Linux 开机后第一个启动的进程,负责按单元文件把服务拉起来、挂了重启、开机自启,并把服务的输出收进日志。 用它还是用别的:服务器上长期运行的程序(Python、Node、Go、Java 都一样)写一个
.service单元文件交给 systemd;只跑 Node、想用一条命令按 CPU 核数起多个进程,用 pm2,pm2 自己再由 systemd 开机拉起;用 Docker 部署的交给 Compose 的restart:(第 13 章)。 核对版本:systemd 255(Ubuntu 24.04.3,WSL)、PowerShell 7.6、pm2 7.0(未安装),2026-10
30.1 安装与版本
Section titled “30.1 安装与版本”systemd 随发行版自带,Ubuntu、Debian、RHEL、Rocky、Fedora 的服务器版都用它,不用装。本地练习用 WSL 里的 Ubuntu(第 29 章 29.1),Ubuntu 24.04 的 WSL 默认已开 systemd,/etc/wsl.conf 里有 systemd=true 一行。macOS 用 launchd,没有 systemd,要练就在 Multipass 虚拟机里练。
查版本,第一行形如 systemd 255 (255.4-1ubuntu8.14):
systemctl --version在 Windows 上不进 WSL 也能查:
wsl -d Ubuntu -- systemctl --versionpm2 是 npm 包,装成全局命令,要先有 Node(第 14 章)。nvm 装的 Node 不用 sudo:
npm install -g pm2pm2 --version30.2 五分钟概念
Section titled “30.2 五分钟概念”- 单元(Unit):systemd 管理的每样东西都叫单元,用文件名后缀区分:
.service是服务,.timer是定时器,.target是一组单元的集合(如开机进入的multi-user.target)。服务的单元文件是一个 INI 格式的文本文件,分[Unit]、[Service]、[Install]三段,写清用谁的身份、在哪个目录、跑哪条命令、挂了怎么办。 start与enable:start是现在启动,重启服务器后不管;enable是在/etc/systemd/system/multi-user.target.wants/下建一个指向单元文件的软链接,下次开机才自动启动,当下并不启动。两件事都要做,就写enable --now。Restart=策略:默认no,进程退出就退出了。on-failure在非零退出码、被SIGKILL之类信号杀掉、超时的时候拉起;always连正常退出也拉起。用systemctl stop停下的,两种都不会拉起。两次拉起之间等RestartSec,默认只有 100 毫秒;10 秒内启动超过 5 次,systemd 就放弃,单元进入failed。- journald:服务写到标准输出和标准错误的内容,由 journald 收下,按单元打上标签,用
journalctl -u <服务>按服务看。程序不用自己写日志文件,也不用管切分;Ubuntu 默认存在/var/log/journal/,重启后还在,上限是所在分区的 10%,最多 4 GB。 - pm2 与 systemd 的分工:pm2 是用 Node 写的进程管理器,专管 Node 程序:重启、日志、按 CPU 核数起多个进程(cluster 模式)。它自己不会开机启动,
pm2 startup生成一个名为pm2-<用户>的 systemd 服务,开机时由 systemd 拉起 pm2,pm2 再拉起它记下的 Node 程序。
下图是一个服务从写好单元文件到稳定运行的全过程。上面一排只在第一次装和改了单元文件时做;之后进程崩溃由 systemd 按 Restart= 自动拉起,拉得太频繁就停在 failed,原因到 journald 里看:
30.3 常用命令
Section titled “30.3 常用命令”改状态的命令要 sudo,只看的命令不用。<服务> 写单元名,.service 后缀可以省略。
| 任务 | 命令 | 说明 |
|---|---|---|
| systemctl | ||
| 看状态 | systemctl status <服务> |
Active: 一行是 active (running) 就是在跑,下面附最近 10 行日志 |
| 启动、停止 | sudo systemctl start <服务> |
停止用 stop;只管这一次,重启服务器后看是否 enable |
| 重启 | sudo systemctl restart <服务> |
发版后让新代码生效 |
| 开机自启并立即启动 | sudo systemctl enable --now <服务> |
不加 --now 只设开机自启;systemctl is-enabled <服务> 查看 |
| 取消开机自启并停止 | sudo systemctl disable --now <服务> |
单元文件还在,随时能再 enable |
| 改了单元文件后重读 | sudo systemctl daemon-reload |
不执行这条,restart 用的还是旧配置 |
| 列出失败的服务 | systemctl list-units --type=service --state=failed |
都正常时显示 0 loaded units listed. |
| 检查单元文件 | systemd-analyze verify myapp.service |
没输出就是通过;ExecStart 指向的程序不存在会报 is not executable |
| journalctl | ||
| 跟踪一个服务的日志 | journalctl -u <服务> -f |
Ctrl+C 停止;去掉 -f、加 -n 100 看最近 100 行 |
| 按时间段看 | journalctl -u <服务> --since "2026-10-08 09:00" --until "2026-10-08 10:00" |
也可以写 --since "1 hour ago"、--since today |
| pm2 | ||
| 启动一个 Node 程序 | pm2 start server.js --name web |
起名后其他命令都用名字 |
| 列出所有程序 | pm2 list |
看 status、重启次数 ↺、内存 |
| 看日志 | pm2 logs web |
不写名字时看全部;日志文件在 ~/.pm2/logs/ |
| 开机自启 | pm2 startup 然后 pm2 save |
startup 打印一条 sudo 命令,复制执行;save 记下当前程序列表 |
30.4 典型工作流
Section titled “30.4 典型工作流”把一个 Python 程序做成服务
Section titled “把一个 Python 程序做成服务”程序是一个用 uvicorn 起的 Python Web 应用,代码在 /srv/myapp/current,用 deploy 用户运行(第 29 章 29.4 建的)。Node 程序把 ExecStart 换成 /usr/bin/node server.js(nvm 装的写 command -v node 查到的全路径),其余照抄。单元文件:
[Unit]# systemctl status 里显示的说明Description=myapp web service# 等网络就绪后再启动After=network-online.targetWants=network-online.target
[Service]# 用普通用户运行,不用 rootUser=deployGroup=deploy# 程序的工作目录,相对路径从这里算WorkingDirectory=/srv/myapp/current# 从文件读环境变量,格式同 .env(第 15 章 15.5);systemd 不读 .bashrcEnvironmentFile=/etc/myapp/myapp.env# 写绝对路径;只写程序名时只在 /usr/local/bin、/usr/bin 等系统目录里找,找不到虚拟环境里的ExecStart=/srv/myapp/current/.venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port 8000# 异常退出时自动拉起,等 5 秒再拉Restart=on-failureRestartSec=5# 进程最多能同时打开的文件数LimitNOFILE=65536
[Install]# enable 时挂到多用户启动目标下,开机自启WantedBy=multi-user.target- 装依赖,生成
/srv/myapp/current/.venv(uv 见第 14 章):cd /srv/myapp/current && uv sync --frozen。 - 建环境变量文件,内容照第 15 章 15.5 的
.env写:sudo mkdir -p /etc/myapp && sudo nano /etc/myapp/myapp.env。 - 只让 root 能读,systemd 以 root 身份读它再交给程序:
sudo chmod 600 /etc/myapp/myapp.env。 - 写入上面的单元文件:
sudo nano /etc/systemd/system/myapp.service。 - 检查写法,没输出就是通过:
sudo systemd-analyze verify /etc/systemd/system/myapp.service。 - 让 systemd 读到新文件:
sudo systemctl daemon-reload。 - 设开机自启并立即启动:
sudo systemctl enable --now myapp。 - 看到
active (running)和Main PID:systemctl status myapp。 - 确认程序在响应:
curl -I http://127.0.0.1:8000。 - 只在刚装好、还没接流量时试一次崩溃,把第 8 步的主进程杀掉,几秒后
Main PID换了新的:sudo kill -9 <Main PID>。
代码用 Git 拉到 /srv/myapp/current,迁移用 Alembic(第 20 章 20.4):
- 记下当前代码版本和迁移版本,回滚时要用:
cd /srv/myapp/current && git rev-parse --short HEAD && uv run alembic current。 - 拉新代码:
git -C /srv/myapp/current pull --ff-only。 - 按锁文件装依赖:
cd /srv/myapp/current && uv sync --frozen,Node 项目用npm ci。 - 先跑迁移再重启,日志里有
Running upgrade:uv run alembic upgrade head。 - 重启服务:
sudo systemctl restart myapp。 - 确认
active (running),Active:后的启动时间是刚才:systemctl status myapp。 - 看启动日志有没有报错:
journalctl -u myapp --since "5 min ago"。 - 出问题回滚:新版本带了迁移就先退到第 1 步记下的迁移版本
uv run alembic downgrade <迁移版本>,再退回代码:git -C /srv/myapp/current reset --hard <版本号>。 - 回滚后装回旧依赖并重启:
uv sync --frozen && sudo systemctl restart myapp。
用 pm2 管 Node 程序并开机自启
Section titled “用 pm2 管 Node 程序并开机自启”- 装 pm2:
npm install -g pm2。 - 在项目目录里启动,读同目录的
.env(Node 20.6 起):pm2 start server.js --name web --node-args="--env-file=.env"。 - 看
status是online:pm2 list。 - 看输出和报错:
pm2 logs web。 - 生成开机启动服务,按提示复制它打印的
sudo env PATH=…那一行执行:pm2 startup。 - 记下当前程序列表,开机时按它恢复:
pm2 save。 - 发版拉完代码后重启:
pm2 restart web。 - 确认 pm2 的服务开机自启:
systemctl status pm2-deploy。
30.5 配置要点
Section titled “30.5 配置要点”| 配置项 | 位置 |
|---|---|
| 自己写的单元文件 | /etc/systemd/system/<服务>.service |
| 软件包装的单元文件 | /usr/lib/systemd/system/,如 cron.service;不要直接改,升级会被覆盖 |
| 覆盖包里的设置 | sudo systemctl edit <服务> 生成 /etc/systemd/system/<服务>.service.d/override.conf,只写要改的几行 |
| 整份复制再改 | sudo systemctl edit --full <服务> 把整份复制到 /etc/systemd/system/<服务>.service |
| 开机自启的链接 | /etc/systemd/system/multi-user.target.wants/,由 enable 建、disable 删 |
| journald 设置 | /etc/systemd/journald.conf |
systemctl edit 存盘后自动重读,自己手改文件后要 daemon-reload。systemctl cat <服务> 显示单元文件连同所有覆盖文件,最终生效的是哪几行一眼看清。
[Service] 段里值得设的项:
| 配置项 | 建议值 | 效果 |
|---|---|---|
Restart |
on-failure |
崩溃自动拉起;默认 no |
RestartSec |
5 |
拉起前等 5 秒;默认 100 毫秒,依赖的数据库还没好时会连续失败,几下就被限流 |
User |
deploy |
不写就是 root 运行,程序被攻破等于整台服务器被拿下 |
EnvironmentFile |
/etc/myapp/myapp.env |
每行 名字=值;路径前加 - 表示文件不存在也照常启动 |
LimitNOFILE |
65536 |
同时打开的文件和连接数上限;连接多的服务报 Too many open files 时调大 |
最小单元文件,跑一个不需要环境变量的脚本。单元文件只认整行的 # 注释,不能写在值的后面:
[Unit]# 说明文字Description=hello script
[Service]# 要运行的命令,绝对路径ExecStart=/usr/bin/python3 /srv/hello/hello.py# 异常退出就拉起Restart=on-failure
[Install]# 开机进入多用户模式时启动WantedBy=multi-user.target30.6 注意事项
Section titled “30.6 注意事项”- 改了单元文件后提示
… changed on disk. Run 'systemctl daemon-reload' to reload units.原因:systemd 手里还是旧版本,restart照旧配置跑。处理:sudo systemctl daemon-reload,再sudo systemctl restart myapp。 - 服务重启服务器后没起来,或者
enable了却到现在都没在跑。 原因:start只管这一次,enable只管下次开机,两者互不代替。处理:sudo systemctl enable --now myapp,再用systemctl is-enabled myapp和is-active各查一次。 - 终端里手动运行正常,做成服务后报
KeyError: 'DATABASE_URL'或连到了默认端口。 原因:systemd 不读.bashrc、.profile,也不读项目里的.env。处理:在[Service]里写EnvironmentFile=/etc/myapp/myapp.env,或让程序自己读.env(第 15 章)。 - 服务停在
failed,日志里有Start request repeated too quickly.。 原因:启动就崩,Restart=always100 毫秒就拉一次,10 秒内超过 5 次被限流(StartLimitBurst,在[Unit]段调)。处理:修好后sudo systemctl reset-failed myapp再start。 df -h里/快满,journalctl --disk-usage显示占了好几个 GB。 原因:journald 默认可占分区的 10%,最多 4 GB。处理:sudo journalctl --vacuum-size=500M;长期在journald.conf里写SystemMaxUse=500M,再重启systemd-journald。
30.7 深入阅读
Section titled “30.7 深入阅读”- systemd.service 手册页:[Service] 段每个配置项的含义和默认值:Type、ExecStart 的路径规则、Restart 各取值在什么情况下拉起、RestartSec。
- systemctl 手册页:start、enable、daemon-reload、edit、reset-failed、list-units 等全部子命令,以及 edit 生成的覆盖文件放在哪里。
- journalctl 手册页:按单元、时间、级别过滤日志,-f 跟踪,–disk-usage 和 –vacuum-size 清理旧日志。
- PM2 快速开始:安装 pm2、启动和管理 Node 程序、看日志、cluster 模式的基本用法。
- PM2 开机启动脚本:pm2 startup 如何检测 systemd 并生成 pm2-用户名 服务,pm2 save 保存程序列表,pm2 unstartup 撤销。