systemd 实战:把服务托管做对

举报
yd_213781742 发表于 2026/11/03 09:19:24 2026/11/03
【摘要】 用 nohup 起的服务一重启就丢?本文从 systemd 的进程归属、状态机与重启策略讲起,手把手把一个脚本写成 unit:Type 怎么选、日志如何进 journal、资源限额与沙箱怎么加、timer 怎么替代 cron,并列出十条最常见的坑和上线前的六步检查。

一、先说现象:那些"手动起的"服务

凌晨两点告警响了,登录机器一看,业务进程没了。翻历史命令,是上个月同事用 nohup ./app & 起的;日志在 nohup.out 里,早被别的输出冲得看不出上下文;机器一重启,服务再也不会自己回来。交接时新人只知道"要手动起",却不知道它依赖哪些目录和环境变量。

根因往往不是程序写得差,而是进程没有被可靠地托管。主流发行版早就不用 SysV init 了,PID 1 是 systemd,但很多人只把它当成 systemctl start/stop 的语法糖,重启策略、日志归集、资源限额、依赖顺序这些真正值钱的能力一个都没用上。下面按"现象 → 原理 → 操作 → 坑"的顺序,把裸进程迁到 systemd 托管。

二、原理:systemd 到底替你做了什么

进程归属:每个 service 独立 cgroup,主进程退出后按 KillMode 清理整个进程组,不留孤儿进程占端口,systemctl stop 因此比 kill 干净。

状态机:inactive → activating → active → deactivating → failed,systemctl status 显示的是这个状态,而不是简单的"进程在不在"。

重启策略:Restart= 决定什么情况下拉起,RestartSec= 决定隔多久,StartLimitBurst 决定连续失败几次后放弃,避免无限重启耗 CPU。

日志:stdout/stderr 默认进 journal,journalctl -u 按 unit 过滤,自带时间戳和优先级,不用自己重定向再配轮转。

依赖与顺序:Wants=/Requires= 决定要不要一起启动,After=/Before= 决定谁先谁后;两者正交,只写 After 不会拉起对方。

沙箱与限额:User=、ProtectSystem=、MemoryMax=、CPUQuota= 让普通服务获得接近容器的隔离度,不用引入容器运行时。

Unit 搜索路径优先级是 /etc/systemd/system > /run/systemd/system > /usr/lib/systemd/system,永远改 /etc 下那份。

三、Type 选错,后面全错

Type= 决定 systemd 如何判断"启动成功"。

  • simple:ExecStart 一 fork 就算成功,默认值,适合前台进程。
  • exec:等到 execve() 成功才算启动,能更早发现"命令不存在"。
  • forking:父进程退出、子进程继续,传统守护进程写法,必须配 PIDFile=。
  • notify:程序主动 sd_notify(READY=1) 才算就绪,适合启动慢、需要预热的应用。
  • oneshot:跑完就退出,适合初始化脚本,常配 RemainAfterExit=yes。

判断很简单:一直占着前台用 simple,自己 daemonize 了用 forking,支持 sd_notify 就优先 notify,最准确。

四、动手:把一个脚本变成服务

4.1 准备一个会说话的示例程序

sudo install -d -m 0755 /opt/demo
sudo tee /opt/demo/app.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
trap 'echo "[demo] bye"; exit 0' TERM INT
while true; do echo "[demo] tick $(date -Is)"; sleep 10; done
EOF
sudo chmod 0755 /opt/demo/app.sh

4.2 写 unit 文件

sudo tee /etc/systemd/system/demo.service >/dev/null <<'EOF'
[Unit]
Description=Demo background worker
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=demo
Group=demo
WorkingDirectory=/opt/demo
Environment="APP_ENV=prod" "TZ=Asia/Shanghai"
EnvironmentFile=-/etc/demo/app.env
ExecStart=/opt/demo/app.sh
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=3s
TimeoutStopSec=20s
KillMode=mixed
StandardOutput=journal
StandardError=journal
SyslogIdentifier=demo

[Install]
WantedBy=multi-user.target
EOF

细节:EnvironmentFile 前的 - 表示文件不存在也不报错;ExecStart 必须是绝对路径;WantedBy 决定 enable 时软链接建到哪个 target 下。

4.3 建运行用户和目录

sudo useradd --system --no-create-home --shell /usr/sbin/nologin demo
sudo install -d -o demo -g demo -m 0750 /var/lib/demo /var/log/demo
sudo install -d -m 0755 /etc/demo
printf 'APP_PORT=8080\n' | sudo tee /etc/demo/app.env
sudo chmod 0640 /etc/demo/app.env

用专用系统用户跑服务是成本最低的安全加固,nologin 让账号泄露后也登不进来。

4.4 加载、启动、看状态

sudo systemctl daemon-reload
sudo systemctl enable --now demo.service
systemctl status demo.service --no-pager -l

enable --now 一步完成"开机自启 + 立即启动"。

4.5 验证自愈能力

systemctl show demo.service -p MainPID -p NRestarts -p ActiveState
sudo kill -9 "$(systemctl show -p MainPID --value demo.service)"
sleep 6
systemctl status demo.service --no-pager | head -6

kill -9 之后如果 NRestarts 加一、状态回到 active,说明重启策略真的生效了。这比看配置文件可靠得多。

4.6 日志怎么查

journalctl -u demo.service -n 50 --no-pager
journalctl -u demo.service -f
journalctl -u demo.service --since "1 hour ago" -p warning
journalctl -u demo.service -o json-pretty -n 1
journalctl --disk-usage

-p warning 只看警告以上;-o json-pretty 能看到 _PID、_SYSTEMD_UNIT 这些结构化字段,写告警脚本时很有用。

4.7 加资源上限和沙箱

sudo systemctl edit demo.service

在打开的覆盖文件里填:

[Service]
MemoryMax=512M
CPUQuota=50%
LimitNOFILE=65535
TasksMax=256
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/demo /var/log/demo
sudo systemctl daemon-reload
sudo systemctl restart demo.service
systemd-cgtop -1 --no-pager | head -5
systemctl show demo.service -p MemoryMax -p CPUQuotaPerSecUSec

systemctl edit 生成的是 drop-in 覆盖文件,放在 /etc/systemd/system/demo.service.d/ 下,比直接改原文件更好维护。ProtectSystem=strict 会把整个文件系统挂成只读,只留 ReadWritePaths 可写——这是最常导致"服务起不来又没有明显报错"的一条。

4.8 依赖和顺序

systemctl list-dependencies --reverse demo.service
systemd-analyze verify /etc/systemd/system/demo.service
systemd-analyze critical-chain demo.service

verify 能在上线前发现 ExecStart 路径不存在、unit 名拼错、指令拼写错误;critical-chain 展示启动链上每一步花了多久。

4.9 用 timer 替代 cron

sudo tee /etc/systemd/system/demo-report.service >/dev/null <<'EOF'
[Unit]
Description=Demo report job
[Service]
Type=oneshot
User=demo
ExecStart=/opt/demo/report.sh
EOF

sudo tee /etc/systemd/system/demo-report.timer >/dev/null <<'EOF'
[Unit]
Description=Demo report every 10 min
[Timer]
OnCalendar=*:0/10
Persistent=true
RandomizedDelaySec=30
Unit=demo-report.service
[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now demo-report.timer
systemctl list-timers demo-report.timer --no-pager

Persistent=true 补跑关机期间错过的任务,RandomizedDelaySec 打散触发时间,避免几十台机器同时打后端。

4.10 排障四板斧

systemctl --failed --no-pager
systemctl cat demo.service
systemctl show demo.service -p ExecMainStatus -p ExecMainCode -p Result
journalctl -u demo.service -b -p err --no-pager

ExecMainStatus 是退出码,Result 是 systemd 的结论(exit-code、signal、timeout),这两个字段能定位八成启动失败。

五、常见坑,一条条对

改完 unit 不 daemon-reload。 文件改了但 systemd 用的是内存里的旧配置,表现为"怎么改都没效果"。任何 /etc/systemd/system 下的改动都要跟一次 daemon-reload。

Type=forking 不写 PIDFile。 systemd 找不到主进程,状态会卡在 activating,最后超时失败。

ExecStart 用相对路径或依赖 shell。 systemd 不经过 shell,ExecStart=./app、ExecStart=app > log 2>&1 都不成立。重定向要写成 StandardOutput=,管道要显式 /bin/sh -c。

环境变量不继承。 shell 里 export 的变量服务里一个都没有,PATH 也只有很窄的默认值,得写 Environment= 或 EnvironmentFile=。

Restart=always 配一个必然失败的命令。 结果是无限重启,journalctl 被刷屏,CPU 也被吃掉。加上 StartLimitIntervalSec=60 和 StartLimitBurst=5,失败到次数就停下来。

User= 之后写不了日志。 服务以普通用户跑,/var/log 下没有权限,程序静默失败。提前 install -d -o demo -g demo 建好目录。

服务里再 fork 出后台进程。 systemd 认为主进程退出,cgroup 被清理,子进程一起没了。要么前台运行,要么 Type=forking 加 PIDFile。

六、小结

一份能上生产的 unit 至少要回答六个问题:用什么身份跑(User)、怎么算启动成功(Type)、失败怎么办(Restart 与限流)、日志去哪(journal)、能占多少资源(MemoryMax/CPUQuota)、什么时候自启(WantedBy)。

上线前的检查顺序可以固定成:systemd-analyze verify → daemon-reload → enable --now → kill -9 验证自愈 → reboot 验证开机自启 → journalctl -u 确认日志可读。这六步走完,才算真正"托管"住,而不是换了个地方手动起进程。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。