服务器进程监控怎么做?服务异常检测与自动重启配置

凌晨三点,一个 Java 服务因为内存溢出悄悄退出了,没有告警,没有日志,直到早上九点用户打来电话才发现——这种"服务静默死亡"是运维最怕的场景之一。进程是业务的实际载体,CPU、内存、磁盘的问题最终都会体现在进程上,而进程本身的问题(崩溃退出、卡死、内存泄漏、僵尸化)又往往不会被资源监控直接反映出来。所以进程监控是监控体系里不可跳过的一层。本文讲三件事:怎么看清进程状态、怎么让服务挂了自动拉起、怎么在自动拉起失败时及时通知到人,形成"自愈 + 告警"的完整闭环。

一、进程监控到底要监控什么

很多人以为进程监控就是"进程在不在",实际上至少要看五个维度:

  • 存活状态:进程是否存在,服务是否在运行。
  • 资源占用:CPU、内存(RSS)、打开文件数、线程数是否异常增长。
  • 数量:预期是几个进程?少了说明崩溃,多了说明重复启动或 fork 异常。
  • 状态异常:僵尸进程(Z 状态)、不可中断睡眠(D 状态,通常卡在 I/O)、被 OOM Killer 杀掉。
  • 健康度:进程在,但接口不响应(假死)。这一条最容易被忽略,也是最危险的——监控全绿但业务已经不可用。

这五条对应不同的检测手段:前四条靠系统命令和 exporter,第五条必须靠业务健康检查(HTTP 探针、端口探测、业务指标)。

二、看清进程:命令与判读

# 最常用:查看所有进程,BSD 风格输出
ps aux
# 查看某服务的进程(aux 列:USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND)
ps aux | grep -v grep | grep nginx

# 按 CPU / 内存排序,找消耗大户
ps aux --sort=-%cpu  | head -10
ps aux --sort=-%mem  | head -10

# 看进程树,理解父子关系(对排查 fork 异常很有用)
ps -ef --forest
pstree -p

# 只看 PID 与命令行
pgrep -a nginx
pgrep -f "java -jar myapp.jar"

# 实时视图:按 P 按 CPU 排序,M 按内存排序,c 显示完整命令,1 展开多核
top
htop        # 需安装,更友好

# 查看某进程的资源上限与实际占用
cat /proc/PID/status | grep -E 'VmRSS|Threads|Name|State'
cat /proc/PID/limits | grep -E 'Max open files|Max processes'
ls /proc/PID/fd | wc -l        # 已打开的文件描述符数量
STAT 状态含义是否需要处理
R运行中 / 可运行正常
S可中断睡眠(等待事件)正常
D不可中断睡眠(通常卡在磁盘或网络 I/O)需排查,大量 D 说明存储有问题
Z僵尸进程,已退出但父进程未回收数量持续增长需处理
T被信号暂停确认是否人为 stop
<高优先级一般无需处理
N低优先级(nice)一般无需处理
s / l / +会话组长 / 多线程 / 前台进程附加标志,无需处理

几个实用判断:僵尸进程(Z)少量存在是正常的(父进程还没来得及 wait),但如果数量持续增长,说明父进程有 bug 没有回收子进程,最终会耗尽 PID;D 状态进程无法用 kill -9 杀死,因为它卡在内核态 I/O,只能等 I/O 恢复或重启机器,所以发现大量 D 状态进程要立刻去查存储(磁盘满、NFS 挂了、RAID 卡故障);进程被 OOM Killer 杀掉不会在应用日志里留下痕迹,需要用 dmesg 或 journalctl -k 查内核日志确认。

# 查 OOM:内核日志里会有 "Out of memory: Kill process"
dmesg -T | grep -i "out of memory"
journalctl -k | grep -i -E "oom|killed process"

# 看系统 OOM 总次数
grep -i oom /var/log/messages

# 统计僵尸进程数量
ps -eo stat | grep -c '^Z'
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /^Z/'

# 查某进程被杀前的内存峰值(systemd 服务)
systemctl show myapp -p MemoryCurrent -p MemoryPeak 2>/dev/null
systemctl status myapp | grep -i -E 'memory|killed'

三、自动重启:用 systemd 把服务托管起来

现代 Linux 发行版都用 systemd,它本身就自带完善的进程守护能力,不需要额外装 supervisor 之类的工具。核心就是 [Service] 段的几个 Restart 相关参数。

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target
OnFailure=alert-mail@%n.service      # 最终失败时触发告警单元

[Service]
Type=simple                          # simple/forking/notify/oneshot,见下表
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -jar /opt/myapp/myapp.jar
ExecReload=/bin/kill -HUP $MAINPID

Restart=always                       # 退出就重启
RestartSec=5s                        # 重启前等待 5 秒
StartLimitBurst=5                    # 300 秒内最多重启 5 次
StartLimitIntervalSec=300            # 超过上限后进入 failed 状态

# 资源限制,防止单服务拖垮整机
MemoryMax=2G                         # 硬限制,超过会触发 OOM
MemoryHigh=1500M                     # 软限制,超过会加大回收压力但不杀进程
CPUQuota=200%                        # 最多用 2 核
LimitNOFILE=65535                    # 最大打开文件数
TasksMax=4096

# 安全加固(按需)
NoNewPrivileges=true
PrivateTmp=true

StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target
# 使配置生效
systemctl daemon-reload
systemctl enable --now myapp

# 查看状态、重启次数、最近日志
systemctl status myapp
systemctl show myapp -p NRestarts
journalctl -u myapp -f
journalctl -u myapp --since "10 minutes ago"

# 手动模拟崩溃,验证自愈是否生效
kill -9 $(pgrep -f myapp)
sleep 8
systemctl status myapp      # 应看到 Active: active (running) 且 NRestarts 增加
参数可选值说明
Restartno / always / on-success / on-failure / on-abnormal / on-abort / on-watchdogalways 最常用;on-failure 仅在非 0 退出时重启
RestartSec如 5s、100ms重启间隔,避免高频重启刷爆日志
StartLimitBurst整数,如 5时间窗口内允许的最大重启次数
StartLimitIntervalSec如 300时间窗口长度,配合上一条使用
Typesimple / forking / notify / oneshot前台进程用 simple;会 fork 到后台的老程序用 forking 并配 PIDFile
WatchdogSec如 30s需程序支持 sd_notify 心跳,超时自动重启(对付假死)
OOMPolicystop / continue被 OOM 后是否继续运行,通常保持默认

StartLimitBurst 是这套配置里最容易被忽略却最关键的一条。没有它,一个"启动即崩溃"的服务会进入无限重启循环:每秒重启一次、每次都写几条日志,几分钟内把磁盘写满、把 CPU 打高,还会触发海量告警。设了 StartLimitBurst=5 与 StartLimitIntervalSec=300 后,5 次尝试失败 systemd 就放弃,把服务置为 failed 并触发 OnFailure——既给了服务自愈的机会,又不会失控,还保证了人能收到通知。这个"有限自愈 + 最终告警"的模式是进程守护的标准做法。

另外 Type=forking 的老式守护进程(如某些老版本 MySQL、Nginx 自定义编译)必须配 PIDFile,否则 systemd 跟踪不到主进程,重启时会报"PID file not readable"或出现重复进程。判断方法:进程启动后前台是否一直占用终端,是则 simple,若自动转后台则 forking。

四、对付"假死":健康检查与看门狗

进程在、端口在,但接口返回 500 或超时——这种情况 Restart=always 完全无能为力,因为进程根本没退出。解决办法有两个层次:

层次一,应用内支持 systemd 的 watchdog。程序定期调用 sd_notify("WATCHDOG=1") 发送心跳,systemd 配 WatchdogSec=30s,超过 30 秒没心跳就杀掉重启。这需要改造程序,成本较高但最精准。

层次二,外部健康探测脚本 + 自动重启。用 cron 定时探测接口,连续失败 N 次就重启服务。这是最通用的方案,不需要改程序。

#!/bin/bash
# /opt/scripts/health_guard.sh —— 服务健康检查与自动重启
SERVICE=myapp
URL="http://127.0.0.1:8080/health"
STATE=/var/tmp/health_fail_count
LIMIT=3          # 连续失败 3 次才重启

code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL" 2>/dev/null || echo 000)

if [ "$code" != "200" ]; then
  cnt=$(cat "$STATE" 2>/dev/null || echo 0); cnt=$((cnt+1)); echo "$cnt" > "$STATE"
  logger -t health-guard "$SERVICE 健康检查失败($code),连续失败 $cnt 次"
  if [ "$cnt" -ge "$LIMIT" ]; then
    echo 0 > "$STATE"
    systemctl restart "$SERVICE"
    printf "主机: $(hostname)\n服务: %s\n动作: 已自动重启\n原因: 连续 %s 次健康检查失败(HTTP %s)\n" \
      "$SERVICE" "$LIMIT" "$code" | mail -s "[AUTO-HEAL] $(hostname) $SERVICE 已重启" ops@yourdomain.com
  fi
else
  echo 0 > "$STATE"
fi

# crontab:每分钟执行一次
# * * * * * /bin/bash /opt/scripts/health_guard.sh >/dev/null 2>&1

自动重启必须有配套的保护:一是重启频率上限(上面的 LIMIT 加上 systemd 的 StartLimitBurst),二是每次自动重启都要发通知,让运维知道系统做了什么,避免"服务反复重启三个月没人知道"。三是自动重启不要用在有状态服务上——数据库、消息队列的重启可能触发数据恢复、主从重选,风险远大于收益,这类服务应该只告警不自动重启。

五、长期监控:Prometheus 侧的进程指标

node_exporter 自带的进程指标比较有限,要做细粒度的进程监控需要 process-exporter。它按进程名分组统计 CPU、内存、读写 IO、线程数、僵尸子进程等。

# 启动 process-exporter(监听 9256)
# /etc/process-exporter/config.yml —— 定义进程分组规则
process_names:
  - name: "{{.Comm}}"        # 按进程名分组
    cmdline:
      - '.+'                 # 匹配所有进程

# 更实用的写法:只监控关心的进程
process_names:
  - name: "java_apps"
    cmdline:
      - 'java\s+-jar'
  - name: "nginx"
    cmdline:
      - 'nginx:\s+worker'
  - name: "{{.Comm}}"
    cmdline:
      - 'mysqld'
      - 'redis-server'

# systemd 服务片段
# ExecStart=/usr/local/bin/process-exporter \
#   --config.path=/etc/process-exporter/config.yml --web.listen-address=:9256
# 常用 PromQL
# 某进程组是否至少有一个实例在跑(0 表示全挂了)
sum by (groupname) (namedprocess_namegroup_num_procs{groupname="java_apps"})

# 进程内存占用(字节)
namedprocess_namegroup_memory_bytes{groupname="java_apps", memtype="resident"}

# 进程 CPU 使用率(%),注意要用 rate
rate(namedprocess_namegroup_cpu_seconds_total{groupname="java_apps"}[5m]) * 100

# 进程打开文件数
namedprocess_namegroup_open_filedesc{groupname="java_apps"}

# 僵尸进程数
sum by (groupname) (namedprocess_namegroup_states{state="Zombie"})

# 告警:关键进程消失
- alert: ProcessDown
  expr: sum(namedprocess_namegroup_num_procs{groupname="java_apps"}) == 0
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "关键进程 java_apps 已全部退出"

如果不想引入 process-exporter,用 node_exporter 也能做基础的存活检测:node_systemd_unit_state{name="myapp.service",state="active"} 这个指标(需要 node_exporter 开启 systemd collector)直接反映 systemd 服务状态,值为 1 表示处于该状态。告警表达式写成 node_systemd_unit_state{name="myapp.service",state="active"} == 0,比查进程数更简洁可靠。

# 直接查 systemd 服务状态(node_exporter 需加 --collector.systemd)
node_systemd_unit_state{name="nginx.service",state="active"}

# 告警规则
- alert: ServiceNotActive
  expr: node_systemd_unit_state{name=~"nginx.service|myapp.service",state="active"} == 0
  for: 2m
  labels:
    severity: critical

六、建设进程监控的实施步骤

  1. 梳理关键进程清单:列出每个服务器上"必须活着"的服务,标注是否有状态(决定能否自动重启)。
  2. 把所有自研服务改成 systemd 托管,配置 Restart=always、RestartSec=5s、StartLimitBurst=5、StartLimitIntervalSec=300。
  3. 为服务加上资源限制(MemoryMax、LimitNOFILE),避免单进程拖垮整机。
  4. 配置 OnFailure 告警单元,确保重启次数用尽后能通知到人。
  5. 用 kill -9 做一次崩溃演练,确认服务能在 RestartSec 后自动拉起,且 systemctl show -p NRestarts 计数增加。
  6. 为无状态服务加健康检查脚本(cron 每分钟),连续失败 3 次自动重启并发通知。
  7. 部署 process-exporter 或启用 node_exporter 的 systemd collector,配置"进程消失"告警。
  8. 配置僵尸进程与 OOM 监控:dmesg OOM 关键字告警、僵尸进程数持续增长告警。
  9. 每月复盘一次自动重启记录,把"反复重启"的服务列为技术债优先治理。

小结

进程监控的完整方案是三层叠加:systemd 提供基础自愈(Restart=always + StartLimitBurst 防止失控)、健康检查脚本解决"进程在但服务假死"、Prometheus 提供长期趋势与告警。其中最关键的设计细节是自愈必须有上限、有通知——无上限的自动重启会把小故障放大成磁盘写满、告警风暴的连锁事故,而无通知的自愈会掩盖真实问题,让技术债越积越多。避坑清单:一是只配 Restart=always 不配 StartLimitBurst,导致无限重启循环;二是 Type 选错(forking 程序写成 simple)导致 systemd 跟踪不到主进程、出现重复进程;三是对数据库等有状态服务开启自动重启,风险大于收益;四是只监控进程存在性不做健康检查,进程假死时监控全绿但业务已挂;五是忽略 OOM,应用被内核杀掉后只看到"进程没了"却不知道原因,需要查 dmesg | grep -i oom 确认。把这几点处理好,服务静默死亡的问题基本就能消除。


关键词:服务器进程监控、服务自动重启、systemd Restart、StartLimitBurst、进程健康检查、僵尸进程