跳转到内容

第 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

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 --version

pm2 是 npm 包,装成全局命令,要先有 Node(第 14 章)。nvm 装的 Node 不用 sudo:

终端窗口
npm install -g pm2
pm2 --version
  • 单元(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 里看:

写单元文件 myapp.service,daemon-reload 让 systemd 重新读文件,enable –now 设开机自启并立即启动,进入 active (running);崩溃后等 RestartSec 按 Restart= 拉起,太频繁则进入 failed;运行中的输出进 journald,用 journalctl -u myapp 查看

改状态的命令要 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 记下当前程序列表

程序是一个用 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.target
Wants=network-online.target
[Service]
# 用普通用户运行,不用 root
User=deploy
Group=deploy
# 程序的工作目录,相对路径从这里算
WorkingDirectory=/srv/myapp/current
# 从文件读环境变量,格式同 .env(第 15 章 15.5);systemd 不读 .bashrc
EnvironmentFile=/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-failure
RestartSec=5
# 进程最多能同时打开的文件数
LimitNOFILE=65536
[Install]
# enable 时挂到多用户启动目标下,开机自启
WantedBy=multi-user.target
  1. 装依赖,生成 /srv/myapp/current/.venv(uv 见第 14 章):cd /srv/myapp/current && uv sync --frozen。
  2. 建环境变量文件,内容照第 15 章 15.5 的 .env 写:sudo mkdir -p /etc/myapp && sudo nano /etc/myapp/myapp.env。
  3. 只让 root 能读,systemd 以 root 身份读它再交给程序:sudo chmod 600 /etc/myapp/myapp.env。
  4. 写入上面的单元文件:sudo nano /etc/systemd/system/myapp.service。
  5. 检查写法,没输出就是通过:sudo systemd-analyze verify /etc/systemd/system/myapp.service。
  6. 让 systemd 读到新文件:sudo systemctl daemon-reload。
  7. 设开机自启并立即启动:sudo systemctl enable --now myapp。
  8. 看到 active (running) 和 Main PID:systemctl status myapp。
  9. 确认程序在响应:curl -I http://127.0.0.1:8000。
  10. 只在刚装好、还没接流量时试一次崩溃,把第 8 步的主进程杀掉,几秒后 Main PID 换了新的:sudo kill -9 <Main PID>。

代码用 Git 拉到 /srv/myapp/current,迁移用 Alembic(第 20 章 20.4):

  1. 记下当前代码版本和迁移版本,回滚时要用:cd /srv/myapp/current && git rev-parse --short HEAD && uv run alembic current。
  2. 拉新代码:git -C /srv/myapp/current pull --ff-only。
  3. 按锁文件装依赖:cd /srv/myapp/current && uv sync --frozen,Node 项目用 npm ci。
  4. 先跑迁移再重启,日志里有 Running upgrade:uv run alembic upgrade head。
  5. 重启服务:sudo systemctl restart myapp。
  6. 确认 active (running),Active: 后的启动时间是刚才:systemctl status myapp。
  7. 看启动日志有没有报错:journalctl -u myapp --since "5 min ago"。
  8. 出问题回滚:新版本带了迁移就先退到第 1 步记下的迁移版本 uv run alembic downgrade <迁移版本>,再退回代码:git -C /srv/myapp/current reset --hard <版本号>。
  9. 回滚后装回旧依赖并重启:uv sync --frozen && sudo systemctl restart myapp。
  1. 装 pm2:npm install -g pm2。
  2. 在项目目录里启动,读同目录的 .env(Node 20.6 起):pm2 start server.js --name web --node-args="--env-file=.env"。
  3. 看 status 是 online:pm2 list。
  4. 看输出和报错:pm2 logs web。
  5. 生成开机启动服务,按提示复制它打印的 sudo env PATH=… 那一行执行:pm2 startup。
  6. 记下当前程序列表,开机时按它恢复:pm2 save。
  7. 发版拉完代码后重启:pm2 restart web。
  8. 确认 pm2 的服务开机自启:systemctl status pm2-deploy。
配置项 位置
自己写的单元文件 /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.target
  • 改了单元文件后提示 … 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=always 100 毫秒就拉一次,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。
  • 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 撤销。