☰
systemctl stop卡住怎么办?systemd停止服务超时排查与解决
2026/10/19 9:16:54 网站建设 项目流程

如果你也在为“systemctl stop xxx 时间太长”头疼,说明你大概率已经不止一次遇到过这种诡异现象:明明服务本身负载不高,进程也没几个,可执行 systemctl stop 之后,终端就像被冻住一样,光标闪了好几分钟才回到提示符。更麻烦的是,这条命令还会拖垮整条发布链路,超时、误判、强制重启接踵而来。

这篇内容我会从 systemd 停止服务的完整生命周期讲起,再带你走一遍可复现的排查链路,最后给出针对不同根因的处置手段和参数取舍。不管你是刚接触 systemd 的初级运维,还是已经被这类问题折磨过的老手,这套思路都值得收藏。

1. 卡住的本质:systemd 停止服务到底在等谁

1.1 systemctl stop 并不仅仅是“杀进程”

很多人对 systemctl stop 的理解是:发个信号过去,进程收到后退出,命令返回。真实情况远没有这么简单。systemd 处理一次 stop 事件,实际要经历好几个阶段,任何一个阶段卡住,都会让命令迟迟不算完。

第一阶段是“进入停止中状态”。systemd 会把目标 unit 标记为 deactivating,然后开始执行停止流程。第二阶段是“发送停止信号”。根据服务单元文件的配置,systemd 会向主进程发送 SIGTERM,如果配置了 ExecStop,还会先执行 ExecStop 里的命令。第三阶段是“等待超时窗口”。systemd 会等待进程退出,这个等待时间由 TimeoutStopSec 控制,默认是 90 秒。第四阶段是“强制清理”。如果 90 秒内进程还没退出,systemd 才会发送 SIGKILL,把残留进程强制清掉。

所以,当你看到 systemctl stop 卡住时,本质上是它在等待某个“超时窗口”结束,或者等待某个依赖单元退出完毕。你以为只过了两分钟,其实 systemd 已经默默等了整整一个 TimeoutStopSec,然后又花了额外时间处理依赖。

1.2 等待依赖单元引发的连锁超时

systemd 对服务之间的依赖关系处理是递归的。你的服务即便自己退出得很快,如果它依赖的网络挂载点、DBus 服务、socket 单元还在慢慢关闭,stop 命令同样要等那些单元彻底结束才算完成。

举一个很典型的例子:服务 A 通过 After=nfs-client.service 和 RequiresMountsFor=/data 依赖远程挂载,如果 /data 挂载点已经因为服务端失联变成了 stale mount,停止服务时 systemd 还得尝试卸载这个挂载点,一次 NFS 挂载的超时探测往往就是 60 秒甚至更久。你以为自己在停服务,实际卡在了下游依赖上。

这也是排查时一定要跳脱出单个服务视角的原因。systemctl stop 返回的是一条命令的完成,不是单个进程的退场。只要这条链路上任何一个环节在等待,你的终端就只能陪它一起等。

2. 排查 systemctl stop 耗时过长的完整链路

2.1 先看系统当前正在“停止中”的单元

遇到 stop 卡住,第一步不要急着猜,直接问 systemd:“你现在到底在等谁?”

systemctl list-units --state=deactivating --no-pager

这条命令会列出所有正处于停止中状态的 unit。如果结果里只有你操作的那个服务,问题基本就锁定在服务自身;如果冒出来好几个依赖单元,说明是整条依赖链在互相等待。

再配合 status 看一眼服务当前的“卡点”:

systemctl status myapp --lines=50 --no-pager

重点看 Active 那一行,状态是 deactivating 时,还会显示一个具体描述,比如 “stop-sigterm” 或 “stop-final-sigterm”,这两个词非常关键。stop-sigterm 表示 systemd 已经发了 SIGTERM,还在等进程自己退出;stop-final-sigterm 表示第一次等待超时后,systemd 发了 SIGKILL,正在等进程被彻底回收。看到后一种情况,你基本可以确定问题就出在进程对 SIGTERM 不敏感或者压根不响应。

2.2 顺着 PID 找“赖着不走”的进程

一旦确认服务进入了 deactivating,下一步就是看进程树里还有什么东西在跑。

systemctl show myapp -p MainPID -p ControlPID -p ActiveEnterTimestamp -p InactiveEnterTimestamp

MainPID 是主进程 ID,ControlPID 是正在执行的 ExecStop 控制进程 ID。如果 ControlPID 不为空,说明 ExecStop 还在跑,而且可能已经卡住了;如果 MainPID 不为空且状态变成了 zombie,那说明进程已经退出但父进程没有回收它,systemd 同样会等。

还要检查 cgroup 里有没有残留进程:

cat /sys/fs/cgroup/system.slice/myapp.service/tasks

如果 tasks 文件里一堆 PID,说明服务的控制组还没有清空。这些子进程哪怕不是主进程,只要还在同一 cgroup 里,systemd 的 KillMode=control-group 逻辑就会把整个组当作目标,直到所有进程都消失,stop 流程才算走完。

2.3 用日志确认卡在执行链路的哪一段

如果控制进程 PID 存在,直接看它在干什么:

ps -o pid,ppid,stat,etime,cmd -p <ControlPID> cat /proc/<ControlPID>/stack 2>/dev/null ps -ef --forest | grep -A 20 myapp

习惯性再打开 journal 看服务日志输出:

journalctl -u myapp --since "5 minutes ago" --no-pager

很多时候你会发现,进程日志最后一行停在某个业务清理逻辑上,说明它不是“退出不了”,而是在做退出前的清理工作,只是清理太慢了。比如它会等待一个 HTTP 接口的超时返回,或者等数据库连接池回收完毕,这些网络类的等待很容易让人产生“卡死”的错觉。

2.4 检查依赖与挂载点,别漏掉下游单元

服务自身没有进程残留,ControlPID 也是空的,可 stop 依然卡住,这时必须把视野扩大。

systemctl list-dependencies myapp --reverse --no-pager systemctl show myapp -p After -p Requires -p Wants -p Requisite

再检查是不是有相关挂载点卡住了:

mount | grep myapp systemctl list-units --type=mount --state=mounting,deactivating --no-pager

一次真实案例里,某个服务的脚本会在停止时尝试卸载一个已经不存在的挂载点,导致 stop 卡了整整 180 秒。这类问题单看服务本身永远看不出毛病,必须把依赖和挂载状态一起纳入排查范围。

3. 四种常见根因与对应解法

3.1 主进程不响应 SIGTERM,熬到超时被强杀

这是最普遍的一种根因。进程收到 SIGTERM 后,既没有执行退出逻辑,也没有忽略信号,而是什么都不干。systemd 会等满 TimeoutStopSec,然后发送 SIGKILL 兜底。

SIGTERM 的本质是一个“请自行退出”的信号,能不能退出成功,取决于应用有没有正确的 signal handler。很多业务代码对 SIGTERM 的处理只是简单地忽略掉,或者注册了一个 handler 但 handler 内部在做耗时操作,比如等待线程池 drain、等待 RPC 连接关闭。

判断方法也很简单:如果 systemctl show 看到状态一直停留在 stop-sigterm,并且 TimeoutStopUSec 从启动 stop 开始计算已达到 90 秒,那大概率就是主进程没响应。

这类问题的最优解不是调大超时时间,而是让业务进程真正响应 SIGTERM。进程内要有一个信号处理器,收到 SIGTERM 后完成必要的清理工作再退出,而且要保证清理动作本身有超时保护,比如设置一个 5 秒的上下文,超时就直接 os.Exit(1),而不是无限等下去。只有把“优雅退出”的控制权从 systemd 的强杀逻辑里夺回来,stop 才能又快又稳。

3.2 cgroup 里残留子进程,主进程退了但“满门抄斩”没完成

systemd 默认的 KillMode=control-group 会把服务 cgroup 里的所有进程都纳入管理范围。主进程如果先退出了,但派生的子进程还活着、被 reparent 到 init 进程,只要这些子进程还在原来那个 cgroup 里,systemd 就会继续等待。

这类情况最常见的场景是:服务主进程是 daemon 模式,自己 fork 了好几个 worker,worker 不响应 SIGTERM,主进程反而先退出了。外面看 MainPID 已经是空,但 systemctl status 依然显示 deactivating,一查 cgroup tasks 才发现还有一堆进程。

处理方式要区分业务类型。如果这些 worker 本来就该独立管理,可以把它们移到单独的 service 里,让 systemd 分开管理;如果不想改架构,就通过 ExecStop 脚本主动把子进程树清干净,或者在 service 里设置 KillMode=mixed,让主进程先收 SIGTERM,其余进程由主进程自己处理,处理不掉的再在超时后统一收 SIGKILL。

要注意,KillMode=mixed 并不适合所有场景。如果子进程和主进程没有清晰的协作关系,随便改 kill 模式反而会造成资源泄漏或状态不一致。改之前先在测试环境模拟一遍 stop 场景,确认子进程能被正常回收再上生产。

3.3 ExecStop 里的命令卡住,把整个停止流程拖死

ExecStop 是很多人忽略的坑。systemd 在停止服务时会先执行 ExecStop 里配置的命令,而且 ExecStop 的命令默认也要受 TimeoutStopSec 约束。如果 ExecStop 里的脚本本身在等待某个不可用的资源,比如 SSH 到另一台机器执行命令、请求一个超时接口、等待一个永远等不到的文件锁,那 systemctl stop 就会一直卡在 ControlPID 阶段。

看 Systemd 状态时,只要 ControlPID 存在且一直不退出,基本就是 ExecStop 的问题。用 ps 看一眼这个控制进程到底在等什么,通常答案立刻浮出水面。

解决方向有两条。第一,把 ExecStop 脚本写成绝对“快失败”的逻辑,所有外部调用都加超时,所有等待都设上限;第二,如果 ExecStop 只是为了做清理,而这些清理动作可以异步完成,不如把这段逻辑挪到服务进程退出后通过单独的一次性 timer 去做,别让 stop 流程承担“清理不干净”的责任。

提示:ExecStop 里不要写没有超时保护的 shell 循环。我见过有人在 ExecStop 里用 while :; do curl xxx && break; sleep 1; done 这种写法,一旦远端接口持续异常,stop 就是一场灾难。

3.4 挂载点或依赖单元退不掉,服务被“殃及池鱼”

服务本身干干净净,问题却出在它依赖的 mount unit 或者其他 socket/device 单元上。比如服务的工作目录在 NFS 挂载点上,停服时 systemd 会先停服务再尝试卸载挂载点,如果 NFS 服务端失联,umount 就会卡在 IO 等待上,一层层传导回 systemctl stop。

这种情况通过 list-dependencies 和 mount 状态可以快速定位。处理上要么调整依赖关系,让服务不再强依赖那个远程挂载;要么给卸载操作配置超时参数,比如 NFS 挂载时增加 soft 模式,避免底层 IO 无限阻塞。总之,不要让一个远程资源的失联,把本地服务的 stop 流程锁死。

4. 别急着把 TimeoutStopSec 调小,先搞懂配置取舍

4.1 TimeoutStopSec 到底应该怎么改

很多人一遇到 stop 卡住,第一反应就是缩短 TimeoutStopSec,让 systemd 快速发送 SIGKILL。表面上命令迅速返回了,但这是用进程的“体面退出”换来的,代价可能很大。

正确的改法是把预期的退出时间明确写进 unit,而不是拍脑袋调小。比如你的服务在收到 SIGTERM 后,理论上需要 20 秒完成落盘和清理,那就配置:

[Service] Type=simple TimeoutStopSec=30 KillSignal=SIGTERM TimeoutStopFailureMode=kill

TimeoutStopSec 的含义是 systemd 等待 SIGTERM 生效的最长时间,如果超时未退,会继续发 SIGKILL。TimeoutStopFailureMode=kill 表示超时后强制清掉同组进程,这个参数默认值就是这个行为,写出来是为了让行为更明确。

这里的关键是:30 秒要基于真实业务需求来设定,不是随便填的。先测出来进程从收到 SIGTERM 到正常退出的最坏耗时,再在这个基础上加 50% 的余量,才是一个可用的值。

4.2 为什么盲目缩小超时时间很危险

有些服务退出前需要完成数据一致性动作,典型如数据库、消息队列、本地缓存持久化。SIGTERM 之后它需要把内存里的脏数据写回磁盘、通知对端、释放锁,这个时间是没有办法省略的。你把 TimeoutStopSec 改成 5 秒,systemd 到点就 SIGKILL,等于允许进程在关键写盘动作做到一半时被强行截断。

轻则重启后数据丢失,重则主从数据不一致、本地文件损坏。我亲眼见过一次线上事故:一个缓存服务为了“快速重启”把 TimeoutStopSec 设成了 3 秒,结果每次重启都会丢一部分缓存重建数据,故障持续了一整天才定位到是强杀导致的写盘未完成。

所以看到 stop 慢,先搞清楚慢在哪,再决定要不要调超时。调超时只是最后手段,而不是优先手段。

4.3 更值得做的方向:让进程“学会”快速退出

与其在 systemd 参数上掰扯,不如从源头优化进程的退出路径。给业务进程加一个退出超时机制,收到 SIGTERM 后只做必要的清理,超过某个阈值就直接退出,那些可以异步完成的刷盘操作留到下一次启动时通过恢复日志处理。

这一层做扎实了,systemd 侧几乎不用做任何特殊配置,默认的 90 秒 TimeoutStopSec 里你的服务 3 秒就退完了,命令当然快。

5. 预防性设计:把 stop 耗时纳入发布检查

5.1 用计时脚本给每次发布“量体温”

先停、后启是发布流程的常态。停服耗时如果不做度量,慢起来就只能靠肉眼看终端干等。我习惯在发布脚本里直接嵌入 stop 耗时测量:

#!/usr/bin/env bash set -euo pipefail unit="myapp.service" echo "[stop] $(date +%H:%M:%S.%N) begin" prev_active=$(systemctl show "$unit" -p ActiveState --value) prev_inactive_ts=$(systemctl show "$unit" -p InactiveEnterTimestamp --value) timeout 120 systemctl stop "$unit" # 重新查 InactiveEnterTimestamp,判断是否真正进入 inactive 状态 final_inactive=$(systemctl show "$unit" -p InactiveEnterTimestamp --value) echo "[stop] $(date +%H:%M:%S.%N) end" echo "final_inactive_timestamp=$final_inactive" echo "prev_inactive_timestamp=$prev_inactive_ts"

通过 InactiveEnterTimestamp 的前后对比,还能看出这次 stop 是不是真的把服务送到了 inactive 状态,避免“命令返回了但其实没停干净”的假象。

5.2 给 stop 设置监控阈值与告警

有监控条件的环境里,建议直接通过 systemd 自带的状态时间戳做指标采集。每次执行停止操作后,计算从 deactivating 到 inactive 的时长,超过 10 秒就产生一条告警,超过 30 秒自动触发发布暂停。

不需要额外的 agent,systemctl show 返回的 InactiveEnterTimestamp 和 ActiveEnterTimestamp 已经足够支撑这套监控逻辑。关键在于持续收集数据,否则一次两次的卡顿很容易被当作偶发现象忽略掉。

5.3 发布平台上的超时防护与降级策略

发布平台调用 systemctl stop 时,要设置比服务本身更严苛的超时判断。比如服务的 TimeoutStopSec 是 90 秒,平台侧在 100 秒还没等到退出就应该主动报错,并允许发布人一键回滚到上一版本。同时保留强制 kill 的运维入口,但要把它设计成需要二次确认的高风险操作。

这本质上是把“系统慢”变成“系统可度量”,让每一次异常都有迹可循。下次再遇到 stop 慢,不用猜,直接看监控数据就知道是哪一次发布开始变慢的。

6. 我实际踩过的坑与最后的补充建议

6.1 一个让我印象深刻的“假死”教训

有一次某服务的 stop 慢得离谱,我排查了半天,发现主进程早就退出了,ExecStop 也是空的,cgroup 里也没有进程。最后用 strace 跟踪 systemd 的 stop 进程才发现,它卡在读取一个超大日志文件的上锁操作上——服务运行期间日志轮转把锁持住了,停止流程里有一段日志落盘逻辑等着拿文件锁,差点让我误判成挂载点问题。

这类问题的共性特征是:进程、日志、挂载点表面都正常,但系统调用层面有锁竞争。遇到这种“查无可查”的情况,建议直接用:

timeout 30 strace -f -p <systemd_pid> -e trace=file,lock,futex

追踪一眼,通常答案就在卡住的那个系统调用上。这也是为什么我一直强调“用工具看现场,而不是靠猜”。

6.2 单元文件里容易被忽略的 StopWhenUnneeded 与 PartOf

部分服务如果配置了 PartOf=xxx.service,那么当 xxx.service 被停止时,该服务也会跟着停止,而这类隐式停止很容易让排查方向跑偏。建议在排查完主服务后,顺手看一眼所有 PartOf、BindsTo 相关配置,确认没有隐藏的联动停止行为。

6.3 我的最终建议

遇到 systemctl stop 时间长,先不要碰任何参数,按顺序做四件事:看 deactivating 单元列表、看服务状态和 ControlPID、看 cgroup tasks、看依赖和挂载。根因定位后,优先改业务进程的退出逻辑,其次改 ExecStop 的超时保护,最后才是调整 TimeoutStopSec。把停止流程的耗时纳入发布监控,长期记录数据,才能真正告别“重启一下等两分钟”的被动局面。

希望这套排查链路能帮你把每次 stop 卡住的时间,从“看运气”变成“按图索骥”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询