systemd:Linux 运维的一线实战参考

2015 年之后,大多数 Linux 发行版默认都用 systemd 做 init 系统。它替掉了老一代 SysV init scripts,也顺手把配置模型、依赖处理、日志、user service 管理这些东西塞进了一套工具链里。

这篇不是 systemd 架构导览。它更像一张干活用的现场备忘录:服务器上真会碰到的命令、unit 文件、日志、限制、排障,放在一起说清楚。

心智模型

systemd 最核心的概念是 unit(单元)。它管理的东西,基本都可以叫 unit:

  • Service (.service) — 长期运行的进程,也是你 90% 时间会摸的东西
  • Socket (.socket) — 监听端口;连接进来时触发服务
  • Mount/Automount (.mount, .automount) — 文件系统挂载点
  • Path (.path) — 监听文件或目录变化;变化后跑一个服务
  • Timer (.timer) — cron 的现代替代品
  • Target (.target) — target(目标状态),一组 unit;比如 multi-user.target 这种启动里程碑
  • Slice/Scope (.slice, .scope) — cgroup 资源分组

unit 文件本身通常在三个目录里。注意,它们的优先级顺序很重要:低编号目录会覆盖高编号目录里的同名配置。

Bash
/lib/systemd/system/        # the OS vendor puts files here — don't edit
/etc/systemd/system/         # admin overrides (your custom units)
/run/systemd/system/        # runtime, ephemeral — survives reboot? no.

经验规则很简单:发行版自带的文件留在 /lib,你自己的修改放到 /etc。如果真要改 vendor 文件,先复制到 /etc 再改副本。不然下一次包升级,辛辛苦苦改的内容就会被覆盖,现场很难看。

速查表

80% 的场景,你其实只需要下面这几类命令:

Bash
systemctl status <unit>      # what's the current state, last few log lines
systemctl start   <unit>     # start it now (does NOT survive reboot)
systemctl stop    <unit>
systemctl restart <unit>
systemctl reload  <unit>     # ask the daemon to reload config without restart
sudo systemctl enable  <unit>   # start on boot — creates symlinks
sudo systemctl disable <unit>   # remove from boot chain
journalctl -u <unit> -f      # tail logs for that unit

头号大坑:改了 unit 文件,忘了跑 systemctl daemon-reload。systemd 会缓存解析后的 unit 文件。你不告诉它缓存已经过期,它就继续拿旧版本跑,排半天障都像撞鬼。

Bash
sudo systemctl daemon-reload   # after any unit file edit
sudo systemctl restart <unit>  # daemon-reload alone doesn't restart

Unit 文件长什么样

一个 service 文件大概长这样。结构是固定的,真正变化的是里面那些 directive。

Ini
[Unit]
Description=My custom app on port 8080
Documentation=https://example.com/docs
After=network-online.target
Requires=network-online.target
Wants=

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/python3 /srv/myapp/main.py
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=/etc/myapp/env
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

按 section 看

Section 用途
[Unit] 顺序提示、依赖关系、描述
[Service] 进程怎么跑,文件主体通常都在这里
[Install] enable 时由哪个 target 把它拉进启动链

After= 不是硬依赖,只是启动顺序。就算 network.target 没准备好,After= 也只是让服务先等一下,并不会保证依赖真的启动成功。需要真正带失败语义的启动关系,用 Requires=Wants=

  • Requires= — 硬依赖。被要求的 unit 失败,这个 unit 也跟着失败。
  • Wants= — 软依赖。想要的 unit 成不成功,这个 unit 都会继续跑。可选服务一般用它。

[Install] 里的 WantedBy=enable 能工作的关键。WantedBy=multi-user.target 的意思是:“系统到达 multi-user target(非图形启动)时,把这个服务放进启动链。” 99% 的普通服务都用它。

Service Types

Type= 决定 systemd 怎么理解“服务已经启动”。这个选错了,行为会非常迷惑。

Ini
Type=simple    # default. Process must stay foreground. Most use this.
Type=forking   # forks daemonizes. systemd considers started when parent exits.
Type=oneshot   # runs to completion and exits. Good for setup scripts.
Type=notify    # service tells systemd it's ready via sd_notify().
Type=dbus      # service gets a D-Bus name when ready.
Type=idle      # like simple, but delays until other jobs finish.

Type=simple 是默认值,绝大多数场景都够用。老式 daemon 会 fork 到后台,比如老 nginx binary、传统 postgres 这类,才考虑 Type=forking。如果 daemon 支持 sd_notify(3),优先用 Type=notify,这是新服务更现代、更靠谱的方式。

一个很常见的坑:给不会 fork 的进程写了 Type=forking。systemd 会一直等父进程退出,但父进程根本不退出。于是服务永远卡在 “activating (start)”,看起来像半死不活。

Restart 和 Watchdog

Ini
Restart=no
Restart=on-success
Restart=on-failure
Restart=on-abnormal
Restart=always
RestartSec=5
StartLimitBurst=5
StartLimitIntervalSec=60

通常你想要的是 Restart=on-failure。如果服务正常退出,返回码是 0,systemd 不会重启它。因为这通常表示服务是有意关闭的,不是崩了。

RestartSec= 是两次重启之间的等待时间。没有它,崩溃服务会原地高速重试,然后把日志刷爆。大多数服务用 5 秒做默认值就挺合理

StartLimitBurst=StartLimitIntervalSec= 合起来就是重启限速:60 秒内最多重启 5 次。超过以后,服务会保持 failed 状态,别再指望它自己活过来,该你上去看日志了。

Security Hardening:先拿容易的收益

systemd 自带 sandbox,而且是按 unit 开启的。大多数生产服务,只加几行就能明显收紧权限:

Ini
[Service]
PrivateTmp=true          # service can't see the system /tmp
ProtectSystem=strict     # /usr and /boot read-only; only /var, /etc/myapp writable
ProtectHome=true         # /home, /root invisible to this service
NoNewPrivileges=true     # service can't escalate via SUID binaries
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

对于任何面向网络、你又不敢完全信任的服务,这算是一个比较正常的默认值。可以用 systemd-analyze security myapp.service 测一下,它会给 unit 打分:1.0 是很松,9.0 是偏执。生产上先冲着 6+ 去,别一上来就追求满分把自己锁死。

这些 hardening directive 是累加的:每一条都拿掉一部分 capability 或 syscall。顺序不重要。服务如果确实需要某些能力,再用对应的 Allow= 类配置精确放回来,别整套裸奔。

资源限制

systemd 把所有东西都跑在 cgroup 里。所以 CPU、内存、IO 这些,都可以按 unit 设上限:

Ini
[Service]
CPUQuota=50%                # 50% of one CPU
MemoryMax=512M              # OOM kill if exceeded
MemoryHigh=384M             # soft pressure threshold (slow down earlier)
IOWeight=500                # 1-10000, default 100, this is medium
IOReadBandwidthMax=/dev/sda 50M
TasksMax=100                # max number of threads/processes
LimitNOFILE=65536

这东西特别适合压住 noisy neighbor。单机上跑多租户、多业务、多条后台任务的时候,给每个服务设资源上限,不然总有一个服务会把整台机器拖下水。

Timers:现代 Cron 替代品

systemd timer 能做 cron 能做的事,而且通常更好管:

Ini
[Unit]
Description=Run myapp-cleanup every 15 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
Persistent=true

[Service]
Type=oneshot
ExecStart=/usr/local/bin/myapp-cleanup

Bash
sudo systemctl enable --now myapp-cleanup.timer

套路是:一个 .timer unit 触发一个 .service unit。timer 对应的 .service 通常写 Type=oneshot,也就是跑完就退出,而不是常驻后台。

想写 cron 风格的时间,用 OnCalendar=,比如 OnCalendar=Mon..Fri 09:00 America/Los_Angeles。它也可以和相对时间一起组合用,灵活度比传统 crontab 高不少。

计划任务的最佳实践:永远加一点随机延迟,避免 thundering herd

Ini
[Timer]
RandomizedDelaySec=120

不然每天 09:00 整,所有机器一起打你的共享后端。那不是自动化,那是集体冲塔。

User Services

systemd 也能管 user-level services,每个用户都有自己的 systemd 实例。桌面应用想开机启动可以用它,服务器管理员想把个人 daemon 从 /etc 里拿出去,也可以用它。

Bash
systemctl --user status    # user instance
systemctl --user enable myapp.timer

User service 放在 ~/.config/systemd/user/。但它默认不一定能跨 logout 存活,要靠 loginctl enable-linger <user>

在 headless server 上,user manager(用户级 systemd 的 PID 1)通常只在用户登录期间运行。想让 user service 没有人登录也继续跑,就开启 lingering:

Bash
sudo loginctl enable-linger myuser

之后,--user 服务会从 boot 阶段就开始运行。这种方式很适合部署不需要 root 权限的长期服务,干净,也少碰系统级配置。

Targets(启动里程碑)

target 本质上就是一组 unit。启动流程里常见的是这些:

Bash
sysinit.target        → early kernel/userspace
basic.target          → sysinit + some boot plumbing
multi-user.target     → networking, login, all the rest (default)
graphical.target      → multi-user + display manager

修改默认 boot target:

Bash
sudo systemctl set-default multi-user.target   # console boot
sudo systemctl set-default graphical.target    # default
sudo systemctl isolate rescue.target           # jump to single-user

应急恢复时可以用 isolate。它会杀掉不在目标 target 树里的所有 unit。比如登录管理器坏了、图形界面进不去,跳到救援目标修东西就很有用。

debug 和恢复

服务起不来时,先别猜,直接看这些:

Bash
sudo systemctl status myapp.service        # see why it failed
sudo journalctl -u myapp.service --since "1 hour ago"
sudo journalctl -u myapp.service -n 200    # last 200 lines
sudo journalctl -f -u myapp.service        # follow new
sudo systemctl show myapp.service          # all properties as key=value
sudo systemd-analyze verify myapp.service  # static analysis, catches typos
sudo systemd-analyze blame                 # what took the longest at boot
sudo systemd-analyze critical-chain myapp.service  # full dependency tree

systemd-analyze verify 被低估得很厉害。它能在你真正启动服务之前,抓出二进制不存在、环境变量写错、依赖关系不一致这些问题。上线前跑一下,省很多“为什么刚才还好好的”的时间。

服务一直失败时,可以先清掉 failed 状态:

Bash
sudo systemctl reset-failed myapp.service   # clear failure state, allow enable again

遇到很深的 boot 问题,可以通过 GRUB_CMDLINE_LINUX 给 kernel command line 加上 systemd.log_level=debug,然后重启。kernel log 会记录每个 unit 的状态切换,信息量很大,但救命时很好用。

Templates:一个文件,多份实例

有些服务会有很多相似实例,比如数据库 shard、按用户区分的 app 实例。这个时候用 unit template:

Bash
# Generic file: myapp@.service
[Service]
ExecStart=/usr/local/bin/myapp --id=%i
WorkingDirectory=/var/lib/myapp/%i

# Spawn concrete instances:
sudo systemctl enable myapp@1.service
sudo systemctl enable myapp@2.service

%i 会替换成实例名。常见用途包括:按用户划分的 XDG 目录(systemd user services)、PostgreSQL replication slot、RabbitMQ node。Postgres 的 deb 包里就有 postgresql@12-main.service 这一类东西,用的就是这个模式。

从 SysV init 迁移

如果你脑子里还留着老发行版那套知识,可以先对应着看:

Bash
# See a runlevel's services (legacy views)
sudo systemctl list-units --type=service --all
systemctl list-dependencies multi-user.target   # what runs at boot

# Convert: systemctl mask prevents ALL start including manual
sudo systemctl mask ntp.service    # symlinks to /dev/null

# Disable old /etc/rc.local
sudo systemctl status rc-local.service

现代发行版有时还会带 /etc/rc.local,包成一个 rc-local.service,主要是照顾怀旧用户。别再用它了。写一个真正的 .service.timer,清楚、可审计、也更容易排障。

实战配方:部署一个自定义 App

把前面的东西拼起来,就是一个小但真实可用的 service 文件,你可以直接改:

Ini
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp production server
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp
ExecStart=/srv/myapp/bin/myapp --config=/etc/myapp/config.toml
ExecReload=/bin/kill -HUP $MAINPID

Restart=on-failure
RestartSec=5

# Hardening
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true

# Resources
CPUQuota=80%
MemoryMax=1G
LimitNOFILE=65535

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Environment
EnvironmentFile=/etc/myapp/env

[Install]
WantedBy=multi-user.target

部署流程:

Bash
sudo install -m644 /tmp/myapp.service /etc/systemd/system/myapp.service
sudo install -d -omyapp -gmyapp -m750 /srv/myapp
sudo install -d -omyapp -gmyapp -m750 /etc/myapp
sudo install -m640 /tmp/env /etc/myapp/env      # mode 640, not 644
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service
sudo journalctl -u myapp.service -f

这就是完整 deploy flow。没有 forking init script,没有 service myapp start,也没有 update-rc.d。少一点历史包袱,现场就少一点玄学。

快速术语表

  • Unit — systemd 管理的任何东西,比如 .service、.socket、.timer 等
  • Target — 一组 unit,类似老 init 里的 runlevel
  • Active/Enabled — Active 是“现在正在跑”,Enabled 是“开机会启动”
  • Mask — 通过 mask 阻止启动;就算 unit 文件还在也不让起
  • Cgroup — systemd 用来做资源限制和隔离的 kernel 功能
  • journal — systemd 的结构化日志
  • Slice — 由 cgroup 定义的层级;适合把相关服务分组
  • Scope — 给外部进程用的临时 cgroup,比如 Docker、libvirt

Resources:

来源:tux.fan 实战经验(参考 systemd 官方文档 + RHEL 9 docs)

最后修改: 2026年7月11日

作者

评论

发表评论

您的邮箱地址不会被公开。