FAQ-8:服务与 systemd 常见问题

预计阅读时间:11 分钟

📖 目录

FAQ-8:服务与 systemd 常见问题(Q93-Q103)

问题速查表

问题章节
Q93: 如何查看服务启动失败原因?Q93
Q94: 如何让脚本开机自启?Q94
Q95: 服务状态 masked?Q95
Q96: 如何修改 service 环境变量?Q96
Q97: 如何限制 service 资源?Q97
Q98: 服务依赖关系怎么配置?Q98
Q99: 如何查看服务依赖?Q99
Q100: 如何创建 systemd Timer?Q100
Q101: 如何不重启服务重载配置?Q101
Q102: 如何临时覆盖 service 参数?Q102
Q103: 如何查看 service 文件完整内容?Q103

Q93: 如何查看服务启动失败原因?

# systemctl status:查看服务状态和最近日志
systemctl status nginx
# 输出包含:Active 状态、最近几行日志、PID

# journalctl:查看服务的完整日志
journalctl -u nginx -n 50    # 最近 50 行
journalctl -xe               # 查看最近的错误日志
# -x:显示解释文本  -e:跳转到末尾

Q94: 如何让脚本开机自启?

# 创建 systemd 服务文件
sudo vim /etc/systemd/system/myscript.service

# 最小配置:
# [Unit]
# Description=My custom script
# After=network.target
#
# [Service]
# Type=oneshot
# ExecStart=/path/to/script.sh
# RemainAfterExit=yes
#
# [Install]
# WantedBy=multi-user.target

# 重新加载并启用
sudo systemctl daemon-reload
sudo systemctl enable myscript    # 创建开机符号链接
sudo systemctl start myscript     # 立即启动

Q95: 服务状态 masked?

masked 是 systemd 的最高级别禁用,即使 systemctl start 也无法启动。

# 查看服务状态
systemctl status nginx
# 显示:Loaded: masked (/dev/null; disabled)

# 取消 masked
sudo systemctl unmask nginx

# masked vs disabled 的区别:
# masked:完全禁止启动(start 也无效)
# disabled:不自动启动,但可以手动 start

Q96: 如何修改 service 环境变量?

# 方法一:在 unit 文件中直接设置
# [Service] 段中添加:
Environment="VAR=value"
Environment="PATH=/usr/local/bin:/usr/bin"

# 方法二:使用环境变量文件(推荐,敏感信息不写入 unit 文件)
# [Service] 段中添加:
EnvironmentFile=/etc/default/myapp

# /etc/default/myapp 文件内容:
# VAR=value
# DB_PASSWORD=secret

Q97: 如何限制 service 资源?

# 在 [Service] 段中添加资源限制
# MemoryMax=512M    # 最大内存使用
# CPUQuota=50%      # CPU 使用率上限(50% = 0.5 核)
# TasksMax=100      # 最大进程/线程数
# IOWeight=50       # I/O 权重

# 临时查看服务资源使用
systemd-cgtop

Q98: 服务依赖关系怎么配置?

# 在 [Unit] 段中配置依赖
# After=network.target      # 在 network 之后启动
# Requires=mysql.service    # 强依赖:mysql 失败则自己也失败
# Wants=redis.service       # 弱依赖:redis 失败不影响自己

# Requires vs Wants:
# Requires:硬依赖,一个失败全部停止
# Wants:软依赖,失败不影响

Q99: 如何查看服务依赖?

# 正向依赖:该服务依赖谁
systemctl list-dependencies nginx

# 反向依赖:谁依赖该服务
systemctl list-dependencies --reverse nginx

# 以树状显示
systemctl list-dependencies nginx --all

Q100: 如何创建 systemd Timer?

systemd Timer 是 cron 的现代替代,支持更灵活的调度和依赖管理。

# 创建 timer unit 文件
# [Timer]
# OnCalendar=*-*-* 02:30:00    # 每天 2:30 执行
# Persistent=true               # 错过的执行会在启动后补上

# 启用并启动 timer
sudo systemctl enable --now mytask.timer

# 查看 timer 状态
systemctl list-timers

# 对比 cron:
# cron:简单、轻量,但日志分散
# systemd timer:集成日志、资源限制、依赖管理

Q101: 如何不重启服务重载配置?

# systemctl reload:发送 SIGHUP 或调用服务的 reload 命令
sudo systemctl reload nginx

# 注意:只有支持 reload 的服务才能用
# 查看服务是否支持 reload
systemctl show nginx -p CanReload
# 输出:CanReload=yes

Q102: 如何临时覆盖 service 参数?

# 创建 override 文件(不修改原始 unit 文件)
sudo systemctl edit nginx

# 这会打开编辑器,在 drop-in 目录中创建 override.conf
# 添加需要的配置,如:
# [Service]
# MemoryMax=1G

# 重新加载 systemd 并重启服务
sudo systemctl daemon-reload
sudo systemctl restart nginx

Q103: 如何查看 service 文件完整内容?

# cat:显示 unit 文件内容(包含所有片段)
systemctl cat nginx

# 查看 unit 文件的加载路径
systemctl show nginx -p FragmentPath
# 输出:FragmentPath=/lib/systemd/system/nginx.service

真实案例

案例 A:服务 masked 导致 systemctl start 无效

现象:尝试启动 nginx 服务,systemctl start nginx 无任何报错但服务没起来。

$ systemctl start nginx
$ systemctl status nginx
● nginx.service
   Loaded: masked (/dev/null; disabled; vendor preset: enabled)
   Active: inactive (dead)

根因:之前执行过 systemctl mask nginx,mask 会创建 /dev/null 的符号链接,完全禁止服务启动。

修复sudo systemctl unmask nginx 取消 masked 状态。

案例 B:systemd timer 的 OnCalendar 格式错误导致不执行

现象:创建的 systemd timer 没有按时执行,systemctl list-timers 显示上次和下次执行时间为空。

根因:OnCalendar 格式写错,如 *-*-* 2:30:00(缺少前导零),systemd 不识别。

修复:改为 *-*-* 02:30:00(小时和分钟必须两位数字)。用 systemd-analyze calendar "daily"` 验证格式。

案例 C:systemctl daemon-reload 遗漏导致 unit 文件修改不生效

现象:修改了 /etc/systemd/system/nginx.service 添加了 MemoryMax=512M,重启 nginx 后 systemctl show nginx | grep MemoryMax 仍然显示旧值。

# 检查 systemd 是否加载了新配置
$ systemctl show nginx -p MemoryMax
MemoryMax=18446744073709551615    # 这是 infinity(未限制)

# 原因:修改 unit 文件后没有执行 daemon-reload

根因:systemd 在启动时加载并缓存所有 unit 文件。修改文件后必须 daemon-reload 重新加载缓存。

修复sudo systemctl daemon-reload && sudo systemctl restart nginx。最佳实践:在部署脚本中将 daemon-reload 作为必要步骤。

预防措施

  • 新创建的服务先用 systemctl status 确认状态,再 systemctl enable
  • systemd timer 替代 cron 前,用 systemd-analyze calendar 验证调度表达式
  • 修改 unit 文件后必须 systemctl daemon-reload
  • systemctl edit 而非直接编辑 unit 文件,便于管理和回滚
  • 服务配置变更后用 systemctl restart 而非 stop + start,避免状态不一致
  • 生产环境服务配置变更前先在测试环境验证,避免错误配置导致服务不可用
  • systemctl list-dependencies --reverse 检查服务影响范围,避免停止关键依赖
  • 服务日志定期轮转,使用 journalctl --vacuum-time=30d 清理旧日志
  • 使用 systemd-analyze plot > boot.svg 可视化启动流程,排查启动耗时
  • 服务配置模板化管理,使用 systemctl edit 的 drop-in 机制便于版本控制

案例 D:服务找不到工作目录导致启动失败

现象:自定义 Python 应用通过 systemd 管理,启动时报 No such file or directory,但文件确实存在。

# 查看服务日志
$ journalctl -u myapp -n 20
Jul 31 10:00:01 server myapp[1234]: FileNotFoundError: [Errno 2] No such file or directory: '/app/config.yml'

# 检查服务文件
$ systemctl cat myapp
[Service]
ExecStart=/usr/bin/python3 /app/main.py
# 缺少 WorkingDirectory

# 修复:添加 WorkingDirectory
$ sudo systemctl edit myapp
[Service]
WorkingDirectory=/app
Environment=PYTHONUNBUFFERED=1

# 重新加载并重启
$ sudo systemctl daemon-reload
$ sudo systemctl restart myapp

# 验证
$ systemctl show myapp -p WorkingDirectory
WorkingDirectory=/app

# 检查进程的实际工作目录
$ ls -la /proc/$(pgrep -f myapp)/cwd
lrwxrwxrwx 1 root root 0 Jun 15 10:00 cwd -> /app

根因:systemd 启动服务时工作目录默认为 /(root),应用中的相对路径(如 config.yml)找不到文件。用户 Shell 登录时会自动进入 HOME 目录,但 systemd 服务没有这个行为。

修复:在 unit 文件的 [Service] 段添加 WorkingDirectory=/app,明确指定工作目录。修改后必须 daemon-reload 重新加载配置。用 ls -la /proc/PID/cwd 验证进程的实际工作目录。

案例 E:Type=forking 服务因 PIDFile 缺失反复重启

现象:服务配置为 Type=forking,但 systemctl status 显示 Main process exited, code=exited, status=1/FAILURE,服务反复重启。

# 查看服务配置
$ systemctl cat myservice
[Service]
Type=forking
ExecStart=/usr/local/bin/myservice --daemon
# 缺少 PIDFile

# 查看进程是否在运行
$ pgrep -f myservice
5678    ← 进程实际在运行

# 查看重启日志
$ journalctl -u myservice -n 10
Jul 31 10:00:01 server myservice[1234]: Started
Jul 31 10:00:03 server myservice[1234]: Main process exited, code=exited, status=1/FAILURE
Jul 31 10:00:03 server systemd: myservice.service: Scheduled restart

# 修复:添加 PIDFile
$ sudo systemctl edit myservice
[Service]
PIDFile=/run/myservice.pid
ExecStartPost=/bin/sleep 1    # 等待 PID 文件创建

# 确保应用写入 PID 文件
$ cat /run/myservice.pid
5678

# 验证 PIDFile 配置
$ systemctl show myservice -p PIDFile
PIDFile=/run/myservice.pid

根因Type=forking 让 systemd 等待主进程 fork 后退出,然后通过 PIDFile 追踪子进程。缺少 PIDFile 时 systemd 无法确认服务是否正常启动,误认为启动失败并重启。这是 systemd 服务配置中常见的陷阱。

修复:添加 PIDFile=/run/myservice.pid,确保应用在 fork 后将 PID 写入该文件。用 ExecStartPost 等待 PID 文件创建完成。用 systemctl show service -p PIDFile 验证配置。

案例 F:Restart=always 导致服务崩溃后疯狂重启

现象:服务配置了 Restart=always,但启动时立即崩溃,systemd 以每秒一次的频率无限重启,日志被刷屏。

# 查看重启频率
$ systemctl show myservice -p NRestarts
NRestarts=1523    ← 已重启 1523 次

# 查看最近的启动失败日志
$ journalctl -u myservice -n 30 --no-pager
Jul 31 10:00:01 server systemd[1]: myservice.service: Scheduled restart
Jul 31 10:00:01 server systemd[1]: Started My Service
Jul 31 10:00:02 server myservice[1234]: Error: config file not found
Jul 31 10:00:02 server systemd[1]: myservice.service: Main process exited
Jul 31 10:00:02 server systemd[1]: myservice.service: Scheduled restart

# 限制重启频率
$ sudo systemctl edit myservice
[Service]
Restart=on-failure
RestartSec=5
StartLimitBurst=5
StartLimitIntervalSec=60

# 效果:60 秒内最多重启 5 次,超过后服务进入 failed 状态
# 需要手动 systemctl reset-failed myservice 后才能重新启动

# 重置失败计数
$ sudo systemctl reset-failed myservice

# 查看服务的重启策略
$ systemctl show myservice -p Restart
Restart=on-failure
$ systemctl show myservice -p RestartSec
RestartSec=5

根因Restart=always 无论退出原因都重启,RestartSec=0(默认)导致无限快速重启,消耗大量系统资源。日志被重启消息刷屏,难以排查根本原因。这是 systemd 服务配置中最常见的陷阱之一。

修复:改用 Restart=on-failure(仅异常退出时重启),设置 RestartSec=5(重启间隔),StartLimitBurst=5(限制重启次数),StartLimitIntervalSec=60(限制时间窗口)。用 systemctl reset-failed 重置失败计数。

案例 G:systemd service 环境变量未传递导致脚本失败

现象:手动执行 /opt/app/deploy.sh 正常,但作为 systemd 服务执行时报 command not found

# 手动执行有完整 PATH
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# systemd 服务的 PATH 极简
$ systemctl show myservice -p Environment
Environment=LANG=en_US.UTF-8    ← 没有自定义 PATH

# 修复:在 unit 文件中设置
$ sudo systemctl edit myservice
[Service]
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
EnvironmentFile=/etc/default/myapp

# 环境变量文件 /etc/default/myapp
DB_HOST=localhost
DB_PORT=5432
APP_SECRET=xxx

# 验证环境变量
$ systemctl show myservice -p Environment
Environment=PATH=/usr/local/sbin:... DB_HOST=localhost DB_PORT=5432

根因:systemd 服务的环境变量与用户 Shell 完全隔离,不继承 ~/.bashrc/etc/profile 中的设置。这是 systemd 的安全设计,避免服务继承用户环境中的敏感变量。

修复:在 [Service] 段用 Environment= 直接设置,或用 EnvironmentFile= 引用环境变量文件。敏感信息(密码)应使用 EnvironmentFile 并设置文件权限为 600。用 systemctl show service -p Environment 验证环境变量是否正确传递。

案例 G:After= 与 Requires= 混淆导致服务启动顺序错误

现象:自定义 Web 应用配置了 After=network.target,但启动时网络尚未就绪,应用连接数据库失败。

# 查看服务启动时间线
$ systemd-analyze blame | grep myapp
2.300s myapp.service

$ journalctl -u myapp -n 10
Jul 31 10:00:01 server systemd[1]: Starting My App...
Jul 31 10:00:02 server myapp[1234]: Connecting to database...
Jul 31 10:00:03 server myapp[1234]: Error: connection refused
Jul 31 10:00:03 server myapp[1234]: Error: ENETUNREACH

# 检查服务配置
$ systemctl cat myapp
[Unit]
After=network.target    ← 只保证顺序,不保证网络就绪

# 修复:使用 network-online.target(等待网络真正就绪)
$ sudo systemctl edit myapp
[Unit]
After=network-online.target
Wants=network-online.target

# 依赖类型说明:
# network.target:网络栈已启动(路由表就绪,但不一定联网)
# network-online.target:至少一个接口获得 IP(真正联网)

# 验证
$ systemctl list-dependencies myapp | grep network
● └─network-online.target    ← 现在依赖真正的网络就绪

根因After=network.target 只保证启动顺序在 network 之后,但 network.target 启动时网络接口可能尚未获得 IP。network-online.target 才表示网络真正可用。这是 systemd 服务配置中最常见的误解之一。

修复:需要网络真正就绪的服务使用 After=network-online.target + Wants=network-online.target。用 systemd-analyze plot > boot.svg 可视化启动时间线,确认服务启动顺序是否正确。

案例 H:systemd 的 ExecStartPre 权限不足导致服务无法启动

现象:服务配置了 ExecStartPre=/usr/local/bin/check-config.sh 检查配置文件,但服务启动时报 Permission denied

# 查看服务日志
$ journalctl -u myapp -n 5
Jul 31 10:00:01 server systemd[1]: myapp.service: ExecStartPre= process exited, code=exited, status=203/EXEC

# 检查脚本权限
$ ls -la /usr/local/bin/check-config.sh
-rw-r--r-- 1 root root 256 Jun 15 10:00 check-config.sh
# 没有执行权限!

# 修复:添加执行权限
$ sudo chmod +x /usr/local/bin/check-config.sh

# ExecStartPre 的常见问题:
# 1. 没有执行权限 → status=203/EXEC
# 2. 路径不存在 → status=203/EXEC
# 3. 脚本第一行 shebang 错误 → status=200

# 验证
$ sudo systemctl restart myapp
$ systemctl status myapp
● myapp.service - My App
   Active: active (running) since ...

根因ExecStartPre 中的脚本/命令需要有执行权限。systemd 不会自动处理脚本的执行权限,与用户在 Shell 中手动执行的行为不同。status=203/EXEC 表示 exec() 系统调用失败。

修复:确保 ExecStartPre/ExecStart/ExecStartPost 中的所有路径都有执行权限。用 systemctl status 的 status code 判断错误类型:203/EXEC(权限或路径)、200(异常退出)。

延伸阅读

↑ 回到顶部