手动能跑交给 systemd 就启动失败:一次 unit 文件与 journalctl 排查复盘

一个手动跑得好好的前台常驻程序,写了 service 文件交给 systemd 管,systemctl start 卡 90 秒后报 failed、Result: timeout。排查梳理:systemd 是服务的全生命周期监护人、Type 字段是判断启动成功的约定、前台常驻程序错配 Type=forking 让 systemd 傻等超时、status 头部 Result/code 怎么读、journalctl -u -xe 查完整日志、改 unit 必须 daemon-reload、journal 日志持久化要建目录,以及一套 systemd 服务排查纪律。

2024 年,我把一个新写的服务部署到服务器上,按惯例写了一个 systemd 的 service 文件,想让它被 systemctl 管起来,开机自启、挂了自动拉。文件写好,我满怀信心地敲下 systemctl start myapp——命令没有立刻返回,卡了大概一分半钟,然后吐出一行红字:Job for myapp.service failed。我赶紧 systemctl status myapp,屏幕上是冷冰冰的 failed,外加一句我看不太懂的 Result: timeout。可诡异的是,我手动在命令行直接跑那个程序,它跑得好好的,稳稳地常驻在前台。一个手动跑得活蹦乱跳的程序,交给 systemd 管,就启动"失败",而且失败前还要莫名其妙地卡一分半钟——这两件怪事凑在一起,让我百思不得其解。后来才发现,程序本身一点问题没有,问题出在我对 systemd 管理服务的方式、尤其是那个 Type 字段的理解,完全是错的。这件事逼着我把 Linux 的 systemd、unit 文件、journalctl 日志这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个前台常驻运行的自研服务
事故现象:
- 手动在命令行直接跑程序,完全正常,稳定常驻
- 写了 myapp.service 交给 systemd 管
- systemctl start 卡约 90 秒,然后报 failed
- status 显示 failed,Result: timeout

现场排查:
# 1. 看 status,信息有限但有线索
$ systemctl status myapp
   Loaded: loaded (/etc/systemd/system/myapp.service; ...)
   Active: failed (Result: timeout)            # ★ timeout
  Process: 9001 ExecStart=/opt/myapp/run (code=killed, signal=TERM)
# ★ 注意:Result 是 timeout,而且进程是被 TERM "killed" 的

# 2. ★ 看完整日志:journalctl -u
$ journalctl -u myapp -n 30
... Starting myapp...
(这里静默了约 90 秒)
... myapp.service: Start operation timed out. Terminating.
... myapp.service: Failed with result 'timeout'.
# ★ systemd 一直在【等待】启动完成,等了 90 秒等不到,
#   认定启动失败,把进程 TERM 掉了

# 3. 看我写的 unit 文件
$ cat /etc/systemd/system/myapp.service
[Service]
Type=forking                                  # ★ 问题就在这一行
ExecStart=/opt/myapp/run

根因(后来想清楚的):
1. ★ Type=forking 的含义是:systemd 期待你的程序
   【启动后 fork 出子进程,然后【父进程退出】】——
   它把"父进程退出"当作"启动成功"的信号。
2. 但我的程序根本不 fork,它是个【前台常驻】进程,
   启动后就一直在前台跑、永远不退出。
3. ★ 于是 systemd 傻等:它等那个"父进程退出"的信号,
   可我的进程压根不会退 -> 一直等 -> 等到 90 秒超时。
4. 超时后,systemd 认定"启动失败",把进程 TERM 掉,
   报 Result: timeout。
5. ★ 正解:前台常驻的程序,Type 要写 simple(默认),
   systemd fork 出进程就【立刻认为启动成功】,不傻等。
unit 的 Type 配错,程序再正常也起不来。

修复 1:systemd 与 unit 文件——服务是怎么被管起来的

# === ★ 先认识 systemd:它是现代 Linux 的"1 号进程" ===
# 开机后第一个起来的进程就是 systemd(PID 1),
# 此后系统上所有服务的启动、停止、监控、重启,都归它管。

# === 一个服务,对应一个 .service "单元(unit)文件" ===
# 你想让 systemd 管理一个程序,就给它写一个 unit 文件。
# 自定义服务,放这里:
$ ls /etc/systemd/system/                # ★ 自己写的放这
# 软件包带的,放这里(★ 别去改这里的,改了会被升级覆盖):
$ ls /usr/lib/systemd/system/

# === ★ 一个最小可用的 unit 文件长这样 ===
$ cat /etc/systemd/system/myapp.service
[Unit]
Description=My App Service                # 描述,给人看的
After=network.target                     # ★ 在网络就绪【之后】再启动

[Service]
Type=simple                               # ★ 服务类型(下文重点)
ExecStart=/opt/myapp/run                  # ★ 启动命令(必须绝对路径)
Restart=on-failure                        # 异常退出就自动重启
User=appuser                               # 用哪个用户身份跑

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

# === 管理一个服务的常用命令 ===
$ systemctl start myapp                   # 启动
$ systemctl stop myapp                    # 停止
$ systemctl restart myapp                 # 重启
$ systemctl status myapp                  # 看状态
$ systemctl enable myapp                  # ★ 设开机自启
$ systemctl disable myapp                 # 取消开机自启
$ systemctl enable --now myapp            # enable + start 一步到位

# === ★ 一个高频坑:改了 unit 文件,必须 daemon-reload ===
$ vi /etc/systemd/system/myapp.service    # 改完
$ systemctl daemon-reload                 # ★ 不执行这句,改动不生效!
$ systemctl restart myapp
# systemd 把 unit 配置缓存在内存里,改了文件得让它重新加载。

修复 2:systemctl status 怎么读——头部信息量很大

# === ★ systemctl status 那几行,每一行都是排查线索 ===
$ systemctl status myapp
● myapp.service - My App Service
   Loaded: loaded (/etc/systemd/system/myapp.service; enabled; ...)
   Active: failed (Result: timeout) since Mon 2024-05-20 03:14 ...
  Process: 9001 ExecStart=/opt/myapp/run (code=killed, signal=TERM)
 Main PID: 9001 (code=killed, signal=TERM)

# === 第一行那个圆点的颜色/状态 ===
# ● 白/灰 = 未启动   ● 绿 = 运行中   ● 红 = 失败

# === Loaded 行:unit 文件【加载】情况 ===
# loaded   = 文件找到了、语法没问题
# not-found = ★ 文件没找到(名字敲错?放错目录?)
# bad-setting / error = unit 文件里有语法错误
# 后面的 enabled/disabled = 开机自启有没有设

# === ★ Active 行:服务【当前】状态 —— 最该看的一行 ===
# active (running)  正常运行中
# active (exited)   ★ 跑完就退了(oneshot 类型正常如此)
# failed            启动/运行失败
# activating        ★ 正在启动中(卡在这 = 启动迟迟不完成)
# 括号里的 Result 告诉你失败原因:
#   timeout    ★ 启动/停止超时(这次就是它)
#   exit-code  程序自己以非 0 退出
#   signal     被信号杀死

# === ★ Process / Main PID 行:进程是怎么结束的 ===
# code=exited, status=0     正常退出
# code=exited, status=203   ★ 203/EXEC = ExecStart 那个命令
#                              没法执行(路径错?没执行权限?)
# code=exited, status=200+  各种 systemd 专属错误码
# code=killed, signal=TERM  ★ 被 systemd 自己 TERM 掉的
#                              (常见于超时后被终止)

# === ★ status 只显示最后 10 行日志,不够看 ===
$ systemctl status myapp -l --no-pager     # -l 不截断长行
# 但真正完整的日志,要靠 journalctl(下一节)。

修复 3:journalctl——查 systemd 服务日志的正道

# === ★ systemd 管的服务,日志统一进 journal,用 journalctl 查 ===
# 别再到处翻 /var/log/xxx 了 —— 服务的 stdout/stderr,
# systemd 全都收进了 journal。

# === ★ 最常用:看某个服务的日志 ===
$ journalctl -u myapp                     # 看 myapp 的【全部】日志
$ journalctl -u myapp -n 50               # 只看最后 50 行
$ journalctl -u myapp -f                  # ★ 实时追踪(像 tail -f)
$ journalctl -u myapp --since '10 min ago'   # 最近 10 分钟
$ journalctl -u myapp --since today          # 今天的
$ journalctl -u myapp --since '2024-05-20 03:00' --until '03:20'

# === ★ 排查启动失败的黄金命令 ===
$ journalctl -xe
# -e  跳到日志末尾(最新)
# -x  ★ 给日志补充"解释和建议" —— 启动失败时常有有用提示
$ journalctl -u myapp -xe                 # 针对某服务,最常用

# === 按优先级过滤,只看错误 ===
$ journalctl -u myapp -p err              # 只看 err 及以上级别
$ journalctl -p err -b                    # 本次开机以来所有错误
# -p 级别:emerg/alert/crit/err/warning/notice/info/debug

# === 按"第几次开机"过滤 ===
$ journalctl -b                           # ★ 本次开机以来的日志
$ journalctl -b -1                        # 上一次开机的日志
$ journalctl --list-boots                 # 列出所有开机记录

# === 看内核日志(等价于 dmesg)===
$ journalctl -k                           # 本次开机的内核日志

# === ★ journal 占了多少磁盘、怎么清 ===
$ journalctl --disk-usage                 # 看 journal 占用
$ journalctl --vacuum-time=7d             # 只保留最近 7 天
$ journalctl --vacuum-size=500M           # 只保留 500M
# ★ journal 默认有上限,但不主动收缩,大了可以手动 vacuum。

修复 4:Type 类型——配错就起不来,这次的核心

# === ★ unit 文件里的 Type,决定 systemd 怎么判断"启动成功了" ===
# 这次的坑就在这:Type 配错,程序再正常也"启动失败"。

# === Type=simple(★ 默认,也是最常用的)===
[Service]
Type=simple
ExecStart=/opt/myapp/run
# 含义:ExecStart 拉起的【那个进程本身】就是主进程,
#   它【在前台一直跑、不 fork、不退出】。
# ★ systemd 一旦 fork 出这个进程,【立刻】认为启动成功。
# ★ 绝大多数现代程序(前台常驻的)都该用 simple。
#   ——这正是我这次该用、却没用的类型。

# === Type=forking —— 我这次错用的那个 ===
[Service]
Type=forking
ExecStart=/opt/myapp/daemon
PIDFile=/run/myapp.pid
# 含义:systemd 期待你的程序启动后,自己 fork 出子进程
#   去后台常驻,然后【启动它的那个父进程主动退出】。
# ★ systemd 把"父进程退出"当作"启动成功"的信号。
# ★ 这是【传统 daemon】程序的模式(自己做了后台化)。
# ★ 我的程序是前台常驻、根本不退 —— systemd 苦等那个
#   "退出信号",等到超时,这就是 90 秒卡死的真相。

# === Type=oneshot —— 跑一次就完事的任务 ===
[Service]
Type=oneshot
ExecStart=/opt/init-data.sh
RemainAfterExit=yes
# 含义:命令跑完就结束,不常驻。适合初始化脚本等。
# 它的 status 正常就是 active (exited),不是 running。

# === Type=notify —— 程序自己"报告"启动好了 ===
# 程序通过 sd_notify 主动告诉 systemd "我就绪了"。
# 最精确,但需要程序代码层面支持。

# === ★ 怎么选:一句话决断 ===
# 程序前台跑、不自己后台化      -> Type=simple(99% 的情况)
# 程序是传统 daemon、自己 fork  -> Type=forking,且配 PIDFile
# 跑一次就退出的脚本/任务       -> Type=oneshot
# ★ 拿不准就用 simple —— 让程序前台跑、把后台化交给 systemd,
#   这也是现代服务推荐的写法。

# === 这次的修复:就改一行 ===
$ vi /etc/systemd/system/myapp.service
- Type=forking
+ Type=simple
$ systemctl daemon-reload && systemctl restart myapp
$ systemctl status myapp          # Active: active (running) ✓

修复 5:日志持久化——为什么重启后日志全没了

# === ★ 这次排查还撞见一个坑:重启机器后,journal 旧日志没了 ===

# === 原因:journal 默认可能只存在【内存】里 ===
$ cat /etc/systemd/journald.conf | grep -i storage
#Storage=auto
# Storage 的取值:
#   volatile  ★ 日志只存内存(/run),【一重启就全丢】
#   persistent 存磁盘(/var/log/journal),重启也在
#   auto      ★ 默认:/var/log/journal 目录【存在】就持久化,
#              不存在就只存内存 —— 很多系统这个目录不存在!
#   none      不存日志

# === ★ 让 journal 持久化:关键就是建出那个目录 ===
$ mkdir -p /var/log/journal
$ systemctl restart systemd-journald
# 或者在配置里显式写死:
$ vi /etc/systemd/journald.conf
Storage=persistent
$ systemctl restart systemd-journald
# ★ 这之后,journalctl -b -1 才真的能看到上次开机的日志。

# === 确认现在日志存哪了 ===
$ journalctl --disk-usage
Archived and active journals take up 88.0M in /var/log/journal/...
# 路径里是 /var/log/journal = 持久化成功;
# 是 /run/log/journal = 还在内存里,重启会丢。

# === ★ 顺带:别忘了 journal 也有大小上限 ===
$ vi /etc/systemd/journald.conf
SystemMaxUse=1G                    # journal 最多用 1G 磁盘
SystemMaxFileSize=100M             # 单个文件上限
$ systemctl restart systemd-journald

# === 程序自己写的日志文件,不受 journal 管 ===
# 如果你的程序自己写 /var/log/myapp/xxx.log,
# 那是程序自己的事,journalctl 看的是 stdout/stderr。
# ★ 排查时两头都要想到:journalctl -u 服务 + 程序自己的日志。

修复 6:systemd 服务排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ 服务起不来,status 看头部 + journalctl 看全程 ===
$ systemctl status myapp          # 看 Active / Result / Process
$ journalctl -u myapp -xe         # 看完整日志和解释

# === 2. ★ Result/code 是关键线索 ===
# Result: timeout    -> 多半 Type 配错,systemd 在傻等
# status=203/EXEC    -> ExecStart 路径错 / 没执行权限
# code=killed signal -> 被信号杀(超时被 TERM,或 OOM)

# === 3. ★ Type 要和程序行为匹配 ===
# 前台常驻 -> simple;传统自 fork daemon -> forking;
# 跑一次就退 -> oneshot。拿不准用 simple。

# === 4. ★ 改了 unit 文件,必须 daemon-reload ===
$ systemctl daemon-reload         # 不做这步,改动不生效

# === 5. ExecStart 必须绝对路径;注意 User= 的权限 ===
# systemd 环境也很"裸",和 cron 一样别依赖 PATH。

# === 6. 想让 journal 持久化,建 /var/log/journal 目录 ===
$ mkdir -p /var/log/journal && systemctl restart systemd-journald

# === 7. 排查 systemd 服务问题的命令链 ===
$ systemctl status 服务 -l        # ① 状态、Result、进程退出码
$ journalctl -u 服务 -xe          # ② 完整日志 + 解释
$ systemctl cat 服务              # ③ 看生效的 unit 文件全文
$ systemd-analyze verify 服务     # ④ 校验 unit 文件语法
$ systemctl list-dependencies 服务 # ⑤ 看它依赖了谁
# 按这个顺序,systemd 服务问题基本能定位。

命令速查

需求                        命令
=============================================================
启动/停止/重启服务          systemctl start/stop/restart 服务
看服务状态                  systemctl status 服务 -l
设/取消开机自启             systemctl enable / disable 服务
改完 unit 文件让其生效      systemctl daemon-reload
看某服务完整日志            journalctl -u 服务
实时追踪服务日志            journalctl -u 服务 -f
排查启动失败黄金命令        journalctl -u 服务 -xe
看本次/上次开机日志         journalctl -b  /  journalctl -b -1
只看错误级别日志            journalctl -u 服务 -p err
看生效的 unit 文件全文      systemctl cat 服务
校验 unit 文件语法          systemd-analyze verify 服务
看 journal 占用磁盘         journalctl --disk-usage

口诀:服务起不来 status 看 Result -> timeout 多半 Type 配错
      改完 unit 必须 daemon-reload;完整日志靠 journalctl -u

避坑清单

  1. 服务起不来,systemctl status 看头部 Result/code,journalctl -u -xe 看全程
  2. Result: timeout 多半是 Type 配错,systemd 在傻等一个等不到的启动信号
  3. Type=simple 给前台常驻程序,forking 给自己 fork 的传统 daemon
  4. 前台常驻程序错配 Type=forking,systemd 会傻等到超时再判定失败
  5. status=203/EXEC 是 ExecStart 命令没法执行,查路径和执行权限
  6. 改了 unit 文件必须 systemctl daemon-reload,否则改动不生效
  7. ExecStart 必须写绝对路径,systemd 环境很裸不能依赖 PATH
  8. systemd 服务日志统一进 journal,用 journalctl -u 查,别到处翻 /var/log
  9. journal 默认可能只存内存,重启就丢,建 /var/log/journal 目录才持久化
  10. journalctl -xe 的 -x 会补充解释和建议,排查启动失败很有用

总结

这次"手动跑得好好的程序,交给 systemd 就启动失败"的事故,纠正了我一个对 systemd 角色的根本性误解。在这次之前,我心里把 systemd 想象成一个"代跑命令的助手":我写在 ExecStart 里的那条命令,就是我想跑的程序;systemd 不过是替我把这条命令敲下去、回个车而已。在这个理解里,systemd 跑命令和我自己在终端里跑命令,本质上是同一件事——既然我手动跑这个程序它活蹦乱跳,那 systemd 替我跑,自然也该一样。正是这个假设,让我对那个 90 秒的卡顿和最终的 failed 完全无法理解:命令是同一条,程序是同一个,凭什么我跑就成功、它跑就失败?复盘到根上,我才真正明白,systemd 远不只是一个"代跑命令的助手"——它是一个"服务的监护人"。它和我手动跑命令最本质的区别在于:我手动跑一个程序,我只管"把它启动起来",程序起来之后是死是活,我不会一直盯着;而 systemd 接管一个服务,它要负责这个服务的【整个生命周期】——它要知道这个服务"什么时候算启动成功了",要持续监控它"是不是还活着",要在它异常退出时"按策略把它拉起来"。而要扮演好这个监护人的角色,systemd 就必须解决一个我从来没替它想过的难题:它到底【凭什么】判断一个服务"已经启动成功了"?这个问题的答案,恰恰就是 unit 文件里那个我随手填错的 Type 字段。Type 这个字段,本质上是我和 systemd 之间的一份"约定"——它是在告诉 systemd:"我这个程序,是用哪种方式来表示'我启动好了'的。"我填的 Type=forking,这份约定的内容是:"我的程序会在启动后 fork 出一个子进程到后台常驻,然后启动它的那个父进程会主动退出;你 systemd 看到那个父进程退出了,就说明我启动成功了。"这是传统 daemon 程序的行事方式。可我的程序根本不是传统 daemon——它是一个现代的、前台常驻的进程,启动起来就一直在前台跑着,永远不会 fork,更永远不会退出。于是一场漫长的误会就此上演:systemd 严格地遵守着我填下的那份 forking 约定,它启动了我的程序,然后开始【等待】那个"父进程退出"的信号——它在等一个永远不会到来的东西。我的进程在前台尽职尽责地运行着,而 systemd 在旁边痴痴地等着它"退出"。一直等到 90 秒的超时上限,systemd 终于认定"这个服务启动失败了",并且——这一点尤其讽刺——它作为监护人,还"负责任"地把这个它认为"卡住了"的进程给 TERM 掉了。我的程序从头到尾都是健康的,它只是被一份错误的约定给冤杀了。正确的约定本该是 Type=simple:它告诉 systemd,"ExecStart 拉起的那个进程本身就是主进程,它会前台常驻、不 fork、不退出;你只要成功把它 fork 出来,就可以认为它启动成功了。"这次从一个"凭什么我跑就成功"的困惑出发,我最大的收获,是把脑子里"systemd = 代跑命令的助手"这个错误的模型,换成了"systemd = 服务的全生命周期监护人"。而既然它是监护人,它就必须有一套判断服务死活的标准,Type 字段就是我亲手交给它的那把标尺。把这把标尺递错了——把一个前台进程说成是会 fork 退出的 daemon——那么程序本身写得再完美,也会在监护人那套尽职尽责的判断逻辑里,被判一个莫须有的"启动失败"。给 systemd 托管一个服务,第一件要想清楚的事,从来不是 ExecStart 那条命令本身,而是:我的程序,究竟用什么方式,向它的监护人宣告"我已经准备好了"。

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

A 说没收到 B 说发了:一次 Linux tcpdump 抓包定位丢包复盘

2026-5-20 19:38:25

Linux教程

调外部接口莫名卡 5 秒:一次 Linux DNS 解析排查复盘

2026-5-20 19:45:53

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