第 13 章

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 内核设备暴露

依赖关系关键词

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 如何判断服务"就绪":

[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章:性能分析 →
本章评分
4.7  / 5  (21 评分)

💬 留言讨论