Linux systemd 从入门到实战:Unit、systemctl、Service 与日志排障
前言
在现代 Linux 发行版中,systemd 几乎已经成为绕不开的基础组件。我们平时执行的:
1 | |
背后都属于 systemd 的服务管理体系。
如果只把 systemd 理解成“用来启动服务的工具”,其实低估了它。systemd 不仅承担 Linux 启动阶段的初始化工作,还负责服务生命周期管理、依赖关系编排、日志、定时任务、挂载点、Socket 激活、用户会话以及部分系统配置管理。
本文结合几篇 systemd 资料重新整理,并对重复内容进行了合并。重点不是罗列所有配置项,而是建立一套真正能用于 Linux 服务器管理、服务部署和故障排查的知识体系。
一、systemd 到底是什么
Linux 内核启动完成以后,需要有一个用户空间进程负责继续初始化系统,例如挂载文件系统、启动网络、启动 SSH、数据库、Web 服务等。
这个特殊进程通常就是 PID 1。
在传统 Linux 中,PID 1 常由 SysV init、Upstart 等系统承担;现代主流发行版则基本采用 systemd。
可以通过下面的命令确认 PID 1:
1 | |
常见输出:
1 | |
很多发行版中的 /sbin/init 实际上最终会指向 systemd。
1.1 systemd 相比传统 SysV init 的主要变化
传统 SysV init 大量依赖 /etc/init.d/ 下的 Shell 脚本,并通过 runlevel 控制不同启动模式。它的问题之一是服务之间的依赖关系不够结构化,启动过程也更偏向顺序执行。
systemd 则把系统中的服务和资源统一抽象为 Unit,并明确描述依赖关系和启动顺序。因此,在依赖条件允许时,多个服务可以并行启动。
systemd 的几个核心特点包括:
- 以 Unit 为统一管理模型;
- 支持服务依赖关系和启动顺序编排;
- 支持并行启动;
- 支持 Socket、D-Bus、Path 等按需激活机制;
- 使用 cgroup 跟踪和管理服务进程;
- 统一提供
systemctl管理接口; - 通过 systemd-journald 和
journalctl管理日志; - 可以使用 Timer Unit 替代部分 cron 场景;
- 可以分析系统启动耗时和依赖链。
所以,与其把 systemd 理解成一个“新版 init”,不如把它理解成 Linux 上的一套 系统与服务管理框架。
二、理解 systemd 的关键:Unit
systemd 管理的基本对象叫做 Unit(单元)。
系统服务、Socket、挂载点、设备、定时器、目标状态等,都可以被表示成不同类型的 Unit。
常见 Unit 类型如下:
| Unit 类型 | 后缀 | 作用 |
|---|---|---|
| Service | .service |
管理后台服务和守护进程 |
| Socket | .socket |
管理 Socket,可用于按需激活服务 |
| Target | .target |
对多个 Unit 进行逻辑分组 |
| Device | .device |
表示内核识别到的设备 |
| Mount | .mount |
管理文件系统挂载点 |
| Automount | .automount |
管理自动挂载 |
| Swap | .swap |
管理 Swap 设备或文件 |
| Timer | .timer |
定时触发任务 |
| Path | .path |
监控文件或目录变化并触发 Unit |
| Slice | .slice |
对进程进行资源层级分组 |
| Scope | .scope |
管理由 systemd 外部创建的进程组 |
不同 systemd 版本的 Unit 类型和细节可能存在差异,因此生产环境中应以当前系统的 man page 为准。
可以查看已经加载的 Unit:
1 | |
查看所有 Service Unit:
1 | |
包括当前未激活的 Unit:
1 | |
查看系统已经安装的 Unit 文件:
1 | |
这两个命令很容易混淆:
list-units更关注当前 systemd 已加载到内存中的 Unit;list-unit-files更关注磁盘上安装的 Unit 文件以及 enable 状态。
三、Unit 文件放在哪里
管理员最常接触的 systemd Unit 文件目录主要有:
1 | |
部分 Debian/Ubuntu 系统中也经常看到:
1 | |
实际目录布局可能因发行版不同而略有差异。
从运维角度,可以简单记住:
1 | |
通常 /etc/systemd/system/ 的优先级高于发行版提供的默认 Unit。
3.1 不建议直接修改 /usr/lib/systemd/system/
例如 Nginx、Docker、PostgreSQL 或 SSH 等通过软件包安装以后,其 Service 文件通常由 RPM/DEB 软件包管理。
如果直接修改:
1 | |
软件升级时可能被覆盖。
更推荐:
1 | |
它会创建类似:
1 | |
的 Drop-in 配置。
查看一个服务最终由哪些配置组成:
1 | |
这在排查“我明明改了配置,为什么实际行为不一样”时非常有用。
四、systemctl:systemd 最常用的管理工具
systemctl 是管理 systemd 的核心命令。
4.1 启动、停止和重启服务
启动:
1 | |
停止:
1 | |
重启:
1 | |
重新加载服务自身的配置:
1 | |
如果支持 reload 就 reload,否则 restart:
1 | |
.service 后缀在很多场景可以省略:
1 | |
4.2 查看服务状态
1 | |
判断当前是否运行:
1 | |
判断是否设置为开机启动:
1 | |
查看详细属性:
1 | |
例如只查询主进程 PID:
1 | |
4.3 start 和 enable 不是一回事
这是 systemd 初学者最常踩的坑之一。
执行:
1 | |
表示:
现在启动 nginx。
但它不意味着下一次开机时一定自动启动。
执行:
1 | |
表示:
建立相应的启动依赖关系,使 nginx 在目标 Target 激活时自动启动。
但 enable 默认不代表“现在立即启动”。
如果希望同时完成:
1 | |
相反,如果希望停止并取消自启动:
1 | |
4.4 mask 比 disable 更彻底
禁用开机启动:
1 | |
并不代表 nginx 无法被手动启动,也不代表它不能作为其他 Unit 的依赖被拉起。
如果希望彻底阻止某个 Unit 启动,可以:
1 | |
取消屏蔽:
1 | |
因此可以简单理解为:
1 | |
五、Unit 的状态:enabled、disabled、static、masked
执行:
1 | |
经常会看到下面几种状态。
enabled
已经建立启动依赖,通常意味着会随某个 Target 自动启动。
disabled
Unit 文件存在,但没有设置为自动启动。
static
这个 Unit 通常没有可用于 enable 的 [Install] 配置,主要作为其他 Unit 的依赖被拉起。
static 不代表这个服务“坏了”,也不代表它不能运行。
masked
Unit 被屏蔽,systemd 会拒绝启动它。
排查一个服务为什么无法启动时,可以先检查:
1 | |
如果看到:
1 | |
那就先别继续和配置文件搏斗了——systemd 已经在门口把它拦下来了。
六、Target:systemd 如何组织系统状态
传统 SysV init 使用 runlevel 表示不同系统运行级别。
systemd 使用 Target Unit 表示某一组 Unit 的逻辑集合。
常见 Target:
| Target | 含义 |
|---|---|
multi-user.target |
多用户命令行环境 |
graphical.target |
图形界面环境 |
rescue.target |
救援模式 |
emergency.target |
更精简的紧急模式 |
reboot.target |
重启 |
poweroff.target |
关机 |
default.target |
默认启动目标 |
查看当前默认 Target:
1 | |
例如服务器通常可以设置为:
1 | |
桌面环境可以使用:
1 | |
临时切换 Target:
1 | |
注意:isolate 不只是“进入某个 Target”,它还可能停止不属于目标依赖关系的其他 Unit,因此在生产服务器上执行前要确认影响范围。
七、systemd 最重要的概念之一:依赖和顺序是两回事
在 Unit 文件里经常会看到:
1 | |
很多人第一次看到会认为这两个配置意思差不多,其实完全不是一个维度。
7.1 Requires 和 Wants:描述依赖关系
强依赖:
1 | |
表示当前 Unit 依赖 PostgreSQL。
弱依赖:
1 | |
表示希望 PostgreSQL 一起启动,但 PostgreSQL 启动失败不一定导致当前 Unit 整体失败。
一般情况下,如果不是严格的生死依赖关系,Wants= 往往更宽松。
7.2 After 和 Before:描述启动顺序
1 | |
表示:
当两个 Unit 都需要启动时,当前 Unit 应该排在 PostgreSQL 之后。
但是 After= 本身不会主动把 PostgreSQL 拉起来。
反过来:
1 | |
表示当前 Unit 排在 xxx 之前启动。
所以:
1 | |
不是“依赖 network.target”的同义词。
可以把它们记成:
1 | |
真实的服务通常会把二者组合使用。
例如:
1 | |
八、Service Unit 文件结构
一个典型的 .service 文件通常包含三个部分:
1 | |
8.1 [Unit]
主要描述:
- Unit 是什么;
- 与其他 Unit 有什么依赖;
- 启动顺序如何安排。
例如:
1 | |
常见配置:
1 | |
8.2 [Service]
主要描述服务进程如何启动、停止、重启以及使用什么运行环境。
常见配置:
1 | |
8.3 [Install]
主要在执行:
1 | |
时使用。
常见配置:
1 | |
它的含义可以理解为:
enable 当前服务时,把它挂到
multi-user.target的启动依赖关系中。
九、Service 的 Type 怎么选
常见 Service 类型包括:
simple
最常见的长时间运行程序之一。
1 | |
systemd 直接把 ExecStart 启动的进程作为主要服务进程进行管理。
forking
适用于传统 Unix daemon:父进程启动后 fork 子进程,然后父进程退出。
1 | |
很多历史服务使用这种模式,但现代应用如果本身支持前台运行,通常没有必要为了“后台化”再 fork 一次。
oneshot
适合执行一次任务然后退出,例如初始化脚本:
1 | |
notify
服务在真正准备就绪后主动通知 systemd。
这需要应用本身支持 systemd notification 机制。
dbus
用于依赖 D-Bus 名称注册完成来判断服务就绪的程序。
对于普通自研 Java、Go、Python、Node.js 后台应用,最常见的原则是:
应用直接以前台进程方式运行,让 systemd 自己负责“后台管理”,不要在应用启动脚本里再套
nohup ... &。
十、实战:把一个 Java 应用交给 systemd 管理
假设应用结构为:
1 | |
创建专用用户:
1 | |
创建:
1 | |
内容:
1 | |
然后执行:
1 | |
查看状态:
1 | |
实时查看日志:
1 | |
重启:
1 | |
10.1 为什么不要写成 nohup
不建议这样:
1 | |
原因是 systemd 本身就是服务管理器。
如果再次人为后台化,反而会让主进程识别、退出状态、重启策略、日志和生命周期控制变复杂。
正确思路是:
1 | |
十一、修改 Unit 后为什么一定要 daemon-reload
修改:
1 | |
以后,只执行:
1 | |
不一定会让 systemd 重新读取 Unit 文件定义。
应该先执行:
1 | |
然后:
1 | |
可以记成:
1 | |
还可以先验证 Unit 文件:
1 | |
这对发现拼写错误和部分配置问题很有帮助。
十二、ExecStart 不是 Shell 命令行
另一个常见坑:
1 | |
很多人以为 systemd 会像 Bash 一样解释 >、2>&1、|、&& 等符号。
实际上,ExecStart= 默认并不是把整行交给 Shell 解释。
因此下面这种复杂 Shell 逻辑不要直接塞进去:
1 | |
如果确实依赖 Shell,可以显式写:
1 | |
但更好的方式通常是把复杂逻辑独立到脚本中:
1 | |
让 Unit 文件保持简单、可读、可维护。
十三、Restart:让 systemd 正确守护服务
一个非常实用的配置是:
1 | |
它表示进程异常退出后等待 5 秒再重启。
常见 Restart 策略包括:
1 | |
普通服务通常优先考虑:
1 | |
而不是无脑:
1 | |
因为“无论为什么退出都拉起来”有时会掩盖正常退出、配置错误或部署过程中的实际意图。
如果程序因为配置错误瞬间退出,还可能形成:
1 | |
这时候应该结合:
1 | |
先找根因,而不是和 systemd 比谁更执着。
十四、journalctl:systemd 体系中的日志查看工具
systemd-journald 会收集大量系统和服务日志,journalctl 用于查询这些日志。
14.1 查看全部日志
1 | |
14.2 查看当前启动周期
1 | |
查看上一次启动:
1 | |
查看启动历史:
1 | |
14.3 查看指定服务
1 | |
只看最后 100 行:
1 | |
持续追踪:
1 | |
这基本就是 systemd 环境中的:
1 | |
14.4 按时间过滤
1 | |
1 | |
1 | |
配合服务:
1 | |
14.5 查看错误级别日志
1 | |
当前启动周期的错误:
1 | |
日志优先级从严重到详细大致为:
1 | |
14.6 查看内核日志
1 | |
上一次启动的内核日志:
1 | |
十五、控制 journal 日志磁盘占用
查看日志占用空间:
1 | |
按大小清理:
1 | |
按时间清理:
1 | |
journald 的主要配置文件通常是:
1 | |
如果日志量很大,生产环境应根据磁盘大小、审计要求和日志平台策略统一设置,而不是等 /var 爆满以后再临时清理。
十六、排查一个 systemd 服务为什么启动失败
遇到:
1 | |
不要第一反应反复执行:
1 | |
建议按下面顺序排查。
第一步:看状态
1 | |
关注:
1 | |
第二步:看详细日志
1 | |
或者:
1 | |
第三步:检查 Unit 定义
1 | |
确认:
ExecStart路径是否正确;- 用户是否有权限;
- WorkingDirectory 是否存在;
- EnvironmentFile 是否存在;
- 端口是否被占用;
- 依赖服务是否正常;
- Unit 是否被 mask。
第四步:检查依赖关系
1 | |
反向查看谁依赖它:
1 | |
第五步:验证 Unit 文件
1 | |
第六步:必要时清除 failed 状态
1 | |
这套顺序通常比“重启大法”有效得多。
十七、systemd-analyze:分析 Linux 为什么启动慢
systemd 还提供启动性能分析工具。
17.1 查看整体启动时间
1 | |
或者直接:
1 | |
17.2 查看各 Unit 启动耗时
1 | |
它会按照耗时排序。
但要注意:systemd 会并行启动大量 Unit,因此“某个服务自己启动花了 10 秒”不一定等于“它让系统整体启动慢了 10 秒”。
所以不能只看 blame。
17.3 查看关键依赖链
1 | |
它更适合判断某个 Target 的关键启动路径。
查看指定 Unit:
1 | |
17.4 生成启动时序图
1 | |
然后可以直接打开 boot.svg,从时间轴观察各 Unit 的启动关系。
如果真的要优化启动速度,通常需要把:
1 | |
结合起来分析,而不是看到 blame 第一名就直接 disable。
十八、Socket、Path 和 Timer:systemd 不只是 Service
18.1 Socket 激活
传统模式:
1 | |
Socket Activation 可以变成:
1 | |
这也是 systemd 支持按需启动的重要机制之一。
18.2 Path 激活
Path Unit 可以监控:
- 文件是否出现;
- 文件是否修改;
- 目录是否发生变化。
然后触发相应 Service。
适合“某个文件出现后自动处理”这一类场景。
18.3 Timer Unit
Timer Unit 可以实现定时执行,并且与 Service、日志、依赖关系天然结合。
查看定时器:
1 | |
很多现代 Linux 发行版中的系统级周期任务已经大量采用:
1 | |
这种组合。
它不是必须取代 cron,但在需要统一 systemd 生命周期、依赖和日志管理时非常方便。
十九、几个非常实用的 systemctl 命令
查看失败 Unit:
1 | |
查看正在运行的服务:
1 | |
查看服务完整配置:
1 | |
查看服务属性:
1 | |
查看依赖:
1 | |
查看默认 Target:
1 | |
重载 systemd Unit:
1 | |
让 systemd 重新执行自身管理器进程,通常日常修改 Unit 不需要使用:
1 | |
关机:
1 | |
重启:
1 | |
挂起:
1 | |
进入救援模式:
1 | |
二十、生产环境编写 Service 的建议
20.1 使用专用用户运行应用
不要所有业务服务都写:
1 | |
应该为应用创建独立用户:
1 | |
把最小权限原则真正落到进程级别。
20.2 使用绝对路径
推荐:
1 | |
不要依赖登录 Shell 中的 PATH。
20.3 配置 WorkingDirectory
如果应用依赖相对路径:
1 | |
否则从终端能运行,放到 systemd 里却报找不到配置文件,是非常典型的问题。
20.4 环境变量放 EnvironmentFile
例如:
1 | |
- 前缀表示文件不存在时不因此直接失败。
相比把数据库地址、JVM 参数等全部硬编码进 Unit,独立环境文件通常更便于维护。
20.5 明确重启策略
1 | |
不要依赖外部无限循环脚本自己守护。
20.6 让应用输出到 stdout/stderr
普通服务通常可以直接让应用写标准输出和标准错误,交给 journald 收集。
这样:
1 | |
就能直接排查。
如果企业已经统一接入 ELK、Loki、Splunk 等日志平台,再根据平台架构决定由 journald 转发还是应用输出独立日志文件。
20.7 可以逐步增加安全限制
确认应用兼容以后,可以考虑:
1 | |
如果应用确实需要写目录,则再显式开放:
1 | |
安全限制不要一次性全开,否则很容易把应用自己锁在门外。生产上建议一项一项验证。
二十一、一个更完整的应用 Service 模板
下面这个模板适合作为自研后台服务的起点:
1 | |
部署流程:
1 | |
检查:
1 | |
升级应用:
1 | |
如果只修改了 Unit 文件:
1 | |
二十二、systemd 常见误区总结
误区一:start 以后就是开机自启动
不是。
1 | |
需要两者一起:
1 | |
误区二:After 就代表依赖
不是。
1 | |
误区三:修改 Unit 后只 restart
应该:
1 | |
误区四:systemd 服务必须用 nohup
恰恰相反。
应用应该保持前台运行,让 systemd 负责守护。
误区五:服务失败只看 systemctl status
status 主要是入口。
进一步应该看:
1 | |
误区六:blame 第一名就是启动慢的罪魁祸首
不一定。
systemd 大量并行启动,需要结合:
1 | |
甚至:
1 | |
一起判断。
总结
掌握 systemd,最重要的不是记住几十个命令,而是理解下面这几个核心关系:
1 | |
服务之间再通过:
1 | |
建立依赖和顺序关系。
日常操作主要依赖:
1 | |
对于服务器运维和应用部署来说,真正高频的操作可以浓缩成下面几条:
1 | |
当我们开始把应用直接以前台进程交给 systemd 管理,并使用 Unit 描述依赖、重启策略、运行用户、日志和启动顺序以后,Linux 服务管理就不再是“各种启动脚本拼起来能跑就行”,而真正变成了一套结构化、可观测、可维护的服务生命周期管理体系。
参考资料
本文基于以下资料进行知识抽取、去重和重新组织,并结合现代 systemd 常用实践进行了补充整理:
- CSDN:systemd
https://blog.csdn.net/a624731186/article/details/22690947 - CSDN:关于 Systemd
https://blog.csdn.net/m0_52316372/article/details/145530053 - 博客园:systemd 详解
https://www.cnblogs.com/KrillLiszt/p/16192522.html - systemd 官方文档与源码说明
https://www.freedesktop.org/software/systemd/man/