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 文件本身通常在三个目录里。注意,它们的优先级顺序很重要:低编号目录会覆盖高编号目录里的同名配置。
/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% 的场景,你其实只需要下面这几类命令:
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 文件。你不告诉它缓存已经过期,它就继续拿旧版本跑,排半天障都像撞鬼。
sudo systemctl daemon-reload # after any unit file edit
sudo systemctl restart <unit> # daemon-reload alone doesn't restartUnit 文件长什么样
一个 service 文件大概长这样。结构是固定的,真正变化的是里面那些 directive。
[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 怎么理解“服务已经启动”。这个选错了,行为会非常迷惑。
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
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 开启的。大多数生产服务,只加几行就能明显收紧权限:
[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 设上限:
[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 能做的事,而且通常更好管:
[Unit]
Description=Run myapp-cleanup every 15 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
Persistent=true
[Service]
Type=oneshot
ExecStart=/usr/local/bin/myapp-cleanupsudo 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。
[Timer]
RandomizedDelaySec=120不然每天 09:00 整,所有机器一起打你的共享后端。那不是自动化,那是集体冲塔。
User Services
systemd 也能管 user-level services,每个用户都有自己的 systemd 实例。桌面应用想开机启动可以用它,服务器管理员想把个人 daemon 从 /etc 里拿出去,也可以用它。
systemctl --user status # user instance
systemctl --user enable myapp.timerUser service 放在 ~/.config/systemd/user/。但它默认不一定能跨 logout 存活,要靠 loginctl enable-linger <user>。
在 headless server 上,user manager(用户级 systemd 的 PID 1)通常只在用户登录期间运行。想让 user service 没有人登录也继续跑,就开启 lingering:
sudo loginctl enable-linger myuser之后,--user 服务会从 boot 阶段就开始运行。这种方式很适合部署不需要 root 权限的长期服务,干净,也少碰系统级配置。
Targets(启动里程碑)
target 本质上就是一组 unit。启动流程里常见的是这些:
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:
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 和恢复
服务起不来时,先别猜,直接看这些:
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 treesystemd-analyze verify 被低估得很厉害。它能在你真正启动服务之前,抓出二进制不存在、环境变量写错、依赖关系不一致这些问题。上线前跑一下,省很多“为什么刚才还好好的”的时间。
服务一直失败时,可以先清掉 failed 状态:
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:
# 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 迁移
如果你脑子里还留着老发行版那套知识,可以先对应着看:
# 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 文件,你可以直接改:
# /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部署流程:
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:
- systemd official docs — Lennart 的项目站点
- systemd.directives — 所有 directive 的一行解释
- systemd-analyze — security、verify、blame、critical-chain 这些子命令
- Practical systemd — Red Hat Enterprise Linux 9 文档,覆盖生产默认配置
评论