本文要点
- 部署新服务固定流程:写 unit →
daemon-reload→enable --now→systemctl status确认;只enable不自启、只start不自启 - 单元文件放
/etc/systemd/system可覆盖软件包自带,改完必须先systemctl daemon-reload才生效 Type选型决定服务是否被误判为已启动(simple/forking/oneshot/notify),Restart=on-failure比always更稳- 服务起不来先
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.service、sshd.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 的输出):

实战:编写自己的 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(默认)、forking、oneshot、notify |
User= / Group= | 以哪个用户运行 | 不要用 root 跑业务服务 |
ExecStart= | 启动命令 | 必须写绝对路径 |
Restart= | 崩溃后重启策略 | no、always、on-failure、on-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 一眼确认状态、主进程与日志):

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

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

第七步:手动触发与日志排查
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 依然是原计划,手动触发没有打乱定时:

OnCalendar 语法速查
OnCalendar 的完整语法是 星期 年-月-日 时:分:秒 [时区],省略的字段有默认值。常用写法:
| 需求 | OnCalendar 写法 |
|---|---|
| 每天 3 点 | *-*-* 03:00:00 或 daily |
| 每小时整点 | 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=15minUbuntu 系统自带的 timer 几乎都显式配置了随机化:apt-daily-upgrade.timer 是 RandomizedDelaySec=60m,fstrim.timer 是 100min。想完全禁用,显式写 RandomizedDelaySec=0。
AccuracySec:触发精度。默认 AccuracySec=1min 允许前后 1 分钟内触发。桌面/笔记本上可以放宽到 10min 省电;需要准点执行的任务收紧到 1s。
常见坑清单
- 忘了
systemctl daemon-reload:新增或修改单元文件后必须重载,否则 systemd 用的还是旧配置; - timer 里忘了
WantedBy=timers.target:没有这行,enable无法建立开机自启,重启后 timer 就没了; - enable 了 service 而不是 timer:定时逻辑在
.timer里,必须 enable.timer; - 脚本里用相对路径/依赖 PATH:
ExecStart=一律写绝对路径,需要环境变量用Environment=显式声明; - 脚本输出找不到: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-reload→enable --now→systemctl status确认 - 排查问题:
systemctl status看退出码 →journalctl -xeu看日志 - 服务异常:
Restart=on-failure自动拉起,别再用nohup裸跑 - 改配置:
systemctl edit加 drop-in,别直接动/usr/lib/systemd/ - 定时任务:timer +
Persistent=true+RandomizedDelaySec替代 cron - 开机必查:
systemctl --failed,把红灯消灭在平时
从"能跑"到"跑得稳",systemd 就是那道分水岭。
评论 (0)
暂无评论,快来抢沙发吧!