暗色模式

systemd 服务管理实战:从 service 单元到 timer 定时与 journalctl 日志排查

技术教程
2026-08-01
157
0
本文要点
  • 部署新服务固定流程:写 unit → daemon-reloadenable --nowsystemctl status 确认;只 enable 不自启、只 start 不自启
  • 单元文件放 /etc/systemd/system 可覆盖软件包自带,改完必须先 systemctl daemon-reload 才生效
  • Type 选型决定服务是否被误判为已启动(simple/forking/oneshot/notify),Restart=on-failurealways 更稳
  • 服务起不来先 systemctl status 看退出码(203=路径错、200=语法错、216=用户不存在),再 journalctl -u 服务名 -xeu 查日志
  • 定时任务用 timer 替代 cron:oneshot 服务 + .timer 配对,OnCalendar 定时、Persistent=true 补跑错过的任务、RandomizedDelaySec 错峰
  • OnCalendar 写错不报错只会不触发,务必用 systemd-analyze calendar '表达式' 验证
  • 日志持久化只需 mkdir -p /var/log/journal 并重启 journald;用 SystemMaxUse/--vacuum-size 控制磁盘占用
  • 改配置用 systemctl edit 加 drop-in 片段,不直接改 /usr/lib/systemd/,系统升级后不会被冲掉

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,命令语法完全兼容。

单元类型:一切皆单元

systemd 把系统上需要管理的东西抽象为「单元」(Unit),每种单元管理一类对象:

单元类型扩展名作用
service.service后台服务/守护进程
timer.timer定时器(替代 cron)
socket.socket套接字监听(可配合服务按需启动)
mount.mount挂载点
target.target单元组,系统状态(如 multi-user.target
slice.slice资源分组(配合 cgroup v2)
v258 起 systemd 只支持 cgroup v2,旧的 System V runlevel 支持已被彻底移除,SysV 脚本也将在 v259 被移除。新写服务请一律使用原生单元文件。

日常管理: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 mask nginx         # 彻底禁用(连手动启动都不行)
systemctl unmask 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 文件
systemctl list-dependencies multi-user.target         # 查看启动依赖树

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,别漏一半。

先看看系统里当前都在跑哪些服务(systemctl list-units 的输出):

systemctl 查看运行中的服务

实战:编写自己的 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=:声明依赖顺序,等网络就绪再启动,防止启动即报"连不上网"。注意 After 只排顺序,Requires/Wants 才建立依赖关系——Wants 是弱依赖(network 失败不影响本服务)

[Service] —— 核心:

指令含义常用值
Type启动类型simple(默认)、forkingoneshotnotify
User= / Group=以哪个用户运行不要用 root 跑业务服务
ExecStart=启动命令必须写绝对路径
Restart=崩溃后重启策略noalwayson-failureon-abnormal
RestartSec=重启间隔秒数防止崩溃后疯狂重启
Environment= / EnvironmentFile=注入环境变量配置文件可用 - 前缀容忍缺失

Type 的选型直接决定服务是否被误判为「已启动」:

  • simple:ExecStart 一执行就认为服务已启动,适合前台运行的程序(Node、Python 脚本等);
  • forking:程序自己 fork 到后台(老式守护进程,如旧版 nginx),需要配合 PIDFile= 指定 pid 文件;
  • oneshot:一次性任务,执行完即退出(适合初始化脚本和定时任务),常配合 RemainAfterExit=yes 让状态保持 active;
  • notify:程序启动成功后通过 sd_notify 主动通知 systemd,需程序支持。

Restart= 建议统一使用 on-failure(进程异常退出才重启),避免 always 让手动 stop 后又被拉起。线上服务建议配合 RestartSec=3 防抖。

资源限制与安全加固

一条龙限制 CPU、内存和文件句柄,防止服务失控拖垮整机:

[Service]
MemoryMax=512M          # 内存硬上限,超出即 OOM kill
MemoryHigh=384M         # 软上限,超过会持续回收
CPUQuota=50%            # CPU 上限为 1 核的 50%
TasksMax=200            # 最大进程/线程数
LimitNOFILE=65535       # 文件句柄上限

sandbox 类指令按需开启,让服务只访问它该访问的东西:

[Service]
ProtectSystem=strict        # 整个文件系统只读
ProtectHome=true            # 隐藏 /home
PrivateTmp=true             # 独立临时目录
ReadWritePaths=/var/lib/myapp   # 只放行这几个可写路径
NoNewPrivileges=true        # 禁止提权
PrivateDevices=true         # 屏蔽设备节点(按需,部分程序需要 /dev)

这些指令大多默认关闭(兼容性考虑),对可信部署建议逐步打开——出问题先关掉再对比。

文件位置与改配置:drop-in 覆盖

/etc/systemd/system/      # 管理员自定义(优先级最高)
/usr/lib/systemd/system/  # 软件包自带(apt/dnf 安装的)

同名的单元文件,/etc/systemd/system 会覆盖 /usr/lib/systemd/system。改完文件记得 systemctl daemon-reload

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

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

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

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

改完怎么验证

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service --no-pager     # 看状态与最近日志
systemctl show myapp.service -P MainPID       # 查主进程 PID
systemctl cat myapp.service                   # 回显最终生效的配置
避坑:修改 unit 文件后必须执行 systemctl daemon-reload,否则 systemd 用的还是旧配置 —— 这是新手最常踩的坑。此时状态会提示 Warning: Unit file changed on disk, 'systemctl daemon-reload' recommended.

执行后的真实输出(enable --now 一步完成自启+启动,status 一眼确认状态、主进程与日志):

myapp 服务启动与状态验证

统一日志:journalctl 实战

journald 把服务 stdout/stderr 和系统日志收进同一套日志,再也不用 cd /var/log 翻文件。例如只看 sshd 服务最近 8 条日志:

journalctl 查看 sshd 服务日志

常用过滤

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"  # 按正则搜索内容

优先级从高到低:emerg(0)、alert(1)、crit(2)、err(3)、warning(4)、notice(5)、info(6)、debug(7)。-p err 表示显示 err 及以上。

排查服务崩溃的标准流程

# 1. 找出失败的服务
systemctl list-units --failed --no-pager

# 2. 查看该服务的错误日志
journalctl -u myapp.service -p err --no-pager

# 3. 如果重启循环,看最近 20 条完整输出
journalctl -u myapp.service -n 20 --no-pager

# 4. 结合本次运行 ID 精确查看某一次运行的全部输出
systemctl show -P InvocationID myapp.service
journalctl _SYSTEMD_INVOCATION_ID=$(systemctl show -P InvocationID myapp.service)

# 5. 崩溃通常是信号 11 (Segmentation fault),全局搜一下
journalctl -g "Segmentation fault|signal 11|core dump" --since "24 hours ago"

常见 exit code 含义:

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

日志持久化与磁盘占用控制

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

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

编辑 /etc/systemd/journald.conf[Journal] 段:

[Journal]
Storage=persistent        # 持久化到 /var/log/journal
SystemMaxUse=2G           # journal 总占用上限 2G
SystemMaxFileSize=128M    # 单个日志文件大小
MaxRetentionSec=6month    # 保留 6 个月

手动清理(对应 --vacuum-time--vacuum-size--vacuum-files 三种策略):

journalctl --disk-usage               # 看占了多少
sudo journalctl --vacuum-size=500M    # 清到 500MB 以下
sudo journalctl --vacuum-time=30d     # 只保留最近 30 天
排查阶段需要完整堆栈信息时,可临时给 journalctl--all 参数,避免长字段被截断。

进阶:用 systemd timer 替代 cron

cron 的痛点是没有统一日志、错过时间不补跑、无依赖管理。timer 在 systemd 生态里日志、依赖、失败追踪全都有。

为什么要换:crontab 的三个痛点

  1. 日志割裂:脚本输出要么丢弃,要么手动重定向到文件,排查一次任务为什么没跑要翻好几个地方;
  2. 错过不补:服务器关机/睡眠时错过的任务直接跳过,不补跑;
  3. 无依赖管理:任务要等网络就绪、等另一个服务起来?crontab 表达不了,只能靠脚本里 sleep 硬扛。
对比项crontabsystemd timer
最小粒度1 分钟秒级(可以精确到秒)
日志手动重定向或丢弃自动进 journald,journalctl -u 查看
错过任务跳过Persistent=true 可补跑一次
依赖排序不支持After= / Wants= 与 systemd 生态互通
随机延迟(避免整点并发)不支持RandomizedDelaySec=
环境变量继承登录 shell 的 PATH 等不继承,需在单元文件里显式声明
开机自启@rebootWantedBy=timers.target + enable

核心概念:一对单元文件

systemd timer 由两个同名的单元文件组成,systemd 按 basename 自动配对:

  • xxx.service —— 定义「做什么」,定时任务一般用 Type=oneshot(执行完即退出,不留驻);
  • xxx.timer —— 定义「什么时候做」,[Timer] 段里写 OnCalendar= 等触发条件,[Install] 段写 WantedBy=timers.target

启动时要 enable 的是 .timer 而不是 .service——enable 了 timer,它到点会自动拉起对应的 service。

实战:定时记录磁盘使用率

以常见的运维需求为例:每 10 分钟记录一次根分区磁盘使用率,追加到日志文件

第一步:编写记录脚本

cat > /usr/local/bin/disk-log.sh <<'SCRIPT'
#!/bin/bash
# 记录根分区磁盘使用情况, 追加到 /var/log/disk-usage.log
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 根分区已用 $(df -h / | awk 'NR==2 {print $5}'), 剩余 $(df -h / | awk 'NR==2 {print $4}')" >> /var/log/disk-usage.log
SCRIPT
chmod +x /usr/local/bin/disk-log.sh

注意脚本要加执行权限,并建议用绝对路径——定时任务的环境不像交互 shell 那么「顺手」。

第二步:编写 service 单元

# /etc/systemd/system/disk-log.service
[Unit]
Description=记录根分区磁盘使用率

[Service]
Type=oneshot
ExecStart=/usr/local/bin/disk-log.sh

第三步:编写 timer 单元

# /etc/systemd/system/disk-log.timer
[Unit]
Description=每 10 分钟记录一次磁盘使用率

[Timer]
OnCalendar=*:0/10
Persistent=true

[Install]
WantedBy=timers.target

OnCalendar=*:0/10 表示「每小时的第 0、10、20、30、40、50 分触发」,即每 10 分钟一次。Persistent=true 让系统错过触发(比如关机)后,下次开机补跑一次。

第四步:重载并启用

systemctl daemon-reload
systemctl enable --now disk-log.timer

下图是演示服务器上三个文件的实际内容与启用结果(enable 后 is-enabled 返回 enabled,同时 /var/lib/systemd/timers/ 下生成了 stamp-disk-log.timer 时间戳文件,供 Persistent 补跑判断使用):

单元文件的实际内容与定时器启用状态

第五步:验证时间表达式

OnCalendar 的语法比 crontab 复杂,写错不会报错,而是永远不触发(或触发时机和你想的不一样)。用 systemd-analyze calendar 验证:

systemd-analyze calendar '*:0/10'
systemd-analyze calendar 'Mon..Fri *-*-* 02:30:00'

它会输出规范化形式下次触发时间。注意简写坑:weekly 等价于 Mon *-*-* 00:00:00周一 0 点),不是「每 7 天」也不是「每周任意一天」。

第六步:查询状态

systemctl list-timers --all

启用后 list-timers 输出(可以看到刚创建的 disk-log.timer 已排入队列,下次触发是下一个整 10 分):

定时器列表与下次触发时间

NEXT 列显示下次触发时间,LAST 列显示上次触发时间,PASSED 列显示已过去多久。下图是启用约 10 分钟后的真实输出:NEXT 显示下次触发 08:30:00 还剩 9 分钟,LAST 显示上次已在 08:20:20 触发过,PASSED 显示已过去 38 秒。

注意计划触发时间和实际执行可能差几十秒——timer 默认有 AccuracySec=1min 的容忍窗口,方便 systemd 把多个唤醒合并省电。对日志类任务毫无影响,若要精确到秒级触发,可以设 AccuracySec=1s

systemd-analyze calendar 验证时间表达式与 list-timers 状态查询

第七步:手动触发与日志排查

systemctl start disk-log.service    # 手动触发一次,不影响 timer 的下次计划
journalctl -u disk-log.service --no-pager -n 5   # 执行结果进 journald

下图是真实执行记录:journald 里能看到自然触发和手动触发(Starting → Deactivated → Finished 三步),日志文件相应新增了两行「根分区已用 25%,剩余 28G」,而 list-timers 里 NEXT 依然是原计划,手动触发没有打乱定时:

手动触发一次后,journalctl 与磁盘日志文件中的执行记录

OnCalendar 语法速查

OnCalendar 的完整语法是 星期 年-月-日 时:分:秒 [时区],省略的字段有默认值。常用写法:

需求OnCalendar 写法
每天 3 点*-*-* 03:00:00daily
每小时整点hourly
每 15 分钟*:0/15
每 5 分钟(含 0 分)*:0/5
工作日 9 点Mon..Fri *-*-* 09:00:00
每周一 2 点Mon *-*-* 02:00:00
每月 1 号 3 点*-*-01 03:00:00
每季度第一天*-1,4,7,10-01 00:00:00
每天 6:00 和 18:00*-*-* 06,18:00:00(或写两行 OnCalendar)
指定时区(UTC)*-*-* 03:00:00 UTC

验证建议统一用 systemd-analyze calendar '<表达式>',每次写完都先验证再上线。

三个值得了解的选项

Persistent=true:错过补跑。默认情况下,系统在计划时间点处于关机/休眠状态,这次任务就静默跳过了。加上 Persistent=true 后,开机时发现上次触发时间早于计划时间,就立即补跑一次。注意:只补最近一次,不会把关机期间的多次全部补上。

RandomizedDelaySec:错峰执行。很多服务器都在同一分钟跑同一类任务(apt 更新、备份、日志轮转),会产生「整点风暴」。RandomizedDelaySec=15min 会在计划时间后随机延迟 0~15 分钟,打散执行:

[Timer]
OnCalendar=daily
RandomizedDelaySec=15min

Ubuntu 系统自带的 timer 几乎都显式配置了随机化:apt-daily-upgrade.timerRandomizedDelaySec=60mfstrim.timer100min。想完全禁用,显式写 RandomizedDelaySec=0

AccuracySec:触发精度。默认 AccuracySec=1min 允许前后 1 分钟内触发。桌面/笔记本上可以放宽到 10min 省电;需要准点执行的任务收紧到 1s

常见坑清单

  1. 忘了 systemctl daemon-reload:新增或修改单元文件后必须重载,否则 systemd 用的还是旧配置;
  2. timer 里忘了 WantedBy=timers.target:没有这行,enable 无法建立开机自启,重启后 timer 就没了;
  3. enable 了 service 而不是 timer:定时逻辑在 .timer 里,必须 enable .timer
  4. 脚本里用相对路径/依赖 PATHExecStart= 一律写绝对路径,需要环境变量用 Environment= 显式声明;
  5. 脚本输出找不到:stdout/stderr 进 journald,用 journalctl -u xxx.service 查。

什么时候继续用 crontab

crontab 没有过时,只是适用场景收窄了:单行简单任务、需要移植到非 systemd 环境的脚本、团队习惯使然。但对于「服务器上的正经运维任务」——有日志、有依赖、要补跑、要错峰——systemd timer 是更省心的默认选择。

调试期可以用 systemd-run 快速起一个临时 timer,不用写任何文件:

systemd-run --on-calendar='*:0/7' --unit=demo-run-test /bin/true
systemctl list-timers --no-pager | grep demo-run-test   # 临时 timer 生效
systemctl stop demo-run-test.timer                      # 用完即删

实战总结

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

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

参考资料

发表评论

暂无评论,快来抢沙发吧!