systemd 服务管理:从开机自启到故障自愈的实战笔记
前言
刚接触 Linux 服务器时,很多人启动一个后台程序就用 nohup ... &,断开终端就靠运气。程序一崩,没人拉它起来;机器一重启,服务全靠手动补。systemd 就是解决这类问题的标准答案:它是绝大多数现代 Linux 发行版的初始化系统兼服务管理器,管开机、管进程、管日志、管依赖。本文用一次真实的部署经历,把 systemd 服务管理最常用的部分讲清楚。
一、一个最小可用的 service 文件
假设我们写了一个 Python 写的 Web 服务,启动命令是 python3 /opt/myapp/app.py,希望它开机自启、崩溃自动重启。先写一个 unit 文件:
[Unit]
Description=My Web App
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
把文件保存为 /etc/systemd/system/myapp.service,然后执行:
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
三条命令分工明确:daemon-reload 让 systemd 重新读取 unit 文件;enable 建立开机自启的软链接;start 立即启动。enable 只是管开机,不管现在;start 只管现在,不管开机——两个动作经常被混淆。
二、[Unit]:描述服务和它的依赖
[Unit] 段回答两个问题:这是什么服务,它依赖谁。
Description:服务的文字说明,systemctl status里会显示。After=network.target:表示在本服务启动之前,先启动网络。注意After只决定顺序,不决定依赖关系;如果还需要“网络没起来就不启动我”,要再加Requires=network.target或Wants=network.target。Requires是强依赖,依赖失败则本服务也失败;Wants是弱依赖,依赖失败不影响本服务。- 常见的依赖目标还有
multi-user.target(多用户命令行模式)、local-fs.target(本地文件系统就绪)。写 Web 服务时,After=network.target几乎是标配。
三、[Service]:进程怎么跑、崩了怎么办
这是最核心的一段,几个关键字段要理解透。
Type:服务是什么启动形态
simple(默认):ExecStart启动的进程就是主进程,systemd 认为命令执行即启动完成。适合前台运行的程序。forking:程序启动后会 fork 出子进程、父进程退出,适合传统 daemon(比如老式 Nginx 的启动方式)。需要配合PIDFile告诉 systemd 主进程的 PID 文件位置。oneshot:执行一次就退出的任务,比如初始化脚本。配合RemainAfterExit=yes可以让它在执行完后仍显示为 active 状态。notify:程序启动完成后会主动通知 systemd(通过 sd_notify),适合需要较长初始化时间的服务。
新手最常见的坑,就是把一个会 fork 的程序写成 simple,或者把前台程序写成 forking,结果 systemctl status 显示启动失败,日志里却一切正常。先确认程序的运行形态,再选 Type。
Restart:崩溃自愈
Restart=on-failure 表示进程非正常退出时自动重启;Restart=always 表示无论什么原因退出都重启;RestartSec=5 表示等 5 秒再重启,避免瞬间反复拉起把 CPU 打满。还有一个容易被忽略的 StartLimitBurst 和 StartLimitIntervalSec,它们限制单位时间内的重启次数,防止程序一启动就崩、崩了又启动的死循环。
运行身份与环境
User/Group:指定服务以哪个用户身份运行。不要用 root 跑应用服务,新建一个专用用户是基本安全习惯。WorkingDirectory:进程的工作目录,相对路径、日志文件、配置文件都以它为基准。Environment:设置环境变量,例如Environment=APP_ENV=production。需要多个变量时可以写多行,或者用EnvironmentFile=/opt/myapp/.env从文件加载。ExecStartPre/ExecStartPost:主命令执行前后的钩子,比如启动前检查配置文件、启动后发一条通知。
四、日常运维:五个高频命令
systemctl status myapp # 看状态、看最近日志、看是否开机自启
systemctl start myapp # 启动
systemctl stop myapp # 停止
systemctl restart myapp # 重启
systemctl reload myapp # 重载配置(仅当服务支持 reload)
status 的输出值得细读:Active: active (running) 是正常运行;Loaded: enabled 说明已设置开机自启;下面几行是最近的日志,排障时先看这里,再决定要不要去翻完整日志。
查看完整日志用 journalctl:
journalctl -u myapp -f # 实时跟随本服务的日志
journalctl -u myapp --since "1 hour ago" # 最近一小时的日志
journalctl -u myapp -p err # 只看错误级别以上的日志
systemd 的日志由 journald 统一收集,不用再到处找日志文件,这是它比 nohup 方式方便得多的地方。
五、排障清单:服务起不来的常见原因
- 改了 unit 文件没 daemon-reload:systemd 缓存了旧配置,改完必须先
daemon-reload再restart。 - ExecStart 路径写错:必须写绝对路径,
python3这种相对命令要写成/usr/bin/python3。可以用which python3确认。 - 权限问题:
User指定的用户读不到代码目录或写不了日志目录,先用ls -l检查权限。 - 端口被占用:
ss -tlnp看看端口谁在用,旧进程没杀干净是常见原因。 - 依赖服务没起来:
After写错目标,或者数据库服务本身启动失败,先查依赖服务的状态。
六、一个真实的教训
有一次我把一个服务的 Type 写成 simple,但程序内部会 fork 两次才进入主循环。systemctl start 总是报超时失败,日志里程序其实跑得好好的。改成 Type=forking 并加上 PIDFile 后,一切正常。教训是:unit 文件要按程序的真实行为写,而不是按想象写。不确定时,先用 ps -ef 看看进程树,或者直接问程序的文档。
结语
systemd 的学习曲线并不陡:先掌握一个最小 service 文件,再理解 [Unit]、[Service]、[Install] 三段的分工,最后把 status、journalctl 变成肌肉记忆,就能覆盖日常 90% 的服务管理场景。把服务交给 systemd,就是把“程序崩了谁来拉起来”这个问题,从人肉值守变成系统兜底。
- 点赞
- 收藏
- 关注作者
评论(0)