systemd 服务管理实战:从 systemctl 到编写自己的 service 单元

作者:

配好一台 Linux 服务器,装完软件只是第一步 —— 真正决定服务器长期健康运行的,是把服务管好:开机自启、异常自动拉起、日志可查、配置可改不重启。在主流发行版(Ubuntu、Debian、RHEL、Arch)上,这套管理工作几乎都由 systemd 接管。它既是 init 进程,又是服务管家,还是日志中心。本文从日常操作讲到手写 service 单元,全是能直接上手的实战内容。

systemd 是什么:三个核心概念

systemd 是如今 Linux 世界的标准初始化系统(PID 1),它把系统的每一个”进程单元”抽象成 unit(单元)。先记住这三个词,后面全都会用到:

  • service:服务单元,后台常驻进程,如 nginx.servicesshd.service
  • target:目标的组合,相当于传统 SysV 的”运行级别”,如 multi-user.target(命令行)和 graphical.target(图形界面)
  • journald:systemd 自带的日志守护进程,用 journalctl 查看,统一收纳所有服务的标准输出和系统日志

你不需要知道每个发行版用哪个 systemd 版本也能照常使用本文所有命令。当前主流:Ubuntu 24.04 LTS 用 systemd 255,Debian 12 用 systemd 252,Ubuntu 22.04 用 249,命令语法完全兼容。

日常管理:systemctl 一页速查

管理服务就是 systemctl + 动作 + 服务名,.service 后缀可以省略:

systemctl start nginx        # 启动
systemctl stop nginx         # 停止
systemctl restart nginx      # 重启
systemctl reload nginx       # 重载配置(不中断进程,前提是服务支持)
systemctl status nginx       # 状态 + 最近日志,排查问题第一步
systemctl enable nginx       # 开机自启
systemctl disable nginx      # 取消开机自启
systemctl enable --now nginx # 一键"开机自启 + 立即启动",最常用

检查状态的三种姿势

systemctl is-active nginx    # active / inactive / failed —— 适合脚本判断
systemctl is-enabled nginx   # enabled / disabled
systemctl is-failed nginx    # active / failed

批量查看

systemctl list-units --type=service --state=running   # 所有运行中的服务
systemctl list-units --type=service --state=failed    # 挂掉的服务
systemctl --failed                                    # 全部失败的 unit,开机后必查
systemctl list-timers --all                           # 定时器及其下次触发时间
systemctl list-unit-files --type=service              # 所有已安装的 unit 文件

启动调优与系统控制

systemd-analyze              # 开机总耗时
systemd-analyze blame        # 每个服务分别耗时多少,找开机慢的元凶
systemd-analyze critical-chain  # 启动关键路径,看卡在哪一环

systemctl reboot             # 重启系统
systemctl poweroff           # 关机
systemctl get-default        # 当前默认 target(multi-user 或 graphical)
systemctl set-default multi-user.target   # 命令行模式

避坑enable 只设置自启不立即启动,start 只启动不自启。生产环境部署新服务,请永远使用 enable --now,别漏一半。

排查故障:从 status 到 journalctl

服务起不来是运维日常。标准的排查链是:

systemctl status myapp          # 第一眼:状态、PID、退出的 exit code
journalctl -u myapp -xeu myapp  # 日志全文:-x 带解释,-e 翻到末尾,-u 指定服务

常见 exit code 含义:

退出码 含义 常见原因
203 Executable 不存在 ExecStart 路径写错或没权限执行
200 加载 unit 失败 文件语法错误,先 daemon-reload
216 用户/组不存在 User=/Group= 指向了不存在的账户

修改了 unit 文件后必须执行 systemctl daemon-reload,否则 systemd 用的还是旧配置 —— 这是新手最常踩的坑。

systemctl daemon-reload
systemctl reset-failed myapp   # 清除 failed 状态,反复重启失败的会触发 rate limit

实战:编写自己的 service 单元

业务脚本、爬虫、API 服务,不要再用 nohup ... & 裸跑 —— 退出后没人帮你拉起来,开机也不自启。手写一个 service 单元只需三步。

一个完整的示例

假设你有一个 Python 应用 /opt/myapp/app.py,创建 /etc/systemd/system/myapp.service

[Unit]
Description=My Python Application
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/env.conf
ExecStart=/usr/bin/python3 /opt/myapp/app.py
Restart=on-failure
RestartSec=3
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

然后一行启用:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp

关键字段逐条讲

[Unit]

  • Description:给人类看的说明,systemctl status 时会显示
  • After=:声明依赖顺序,等网络就绪再启动,防止启动即报”连不上网”

[Service] —— 核心:

  • Type=simple:默认值,ExecStart 起的进程就是主进程,多数场景选它
  • Type=forking:进程启动后会 fork 出子进程并自己退出(老派守护进程,如某些 Java 应用),需要配合 PIDFile=
  • User= / Group=永远用专用用户跑服务,不要用 root
  • Restart=on-failure:崩溃/非正常退出时自动拉起;线上服务建议配合 RestartSec=3 防抖。always 连正常退出也会拉起,慎用
  • EnvironmentFile=:把敏感配置(密码、token)放独立文件,权限收紧到 600,改配置不用动 unit
  • LimitNOFILE=:打开文件数上限,高并发服务必须调大,默认 1024 往往不够

[Install]:只有写了 WantedBy= 的 unit 才能被 enable,开机自启就是把自己挂到对应 target 下。

改配置不重启:drop-in 覆盖

官方推荐用 systemctl edit 加覆盖片段,不改动系统原文件,系统升级后配置也不会被冲掉:

sudo systemctl edit myapp
# 在弹出的编辑器中写入:
[Service]
RestartSec=5

保存后自动 daemon-reload,再 systemctl restart myapp 生效。查看合并后的最终配置:

systemctl cat myapp    # 显示原文件 + 所有 drop-in 合并结果

统一日志:journalctl 实战

journald 把服务 stdout/stderr 和系统日志收进同一套日志,再也不用 cd /var/log 翻文件。

常用过滤

journalctl -u myapp              # 只看某个服务的日志
journalctl -u myapp -f           # 实时跟踪,等价于 tail -f,排查首选
journalctl -n 50                 # 最近 50 条
journalctl -k                    # 内核日志
journalctl -b                    # 本次开机以来的日志
journalctl -b -1                 # 上次开机(需持久化,见下文)
journalctl --list-boots          # 列出所有开机记录
journalctl -p err                # 只看 err 及以上级别
journalctl --since "1 hour ago"  # 时间过滤:today / "30min ago" / 具体时间点
journalctl -u myapp --since today --until "10 minutes ago"   # 组合过滤
journalctl --grep="OutOfMemory"  # 按正则搜索内容

日志持久化:重启后日志不丢

默认日志存在 /run/log/journal(内存盘),重启即清空。想要跨重启保留,只需创建持久化目录,systemd 检测到后自动切换:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

磁盘占用控制

日志跑久了会占磁盘,定期清理是基本功:

journalctl --disk-usage               # 看占了多少
sudo journalctl --vacuum-size=500M    # 清到 500MB 以下
sudo journalctl --vacuum-time=30d     # 只保留最近 30 天

更进一步:在 /etc/systemd/journald.conf.d/ 下建 00-storage.conf[Journal] + Storage=persistent / SystemMaxUse=200M,一次性限制日志上限,比反复手工清理省心。

进阶:用 systemd timer 替代 cron

cron 的痛点是没有统一日志、错过时间不补跑。timer 在 systemd 生态里日志、依赖、失败追踪全都有。示例:每天 3 点跑备份脚本。

创建 /etc/systemd/system/backup.service

[Unit]
Description=Nightly backup job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

创建 /etc/systemd/system/backup.timer

[Unit]
Description=Run backup daily at 3am

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

启用方式与 service 相同:

sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer   # 查看下次触发时间
journalctl -u backup.service         # 查看每次执行日志

Persistent=true 是杀手锏:机器当时关机没执行,下次开机自动补跑 —— 这是 cron 做不到的。OnCalendar= 语法与 cron 类似但更灵活,如 Mon..Fri *-*-* 09:00:00 表示工作日早上 9 点。

实战总结

  • 部署服务:写 unit → daemon-reloadenable --nowsystemctl status 确认
  • 排查问题:systemctl status 看退出码 → journalctl -xeu 看日志
  • 服务异常:Restart=on-failure 自动拉起,别再用 nohup 裸跑
  • 改配置:systemctl edit 加 drop-in,别直接动 /usr/lib/systemd/
  • 开机必查:systemctl --failed,把红灯消灭在平时

从”能跑”到”跑得稳”,systemd 就是那道分水岭。

参考资料

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注