systemd 架构概述
systemd 是现代 Linux 发行版的事实标准 init 系统,从 2015 年起已取代 SysVinit 成为绝大多数主流发行版的默认 init。它不只是"启动服务的工具",而是一个完整的系统和服务管理器。
核心 unit 类型
systemd 通过 unit(单元)来管理系统资源,每种 unit 类型对应一种资源:
| unit 类型 | 扩展名 | 作用 |
|---|---|---|
| service | .service | 系统服务(守护进程) |
| socket | .socket | IPC 套接字(支持套接字激活) |
| timer | .timer | 定时任务(替代 cron) |
| target | .target | 服务组(类似传统 runlevel) |
| mount | .mount | 文件系统挂载点 |
| device | .device | 内核设备 |
| path | .path | 文件路径监控(文件出现时触发服务) |
| slice | .slice | cgroup 资源分配层级 |
target 与传统 runlevel 的映射
# 查看当前默认 target
systemctl get-default
# 通常输出: multi-user.target(对应 runlevel 3,多用户命令行模式)
# 切换到图形界面(对应 runlevel 5)
sudo systemctl isolate graphical.target
# runlevel 与 target 对应关系:
# runlevel 0 → poweroff.target
# runlevel 1 → rescue.target
# runlevel 3 → multi-user.target
# runlevel 5 → graphical.target
# runlevel 6 → reboot.target
cgroup 集成
systemd 是 cgroup v1/v2 的主要使用者。每个 service 自动创建一个 cgroup,路径格式为:
# cgroup v2 路径示例
/sys/fs/cgroup/system.slice/nginx.service/
# 查看某服务的 cgroup 信息
systemctl status nginx
# 输出中会包含:
# CGroup: /system.slice/nginx.service
# ├─4567 "nginx: master process /usr/sbin/nginx"
# └─4568 "nginx: worker process"
systemd 的完整架构和配置参考见 systemd 官方文档。
service unit 文件深入分析
unit 文件采用 INI 风格格式,分为三个主要段落。下面以一个完整的 Nginx service 文件为例逐段解析:
[Unit] 段
[Unit]
Description=Nginx HTTP Server
Documentation=man:nginx(8)
Documentation=https://nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
Conflicts=apache2.service
| 字段 | 说明 |
|---|---|
Description | 服务的可读描述,systemctl status 输出中显示 |
Documentation | 文档引用,支持 man:、http:、info: 前缀 |
After | 当前 unit 在指定 unit 之后启动(仅排序,不建立依赖) |
Before | 当前 unit 在指定 unit 之前启动 |
Requires | 强依赖:如果依赖启动失败,当前 unit 也会失败 |
Wants | 弱依赖:尝试启动依赖,失败不影响当前 unit |
Requisite | 强依赖(不等待):依赖必须已启动,否则立即失败 |
Conflicts | 互斥:指定的 unit 不能与当前 unit 同时运行 |
PartOf | 当依赖 unit 被停止/重启时,当前 unit 也跟随操作 |
BindsTo | 最强绑定:依赖停止时当前 unit 也停止,且不自动重启 |
[Service] 段
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/usr/sbin/nginx -s stop
Restart=on-failure
RestartSec=5s
TimeoutStartSec=30
TimeoutStopSec=30
KillMode=mixed
KillSignal=SIGQUIT
LimitNOFILE=65535
Type 字段决定了 systemd 如何判断服务是否启动成功,这是最容易出错的地方:
| Type 值 | 启动成功标志 | 适用场景 |
|---|---|---|
simple | ExecStart 立即返回 | 前台守护进程 |
forking | ExecStart fork 的子进程退出,父进程返回 | 传统 daemon(nginx、sshd) |
oneshot | ExecStart 执行完毕 | 一次性脚本 |
notify | 服务通过 sd_notify() 发送 READY=1 | 支持 systemd 通知的服务 |
idle | 等到所有任务完成再启动 | 不影响启动速度的控制台服务 |
关键启动/停止命令:
ExecStartPre:主进程启动前执行的准备命令(如配置检查)ExecStartPost:主进程启动后执行的命令ExecStopPre:停止前执行的命令ExecStop:停止命令(不写则发送 KillSignal)ExecStopPost:停止后执行的清理命令
Restart 策略:
| 值 | 重启条件 |
|---|---|
no | 不重启(默认) |
on-success | 仅退出码为 0 时重启 |
on-failure | 非零退出码或被信号杀死时重启 |
on-abnormal | 被信号杀死或超时时重启 |
on-watchdog | watchdog 超时时重启 |
always | 总是重启 |
[Install] 段
[Install]
WantedBy=multi-user.target
Alias=nginx.service
| 字段 | 说明 |
|---|---|
WantedBy | systemctl enable 时创建 .wants 软链接到指定 target |
RequiredBy | systemctl enable 时创建 .requires 软链接 |
Alias | 服务别名 |
Also | 同时 enable 指定的其他 unit |
依赖管理:Requires/Wants/After/Before 的区别
这是 systemd 中最容易混淆的概念。核心区别在于依赖和顺序是两个独立维度。
依赖(Requires / Wants)
依赖决定"要不要一起启动":
# 强依赖:network 必须成功启动,否则本服务启动失败
Requires=network.target
# 弱依赖:希望 network 启动,但如果 network 启动失败,本服务照常启动
Wants=network.target
顺序(After / Before)
顺序决定"谁先谁后",但不建立依赖关系:
# 本服务在 network.target 之后启动,但不依赖它
# 如果 network.target 不在启动列表中,本服务照样启动
After=network.target
常见陷阱
陷阱 1:只写 After 不写 Wants/Requires
# 错误写法:以为这样就会启动 network
[Unit]
After=network.target
# 实际上 network.target 不一定被拉起
# 正确写法:同时声明依赖和顺序
[Unit]
Wants=network.target
After=network.target
陷阱 2:Requires 不保证顺序
# 错误写法:Requires 保证了依赖,但不保证 network 先启动完
[Unit]
Requires=network.target
# 本服务可能在 network 还没完全启动时就启动了
# 正确写法
[Unit]
Requires=network.target
After=network.target
陷阱 3:Requires 的传播性
如果 A Requires B,而 B 启动失败,systemd 会尝试启动 B 依赖的所有 unit。这可能导致连锁失败。对于"尽量启动但不影响我"的场景,用 Wants 更安全。
验证依赖链
# 查看服务的依赖树
systemctl list-dependencies nginx.service
# 查看反向依赖(谁依赖了 nginx)
systemctl list-dependencies --reverse nginx.service
# 查看启动顺序
systemctl list-dependencies --after nginx.service
systemctl list-dependencies --before nginx.service
资源控制:CPU/Memory/BlockIO 限制
systemd 通过 cgroup 实现服务级别的资源控制,无需手动配置 cgroup。
cgroup v1 vs v2
# 检查当前使用的 cgroup 版本
stat -fc %T /sys/fs/cgroup/
# 输出 cgroup2fs → cgroup v2
# 输出 tmpfs → cgroup v1
CPU 限制
[Service]
# cgroup v2 写法
CPUQuota=200% # 限制最多使用 2 个 CPU 核
CPUWeight=500 # CPU 权重(默认 100,范围 1-10000)
# cgroup v1 写法(向后兼容)
CPUAccounting=true
CPUQuota=200%
CPUShares=512
Memory 限制
[Service]
# cgroup v2
MemoryMax=2G # 硬限制:超过则触发 OOM
MemoryHigh=1536M # 软限制:超过后开始回收内存
MemorySwapMax=512M # swap 使用上限
# cgroup v1(部分参数名不同)
MemoryLimit=2G
BlockIO 限制
[Service]
# cgroup v2
IOWeight=500 # IO 权重(1-10000,默认 100)
IOReadBandwidthMax=/dev/sda 10M # 读带宽限制
IOWriteBandwidthMax=/dev/sda 5M # 写带宽限制
# cgroup v1
BlockIOWeight=500
BlockIOReadBandwidth=/dev/sda 10485760
动态调整资源限制
systemd 支持运行时动态修改资源限制,无需重启服务:
# 临时将 nginx 的内存上限调整为 1G
systemctl set-property nginx.service MemoryMax=1G
# 永久生效(写入配置文件)
systemctl set-property nginx.service MemoryMax=1G --runtime=no
# 查看当前资源使用
systemctl show nginx.service -p CPUUsageNSec -p MemoryCurrent -p CPUQuotaPerSecUSec
journalctl 日志管理实战
systemd 的日志系统 journald 是结构化日志系统,支持索引、过滤和持久化。
基础查询
# 查看某服务的日志
journalctl -u nginx.service
# 查看今天的日志
journalctl --since today
# 查看最近 1 小时
journalctl --since "1 hour ago"
# 实时跟踪日志(类似 tail -f)
journalctl -u nginx.service -f
# 查看上次启动的日志
journalctl -b -1
# 查看本次启动的日志
journalctl -b 0
高级过滤
# 按优先级过滤(0-7,0=emerg,7=debug)
journalctl -p err # 只看 error 及以上
journalctl -p warning # 只看 warning 及以上
# 按进程 PID
journalctl _PID=4567
# 按可执行文件路径
journalctl /usr/sbin/nginx
# 组合过滤(AND 关系)
journalctl -u nginx.service -p err --since "1 hour ago"
# 按日志字段过滤(结构化日志的字段)
journalctl SYSLOG_FACILITY=10
# JSON 格式输出(适合脚本处理)
journalctl -u nginx.service -o json | python3 -m json.tool
日志持久化
默认情况下 journald 的日志存储在 /run/log/journal/(内存中),重启后丢失。生产环境必须配置持久化:
# 创建持久化日志目录
sudo mkdir -p /var/log/journal/
sudo systemd-tmpfiles --create --prefix /var/log/journal
# 重启 journald
sudo systemctl restart systemd-journald
配置文件 /etc/systemd/journald.conf:
[Journal]
Storage=persistent # 持久化到磁盘
Compress=yes # 压缩旧日志
SystemMaxUse=2G # 磁盘日志最大 2G
SystemMaxFileSize=100M # 单个日志文件最大 100M
MaxRetentionSec=30day # 日志保留 30 天
ForwardToSyslog=no # 不转发到 syslog(避免重复)
# 重启配置生效
sudo systemctl restart systemd-journald
# 验证磁盘使用量
journalctl --disk-usage
# 输出示例: Archived and active journals take up 1.2G in the file system.
远程日志收集
systemd 原生支持通过 systemd-journal-remote 将日志发送到远程服务器:
# 服务端安装
sudo apt install -y systemd-journal-remote
sudo systemctl enable --now systemd-journal-remote.service
# 客户端配置转发
sudo apt install -y systemd-journal-remote
# 编辑 /etc/systemd/journal-upload.conf
# [Upload]
# URL=https://log-collector.internal:19532
# 启动客户端日志转发
sudo systemctl enable --now systemd-journal-upload.service
实战:编写生产级 systemd service 文件
下面是一个生产级的应用服务配置示例,综合运用了本文涉及的所有知识点:
[Unit]
Description=MyApp Production Service
Documentation=https://wiki.internal/myapp/deployment
After=network-online.target postgresql.service redis.service
Wants=network-online.target
Requires=postgresql.service redis.service
[Service]
Type=notify
NotifyAccess=main
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/env
Environment=GIN_MODE=release
Environment=APP_ENV=production
# 启动命令
ExecStartPre=/opt/myapp/bin/myapp migrate
ExecStart=/opt/myapp/bin/myapp server
ExecReload=/bin/kill -HUP $MAINPID
# 重启策略
Restart=on-failure
RestartSec=5s
StartLimitInterval=60
StartLimitBurst=3
TimeoutStartSec=60
TimeoutStopSec=30
TimeoutAbortSec=30
# 进程管理
KillMode=mixed
KillSignal=SIGTERM
SendSIGKILL=yes
WatchdogSec=60
# 资源限制
CPUQuota=300%
MemoryMax=4G
MemoryHigh=3G
LimitNOFILE=100000
LimitNPROC=65535
TasksMax=4096
# 安全加固
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RemoveIPC=true
CapabilityBoundingSet=
AmbientCapabilities=
# 日志
SyslogIdentifier=myapp
LogLevelMax=info
[Install]
WantedBy=multi-user.target
这个配置的关键设计点:
Type=notify+WatchdogSec:应用通过sd_notify()主动报告就绪状态,systemd 还会监控心跳,应用卡死时自动重启。StartLimitBurst=3:限制 60 秒内最多重启 3 次,防止崩溃循环。KillMode=mixed:主进程收 SIGTERM,子进程收 SIGKILL,确保优雅关闭。- 安全加固:通过一系列
Protect*和Restrict*指令实现服务沙箱化,即使应用被攻破也难以逃逸。 ReadWritePaths:在ProtectSystem=strict下精确声明可写路径,最小化文件系统暴露面。
部署方式:
# 安装 service 文件
sudo cp myapp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
# 验证状态
systemctl status myapp.service
journalctl -u myapp.service -f
小结
systemd 不只是一个 init 系统,它是 Linux 服务管理的完整框架。掌握 unit 文件结构、依赖与顺序的区别、cgroup 资源控制和 journalctl 日志管理,是运维工程师的基本功。
生产环境中,每个自定义服务都应当配置资源限制和安全加固——这不是可选项,而是基础设施的基本要求。systemd 提供了丰富的指令来实现这些目标,关键在于理解每个字段的含义并正确组合使用。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- systemd 官方文档 — freedesktop.org 社区,参考了systemd 官方文档相关内容