服务重启后没自己起来:一次 Linux systemd 排查复盘

断电演练后服务没自动起来,另一台机器服务反复重启。排查梳理:start 与 enable 是两回事、读懂 systemctl status 每一行、写对一个 .service unit 文件、Restart 策略与重启风暴、After/Wants/Requires 的依赖区别,以及 systemd 管理纪律。

2024 年我们机房做了一次例行断电演练,机器重启回来后,运维群里炸了:好几个服务没起来。我登上其中一台,服务进程确实不在,我手敲 systemctl start 一下就起来了,一切正常。可问题是——为什么重启之后它没有自己起来?与此同时另一台机器上,一个服务在 systemctl status 里显示 activating,过几秒又 failed,然后又 activating,像鬼打墙一样反复重启。那一天我才意识到,我对 systemd 的理解,一直停留在"会敲 start/stop/restart"这种皮毛上。这次排查逼着我把 systemd 这套东西彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一批用 systemd 管理的自研服务
事故现象:
- 机房断电演练,机器重启后,有几个服务没自动起来
- 手动 systemctl start 一下,立刻就正常 —— 服务本身没问题
- 另一台机器上,某服务反复 activating -> failed -> activating

现场排查:
# 1. 看那个"重启后没起来"的服务
$ systemctl status myapp
● myapp.service - My App
   Loaded: loaded (/etc/systemd/system/myapp.service; disabled)
   Active: inactive (dead)
# ★ 第一个坑:Loaded 那行末尾是 disabled
#   disabled = 开机【不会】自动拉起它
#   之前的人只 start 了,从来没 enable 过

# 2. 看那个"反复重启"的服务
$ systemctl status worker
   Active: activating (auto-restart) (Result: exit-code)
$ journalctl -u worker -n 30 --no-pager
worker[2451]: Error: cannot open /data/worker/conf.yml
worker.service: main process exited, code=exited, status=1
worker.service: Scheduled restart job...
# ★ 第二个坑:配置文件路径不对,进程起来就退,
#   Restart=always 又把它无限拉起 —— 形成重启风暴

根因(后来定位到的):
1. 没起来的服务:只 systemctl start 过,从没 enable,
   所以开机自启列表里根本没有它。
2. 反复重启的服务:它依赖的配置文件不在,启动即失败,
   而 unit 里配了 Restart=always,systemd 就不停地拉。
两件事都指向同一个认知缺口:我没真正搞懂 systemd。

修复 1:start 和 enable 是两回事

# === systemctl 最常用的几个动作 ===
$ systemctl start  myapp     # 【现在】把服务启动
$ systemctl stop   myapp     # 【现在】把服务停掉
$ systemctl restart myapp    # 停掉再启动
$ systemctl reload myapp     # 让服务重新加载配置(不重启进程)
$ systemctl status myapp     # 看当前状态

# === ★ 最关键、也最容易混淆的区别:start ≠ enable ===
$ systemctl start  myapp     # 只影响【这一次】,重启就没了
$ systemctl enable myapp     # 把它【登记进开机自启】
# start  = 现在跑起来
# enable = 以后每次开机都自动跑起来
# 这俩互相独立!这次的坑就是:
#   只 start 没 enable -> 服务在跑,但开机自启没登记 ->
#   机器一重启,它就不会自己回来。

# === 一步到位:启动 + 设为自启 ===
$ systemctl enable --now myapp
# --now 让 enable 的同时立刻 start,常用

# === 看一个服务到底会不会开机自启 ===
$ systemctl is-enabled myapp
enabled        # 或 disabled
# 部署完服务,一定要 is-enabled 确认一遍!

# === 看所有"开机自启"的服务,做体检 ===
$ systemctl list-unit-files --type=service --state=enabled
# 重启演练前,拿这个清单核对一遍 ——
# "该自启的有没有都 enable" —— 就不会有这次的事故。

# === disable:取消开机自启(但不影响当前运行)===
$ systemctl disable myapp

修复 2:读懂 systemctl status 的每一行

# === systemctl status 信息量很大,要会读 ===
$ systemctl status myapp
● myapp.service - My Application
   Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
   Active: active (running) since Tue 2024-05-14 10:00:00; 2h ago
 Main PID: 3721 (java)
   CGroup: /system.slice/myapp.service
           └─3721 java -jar /opt/myapp/app.jar

# === 第一行那个圆点的颜色/状态 ===
# ● 白/绿 = 正常   ● 红 = failed

# === Loaded 行:unit 文件加载情况 ===
# loaded   = unit 文件找到了、加载了
# 括号里的路径 = 这个 unit 文件在哪
# 末尾 enabled / disabled = 开机自启没自启 ★

# === Active 行:当前运行状态(最重要)===
# active (running)    正在跑
# active (exited)     跑完就退了(一次性任务,正常)
# inactive (dead)     没在跑
# failed              启动/运行失败了 ★ 重点排查对象
# activating          正在启动中(一直停在这 = 卡住了)

# === Main PID:主进程 PID;CGroup:这个服务的所有进程 ===
# systemd 用 cgroup 把服务的所有子进程都"圈"起来,
# status 末尾会把它们都列出来 —— 看服务派生了哪些进程很有用。

# === status 末尾还会带最近几行日志 ===
# 服务 failed 时,这几行日志往往直接点出原因。

# === 快速看哪些服务挂了 ===
$ systemctl --failed
# 列出所有 failed 的 unit,机器异常后第一个该敲的命令。

修复 3:写一个 .service unit 文件

# === systemd 靠 unit 文件描述一个服务怎么管 ===
# 自研服务放这里:/etc/systemd/system/myapp.service
$ vim /etc/systemd/system/myapp.service

[Unit]
Description=My Application
After=network.target          # 在网络就绪【之后】再启动
Wants=network-online.target

[Service]
Type=simple                   # 进程在前台跑(最常用)
User=appuser                  # 用哪个用户身份跑(别用 root)
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure            # 异常退出才重启(见修复 4)
RestartSec=5                  # 重启前等 5 秒
Environment=JAVA_OPTS=-Xmx2g  # 环境变量
StandardOutput=journal        # 标准输出收进 journald
StandardError=journal

[Install]
WantedBy=multi-user.target    # enable 时挂到这个目标下

# === 三个段各管什么 ===
# [Unit]    : 描述、依赖关系(After/Wants/Requires)
# [Service] : 服务本身怎么启停(ExecStart、用户、重启策略)
# [Install] : systemctl enable 时怎么挂载 ★
# ★ 没有 [Install] 段,enable 会失败 ——
#   "没有 [Install] 段" 是新手写 unit 最常见的错。

# === 改完 unit 文件,必须重载 systemd ===
$ systemctl daemon-reload     # ★ 不 reload,你的修改不生效
$ systemctl restart myapp

# === Type 几种值的区别(简记)===
# simple  : ExecStart 的进程就是主进程,前台跑(默认/最常用)
# forking : 进程会自己 fork 到后台(传统守护进程)
# oneshot : 跑一次就结束的任务(如初始化脚本)
# notify  : 进程会主动通知 systemd "我就绪了"

修复 4:服务反复重启的排查

# === 这次"鬼打墙"式反复重启,根子在 Restart 策略 ===

# === Restart 各个值的含义 ===
# Restart=no          退出就不管了(默认)
# Restart=on-failure  只有【异常退出】(非 0 退出码)才重启
# Restart=always      不管怎么退出,都重启 ★ 用它要谨慎

# === 为什么会形成"重启风暴" ===
# 服务因为配置错/依赖缺,启动即失败、立刻退出。
# Restart=always 看到它退了,马上又拉起来,
# 它又立刻失败... 一秒钟能循环好多次,
# 日志疯狂刷屏,CPU 也被这无效循环吃掉。

# === systemd 的"刹车":启动限流 ===
$ vim /etc/systemd/system/worker.service
[Unit]
StartLimitIntervalSec=60     # 在 60 秒的窗口内
StartLimitBurst=4            # 最多允许重启 4 次
# 超过这个频率,systemd 就【放弃】,不再拉它,
# 状态变成 failed。这能防止无意义的重启风暴。
# 看到 "start request repeated too quickly" 就是它触发了。

# === 排查反复重启,核心命令:journalctl -u ===
$ journalctl -u worker -n 50 --no-pager
# -u 指定服务  -n 50 最近 50 行
# 重点找进程【退出前】打的最后几行日志 ——
# 那里通常就是它起不来的真正原因。
$ journalctl -u worker -f          # -f 实时跟,边重启边看
$ journalctl -u worker --since "10 min ago"   # 按时间筛

# === 正确的处理姿势 ===
# 1. 先把服务停掉,打断重启循环:
$ systemctl stop worker
# 2. 看 journalctl 找到真正的失败原因(这次是配置路径错)
# 3. 修好根因
# 4. 把 Restart 改成 on-failure,而不是无脑 always
# 5. daemon-reload 再 start
# ★ 别用调大 StartLimitBurst 来"解决"反复重启 ——
#   那是治标,真正要修的是它为什么起不来。

修复 5:依赖与启动顺序

# === 服务之间有依赖,systemd 用 [Unit] 段来表达 ===

# === After / Before:控制启动【顺序】 ===
[Unit]
After=network.target mysql.service
# After=X:本服务在 X 启动【之后】才启动
# Before=X:本服务在 X 【之前】启动
# ★ 注意:After 只管【顺序】,不管"X 必须成功"。

# === Wants / Requires:控制依赖【关系】 ===
# Wants=X   :希望 X 也启动,但 X 失败【不影响】本服务
#             —— 弱依赖,推荐默认用它
# Requires=X:强依赖,X 没起来 / X 挂了,本服务也跟着挂
#             —— 用它要想清楚,容易连锁失败

# === 一个常见误区:以为 After 就保证了依赖 ===
# After=mysql.service 只保证"mysql 先启动",
# 但如果 mysql 启动失败,你的服务照样会被启动 ——
# 然后因为连不上数据库而失败。
# 想"mysql 没起来我就别起",要配:
[Unit]
After=mysql.service
Requires=mysql.service

# === 看一个服务依赖了谁、被谁依赖 ===
$ systemctl list-dependencies myapp
$ systemctl list-dependencies myapp --reverse   # 反过来:谁依赖它

# === target:一组 unit 的集合,类似旧的"运行级别" ===
$ systemctl get-default
multi-user.target          # 服务器默认目标(多用户、无图形)
# multi-user.target  ≈ 旧的运行级别 3
# graphical.target   ≈ 旧的运行级别 5(带图形界面)
# unit 里 WantedBy=multi-user.target,意思就是
# "系统进入 multi-user 这个目标时,把我也带起来"。

# === 看系统启动各阶段耗时(优化开机速度)===
$ systemd-analyze
$ systemd-analyze blame      # 按耗时排序,哪个服务拖慢了启动

修复 6:systemd 管理的纪律

# === 这次事故暴露的管理问题,定几条纪律 ===

# === 1. 部署服务,start 和 enable 一个都不能少 ===
# 永远用 systemctl enable --now,
# 部署完再 systemctl is-enabled 确认一遍。
# "服务在跑"和"开机会自启"是两件事,必须都验证。

# === 2. 改了 unit 文件,必须 daemon-reload ===
# 改完不 reload,你看到的还是旧配置,
# 排查时会被"我明明改了啊"骗得团团转。
$ systemctl daemon-reload

# === 3. Restart 默认用 on-failure,别无脑 always ===
# always 会在服务"正常退出"时也重启,
# 还容易和配置错误一起酿成重启风暴。
# 配合 StartLimitBurst 给个上限,留个刹车。

# === 4. 服务别用 root 跑,unit 里指定 User= ===
[Service]
User=appuser
Group=appgroup
# 最小权限原则,服务被攻破时影响范围小得多。

# === 5. 日志统一交给 journald,会查会清 ===
$ journalctl -u 服务名              # 看某服务日志
$ journalctl -p err -b              # 看本次开机以来的错误
$ journalctl --disk-usage           # journal 占了多少盘
$ journalctl --vacuum-time=2weeks   # 只留最近两周

# === 6. 重启演练前,先做一次自启体检 ===
$ systemctl list-unit-files --state=enabled --type=service
# 拿这份清单核对"该自启的服务有没有都 enable"。
$ systemctl --failed                # 也顺手看有没有已经挂的

# === 7. 改完配置先 verify,再重启 ===
$ systemd-analyze verify /etc/systemd/system/myapp.service
# 它会检查 unit 文件有没有语法/字段错误,
# 比"重启了才发现写错"要早得多。

命令速查

需求                        命令
=============================================================
启动/停止/重启服务          systemctl start/stop/restart 服务
看服务状态                  systemctl status 服务
设为开机自启并立即启动      systemctl enable --now 服务
看会不会开机自启            systemctl is-enabled 服务
看所有自启服务              systemctl list-unit-files --state=enabled
看所有失败的服务            systemctl --failed
改 unit 后重载              systemctl daemon-reload
看某服务日志                journalctl -u 服务名
实时跟某服务日志            journalctl -u 服务名 -f
看服务依赖关系              systemctl list-dependencies 服务
校验 unit 文件              systemd-analyze verify unit文件
看开机各服务耗时            systemd-analyze blame

口诀:start 是这次 enable 是以后 -> 改 unit 必 daemon-reload
      -> 反复重启先 stop 再看 journalctl 找根因

避坑清单

  1. start 只让服务这次跑起来,enable 才登记开机自启,两者必须都做
  2. 部署完务必 systemctl is-enabled 确认,别等机器重启才发现没自启
  3. systemctl status 的 Loaded 行末尾 enabled/disabled 直接告诉你会不会自启
  4. Active 行 failed 是重点排查对象,activating 卡住不动说明启动卡死
  5. unit 文件没有 [Install] 段会导致 enable 失败,这是写 unit 最常见的错
  6. 改了 unit 文件必须 systemctl daemon-reload,否则修改不生效
  7. Restart=always 配上启动失败会酿成重启风暴,默认应该用 on-failure
  8. 反复重启先 systemctl stop 打断循环,再用 journalctl -u 找退出原因
  9. After 只管启动顺序不管依赖成败,要"依赖失败就别起"得配 Requires
  10. 服务别用 root 跑,unit 的 [Service] 段用 User= 指定低权限用户

总结

这次断电演练引发的 systemd 排查,把我对这个工具那种"会敲几个命令就算会用"的错觉,彻底打破了。出事之前,我对 systemd 的认识基本就停留在 start、stop、restart、status 这四个动词上,我以为这就是它的全部。可这一天的两个故障,像两记闷棍,各自敲开了一个我从来没想过的认知盲区。第一个故障是机器重启后服务没自己起来,而它的根因,简单得让我有点难堪——我们之前部署这个服务时,只 systemctl start 了它,却从来没有 systemctl enable 过。在我过去模糊的理解里,这两个命令像是差不多的东西,可这次我才真正想明白:start 和 enable 是两个维度完全正交、互不相关的操作。start 回答的是"现在,把这个服务跑起来吗",它的效力只覆盖当下这一次,机器一重启就烟消云散;而 enable 回答的是另一个完全不同的问题——"以后,每次这台机器开机,要不要自动把它带起来",它做的事情是把这个服务登记进开机自启的清单里。一个服务完全可以正在好端端地运行着(因为有人 start 过它),同时却根本不在开机自启清单里(因为没人 enable 过它)——这正是我们这次的处境,平时风平浪静,因为机器一直没重启,直到一场断电演练把这个沉睡的隐患彻底掀了出来。从此我给自己立下一条铁律:部署任何一个需要常驻的服务,start 和 enable 一个都不能少,最稳妥的是直接用 systemctl enable --now 一步到位,然后再用 systemctl is-enabled 亲眼确认一遍——"服务在跑"和"服务会自启"是必须分别验证的两件事。第二个故障是服务像鬼打墙一样反复重启,它教给我的,是 Restart 策略这把双刃剑。我们那个服务因为配置文件路径不对,一启动就立刻失败退出,而它的 unit 文件里写着 Restart=always,于是 systemd 尽职尽责地、一遍又一遍地把这个注定会立刻死掉的进程拉起来,一秒钟能循环好多次,日志被刷得密不透风。这让我想清楚了两件事。其一,Restart=always 这个看起来很"省心"的配置其实暗藏杀机,它不分青红皂白,哪怕服务是因为配置错误这种根本不可能自愈的原因失败,它也照拉不误,结果就是把一个"启动失败"的静态问题,放大成了一场消耗 CPU、淹没日志的"重启风暴";更克制、也更合理的默认选择是 Restart=on-failure,再配上 StartLimitBurst 给它一个重启次数的上限,留一个刹车。其二,也是更重要的——面对反复重启,正确的第一反应不是去想办法让它"重启成功",而是先 systemctl stop 把这个疯狂的循环按停,让现场安静下来,然后用 journalctl -u 这个服务名,去翻它每一次进程退出之前打下的最后几行日志,那里几乎总是写着它起不来的真正原因。修复永远应该是去解决"它为什么起不来"这个根因,而绝不是去调大重启次数的上限来粉饰太平。这次排查也顺带逼着我把一个完整的 unit 文件从头到尾读懂、写对了:[Unit] 段描述它和别的服务之间的依赖与启动顺序,[Service] 段规定它本身怎么启动、用哪个用户跑、用什么重启策略,而 [Install] 段则决定了 systemctl enable 的时候它该被挂到哪里——少了这个 [Install] 段,enable 就会直接失败。我还分清了一组以前一直含糊的概念:[Unit] 段里的 After 只负责控制启动的先后顺序,它并不保证被依赖的服务一定成功,真要表达"它没起来我也别起"这种强依赖,得用 Requires。这次从两个看似毫不相干的故障出发,我最大的收获,是终于把 systemd 从一个"我背了几条命令的黑盒",变成了一个我能讲清楚其运作模型的、透明的服务管理系统——而这套理解教给我的第一课就是:start 与 enable、After 与 Requires、always 与 on-failure,这些看起来相近的东西,差别恰恰藏在那些最容易被忽略的地方,而生产事故,往往就从这些被忽略的缝隙里钻出来。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

域名解析时灵时不灵:一次 Linux DNS 排查复盘

2026-5-20 17:47:18

Linux教程

脚本手动跑正常、换个环境就报错:一次 Linux 环境变量排查复盘

2026-5-20 17:53:24

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索