systemctl stop卡住怎么办?systemd停止服务超时排查与解决
如果你也在为“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 通过 Afternfs-client.service 和 RequiresMountsFor/data 依赖远程挂载如果 /data 挂载点已经因为服务端失联变成了 stale mount停止服务时 systemd 还得尝试卸载这个挂载点一次 NFS 挂载的超时探测往往就是 60 秒甚至更久。你以为自己在停服务实际卡在了下游依赖上。这也是排查时一定要跳脱出单个服务视角的原因。systemctl stop 返回的是一条命令的完成不是单个进程的退场。只要这条链路上任何一个环节在等待你的终端就只能陪它一起等。2. 排查 systemctl stop 耗时过长的完整链路2.1 先看系统当前正在“停止中”的单元遇到 stop 卡住第一步不要急着猜直接问 systemd“你现在到底在等谁”systemctl list-units --statedeactivating --no-pager这条命令会列出所有正处于停止中状态的 unit。如果结果里只有你操作的那个服务问题基本就锁定在服务自身如果冒出来好几个依赖单元说明是整条依赖链在互相等待。再配合 status 看一眼服务当前的“卡点”systemctl status myapp --lines50 --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 InactiveEnterTimestampMainPID 是主进程 IDControlPID 是正在执行的 ExecStop 控制进程 ID。如果 ControlPID 不为空说明 ExecStop 还在跑而且可能已经卡住了如果 MainPID 不为空且状态变成了 zombie那说明进程已经退出但父进程没有回收它systemd 同样会等。还要检查 cgroup 里有没有残留进程cat /sys/fs/cgroup/system.slice/myapp.service/tasks如果 tasks 文件里一堆 PID说明服务的控制组还没有清空。这些子进程哪怕不是主进程只要还在同一 cgroup 里systemd 的 KillModecontrol-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 --typemount --statemounting,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 默认的 KillModecontrol-group 会把服务 cgroup 里的所有进程都纳入管理范围。主进程如果先退出了但派生的子进程还活着、被 reparent 到 init 进程只要这些子进程还在原来那个 cgroup 里systemd 就会继续等待。这类情况最常见的场景是服务主进程是 daemon 模式自己 fork 了好几个 workerworker 不响应 SIGTERM主进程反而先退出了。外面看 MainPID 已经是空但 systemctl status 依然显示 deactivating一查 cgroup tasks 才发现还有一堆进程。处理方式要区分业务类型。如果这些 worker 本来就该独立管理可以把它们移到单独的 service 里让 systemd 分开管理如果不想改架构就通过 ExecStop 脚本主动把子进程树清干净或者在 service 里设置 KillModemixed让主进程先收 SIGTERM其余进程由主进程自己处理处理不掉的再在超时后统一收 SIGKILL。要注意KillModemixed 并不适合所有场景。如果子进程和主进程没有清晰的协作关系随便改 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] Typesimple TimeoutStopSec30 KillSignalSIGTERM TimeoutStopFailureModekillTimeoutStopSec 的含义是 systemd 等待 SIGTERM 生效的最长时间如果超时未退会继续发 SIGKILL。TimeoutStopFailureModekill 表示超时后强制清掉同组进程这个参数默认值就是这个行为写出来是为了让行为更明确。这里的关键是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 unitmyapp.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 秒自动触发发布暂停。不需要额外的 agentsystemctl 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 tracefile,lock,futex追踪一眼通常答案就在卡住的那个系统调用上。这也是为什么我一直强调“用工具看现场而不是靠猜”。6.2 单元文件里容易被忽略的 StopWhenUnneeded 与 PartOf部分服务如果配置了 PartOfxxx.service那么当 xxx.service 被停止时该服务也会跟着停止而这类隐式停止很容易让排查方向跑偏。建议在排查完主服务后顺手看一眼所有 PartOf、BindsTo 相关配置确认没有隐藏的联动停止行为。6.3 我的最终建议遇到 systemctl stop 时间长先不要碰任何参数按顺序做四件事看 deactivating 单元列表、看服务状态和 ControlPID、看 cgroup tasks、看依赖和挂载。根因定位后优先改业务进程的退出逻辑其次改 ExecStop 的超时保护最后才是调整 TimeoutStopSec。把停止流程的耗时纳入发布监控长期记录数据才能真正告别“重启一下等两分钟”的被动局面。希望这套排查链路能帮你把每次 stop 卡住的时间从“看运气”变成“按图索骥”。