systemd 架构概述

systemd 是现代 Linux 发行版的事实标准 init 系统,从 2015 年起已取代 SysVinit 成为绝大多数主流发行版的默认 init。它不只是"启动服务的工具",而是一个完整的系统和服务管理器。

核心 unit 类型

systemd 通过 unit(单元)来管理系统资源,每种 unit 类型对应一种资源:

unit 类型扩展名作用
service.service系统服务(守护进程)
socket.socketIPC 套接字(支持套接字激活)
timer.timer定时任务(替代 cron)
target.target服务组(类似传统 runlevel)
mount.mount文件系统挂载点
device.device内核设备
path.path文件路径监控(文件出现时触发服务)
slice.slicecgroup 资源分配层级

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 值启动成功标志适用场景
simpleExecStart 立即返回前台守护进程
forkingExecStart fork 的子进程退出,父进程返回传统 daemon(nginx、sshd)
oneshotExecStart 执行完毕一次性脚本
notify服务通过 sd_notify() 发送 READY=1支持 systemd 通知的服务
idle等到所有任务完成再启动不影响启动速度的控制台服务

关键启动/停止命令

  • ExecStartPre:主进程启动前执行的准备命令(如配置检查)
  • ExecStartPost:主进程启动后执行的命令
  • ExecStopPre:停止前执行的命令
  • ExecStop:停止命令(不写则发送 KillSignal)
  • ExecStopPost:停止后执行的清理命令

Restart 策略

重启条件
no不重启(默认)
on-success仅退出码为 0 时重启
on-failure非零退出码或被信号杀死时重启
on-abnormal被信号杀死或超时时重启
on-watchdogwatchdog 超时时重启
always总是重启

[Install] 段

[Install]
WantedBy=multi-user.target
Alias=nginx.service
字段说明
WantedBysystemctl enable 时创建 .wants 软链接到指定 target
RequiredBysystemctl 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

这个配置的关键设计点:

  1. Type=notify + WatchdogSec:应用通过 sd_notify() 主动报告就绪状态,systemd 还会监控心跳,应用卡死时自动重启。
  2. StartLimitBurst=3:限制 60 秒内最多重启 3 次,防止崩溃循环。
  3. KillMode=mixed:主进程收 SIGTERM,子进程收 SIGKILL,确保优雅关闭。
  4. 安全加固:通过一系列 Protect*Restrict* 指令实现服务沙箱化,即使应用被攻破也难以逃逸。
  5. 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 提供了丰富的指令来实现这些目标,关键在于理解每个字段的含义并正确组合使用。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. systemd 官方文档 — freedesktop.org 社区,参考了systemd 官方文档相关内容