systemd
第13章:systemd 深度解析
systemd 是现代 Linux 发行版的 init 系统与服务管理器,以 PID 1 身份运行,负责启动用户空间、管理服务生命周期、采集日志、控制 cgroup 资源。本章从架构原理出发,逐步掌握 unit 文件编写、服务管理、定时任务、日志查询与性能分析,最终实现一个生产级 Go 服务的完整 systemd 配置。
1. systemd 架构与 unit 类型
systemd 以 PID 1 运行,内核启动后第一个用户态进程即为 systemd。它并行启动依赖满足的服务,显著缩短了启动时间。systemd 使用 unit 作为管理对象,每种 unit 对应一类资源或行为:
| Unit 类型 | 扩展名 | 用途 |
|---|---|---|
| service | .service | 守护进程或一次性命令 |
| socket | .socket | 网络/Unix socket,按需激活服务 |
| timer | .timer | 定时触发,替代 cron |
| path | .path | 文件系统路径变化触发 |
| mount | .mount | 挂载点管理 |
| target | .target | 服务分组同步点(类似 runlevel) |
| slice | .slice | cgroup 层级资源划分 |
| device | .device | 内核设备暴露 |
依赖关系关键词
- Requires= — 强依赖:若依赖失败则本 unit 也失败
- Wants= — 软依赖:依赖失败不影响本 unit
- After= — 顺序约束:在指定 unit 启动后再启动
- Before= — 顺序约束:在指定 unit 启动前先启动
- BindsTo= — 强绑定:依赖停止则本 unit 也停止
- Conflicts= — 互斥:启动本 unit 会停止冲突的 unit
Target 概念
Target 是 unit 的同步点,类似 SysV 的 runlevel。常用 target:multi-user.target(无 GUI 的多用户模式,对应 runlevel 3)和 graphical.target(图形界面,对应 runlevel 5)。default.target 是系统默认目标,通常软链到 graphical.target。
2. systemctl 命令完全参考
服务生命周期
# 启动 / 停止 / 重启 / 优雅重载配置(不重启进程)
systemctl start nginx.service
systemctl stop nginx.service
systemctl restart nginx.service
systemctl reload nginx.service # 发送 SIGHUP,要求服务支持
# 开机自启 / 禁用 / 屏蔽(mask 防止任何方式启动)
systemctl enable nginx.service
systemctl disable nginx.service
systemctl mask nginx.service
systemctl unmask nginx.service
# 查看状态(含最近日志)
systemctl status nginx.service
# 重新读取磁盘上的 unit 文件(改完配置必须执行)
systemctl daemon-reload
状态查询
# 退出码 0=active / 非0=inactive
systemctl is-active nginx.service
systemctl is-enabled nginx.service
systemctl is-failed nginx.service
# 列出所有已加载的 unit
systemctl list-units
systemctl list-units --type=service --state=running
# 列出所有 unit 文件及其 enable 状态
systemctl list-unit-files --type=service
# 查看某 unit 的所有依赖树
systemctl list-dependencies nginx.service
提示: 在脚本中判断服务状态时,用
systemctl is-active --quiet nginx,安静模式只返回退出码,不产生输出,易于 if 条件判断。
3. 编写 .service unit 文件
系统级 unit 文件存放于 /etc/systemd/system/(优先级最高)或 /lib/systemd/system/(发行版提供)。自定义服务应始终放在 /etc/systemd/system/ 中。
[Unit] 段:描述与依赖
[Unit]
Description=My Application Service
Documentation=https://example.com/docs
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service] 段:进程控制
Type= 决定 systemd 如何判断服务"就绪":
- simple — 默认;ExecStart 的进程就是主进程
- forking — 服务 fork 后主进程退出(传统守护进程模式)
- oneshot — 执行一次性任务,完成后退出
- notify — 服务调用 sd_notify() 主动告知就绪
- exec — 类似 simple,但等待 exec() 完成后才认为就绪
[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
# 环境变量(直接写或从文件读)
Environment=APP_ENV=production
Environment=PORT=8080
EnvironmentFile=/etc/myapp/env # 文件中每行 KEY=VALUE
ExecStart=/opt/myapp/bin/server
ExecStop=/bin/kill -s TERM $MAINPID
ExecReload=/bin/kill -s HUP $MAINPID
# 自动重启策略
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=3
# 资源限制
LimitNOFILE=65536
LimitNPROC=4096
[Install] 段:安装目标
[Install]
WantedBy=multi-user.target
注意: 修改 unit 文件后必须运行
systemctl daemon-reload,否则 systemd 仍使用内存中的旧配置,即使文件已更新。
4. .timer unit:替代 cron
systemd timer 需要与同名 .service unit 配对。timer 触发时,systemd 启动对应的 service。
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily database backup timer
[Timer]
# 系统启动后 5 分钟执行一次
OnBootSec=5min
# 每次触发后 24 小时再次触发
OnUnitActiveSec=24h
# 或者使用 cron 风格(每天 03:00)
OnCalendar=*-*-* 03:00:00
# 系统关机期间错过的任务,开机后补执行
Persistent=true
# 随机延迟,避免整点流量洪峰
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup job
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh
# 启用并启动 timer(注意启用的是 timer 而不是 service)
systemctl enable --now backup.timer
# 查看所有 timer 及下次触发时间
systemctl list-timers --all
crontab vs systemd timer 对比
| 特性 | crontab | systemd timer |
|---|---|---|
| 配置方式 | cron 表达式 | OnCalendar / OnBootSec / OnUnitActiveSec |
| 错过任务补执行 | 不支持 | Persistent=true 支持 |
| 日志集成 | 需单独配置 | 自动收入 journald |
| 资源限制 | 不支持 | 通过 .service 的 cgroup 限制 |
| 依赖管理 | 不支持 | 完整 unit 依赖图 |
| 随机延迟 | 需手动 sleep | RandomizedDelaySec |
5. .socket unit:按需激活服务
Socket 激活是 systemd 的核心特性之一:systemd 预先创建并持有 socket,当客户端连接时才启动对应服务,实现零监听时间和加快启动速度。
# /etc/systemd/system/myapp.socket
[Unit]
Description=MyApp TCP socket
[Socket]
ListenStream=0.0.0.0:8080
# Unix domain socket 示例
# ListenStream=/run/myapp.sock
# 数据报 socket(UDP)
# ListenDatagram=514
# socket 传给服务后保持监听(多实例场景)
Accept=false
[Install]
WantedBy=sockets.target
原理: 服务通过文件描述符继承接收 socket(fd 编号从 SD_LISTEN_FDS_START=3 开始),无需自行调用 bind()/listen()。Go 中可用
net.FileListener从继承的 fd 创建 Listener。
6. .path unit:文件系统监控触发
# /etc/systemd/system/process-uploads.path
[Unit]
Description=Watch upload directory for new files
[Path]
# 路径存在时触发(首次检测)
PathExists=/var/spool/uploads/trigger
# 路径内容变化时触发(inotify IN_CLOSE_WRITE)
PathChanged=/var/spool/uploads
# 路径被修改时触发(包括属性变化)
PathModified=/var/spool/uploads
# 触发后启动同名 .service(或用 Unit= 指定其他服务)
Unit=process-uploads.service
[Install]
WantedBy=multi-user.target
7. journalctl 完全指南
journald 是 systemd 内置的结构化日志系统,取代了传统的 syslog。所有服务的 stdout/stderr 自动进入 journal,无需配置日志文件路径。
# 查看某个服务的日志(-u 过滤 unit)
journalctl -u nginx.service
# 实时跟踪(类似 tail -f)
journalctl -u nginx.service -f
# 时间范围过滤
journalctl -u nginx --since "2025-01-01 00:00:00" --until "2025-01-02 00:00:00"
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx --since yesterday
# 按优先级过滤(emerg/alert/crit/err/warning/notice/info/debug)
journalctl -p err # 只看 err 及更高级别
journalctl -p warning..err # 范围过滤
# 当前启动的日志
journalctl -b
journalctl -b -1 # 上次启动
journalctl --list-boots # 列出所有启动记录
# 输出格式
journalctl -u nginx -o json # JSON 格式,每条一行
journalctl -u nginx -o json-pretty # 格式化 JSON
journalctl -u nginx -o verbose # 所有字段
journalctl -u nginx -o short-iso # ISO 时间戳
# 日志磁盘占用与清理
journalctl --disk-usage
journalctl --vacuum-size=500M # 清理到 500MB
journalctl --vacuum-time=30d # 清理 30 天以前的日志
# 按进程/用户过滤
journalctl _PID=1234
journalctl _UID=1000
# 内核日志
journalctl -k # 等同 dmesg
生产技巧: 将 journal 日志持久化:默认存于
/run/log/journal/(重启丢失),创建/var/log/journal/目录后重启 journald 即可持久化到/var/log/journal/。
8. cgroup v2 资源控制
systemd 为每个 service 创建独立的 cgroup,可在 unit 文件的 [Service] 段直接设置资源限制,无需手动操作 /sys/fs/cgroup。
[Service]
# CPU 配额:最多使用 2 个核心的时间(200% 表示 2 核)
CPUQuota=200%
# CPU 权重(相对调度优先级,默认 100,范围 1-10000)
CPUWeight=200
# 内存限制
MemoryMax=512M # 硬上限,超过触发 OOM kill
MemoryHigh=400M # 软上限,超过开始节流
MemorySwapMax=0 # 禁止使用 swap
# IO 权重(相对,默认 100)
IOWeight=50
# 任务(线程)数限制
TasksMax=128
# 实时监控各 cgroup 资源占用(类似 top)
systemd-cgtop
# 查看某服务的 cgroup 路径
systemctl status nginx | grep "CGroup"
# 直接查看 cgroup v2 层级树
ls /sys/fs/cgroup/system.slice/nginx.service/
# 查看某 cgroup 的 CPU 统计
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
9. systemd-analyze:启动性能分析
# 总体启动时间分解
systemd-analyze
# 各 unit 启动耗时排行(最慢的在最上面)
systemd-analyze blame
# 关键路径(决定最终启动时间的依赖链)
systemd-analyze critical-chain
# 生成启动时序 SVG 图(可在浏览器打开)
systemd-analyze plot > boot.svg
# 检查 unit 文件语法
systemd-analyze verify /etc/systemd/system/myapp.service
# 分析特定 unit 的安全评分
systemd-analyze security nginx.service
优化建议: 用
systemd-analyze blame找出启动瓶颈后,检查是否可以:① 将After=network.target改为After=network-online.target(更精确等待网络就绪),② 对非必要服务设置WantedBy=multi-user.target而非Requires,③ 使用 socket 激活推迟服务启动。
10. 用户级 systemd
每个登录用户可以运行自己的 systemd 实例,管理用户级服务,无需 root 权限。unit 文件放在 ~/.config/systemd/user/。
# 用户级 systemctl(加 --user 标志)
systemctl --user start myservice
systemctl --user enable myservice
systemctl --user status myservice
# 查看用户级 journal
journalctl --user -u myservice -f
# 允许服务在用户注销后继续运行(默认注销时停止)
loginctl enable-linger $USER
# 用户级 unit 文件路径
mkdir -p ~/.config/systemd/user/
# 放置 unit 后需要 reload
systemctl --user daemon-reload
11. 实战:Go Web 服务的完整 systemd 配置
以下是一个生产就绪的 Go Web 服务 systemd 配置,包含安全加固、自动重启、资源限制和日志配置。
# /etc/systemd/system/goapp.service
[Unit]
Description=Go Web Application Server
Documentation=https://github.com/myorg/goapp
After=network-online.target postgresql.service redis.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=notify
User=goapp
Group=goapp
WorkingDirectory=/opt/goapp
# 二进制和配置
ExecStart=/opt/goapp/bin/goapp serve --config /etc/goapp/config.yaml
ExecReload=/bin/kill -s HUP $MAINPID
# 环境变量
Environment=GOMAXPROCS=4
EnvironmentFile=-/etc/goapp/env # - 前缀表示文件不存在时不报错
# 自动重启:非0退出码或信号退出时重启,最多5分钟内3次
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=300s
StartLimitBurst=3
# 安全加固
NoNewPrivileges=yes
PrivateTmp=yes # 独立 /tmp 目录
ProtectSystem=strict # 只读挂载 /usr /boot /etc
ProtectHome=yes # 无法读取家目录
ReadWritePaths=/var/lib/goapp /var/log/goapp
CapabilityBoundingSet=CAP_NET_BIND_SERVICE # 只允许绑定低端口
AmbientCapabilities=CAP_NET_BIND_SERVICE
# 资源限制
LimitNOFILE=65536
CPUQuota=400%
MemoryMax=1G
MemoryHigh=800M
TasksMax=256
# 标准输出直接进 journal
StandardOutput=journal
StandardError=journal
SyslogIdentifier=goapp
[Install]
WantedBy=multi-user.target
配套 timer:定时清理日志
# /etc/systemd/system/goapp-cleanup.timer
[Unit]
Description=GoApp log cleanup timer
[Timer]
OnCalendar=Sun *-*-* 02:00:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
---
# /etc/systemd/system/goapp-cleanup.service
[Unit]
Description=GoApp log cleanup
[Service]
Type=oneshot
User=goapp
ExecStart=/opt/goapp/scripts/cleanup-logs.sh
# 部署步骤
sudo cp goapp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now goapp.service
# 验证状态
sudo systemctl status goapp.service
sudo journalctl -u goapp.service -f
# 查看服务的 cgroup 资源占用
sudo systemd-cgtop -d 1
安全评分: 运行
systemd-analyze security goapp.service可得到沙箱安全评分(满分10分,分数越高越安全)。上述配置通过NoNewPrivileges/PrivateTmp/ProtectSystem/ProtectHome可达到约7分以上。
上一章
← 第12章:脚本工程化
下一章
第14章:性能分析 →