07-系统服务管理
上一篇《进程管理》解决的是运行时观察与处置问题:进程从哪里来、消耗了什么资源、处于什么状态、该发送什么信号,以及如何在事故中保留现场。但生产服务器不能依赖管理员登录后手工执行命令,也不能把长期任务寄托在 nohup、screen 或某个无人维护的 Shell 会话里。一个可交付的服务至少。

从进程走向可管理的服务
上一篇《进程管理》解决的是运行时观察与处置问题:进程从哪里来、消耗了什么资源、处于什么状态、该发送什么信号,以及如何在事故中保留现场。但生产服务器不能依赖管理员登录后手工执行命令,也不能把长期任务寄托在 nohup、screen 或某个无人维护的 Shell 会话里。一个可交付的服务至少要回答:谁来启动、何时启动、以什么身份运行、依赖谁、失败后如何处理、日志在哪里、停止时如何收尾、资源上限是多少、系统重启后是否恢复。
现代主流 Linux 发行版通常使用 systemd 作为系统与服务管理器。systemd 不只是“新的启动脚本工具”,它把服务单元、依赖关系、启动顺序、cgroup、日志、socket 激活、定时任务、资源控制和安全限制放在统一模型里。管理员面对的不再只是 PID,而是一个有配置、有状态、有生命周期、有审计入口的 unit。
本文是 Linux 系统管理系列第 7 篇,采用实战 walkthrough 与运维 runbook 相结合的方式,目标是建立一条从“读懂已有 unit”到“交付自定义服务”,再到“处理启动失败和反复重启”的完整路径。示例以常见 systemd 环境为主,具体指令和可用配置项仍应以目标发行版所带 systemd 版本为准。
通过本文,您将能够:
- 理解 unit、service、target、socket、timer、cgroup 和 journal 之间的关系。
- 正确使用
systemctl、journalctl、systemd-analyze管理和验证服务。 - 编写一个具备专用账号、环境文件、重启策略、资源限制和安全基线的服务单元。
- 区分依赖关系与启动顺序,避免滥用
After=、Requires=和network.target。 - 使用 drop-in 覆盖供应商 unit,避免直接修改包管理器维护的文件。
- 处理 unit 语法错误、权限问题、路径错误、端口冲突、超时、OOM、启动限速等故障。
- 使用 timer + oneshot service 建立可观察、可补跑的周期任务。
systemd 管理的不是一个命令,而是一组生命周期对象
图中将常见管理入口、unit、cgroup、进程和日志放在同一视角里。理解这些边界后,很多“服务明明有进程却显示失败”或“进程退出后为什么又被拉起”的现象就不再神秘。
systemd 使用 unit 描述需要管理的对象。unit 名称通常由名称和类型后缀组成,例如 sshd.service、backup.timer、app.socket、multi-user.target。不同 unit 类型承担不同职责:
| Unit 类型 | 典型作用 | 常见示例 |
|---|---|---|
.service |
启动和监督一个长期服务或一次性任务 | sshd.service、demo-api.service |
.socket |
监听 socket,按需激活关联服务 | systemd-journald.socket |
.timer |
按单调时间或日历时间触发服务 | apt-daily.timer |
.target |
聚合一组 unit,表达系统状态或同步点 | multi-user.target |
.path |
监视路径变化并触发服务 | 文件到达后触发导入 |
.mount / .automount |
管理挂载和按需挂载 | /etc/fstab 可被转换为 mount unit |
.slice / .scope |
组织 cgroup 和非 service 进程 | system.slice、登录会话 scope |
服务 unit 通常把进程放入自己的 cgroup。即使一个服务创建多个 worker,systemd 也能以 cgroup 为边界执行停止、统计资源和应用限制。这比保存单个 PID 文件更可靠,因为子进程不会轻易逃离服务边界。
unit 文件从哪里来
常见 unit 搜索路径包括:
| 路径 | 用途 | 运维建议 |
|---|---|---|
/usr/lib/systemd/system/ 或 /lib/systemd/system/ |
软件包提供的供应商 unit | 不直接修改,升级可能覆盖 |
/etc/systemd/system/ |
管理员创建的 unit 与 drop-in | 本地配置首选位置 |
/run/systemd/system/ |
运行时生成的临时 unit | 重启后通常消失 |
~/.config/systemd/user/ |
用户级 unit | 适合用户会话服务,不等同系统服务 |
查看实际加载来源:
systemctl show sshd.service -p FragmentPath -p DropInPaths
systemctl cat sshd.service
systemctl cat 会把主 unit 与所有 drop-in 一起展示,是排查“配置文件看起来正确,但运行行为不一致”的重要入口。不要只打开 /usr/lib/systemd/system/xxx.service,因为 /etc/systemd/system/xxx.service.d/*.conf 可能已经改变了行为。
unit 的加载、启用和运行是三个不同概念
这三个状态经常被混淆:
- loaded:systemd 能找到并解析 unit。
- enabled:unit 被接入某个启动目标,通常通过符号链接实现,表示未来启动时应该被拉起。
- active:unit 当前处于运行或完成状态。
因此:
systemctl is-enabled nginx.service
systemctl is-active nginx.service
systemctl is-failed nginx.service
可能得到“enabled 但 inactive”,例如服务被手工停止;也可能得到“disabled 但 active”,例如管理员手工启动了一个不随开机启动的服务。enable 不等于立即启动,start 也不等于开机启用。需要同时完成时使用:
sudo systemctl enable --now nginx.service
日常服务管理:先问 systemd 看到什么
状态查看不只看绿色或红色
systemctl status nginx.service --no-pager -l
systemctl show nginx.service \
-p LoadState -p ActiveState -p SubState -p Result \
-p MainPID -p ExecMainCode -p ExecMainStatus \
-p NRestarts
systemctl status 适合人工快速查看,systemctl show 适合脚本化取值。排障时重点关注:
LoadState:是否成功加载,还是 not-found、bad-setting、masked。ActiveState/SubState:总体状态与更具体的子状态。Result:上一次运行结果,如 exit-code、signal、timeout、oom-kill。ExecMainStatus:主进程退出码。NRestarts:systemd 已重启多少次。
不要只因为状态里出现 inactive (dead) 就认定异常。Type=oneshot 的一次性任务执行成功后可以正常回到 inactive;如果配置 RemainAfterExit=yes,也可以在命令退出后保持 active 状态。判断必须结合 unit 类型与预期生命周期。
start、stop、restart、reload 的边界
sudo systemctl start nginx.service
sudo systemctl stop nginx.service
sudo systemctl restart nginx.service
sudo systemctl reload nginx.service
sudo systemctl reload-or-restart nginx.service
restart会停止再启动,连接和内存状态通常会中断。reload只有 unit 定义了重载动作且程序支持时才有效,常用于重新读取配置。reload-or-restart在支持 reload 时优先重载,否则重启。
在生产环境中,应先使用程序自带的配置检查命令,再执行 reload。例如 Nginx 常见路径是:
sudo nginx -t
sudo systemctl reload nginx.service
systemctl status nginx.service --no-pager
journalctl -u nginx.service --since "-5 min" --no-pager
配置验证失败时不要继续 reload。命令返回 0 也不是完整验收,还要检查监听端口、健康接口、错误日志和业务请求。
mask 比 disable 更强
disable 只是移除开机启动关联,服务仍可被手工或依赖启动。mask 通常把 unit 链接到 /dev/null,阻止任何方式启动:
sudo systemctl disable demo.service
sudo systemctl mask demo.service
sudo systemctl unmask demo.service
mask 适合明确禁止某项服务的场景,例如冲突的网络管理服务或不允许启用的遗留组件。但它会让依赖该服务的其他 unit 失败,执行前要检查反向依赖:
systemctl list-dependencies --reverse demo.service
读懂 service unit 的关键段落
一个典型服务由 [Unit]、[Service]、[Install] 三部分组成:
[Unit]
Description=Demo API service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=demo
Group=demo
WorkingDirectory=/opt/demo
EnvironmentFile=-/etc/demo/demo.env
ExecStart=/usr/local/bin/demo-api --config /etc/demo/config.yaml
Restart=on-failure
RestartSec=5s
TimeoutStartSec=60s
TimeoutStopSec=30s
[Install]
WantedBy=multi-user.target
[Unit]:描述关系,不执行程序
Description=提供人类可读说明。Documentation=可链接到 man page 或内部文档。Wants=、Requires=表达需求关系。After=、Before=表达顺序关系。ConditionPathExists=等条件可以在条件不满足时跳过 unit。
[Service]:定义进程如何运行
Type=告诉 systemd 如何判断启动完成。User=、Group=指定运行身份。WorkingDirectory=设置工作目录。Environment=、EnvironmentFile=提供非秘密运行参数。ExecStart=定义主启动命令。ExecStartPre=、ExecStartPost=用于前后检查,但不应承载复杂编排。ExecReload=定义重载动作。Restart=、RestartSec=定义失败恢复。TimeoutStartSec=、TimeoutStopSec=定义启动与停止边界。
[Install]:定义 enable 时接入哪里
WantedBy=multi-user.target 表示执行 systemctl enable 时,创建指向该 unit 的关联,使其随多用户系统目标启动。[Install] 不参与当前正在运行的依赖计算;它主要指导 enable/disable 操作。
Type 选择决定 systemd 如何理解“启动成功”
| Type | systemd 的判断方式 | 适用场景 |
|---|---|---|
simple |
ExecStart 进程创建后即认为已启动 |
前台运行、不会 fork 的服务 |
exec |
成功执行目标二进制后认为已启动 | 比 simple 更能捕获执行文件不存在等错误 |
notify |
服务主动发送 READY 通知 | 支持 sd_notify 的服务 |
forking |
启动程序 fork,父进程退出后认为完成 | 传统 daemon,尽量配合可靠 PIDFile |
oneshot |
等待命令执行结束 | 初始化、备份、迁移、批处理 |
dbus |
获得指定 D-Bus 名称后认为已启动 | D-Bus 服务 |
新服务优先让程序以前台模式运行,并使用 Type=exec、simple 或 notify。不要为了“像传统 daemon”而让程序自行 fork、写 PID 文件、重定向日志;systemd 已经能完成进程监督和日志接收。错误选择 Type 会造成启动过早被判定成功、后续 unit 提前启动,或者 systemd 跟踪错主进程。
对于 shell 命令,ExecStart= 不是默认通过完整 shell 解析的。管道、重定向、通配符和复杂条件不能按交互式 Shell 的直觉书写。以下写法通常不符合预期:
ExecStart=/usr/bin/myapp > /var/log/myapp.log 2>&1
更好的方式是让应用写标准输出与标准错误,由 journal 接收;确实需要 shell 语法时,应显式调用 shell,并控制复杂度:
ExecStart=/bin/sh -c 'exec /usr/local/bin/myapp --mode batch'
长期维护的复杂逻辑应放进有版本管理、可单独测试的脚本,而不是堆在 unit 行里。
依赖与顺序:最容易写错的 systemd 配置
图中最重要的结论是:Wants=、Requires= 与 After=、Before= 属于两类不同关系。拉起谁和谁先启动不是同一件事。
Wants 与 Requires
Wants=database.service:启动当前 unit 时,希望同时拉起数据库;数据库失败不必然让当前 unit 被停止。Requires=database.service:需求关系更强;依赖未能启动时,当前 unit 通常也无法正常启动。
但它们不自动规定顺序。要表达“先尝试启动数据库,再启动应用”,通常还需要:
Requires=database.service
After=database.service
反过来,只有 After=database.service 并不会主动拉起数据库;它只在两者都参与同一事务时约束顺序。
network.target 不代表网络已经可用
network.target 更多表示网络管理栈已经进入某个启动阶段,不保证地址、路由、DNS 或外部依赖已经可达。确实需要等待网络配置完成的服务,常见做法是:
Wants=network-online.target
After=network-online.target
同时必须确保发行版对应的 wait-online 服务已正确启用和配置。即便如此,network-online.target 也不等于远端数据库或第三方 API 一定可用。分布式系统必须在应用层实现连接重试、退避、超时和健康检查,而不是试图用启动顺序消除所有运行时不确定性。
查看依赖图和关键启动链
systemctl list-dependencies demo-api.service
systemctl list-dependencies --reverse demo-api.service
systemd-analyze critical-chain demo-api.service
systemd-analyze blame | head -30
critical-chain 关注启动关键链,blame 按 unit 激活耗时排序。耗时长不一定是问题,例如等待硬件或网络的 unit 可能本来就需要时间;需要结合依赖关系和启动目标判断。
实战:交付一个自定义 API 服务
服务交付不是把 unit 文件复制进去就结束。图中把账号、目录、unit、验证、启用、业务检查与回滚串成一条可审计路径。
假设有一个二进制 /opt/demo/bin/demo-api,监听 127.0.0.1:8080,配置位于 /etc/demo/config.yaml,数据写入 /var/lib/demo。以下示例展示推荐基线,实际参数需按程序能力调整。
1. 创建专用运行账号和目录
sudo useradd \
--system \
--home-dir /var/lib/demo \
--shell /usr/sbin/nologin \
demo
sudo install -d -o root -g demo -m 0750 /etc/demo
sudo install -d -o demo -g demo -m 0750 /var/lib/demo
sudo install -d -o demo -g demo -m 0750 /var/log/demo
sudo chown -R root:root /opt/demo
sudo chmod -R go-w /opt/demo
二进制和代码应由 root 或部署系统持有,运行账号不应能修改自身可执行文件,否则一旦服务进程被利用,攻击者可持久化替换程序。数据目录则由运行账号拥有。配置文件是否允许服务账号读取,要按内容最小授权。
2. 准备环境文件
sudo install -o root -g demo -m 0640 /dev/null /etc/demo/demo.env
示例内容:
DEMO_LOG_LEVEL=info
DEMO_LISTEN=127.0.0.1:8080
systemd 的 Environment= 和 EnvironmentFile= 不应被当作高安全级别秘密存储。环境变量可能通过进程检查、错误报告或调试工具暴露。数据库密码、API token、私钥应优先使用专门的凭据机制、受限文件或企业秘密管理系统,并确保 unit 与应用的读取方式匹配。
3. 编写 unit
创建 /etc/systemd/system/demo-api.service:
[Unit]
Description=Demo internal API
Documentation=file:/opt/demo/README.md
Wants=network-online.target
After=network-online.target
[Service]
Type=exec
User=demo
Group=demo
WorkingDirectory=/var/lib/demo
EnvironmentFile=-/etc/demo/demo.env
ExecStart=/opt/demo/bin/demo-api --config /etc/demo/config.yaml
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=120s
StartLimitBurst=5
TimeoutStartSec=60s
TimeoutStopSec=30s
KillSignal=SIGTERM
LimitNOFILE=65536
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/demo /var/log/demo
[Install]
WantedBy=multi-user.target
这里的安全限制需要逐项验证:
ProtectSystem=strict让大部分文件系统对服务只读。ReadWritePaths=明确开放业务需要写入的目录。PrivateTmp=yes给服务独立的临时目录视图。NoNewPrivileges=yes阻止通过 exec 获得额外权限。ProtectHome=yes限制访问用户家目录。
如果程序需要写入其他路径、访问设备、使用特殊 capability 或读取用户目录,限制可能导致启动失败。正确做法是根据真实需求开放最小范围,而不是遇到失败就删除全部安全项。
4. 在加载前验证
sudo systemd-analyze verify /etc/systemd/system/demo-api.service
sudo -u demo test -x /opt/demo/bin/demo-api
sudo -u demo test -r /etc/demo/config.yaml
sudo -u demo test -w /var/lib/demo
systemd-analyze verify 可以发现未知指令、缺失依赖和部分语法问题,但不能证明应用配置、端口、数据库和业务逻辑都正确。还应使用程序自带的配置检查或前台测试模式。
5. 加载、启动和启用
sudo systemctl daemon-reload
sudo systemctl start demo-api.service
systemctl status demo-api.service --no-pager -l
journalctl -u demo-api.service -b --no-pager -n 100
确认启动成功后再启用开机启动:
sudo systemctl enable demo-api.service
systemctl is-enabled demo-api.service
在变更窗口中,把 start 与 enable 分开执行更容易控制风险:先证明当前版本能运行,再决定下次重启是否自动启动。
6. 验证服务而不是只验证 unit
systemctl is-active --quiet demo-api.service
ss -lntp 'sport = :8080'
curl --fail --silent --show-error http://127.0.0.1:8080/health
journalctl -u demo-api.service --since "-10 min" -p warning --no-pager
完整验证至少覆盖:
- unit 处于预期状态。
- 主进程与 cgroup 正常。
- 端口监听地址正确,没有意外暴露到
0.0.0.0。 - 健康接口返回成功。
- 日志没有持续错误。
- 下游依赖和真实业务请求可用。
- 重启一次后仍能恢复。
7. 回滚
如果新服务无法稳定运行:
sudo systemctl disable --now demo-api.service
sudo cp /etc/systemd/system/demo-api.service.previous \
/etc/systemd/system/demo-api.service
sudo systemctl daemon-reload
sudo systemctl start demo-api.service
回滚文件必须在变更前准备,不能到失败后才寻找。若二进制和配置也变化,应采用版本化发布目录与原子符号链接切换,例如 /opt/demo/releases/<version> 与 /opt/demo/current,并明确数据迁移是否可逆。
不要直接改供应商 unit:使用 drop-in
软件包升级会替换 /usr/lib/systemd/system/ 下的文件。直接修改看似快捷,却会制造难以审计的漂移。使用:
sudo systemctl edit nginx.service
系统会在 /etc/systemd/system/nginx.service.d/override.conf 创建 drop-in。示例:
[Service]
LimitNOFILE=131072
Restart=on-failure
RestartSec=3s
保存后:
sudo systemd-analyze verify nginx.service
sudo systemctl daemon-reload
sudo systemctl restart nginx.service
systemctl cat nginx.service
查看 unit 与供应商版本差异:
systemd-delta
systemd-delta nginx.service
清空列表型配置时要注意 systemd 的覆盖语义。某些可重复指令需要先赋空值再重新定义,例如:
[Service]
ExecStart=
ExecStart=/usr/sbin/example --new-config /etc/example/config.yaml
在不理解指令是否可重复、是否允许重置前,不要盲目覆盖 ExecStart=。修改后必须用 systemctl cat 查看最终合并结果。
重启策略:恢复故障,不制造重启风暴
常见策略:
| Restart 值 | 行为 | 适用建议 |
|---|---|---|
no |
不自动重启 | 一次性任务或需要人工判断的服务 |
on-failure |
非正常退出、信号、超时等情况下重启 | 多数长期服务的合理起点 |
on-abnormal |
信号、超时等异常退出时重启 | 不希望普通非零退出都重启 |
always |
无论退出原因都重启 | 必须长期驻留且正常退出也应恢复 |
重启策略不能替代根因修复。配置错误、凭据失效、端口冲突、依赖不可达时,快速重启只会放大日志、请求和下游压力。必须配合:
Restart=on-failure
RestartSec=10s
StartLimitIntervalSec=300s
StartLimitBurst=5
当启动次数超过限制时,unit 会进入 failed。修复根因后:
sudo systemctl reset-failed demo-api.service
sudo systemctl start demo-api.service
不要在没有修复的情况下循环执行 reset-failed。如果服务有多个实例或上游负载均衡,应先摘流、保留故障实例现场,再逐步恢复。
日志:unit 状态与 journal 必须一起看
常用查询:
journalctl -u demo-api.service -b
journalctl -u demo-api.service --since "2026-06-11 09:00:00"
journalctl -u demo-api.service -p warning
journalctl -u demo-api.service -f
journalctl _PID=1234
journalctl -xeu demo-api.service
关键选项:
-u按 unit 过滤。-b只看本次启动,-b -1看上一次启动。--since/--until限定事故窗口。-p按优先级过滤。-f持续跟踪。-o short-iso-precise输出精确时间,便于跨系统对齐。
事故中建议保存:
sudo journalctl -u demo-api.service \
--since "2026-06-11 09:00:00" \
--until "2026-06-11 10:00:00" \
-o short-iso-precise \
> demo-api-incident.log
日志可能包含 token、连接串、请求参数或用户数据,进入工单和知识库前必须脱敏。journal 的持久化、容量、轮转与转发将在后续《日志管理》章节中展开。
timer:把周期任务变成可观察的服务
timer 通常与一个同名 service 配对。timer 负责何时触发,service 负责执行什么;日志、退出码、超时、资源限制和失败状态都沿用 service 管理模型。
创建 oneshot service
/etc/systemd/system/demo-backup.service:
[Unit]
Description=Backup demo application data
[Service]
Type=oneshot
User=demo
Group=demo
ExecStart=/opt/demo/bin/backup --config /etc/demo/backup.yaml
TimeoutStartSec=30min
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
创建 timer
/etc/systemd/system/demo-backup.timer:
[Unit]
Description=Run demo backup every day
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15min
Persistent=yes
Unit=demo-backup.service
[Install]
WantedBy=timers.target
RandomizedDelaySec= 可避免大量主机在同一秒访问仓库、备份存储或控制平台。Persistent=yes 表示如果计划时间因关机错过,系统恢复后可补触发一次;是否适合补跑取决于任务幂等性和业务窗口。
验证日历表达式与 timer:
systemd-analyze calendar '*-*-* 02:30:00'
sudo systemd-analyze verify \
/etc/systemd/system/demo-backup.service \
/etc/systemd/system/demo-backup.timer
sudo systemctl daemon-reload
sudo systemctl enable --now demo-backup.timer
systemctl list-timers --all
先手工执行 service 验证任务:
sudo systemctl start demo-backup.service
systemctl status demo-backup.service --no-pager
journalctl -u demo-backup.service -n 100 --no-pager
定时任务必须考虑重复执行、补跑、并发、锁、超时、失败告警和输出产物校验。timer 负责触发,不会自动让脚本具备幂等性。
资源控制与安全加固
systemd 可以直接把资源限制映射到服务 cgroup。常见配置:
[Service]
CPUQuota=200%
MemoryHigh=1G
MemoryMax=1500M
TasksMax=512
LimitNOFILE=65536
CPUQuota=200%代表最多使用约两个 CPU 核心的时间配额。MemoryHigh=是压力阈值,超过后内核会加强回收。MemoryMax=是硬上限,触顶可能触发 cgroup 内 OOM。TasksMax=限制进程与线程数量。LimitNOFILE=设置文件描述符上限。
查看运行时数据:
systemctl show demo-api.service \
-p CPUUsageNSec -p MemoryCurrent -p MemoryPeak \
-p MemoryHigh -p MemoryMax -p TasksCurrent -p TasksMax
systemd-cgtop
不要未经压测就设置过紧上限。内存硬限制可能把一次流量高峰变成服务 OOM;CPU 限制可能放大延迟和超时。推荐先观测峰值,再设置预警阈值和保护上限。
安全分析:
systemd-analyze security demo-api.service
该命令提供暴露面评分和加固建议,但不是合规证明。加固指令必须结合应用功能验证,常见项包括:
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
网络访问、地址族、系统调用和 capability 也可以进一步限制,但越深入越需要测试。最稳妥的方法是从专用账号、只读系统目录、明确可写路径开始,逐步收紧并保留自动化验收。
故障排查 runbook
排障应从 systemd 的事实状态开始,再进入应用与依赖层。不要先反复 restart,因为重启会覆盖现场、刷新 PID 和滚动日志。
第一步:确认 unit 是否加载成功
systemctl status demo-api.service --no-pager -l
systemctl show demo-api.service -p LoadState -p FragmentPath -p DropInPaths
systemctl cat demo-api.service
sudo systemd-analyze verify demo-api.service
常见问题:
- unit 名称拼错或文件未放在搜索路径。
- 指令写错、段落错误、等号格式错误。
- 修改后未执行
daemon-reload。 - unit 被 mask。
- drop-in 覆盖了预期配置。
第二步:读取退出码与本次启动日志
systemctl show demo-api.service \
-p Result -p ExecMainCode -p ExecMainStatus -p MainPID
journalctl -u demo-api.service -b -n 200 --no-pager
systemd 文档为部分特殊退出状态提供了可读含义,例如执行文件不存在、用户无法解析、工作目录不可访问等。即使状态显示 203/EXEC 一类结果,也要结合日志和路径权限确认,不要只记数字。
第三步:以运行身份验证路径和命令
namei -l /opt/demo/bin/demo-api
sudo -u demo test -x /opt/demo/bin/demo-api
sudo -u demo test -r /etc/demo/config.yaml
sudo -u demo test -w /var/lib/demo
sudo -u demo /opt/demo/bin/demo-api --check-config /etc/demo/config.yaml
root 能执行不代表服务账号能执行。逐级目录缺少 x 权限、文件属组错误、SELinux/AppArmor 限制、只读保护和 ReadWritePaths= 不完整,都会造成权限问题。
第四步:检查环境、端口和依赖
systemctl show demo-api.service -p Environment -p EnvironmentFiles
ss -lntp 'sport = :8080'
getent hosts database.internal
timeout 3 bash -c '</dev/tcp/database.internal/5432'
不要把敏感环境变量完整输出到共享记录。依赖检查要分层:DNS 是否解析、路由是否存在、端口是否可达、TLS 是否成功、认证是否通过、数据库是否接受业务查询。
第五步:检查超时、资源限制和 OOM
systemctl show demo-api.service \
-p TimeoutStartUSec -p TimeoutStopUSec \
-p MemoryCurrent -p MemoryMax -p TasksCurrent -p TasksMax
journalctl -k -b | grep -Ei 'oom|killed process|out of memory'
如果启动期间需要迁移数据库、加载大模型或扫描大量文件,默认启动超时可能不合适。但简单延长超时只能缓解症状,更好的做法是拆分初始化任务、提供 readiness 通知或优化启动过程。
第六步:处理反复重启与启动限速
systemctl show demo-api.service \
-p NRestarts -p Restart -p RestartUSec \
-p StartLimitIntervalUSec -p StartLimitBurst
journalctl -u demo-api.service --since "-15 min" --no-pager
先停止重启风暴并保存现场:
sudo systemctl stop demo-api.service
修复配置、依赖或资源问题后:
sudo systemctl reset-failed demo-api.service
sudo systemctl start demo-api.service
第七步:验证恢复与重启后行为
systemctl is-active demo-api.service
systemctl is-enabled demo-api.service
curl --fail http://127.0.0.1:8080/health
journalctl -u demo-api.service --since "-5 min" -p warning
必要时在维护窗口进行一次受控重启,证明 enable、依赖、目录挂载、网络和凭据在系统启动路径中同样有效。
常见反模式
1. 直接修改 /usr/lib/systemd/system
升级后修改会丢失,也无法一眼区分供应商默认与本地策略。应使用 /etc/systemd/system 下的本地 unit 或 drop-in,并用 systemd-delta 审计。
2. 把 After= 当作健康检查
After=database.service 只约束启动顺序,不保证数据库已能处理业务请求。应用仍要实现重试、超时和连接恢复。
3. 所有服务都配置 Restart=always
配置错误会形成重启风暴。需要结合失败类型、退避时间、启动限速、告警和人工介入边界。
4. 在 unit 中放大量 Shell 编排
复杂管道、条件、重试和清理逻辑难以测试与复用。应把逻辑放入脚本或专门任务工具,unit 负责生命周期边界。
5. 使用 root 运行所有业务服务
root 权限会扩大应用漏洞影响。应使用专用系统账号、最小文件权限、明确可写目录和必要 capability。
6. 修改 unit 后只执行 restart
unit 文件变化需要 daemon-reload。如果只改应用配置,通常不需要 daemon-reload;如果改了 unit 或 drop-in,则需要重新加载管理器配置。
7. 只看 systemctl status 最后十行
状态摘要可能截断关键日志,也不会展示完整依赖、覆盖配置和内核 OOM。应结合 systemctl show、systemctl cat、journalctl、systemd-analyze verify 与应用检查。
生产交付与巡检清单
新服务上线前
- 是否使用专用账号,且账号不能交互登录。
- 二进制、配置、数据、日志目录的 owner 和权限是否分离。
- unit 是否通过
systemd-analyze verify。 Type=是否符合程序实际启动行为。- 是否明确启动、停止、重载和超时语义。
- 依赖与顺序是否分开表达。
- 重启策略是否有退避和启动限速。
- 是否设置合理资源上限并完成压测。
- 是否采用 drop-in 管理供应商 unit 修改。
- 是否有端口、健康检查、真实业务和重启恢复验证。
- 是否有版本化回滚路径。
- 日志是否进入可查询、可轮转、可告警的体系。
日常巡检
systemctl --failed
systemctl list-units --type=service --state=activating,deactivating
systemctl list-timers --all
systemd-delta
journalctl -p err -b
巡检重点不是追求零 failed,而是确认每个失败 unit 是否有归属、有影响判断、有处置记录。某些一次性硬件或可选组件失败可能不影响业务,但长期无人解释的失败会掩盖真正问题。
变更记录至少包含
| 项目 | 内容 |
|---|---|
| 变更对象 | unit 名称与主机范围 |
| 配置来源 | 主 unit、drop-in、环境文件、应用配置 |
| 变更前状态 | active/enabled、版本、端口、健康检查 |
| 执行动作 | verify、daemon-reload、reload/restart |
| 验证结果 | unit、日志、端口、业务指标 |
| 回滚条件 | 哪些现象触发回滚 |
| 回滚方式 | 配置、二进制、数据和启动状态恢复 |
三个生产案例:从现象回到服务边界
案例一:服务启动成功,但上游持续报告不可用
某内部接口完成版本发布后,systemctl status 显示服务为 active,主进程也存在,但负载均衡健康检查持续失败。管理员最初把问题归因于负载均衡缓存,连续重启两次服务仍未恢复。
正确的调查顺序是先确认“systemd 所说的成功”和“业务所说的可用”是否属于同一层。unit 使用 Type=simple,进程刚创建就被判定为启动成功;应用随后需要读取远程配置、建立数据库连接、执行缓存预热,约四十秒后才真正开始监听端口。发布系统在进程创建后立即把实例加入负载均衡,导致请求在准备阶段进入。
现场证据包括:
systemctl show demo-api.service \
-p ActiveEnterTimestamp -p MainPID -p Type
journalctl -u demo-api.service -o short-iso-precise \
--since "-10 min"
ss -lntp 'sport = :8080'
时间线显示 unit 进入 active 与端口开始监听之间存在明显间隔。修复不是简单增加 RestartSec,而是重新定义就绪边界:
- 如果应用支持 systemd 通知,改为
Type=notify,在完成初始化后发送就绪通知。 - 如果应用暂不支持通知,发布系统必须等待健康接口通过后再接入流量。
- 把数据库迁移从服务启动路径拆出,作为独立 oneshot unit 在发布前执行。
- 健康检查区分存活与就绪:存活用于判断进程是否需要重启,就绪用于判断实例是否应该接收流量。
- 为启动耗时建立指标,超过历史基线时报警,而不是只看启动是否最终成功。
这个案例说明,systemd 负责进程生命周期,但不会自动理解业务就绪。服务管理设计必须把进程启动、端口监听、依赖可用和业务接流量拆成可验证阶段。
案例二:配置错误触发重启风暴,掩盖了最初错误
某采集代理更新配置后立即退出,unit 配置了 Restart=always 和很短的重启间隔。数分钟内产生数千条重复日志,远端认证接口收到大量失败请求,最初那条最有价值的语法错误被日志滚动覆盖。监控只报告“进程重启次数上升”,值班人员不断执行 restart,进一步放大问题。
处理时应先停止自动重启,保留当前配置和最近日志:
sudo systemctl stop collector.service
sudo journalctl -u collector.service \
--since "-20 min" -o short-iso-precise \
> /tmp/collector-incident.log
systemctl cat collector.service > /tmp/collector-unit.txt
随后以服务账号运行配置检查:
sudo -u collector /opt/collector/bin/collector \
--check-config /etc/collector/config.yaml
根因是一处缩进错误,程序以明确非零退出码结束。改进措施包括:
- 发布前强制运行配置验证,验证失败则禁止进入重启步骤。
- 把
Restart=always改为符合程序语义的on-failure,并设置合理退避。 - 设置启动限速,让持续失败进入稳定的 failed 状态并触发告警。
- 日志告警优先保留每轮启动的第一条错误,而不是只统计重复数量。
- 配置文件进入版本管理,发布记录保存变更前后校验值。
- 回滚流程同时恢复配置与 unit,不允许只回滚二进制。
一个可靠的服务不应该无限快速地重试确定性错误。自动恢复适合瞬时故障;对于语法、权限和凭据这类持续故障,系统应尽快停止、清晰告警并等待修复。
案例三:服务停止超时,强制终止后留下不一致数据
某批处理服务在维护窗口执行 restart。systemd 发送终止信号后等待三十秒,进程未退出,于是强制终止。服务重新启动后发现一个作业长期处于“处理中”,数据库记录与对象存储产物不一致。
调查发现程序收到终止信号后会停止接收新任务,但正在处理的大文件通常需要两到五分钟才能完成。unit 使用默认停止超时,且应用没有在超时前写入可恢复检查点。管理员过去把偶发的强制终止当作普通现象,没有关联到数据一致性风险。
改进需要同时修改应用与 unit:
- 应用收到终止信号后立即进入排空状态,对外就绪检查返回失败,避免新任务进入。
- 正在执行的任务定期写入检查点,进程重启后能够恢复或明确回滚。
- 根据真实任务时长设置停止超时,而不是无限延长。
- 超过正常排空时间时触发告警,提示可能存在卡死的外部依赖。
- 在负载均衡或任务调度层先摘除实例,再执行服务停止。
- 对强制终止事件建立审计,任何一次
SIGKILL都需要确认数据完整性。
unit 可以设置:
[Service]
TimeoutStopSec=5min
KillSignal=SIGTERM
SendSIGKILL=yes
延长超时不是核心答案。真正的目标是让程序具备可预测的终止协议:停止接收、完成或保存当前工作、关闭连接、刷新缓冲区、退出并返回明确状态。systemd 只是执行这份协议并在协议失效时提供最终边界。
从 SysV 脚本迁移到 systemd 的实施方法
老旧环境常见 /etc/init.d/example 脚本,脚本内部自行 fork、维护 PID 文件、重定向日志并实现 start、stop、restart。迁移不能只把启动命令复制到 ExecStart=,而要先识别脚本真正承担的职责。
第一步:拆解原脚本
需要记录:
- 程序以哪个用户运行。
- 启动前创建哪些目录、修改哪些权限。
- 从哪些文件读取环境变量。
- 是否切换工作目录或设置资源限制。
- 程序是否 fork,PID 文件由谁创建。
- 日志写到文件还是标准输出。
- 停止时发送什么信号,等待多久。
- restart 是否包含配置检查。
- 脚本依赖哪些网络、挂载和数据库资源。
不要把脚本中的所有动作都原样搬进 ExecStartPre=。目录创建可由 tmpfiles 机制或部署过程负责,日志可进入 journal,资源限制可由 unit 声明,权限可在安装阶段完成。迁移是职责重构,不是语法翻译。
第二步:让程序以前台方式运行
如果程序支持 --foreground、--no-daemon 或类似参数,应优先使用。这样 systemd 可以直接跟踪主进程,避免 PID 文件陈旧、fork 层次变化和错误主进程识别。
无法取消 daemon 模式时,才考虑 Type=forking 与 PIDFile=。PID 文件目录应在启动前可靠创建,文件内容必须指向真正主进程,停止后要清理。迁移完成后要验证主进程退出、子进程处理和 cgroup 清理是否符合预期。
第三步:建立等价验收
迁移前后都执行同一组测试:
| 测试场景 | 验收点 |
|---|---|
| 正常启动 | 端口、日志、健康接口、依赖连接 |
| 重复启动 | 不产生多个实例,不覆盖有效 PID |
| 正常停止 | 完成排空,进程和子进程全部退出 |
| 强制故障 | 返回明确失败,按策略重启或停止 |
| 系统重启 | 按依赖顺序恢复,不依赖人工操作 |
| 配置错误 | 启动失败信息清晰,不形成重启风暴 |
| 日志轮转 | 不丢失关键日志,不长期持有已删除文件 |
第四步:保留回退窗口
在确认新 unit 稳定前,不要立即删除旧脚本。可先禁用旧启动入口,保留经过校验的回退包,并明确只能由一套机制管理服务,避免 systemd 与旧脚本同时拉起两个实例。迁移完成后清理遗留 cron、rc.local、启动脚本和人工运维文档,否则未来故障中可能有人继续使用旧入口。
服务治理成熟度:从“能启动”到“可运营”
服务管理可以按四个阶段评估:
阶段一:能启动
服务有 unit,能够启动、停止并随系统恢复。这个阶段解决了手工后台进程问题,但通常缺少专用账号、资源限制和业务验证。
阶段二:可诊断
服务有统一日志、明确退出码、配置检查、健康接口、启动耗时和重启次数指标。值班人员能够从 unit 状态快速进入应用证据,而不是依赖个人经验。
阶段三:可恢复
服务具备合理重启策略、退避、启动限速、优雅停止、数据恢复、版本回滚和依赖降级。故障处置可以按 runbook 执行,减少临场试错。
阶段四:可治理
unit、drop-in、环境文件和安全策略由配置管理统一维护;变更通过代码审查和自动验证;资源限制基于容量数据;服务账号、目录权限、日志保留和告警策略有统一标准;所有偏离基线的配置都能被发现和解释。
成熟度提升不要求一次完成所有加固。优先顺序通常是:消除手工启动,建立专用身份和日志入口,补齐健康与回滚,再逐步增加资源、安全和自动化治理。每增加一个限制都应有明确目的、验证方法和故障时的识别路径。
总结
系统服务管理的核心不是记住更多 systemctl 命令,而是把长期运行的软件纳入明确生命周期。unit 描述对象,依赖与顺序描述启动关系,service 段定义运行身份与监督方式,cgroup 提供进程边界,journal 提供日志入口,timer 提供周期触发,资源与安全指令提供保护边界。
关键要点回顾:
- loaded、enabled、active 是不同状态,排障时必须分别确认。
- 新服务优先以前台方式运行,由 systemd 监督,不依赖自行 fork 和 PID 文件。
Wants=、Requires=表达需求,After=、Before=表达顺序,两者不能互相替代。- 网络启动目标不能替代应用层重试和健康检查。
- 供应商 unit 使用 drop-in 覆盖,不直接修改包管理器维护的文件。
- 重启策略必须配合退避、启动限速和告警,避免重启风暴。
- 自定义服务上线要经过账号、目录、语法、权限、启动、端口、日志、业务和回滚验证。
- timer 与 oneshot service 可以把周期任务纳入统一的状态、日志和失败管理。
- 资源控制和安全加固要从真实需求出发,逐项验证,不能一次性盲目收紧。
- 服务故障应先保存 unit 状态与日志,再修改配置或重启。
下一篇《网络配置》将继续沿着运行治理路径,系统讲解 NetworkManager、systemd-networkd、Netplan、地址、路由、DNS、VLAN、bond、多网卡和网络故障排查。
参考资料
- systemd.unit
- systemd.service
- systemd.exec
- systemd.resource-control
- systemd.timer
- systemctl
- journalctl
- Red Hat Enterprise Linux 9: Managing systemd
本文最后更新于 2026-06-11