凌晨三点监控告警:某服务不可用。你登上机器,进程已经没了。ps aux 查不到、日志停在半小时前、没有报错、没有 core 文件——这就是进程异常退出排障的典型开局。慌乱之下直接重启服务往往能救急,但如果不搞清"它是被谁杀的、为什么死",同样的告警三天后会再来一次。本文给出一套从退出码判定 → 日志取证 → 根因定位 → 恢复与防复发的标准动作。
一、第一步不是看日志,是看退出码
进程是怎么死的,答案藏在退出状态里。这步几秒钟完成,却直接决定了后续排查方向。
# systemd 托管的服务:直接看状态摘要 systemctl status app.service --no-pager -l # 不是 systemd 托管时,用 shell 的上一条命令退出码 echo $? # 最近一次的失败/终结核查 journalctl -u app.service -n 200 --no-pager
systemctl status 的输出里最关键的一行是 "Process: xxx ExecStart=... (code=exited, status=1/FAILURE)" 或 "(code=killed, signal=KILL)"。据此分两大类:
| 退出状态 | 含义 | 最常见的根因 | 下一步动作 |
|---|---|---|---|
| 0(exited, status=0) | 正常退出 | 程序主动结束;或 Type=oneshot 类任务完成 | 检查是不是守护逻辑写错,或 RemainAfterExit 未配 |
| 1 / 2 | 通用失败 / 用法错误 | 配置文件错误、参数不支持、依赖文件缺失 | 手动执行二进制看原始 stderr |
| 126 | 找到但无法执行 | 可执行权限缺失 | ls -l 看权限位 |
| 127 | 命令或文件不存在 | 路径写错、动态库缺失、shebang 指向的解释器不存在 | ldd 检查依赖,核对 ExecStart 路径 |
| 137 | 128+9,被 SIGKILL 杀死 | OOM killer,或人为 kill -9、容器内存超限 | 立刻去查内核日志中的 oom-kill 记录 |
| 139 | 128+11,SIGSEGV 段错误 | 内存越界、脏数据、二进制与库版本不匹配 | 找 core dump,用 gdb 回溯 |
| 143 | 128+15,SIGTERM | 正常停止、容器优雅退出、systemctl stop、编排系统驱逐 | 确认是谁发的信号,多为预期行为 |
| 203/EXEC | systemd 自身的 EXEC 失败 | 二进制不存在、不是可执行文件、SELinux/AppArmor 拦截 | 检查路径、权限与安全模块日志 |
补充一条经验规则:大于等于 128 且减去 128 后是常见信号编号的,一律按"被信号杀死"处理;2xx 开头的是 systemd 自身抛出的特殊码(详见 man systemd.exec 的 "Process exit codes" 一节),意味着进程还没真正启动起来就已经失败了。
二、OOM:最常见的"无声杀手"
137 状态码里,十有八九是被内核的 OOM killer(Out-Of-Memory killer,内存耗尽杀手)干掉的。它的特点是:应用层完全不知道发生了什么,日志没有任何异常,因为进程连处理 SIGTERM 的机会都没有。
2.1 取证命令
# 内核日志是最权威的证据 sudo dmesg -T | grep -iE 'out of memory|oom-kill|oom_reaper|killed process' sudo journalctl -k --no-pager | grep -iE 'oom|killed process' # 只看上一次落盘的启动周期(常用于排查昨晚的故障) journalctl -b -1 -k --no-pager | grep -i oom # systemd 也会记录该 unit 被 OOM 杀掉 journalctl -u app.service --no-pager | grep -i 'oom\|killed'
典型的 OOM 内核日志长这样,要能读懂关键字段:
Memory cgroup out of memory: Killed process 28417 (java) total-vm:8388608kB, anon-rss:3145728kB, file-rss:0kB, shmem-rss:0kB, UID:1001 pgtables:6144kB oom_score_adj:0
其中 anon-rss 是进程实际占用的物理内存(匿名页),pgtables 是页表开销,UID 和进程名用来确认到底挂的是哪个服务。如果日志里出现的是 Memory cgroup out of memory 而不是 plain 的 Out of memory,说明触发的是 cgroup 层面的限制(容器或 systemd 的 MemoryMax),而非整机内存耗尽——这个区别决定了后续是扩容还是调限制。
2.2 cgroup 层面的确认
# 整机内存概览(-m 以 MB 为单位,关注 available 而非 free) free -mh cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|CommitLimit|Committed_AS' # 该服务的内存上限配置 systemctl show app.service -p MemoryMax -p MemoryHigh -p MemorySwapMax -p MemoryLimit # cgroup v2 下查看实际用量与 OOM 计数 CAT=/sys/fs/cgroup/system.slice/app.service cat $CAT/memory.max cat $CAT/memory.current cat $CAT/memory.peak cat $CAT/memory.events # 重点看 oom_kill / max / oom 这几个计数 cat $CAT/memory.stat | grep -E '^(anon|file|slab|sock) ' # 按 cgroup 维度看实时占用排行 systemd-cgtop -m --order=memory
memory.events 里的 oom_kill 如果大于 0,就是铁证。注意 memory.max 默认值往往显示为 max(无限),若显示为一串数字,说明该服务被显式限制了内存。
2.3 应对
- 先止血:如果是整机内存不足,临时释放页缓存并关闭非核心服务,随后评估扩内存或迁移;如果是服务自身内存限制过小,适当上调
MemoryMax。 - 再定位:确认是内存泄漏(RSS 随时间单调上涨)还是瞬时峰值(批量任务、大查询)。用
systemd-cgtop或周期性采样 RSS 判断。 - 最后留证据:给服务配置
MemoryAccounting=yes(systemd 较新版本默认开启)并保留journald持久化,否则重启后一切证据归零。
三、段错误与崩溃:core dump 怎么用
139/SIGSEGV 说明程序访问了不该访问的内存。要拿到根因,必须有 core dump。
# 1. 确认 core 文件的转储方式 cat /proc/sys/kernel/core_pattern ulimit -c # 2. 安装 coredump 收集组件(Debian/Ubuntu) sudo apt install -y systemd-coredump gdb # RHEL/CentOS/Rocky: # sudo yum install -y systemd-oomd gdb 或 dnf install gdb # 3. 列出已捕获的 core coredumpctl list coredumpctl list app.service # 4. 查看某次崩溃的详情 coredumpctl info 28417 coredumpctl info app.service --since '2 hours ago' # 5. 直接进 gdb 看调用栈 coredumpctl gdb 28417 # 进入 gdb 后: bt full → 打印完整栈帧 # info threads → 查看线程 # quit
如果 ulimit -c 返回 0,core 文件不会生成。临时放开用 ulimit -c unlimited,永久放开要在 systemd 侧设置(PAM 的 limits.conf 对服务无效):
sudo systemctl edit app.service # [Service] # LimitCORE=infinity sudo systemctl daemon-reload && sudo systemctl restart app.service systemctl show app.service -p LimitCORE
注意 systemd-coredump 会把 core 压缩后存在 /var/lib/systemd/coredump/,受 /etc/systemd/coredump.conf 中的空间限制约束。磁盘紧张时它可能自动清理旧 core——这往往发生在你最需要它的时候。
四、日志取证:把 journal 用好
很多人的排障方式是 cat /var/log/syslog | grep xxx,这在 journald 时代效率很低。掌握下面几条足够覆盖 90% 场景。
# 让日志真正落盘(默认可能为易失,重启即丢) sudo mkdir -p /var/log/journal sudo systemd-tmpfiles --create --prefix /var/log/journal sudo systemctl restart systemd-journald journalctl --disk-usage # 按时间窗口精准定位 journalctl -u app.service --since '2024-05-01 02:50' --until '2024-05-01 03:10' --no-pager # 只看某个 PID 的全部日志 journalctl _PID=28417 --since yesterday --no-pager # 按优先级过滤(0 emerg ... 7 debug;常用 3=err、4=warning) journalctl -p warning..err -b --no-pager # 上一次开机周期(排查重启过的问题必备) journalctl --list-boots journalctl -b -1 -u app.service --no-pager # 与该服务相关的所有单元(含依赖) journalctl -u 'app*' -b --no-pager # 实时盯 journalctl -u app.service -f
三个实用技巧:
- 先看
--list-boots。如果机器在故障时间点重启过,当前 boot 的日志里往往什么都查不到。 - 给关键服务配
SyslogIdentifier,业务日志就会带上统一标识,检索效率成倍提升。 - 永久化 + 限容一起配:在
/etc/systemd/journald.conf的[Journal]段设置Storage=persistent和SystemMaxUse=2G,避免日志把磁盘撑爆后又反过来导致服务崩溃。
五、常见根因速查表
| 根因 | 典型现象 | 验证命令 | 处置 |
|---|---|---|---|
| 整机内存耗尽 | 多个进程同时被杀,137,free 中 available 接近 0 | dmesg -T \| grep -i oom | 限流/降配/扩容,必要时引入 earlyoom 或 systemd-oomd 做可控回收 |
| 磁盘写满(ENOSPC) | 报 No space left on device,但 df -h 还有余量 | df -i、lsof \| grep deleted | 清空日志、回收被删除但仍被占用的文件句柄 |
| inode 耗尽 | 无法创建文件,df -i 使用率 100% | df -i、find /dir -xdev -type f \| wc -l | 清理小文件密集目录(如 session、cache) |
| 文件描述符耗尽 | 报 Too many open files,accept 失败 | ls /proc/PID/fd \| wc -l、ulimit -n | 调 LimitNOFILE,排查 fd 泄漏 |
| 端口被占用 | 启动时报 Address already in use | ss -lntup \| grep :8080 | 停掉占用者或改用健康检测端口 |
| 配置/代码发布后崩溃 | 重启时间附近有代码或配置变更 | stat /etc/app/*.conf、查看发布记录 | 回滚配置,用 -t 类自检命令验证 |
| 依赖服务不可用 | Connection refused 到数据库/Redis | ss -lntp、依赖服务的 systemctl status | 配置 After=/Requires= 依赖关系 |
| systemd 启动频控触发 | 报 Start request repeated too quickly,反复 restart | systemctl show -p NRestarts | 先 reset-failed,再调整 StartLimit* |
| 二进制被包更新替换 | 升级后立刻失败,127/203 | ldd $(which app)、apt history | 补依赖、核对 ExecStart 路径 |
5.1 手动复现:最被低估的排障手段
服务由 systemd 拉起时,stderr 常常被吞掉。用相同的用户和环境手动跑一遍,原始错误通常立刻出现:
# 找出配置里的实际 User / ExecStart / WorkingDirectory systemctl cat app.service # 用同一用户手动执行,看原始报错 sudo -u appuser -H /opt/app/bin/app --config /etc/app/app.conf # 若怀疑是解释器/动态库问题 ldd /opt/app/bin/app | grep 'not found' head -1 /opt/app/bin/app # 若服务启动后立刻退出,盯住它的资源限制 cat /proc/$(pgrep -f 'bin/app' | head -1)/limits
六、恢复与防复发
6.1 正确恢复的步骤
- 清理失败状态。当启动频控(StartLimit)被触发时,单元进入
failed状态,直接start会被拒绝,必须先清计数:
systemctl reset-failed app.service systemctl start app.service systemctl is-active app.service
- 确认恢复而非假活:
ss -lntp看端口是否真的监听,业务健康检查是否通过,而不只看active (running)。 - 保留现场:即使恢复了,也先把这一周期的日志导出来存到别处:
journalctl -u app.service -b --no-pager > /tmp/app-$(date +%F-%H%M).log。
6.2 让服务"死了能自己爬起来"
sudo systemctl edit app.service # [Service] # Restart=on-failure # RestartSec=5s # RestartPreventExitStatus=0 2 143 # WatchdogSec=30 # TimeoutStartSec=120 # StartLimitIntervalSec=300 # StartLimitBurst=10 sudo systemctl daemon-reload sudo systemctl restart app.service # 验证自定义配置已生效 systemd-analyze verify /etc/systemd/system/app.service.d/override.conf systemctl show app.service -p Restart -p RestartSec -p StartLimitBurst
这里的默认值很关键:systemd 的默认启动频控是 10 秒内失败 5 次(DefaultStartLimitIntervalSec=10s、DefaultStartLimitBurst=5,见 /etc/systemd/system.conf)。也就是说,一个启动失败后 5 秒内就尝试重启的服务,默认配置下很快就再也起不来了。生产环境务必显式调大这两个值。
另一个常被忽略的参数是 RestartPreventExitStatus。把 143(正常的 SIGTERM)写进去,可以避免运维手动 systemctl stop 时 systemd 又把它拉起来——这个坑会让 stop 变成"永不生效"。
6.3 把监控补上
- 对
NRestarts做监控告警,比单纯监控"进程是否存在"更能反映反复崩溃。 - 对 cgroup 的
memory.events里oom_kill计数做监控,OOM 就能第一时间被感知。 - 监控 RSS 趋势而非只看瞬时值,内存泄漏在 RSS 曲线上是一条稳定的上升斜线。
- 关键服务配
WatchdogSec,让卡死(deadlock)而不退出的进程也能被 systemd 判定并重拉。
小结
进程异常退出的排查顺序可以固化成一句话:先看退出码定性,再按类别找证据,最后才动手动 restart。137 找 OOM,139 找 core,203 找路径和权限,143 多半是预期行为。
避坑清单:
- 不要一开始就
systemctl restart——它会销毁现场。至少先执行systemctl status、导出本周期日志、记录 PID 后再动。 - 务必开启 journald 持久化(
Storage=persistent),否则重启后日志全丢,OOM 与 panic 类的证据再也找不回来。 - 别把
LimitNOFILE、LimitCORE写在/etc/security/limits.conf就想当然认为对 systemd 服务生效,那里只对 PAM 登录会话起作用。 - 调
Restart=always之前想清楚:一个必然失败启动的服务配always,等于给自己制造一台不断重启的日志生成器。 StartLimitBurst默认只有 5,生产服务的重启策略一定要显式配置。- 磁盘满和 inode 满的表现都是"写不进去",但排查命令完全不同(
df -hvsdf -i),别只查一半。 - core dump 依赖磁盘空间,
/tmp或/var太小时 coredumpctl 里会空空如也。
能复盘一次的故障才算真正解决。把每次排障的证据链写进内部 wiki,下次遇到同类问题时,三分钟就能定性。
企业QQ咨询




