凌晨两点,手机在床头柜上震了一下。我迷迷糊糊摸起来一看,是一个服务进程挂掉了,内存占用异常飙高,把后面一连串请求全部拖垮。当时我刚接手一批服务器,还没有任何监控手段,全靠第二天早上登录机器用top和ps翻历史。那次之后我花了几天把整个告警链路从零搭起来,之后夜里手机基本只有误报测试才响。
这个事让我意识到,所谓“守护服务器”,真正落地的第一步,不是上什么高端系统,而是先把关键进程盯住,把异常状态用最快的方式通知到自己。这篇文章我想完整整理一套 Linux 进程监控与实时告警的入门方案:先讲清楚要监控什么、为什么这么监控,再给一套零依赖的 Bash 脚本实现,最后拆解告警渠道、定时调度、常见坑位,以及后续往监控平台平滑迁移的路线。适合刚接手服务器、对 Linux 有一定基础、想快速搭起一套“能守夜”的监控体系的人。
1. 先搞清楚:进程监控到底在看什么
动手写脚本之前,最应该想明白的并不是“用什么命令”,而是你期望监控替你做哪几件事。
1.1 进程监控的四个观察维度
我刚做监控时犯过一个典型错误:只检查进程在不在。后来一个 Java 服务进程一直在,但端口已经不监听了,线上请求全部超时,监控却一声不吭。那次之后我把进程监控拆成了四个维度:
- 存活状态:进程是否还在运行,pid 是否存在,systemd 管理的服务是否处于 active 状态。
- 资源占用:CPU 使用率、内存 RSS 占用、磁盘 I/O、文件描述符数量有没有越界。
- 行为异常:进程是否变成了僵尸状态,是否出现短时间内反复崩溃重启,子进程数量是否异常膨胀。
- 业务可用性:端口是否正常监听、健康检查接口是否返回预期结果。这一层严格说已经超出“进程监控”本身,但它是进程监控最自然的延伸。
这四个维度不是平行的,而是递进关系。进程活着不一定代表服务正常,资源没超不代表响应快。对一个“守护”级监控来说,至少要把前两层做扎实,第三层是性价比很高的加分项,第四层建议在进程监控跑通后尽快补上。
1.2 阈值怎么定:不拍脑袋,看基线
很多人在配置阈值时有一个误区:随便设一个“看起来合理”的数字,比如 CPU 超过 80% 就报警。结果不是误报满天飞,就是真出事了还没触发。
我现在的做法是先跑基线。新机器上线后,先让监控脚本只记录不告警,跑三天到一个星期,把同一时段内的 CPU、内存、进程数的正常波动区间摸出来。比如一个业务进程平时内存稳定在 2.1GB,峰值 2.6GB,突然某天涨到 4GB,这就是明确的信号;如果平时就是 1GB 到 4GB 波动,那 4GB 根本不该报警。
常用的进程监控指标和参考维度,我整理成一个表格:
| 指标 | 推荐采集方式 | 参考阈值思路 |
|---|---|---|
| 存活状态 | pgrep -f或 systemd 状态 | 进程不存在立即告警 |
| CPU 使用率 | /proc/<pid>/stat或ps -o %cpu | 单进程持续超过 100%(即一整个核心)就要关注,多核机器可放宽到 200% |
| 内存 RSS | /proc/<pid>/status的 VmRSS,或ps -o rss | 较稳定基线基础上超出 40%-50% 持续 5 分钟以上 |
| 文件描述符 | /proc/<pid>/fd目录计数 | 超过ulimit -n上限的 80% 就要排查泄漏 |
| 僵尸进程 | ps -eo stat中带 Z 状态 | 任何数量的僵尸进程都值得调查来源 |
| 重启次数 | systemd 的NRestarts或自建计数 | 60 秒内超过 3 次立即告警 |
阈值本身不是死的。我的经验是:存活状态必须 0 延迟、立即告警;资源类指标要设置“持续 N 分钟”的条件,避免瞬时尖峰误报;重启类指标则要配合防抖逻辑,避免崩溃循环带来的告警风暴。
2. 化繁为简:先写脚本而不是上平台
市面上现成的监控工具很多,Prometheus、Zabbix、Grafana,功能都很强。但如果你刚接手几台服务器,我强烈建议先写脚本,把流程跑通,再决定要不要上平台。
2.1 为什么我推荐脚本起步
脚本方案的核心优势是“零依赖、零侵入、完全可控”。
- 不需要额外安装服务,不会占用太多系统资源。一个每分钟执行的 Bash 脚本,CPU 开销基本可以忽略。
- 逻辑完全透明,出了问题你自己能看懂。监控平台的一句话告警“内存使用率超过阈值”,排查时你还得自己去翻面板;脚本里可以顺手把进程的完整命令行、最近日志片段一起捞出来。
- 后续不管是接企业微信、钉钉、Telegram 还是单纯发邮件,改动都在脚本内部,非常灵活。
我当时对比过两种方案:直接部署一套完整的监控平台,还是先用脚本。平台方案光是部署、配置、学面板就得花一两天,而且它默认的告警规则是针对整个主机而不是具体某个业务进程,定制起来反而绕。脚本方案当天晚上就能跑起来。
另一个实际原因是:脚本在最小范围内验证了“监控什么指标真正有用”,比如确认你的进程在不同负载下的正常范围。等到数据积累够了、告警规则打磨好了,再平移到平台上,规则直接照着脚本里的参数抄过去就行。平台是放大工具,不是定义工具。
2.2 采集数据依赖的几把“尺子”
脚本里的核心命令要先说清楚,因为“用什么命令”直接决定了监控能不能抓准。
pgrep是最常用的进程查找命令。pgrep -f <pattern>会匹配完整的命令行,比只匹配进程名要准确得多。比如有个程序二进制名是app-server,但你可能启动了多个实例,命令行里带了不同的-config参数,这时按完整命令行匹配才能区分目标。
但pgrep -f有个容易踩的坑:你自己写的监控脚本名字里如果包含了要匹配的关键字,脚本运行时也会被当成目标进程匹配出来。我第一次写监控脚本时,脚本名就叫check_app.sh,里面用pgrep -f app,结果脚本永远认为自己还活着。解决办法有两个:一是进程匹配关键词写得更精确,比如带唯一参数;二是在匹配时排除掉自己,最常用的是pgrep -f "app-server" | grep -v $$,或者直接用完整的启动命令作为匹配串。
采集 CPU 和内存时,ps命令要注意单位。ps -o %cpu显示的百分比是进程相对于单核 CPU 的使用率,不是相对于整机所有核心。8 核的机器上,一个进程跑满 8 个线程,%cpu可能显示 700%,但这并不一定是异常。内存方面,要关注 RSS 而不是 VSZ。RSS 是进程实际占用的物理内存,VSZ 是虚拟内存,包含了很多可能永远不会被真正使用的地址空间,数值虚高,没有任何告警价值。
判断 systemd 管理的服务时,systemctl is-active <service>返回active才是正常的。但要注意,一个服务处于 active 状态并不代表它的端口在监听,所以更可靠的方式是配合端口检查。可以用ss -ltn或者ss -ltnp看指定端口是否有进程监听,这一步对“业务可用性”维度尤其重要。
2.3 核心命令组合:模拟一次“体检”
假设我们要监控一个名为app-server的进程,它在 8080 端口提供服务。一个完整的监控判断流程是这样的:
# 1. 先确认进程是否存在 pgrep -f "app-server" > /dev/null if [ $? -ne 0 ]; then echo "进程不存在" fi # 2. 进程存在时,采集它的 PID PID=$(pgrep -f "app-server" | head -1) # 3. 采集 CPU 和内存占用 ps -o %cpu=,rss= -p "$PID" # 4. 检查端口是否在监听 ss -ltn | grep -q ":8080"这套命令组合的价值在于分层判断:第一步是“还在不在”,第四步是“业务通不通”,中间两步是“活得好不好”。告警文案里如果能把这四层的结果都带上,你半夜收到消息时基本不用再上服务器二次确认。
我实际用的监控脚本会把这些结果汇总成一段告警消息,比如进程不存在、端口无响应、CPU 飙到 300% 持续了 5 分钟,消息里直接写清楚。这里多花十分钟,后面能省无数次深夜登录服务器排查的时间。
3. 守护的两个层面:监控与自愈
监控发现问题只是第一步,真正能在夜里帮你减少损失的,是“自愈机制”——让服务在异常时自动恢复,而不是等你去处理。Linux 上实现自愈分两种情况。
3.1 systemd 自带的服务守护姿势
如果你的服务是通过 systemd 管理的,其实自带了一层守护功能,很多人没有充分利用它。
给某个服务写 systemd unit 文件时,最关键的三个参数:
[Service] Restart=always RestartSec=5s StartLimitIntervalSec=60s StartLimitBurst=3Restart=always:进程退出后立即重新拉起。RestartSec=5s:重启前等待 5 秒,防止快速失败时把 CPU 打满。StartLimitIntervalSec和StartLimitBurst:限制在 60 秒内最多重启 3 次,超过这个次数就放弃,系统不会被一个“反复崩溃又反复拉起”的进程拖垮。
这个配置的本质是:能自动恢复的服务让它自己恢复,但如果它进入“死循环崩溃”模式,要继续保留状态让监控发出明确告警,而不是让它像电脑抽风一样无限重启。
有一个细节需要注意:Restart=always并不等于“进程一直在跑”。如果你没有设置上面两个启动限制,一个入参校验失败就退出的进程会在 60 秒内被拉起几十次,整台机器都跟着遭殃。我见过一台机器因为这种配置问题,系统负载飙升到 20 多,最后还是重启才解决。
3.2 非 systemd 进程的看守逻辑
业务里还有一类进程不在 systemd 管理之下,比如直接在命令行里启动的、通过 nohup 后台跑的、或者打包在第三方脚本里启动的。这时候自愈就得自己写看守逻辑。
看守逻辑最常见的问题是“误判进程不存在”,然后重复拉起,导致多份实例同时运行。要避免这个问题:第一,用 PID 文件记录进程 ID;第二,拉起前检查进程是否真的存在;第三,拉起操作本身要加锁,防止两个监控循环同时执行。
一个简单的带锁拉起逻辑:
# 用 flock 保证同一时间只有一个看守进程在操作 exec 9>/var/lock/app-monitor.lock flock -n 9 || exit 0 # 检查 PID 文件对应的进程是否还活着 if [ -f /var/run/app-server.pid ]; then OLD_PID=$(cat /var/run/app-server.pid) if kill -0 "$OLD_PID" 2>/dev/null; then exit 0 # 进程正常,不需要拉起 fi fi # 进程不存在,重新启动并记录新 PID nohup /opt/app/bin/app-server > /var/log/app-server.log 2>&1 & echo $! > /var/run/app-server.pid这套自愈逻辑的核心是“先验证、再操作”。还要注意,自愈动作本身必须记录日志,否则第二天你发现服务时间线对不上,根本不知道它夜里自己重启了几次。
自愈机制和监控脚本的分工要明确:监控脚本负责观察和上报,自愈动作如果和监控放在同一个脚本里,要保证在进程反复崩溃超过一定次数之后停止自动拉起,把问题暴露出来交给告警,而不是无限隐性地“自我修复”。
4. 告警链路搭建与防打扰设计
监控是眼睛,告警是嘴巴。嘴巴如果一直乱叫,或者该叫的时候叫不出来,前面的监控做得再好也白搭。
4.1 告警渠道选择与消息内容设计
告警渠道的选择要看你的使用场景。我的经验是这样的:
| 渠道 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 邮件 | 正式记录、后续审计 | 可留痕、可归档 | 可能被垃圾邮件过滤,不适合紧急告警 |
| 企业微信/钉钉群机器人 | 团队协作最常用 | 实时性好,手机必达,支持 markdown | 依赖外网 API,内网机器需要额外配置出口代理 |
| Telegram Bot | 个人服务器、海外服务器 | 免配置、支持群组 | 国内网络环境不友好 |
| 短信/电话 | 核心业务故障 | 必达性最强 | 成本高、需要专业服务 |
对多数人来说,最现实的组合是:内网服务器用邮件做归档,同时给企业微信或钉钉的群机器人发 webhook,实现手机实时通知。如果你不想接外部 API,本地直接用mailx发邮件是最简单的方案。
设计 webhook 的时候,告警消息内容要比“某进程异常”更有价值。我实际用的消息模板长这样:
[生产环境] 服务异常告警 主机名:prod-01 时间:2025-01-18 03:22:10 监控项:app-server 进程 状态:进程不存在 当前值:未发现匹配进程 持续时间:约 3 分钟 最近日志:/var/log/app-server.log 最后 20 行见附件为什么要把主机名、时间、当前值、最近日志都塞进去?因为半夜收到告警的你,第一反应肯定不是登录服务器,而是先判断“要不要现在爬起来处理”。如果消息里已经写清楚“进程不存在,且最近日志里有 OutOfMemoryError”,你基本能判断是内存问题,可以先去睡,白天再处理;如果写的是“进程反复崩溃,已被自动拉起”,你甚至可以直接忽略。
curl 发企业微信机器人 webhook 的例子:
curl -s -X POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的机器人key' \ -H 'Content-Type: application/json' \ -d '{ "msgtype": "markdown", "markdown": { "content": "## 服务异常\n**主机**:prod-01\n**服务**:app-server\n**状态**:进程不存在" } }'注意 webhook 的 URL 里不要直接写永久密钥,要放在独立的配置文件里,脚本从文件里读取。否则脚本一旦被同事看到或者传到代码仓库里,任何人都能往你的群里发垃圾消息。
4.2 告警去重、恢复通知与静默窗口
告警领域有一个很实际的问题:重复告警。进程挂了 10 分钟,监控每分钟执行一次,如果你的告警逻辑是“发现异常就发消息”,10 分钟你就得收 10 条消息。我见过有人一夜收了两百多条同样的告警,最后把手机静音继续睡,等真正需要处理时人已经麻了。
去重机制不复杂:用状态文件记录上一次的告警状态。只有状态发生变化时才发消息。比如上次状态是“正常”,这次变成“异常”,发一条“进程挂掉”;如果上次已经是“异常”,这次还是异常,说明还未恢复,不再重复发,只更新状态文件里的持续时间。
下面是一个状态文件去重的简化示例:
STATE_FILE=/var/run/app-monitor.state # 读取上一次状态 PREV_STATE="" [ -f "$STATE_FILE" ] && PREV_STATE=$(cat "$STATE_FILE") # 本次状态 CURRENT_STATE="ALERT" if [ "$PREV_STATE" != "$CURRENT_STATE" ]; then # 状态发生了变化,才发送告警 echo "$CURRENT_STATE" > "$STATE_FILE" send_alert "app-server 进程异常" fi恢复通知也要做。进程恢复正常后,发一条“服务已恢复”的消息,这样夜里被吵醒的人知道事情已经解决,可以安心睡。恢复通知的去重和异常去重是同一套逻辑:状态从“异常”变回“正常”时发。
静默窗口和告警升级是更进阶的设计。所谓静默窗口,就是在某些已知的维护时段(比如凌晨做备份的时段)暂时关闭资源类告警,避免误报;告警升级是指:进程挂掉后如果 5 分钟内没有恢复,给更高优先级的渠道(比如电话)再发一条,提醒一线人员翻车了。脚本里用一个时间戳文件记录首次异常时间,超过预设时长就触发升级逻辑。这些都是典型的生产环境实战经验,建议从第一天就设计进去,避免后续频繁改动。
5. 定时调度、日志审计与开机自启
监控脚本本身需要被周期性地执行,还要保证自身在系统重启后能重新跑起来,并且运行过程有记录。这一节把脚本工程化。
5.1 定时调度:cron 还是 systemd timer
手动跑一次监控脚本没有任何意义,你需要让它在后台定期执行。最常用的两个工具是 cron 和 systemd timer。
cron 简单直接,适合“系统已经配好 cron 服务、不用多做一轮适配”的环境。一行配置即可:
* * * * * /opt/monitor/app_monitor.sh > /dev/null 2>&1但 cron 有一个短板:它的执行记录默认要看系统日志,如果脚本执行失败、超时甚至整个 cron 服务挂了,cron 本身不会给你任何通知。对“守护”工作来说,监控器本身失聪是很危险的。
我推荐有条件的环境用 systemd timer。它自带执行日志管理、失败状态查询、执行时间统计,而且和 systemd 的集成度更高。一个监控任务的 timer 文件如下:
[Unit] Description=Run app monitor every minute [Timer] OnCalendar=*:0/1 AccuracySec=1s Persistent=true [Install] WantedBy=timers.target对应的 service 文件:
[Unit] Description=App Monitor Service [Service] Type=oneshot ExecStart=/opt/monitor/app_monitor.sh启用方式:
systemctl daemon-reload systemctl enable app-monitor.timer systemctl start app-monitor.timer和 cron 相比,systemd timer 带来的最大好处是“可观测”:systemctl status app-monitor.timer能看到上次执行时间、下次执行时间;journalctl -u app-monitor.service能看每次执行的完整输出。排查问题的时候,这些信息比起翻 cron 的日志舒服太多了。
5.2 日志留痕与告警审计
监控脚本自身也要写日志。最理想的状态是:把每次检查的动作、结果、异常状态都完整记下来,方便事后回溯。
一个简单的日志函数:
LOG_FILE=/var/log/app-monitor.log log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG_FILE" } log "检查开始" # ... 执行监控逻辑 log "检查结束,状态:正常"日志里一定要记录几个固定字段:时间、主机名、监控项、状态、当前值、告警结果。这样后续用grep也能快速检索。这些日志文件要留意大小,避免服务器跑几个月后磁盘被日志撑爆。
解决办法是配置 logrotate。Linux 发行版默认都装了 logrotate,只需在/etc/logrotate.d/下放一个配置:
/var/log/app-monitor.log { weekly rotate 4 compress missingok notifempty }这个配置的意思是:每周轮转一次,保留最近 4 份,轮转后的旧日志压缩存放。运维里 90% 的日志相关问题,都是因为没有考虑“日志文件会无限增长”这件事。
开机自启方面,如果你用 systemd timer,systemctl enable就已经处理了开机自启。如果用的 cron,要确认 cron 服务本身是 enabled 状态,同时建议把监控脚本放在/opt/monitor/这种固定目录,不要放在会被清理的临时目录里。
6. 常见坑位与排查速查
搭监控的坑,往往不是搭不起来,而是搭起来之后开始出现各种误报和漏报。我把我踩过的主要坑位整理成了速查表,并按排查优先级排序。
6.1 五个高频问题复盘
| 现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 监控脚本判断“进程不存在”,但服务其实在跑 | pgrep -f匹配词太宽泛,匹配到了脚本自身、被排除或权限不足 | 在脚本里加上 `ps -ef | grep |
| 告警风暴,同一告警短时间内刷屏 | 状态去重逻辑缺失,每次检查都触发发送 | 查看告警发送时间间隔是否都是调度周期 | 引入状态文件去重、静默窗口 |
| 进程在但端口不通,服务实际不可用 | 只做了进程存活检查,没做端口/健康检查 | ss -ltn查看监听状态,curl -f访问健康检查接口 | 在监控维度中增加端口和 HTTP 健康检查 |
| 僵尸进程杀不死,反复告警 | 僵尸进程已经死掉但父进程没回收它,kill 无效 | ps -eo stat,pid,ppid找到僵尸进程,检查其父进程 | 处理父进程或重启父进程,而不是尝试杀僵尸进程 |
| 监控脚本自身占用过高 CPU | 循环检查多个进程时脚本写得低效,或采集命令太过频繁 | top看脚本进程资源占用 | 合并采集命令,拉长检查周期,避免脚本内嵌大循环 |
第一个坑值得多说一句。pgrep -f配合head -1取第一个 PID,在多实例部署时可能抓到的是错误实例。如果你的机器上既有app-server-1又有app-server-2,匹配词写得太模糊,pgrep结果可能不是你想要的。这种情况下,更推荐用进程启动时自带的唯一命令行参数来定位,比如pgrep -f "app-server --config=/etc/app/prod.yaml"。宁可匹配条件苛刻一点,也不能让它抓到别的进程。
第二个坑是告警风暴,绝大多数刚搭监控的人都会碰到。我把它理解为:监控系统的“误报”比“漏报”更消耗人,因为误报多了以后,真正告警来了你反而忽略了。所以去重和静默窗口不是可选项,是必选项。
第四个坑和“看门狗思维”有关。你以为“杀完僵尸进程就好了”,实际上僵尸进程是父进程没有调用wait回收子进程导致的,它本身已经不消耗 CPU 和内存,但会占着 PID 号。长期积累的僵尸进程会把 PID 号耗尽,新进程无法启动。监控脚本看到有僵尸,最优做法是通知你去看它的父进程,而不是在脚本里自动杀掉(很可能是误杀)。
6.2 独门排查心法
分享一条常用排查路径,从告警消息反推问题源头,可以节省大量时间。
收到一条“CPU 使用率超过 300%”的告警,不要急着跑top,先看告警消息里的最近日志和进程启动时间。如果最近日志里出现线程 dump 或者 OutOfMemoryError,先猜内存泄漏;如果日志一切正常,再用top -Hp <pid>看进程内线程占用,确认是哪个线程烧的 CPU;如果是配置文件被改、流量突增导致的正常波动,看入口流量和最近部署记录,通常一翻就明白了。
这条心法的核心是:用监控采集的数据缩小范围,而不是拿着top漫无目的地按 CPU 排序。一次省下十几分钟,高峰期可能就是避免故障扩大的关键。
7. 从脚本到平台:可视化的下一步
脚本方案解决了“有没有”的问题,但它毕竟只是单机视角。当服务器数量多起来,或者你需要回看历史趋势、对比多个节点的同一指标,脚本的边界就出来了。这时候可以考虑上一个轻量级监控平台。
7.1 什么信号说明该上平台了
如果出现下面几个迹象,说明脚本方案的维护成本开始追平平台方案的部署成本:
- 服务器数量超过 5 台,相同的脚本要在每台机器上部署一份,版本更新要一台台手动同步。
- 需要把历史曲线拉出来看:一个星期前的 CPU 到底是多少,脚本做不到,它的历史只有日志里零散的记录。
- 告警规则复杂了:要根据标签分环境、按业务分组、按不同阈值配置不同的接收人。这些在脚本里靠分支逻辑实现,每增加一条规则就要改一次脚本,越来越难维护。
- 需要可视化展示:老板或者同事想看“服务的实时负载、资源使用率”,脚本没有仪表盘,还得临时手动登录机器。
出现两个以上迹象,就可以把平台迁移提上日程了。
7.2 成熟路线的选型参考
一个轻量但够用的常用组合是:Node Exporter 或进程级 Exporter 负责采集主机和进程指标,Prometheus 负责存储和计算告警规则,Alertmanager 负责告警路由和去重,Grafana 负责画仪表盘。整个过程跑下来,告警规则可以直接从脚本里抄,监控指标比脚本还要多一个维度:持续的历史趋势。
迁移平台有几个坑要提前注意:端口冲突、内存占用、数据保留时间。Prometheus 默认只保留 15 天的数据,如果要求保留一个月,需要配置retention参数。对资源紧张的小服务器来说,Prometheus 的内存占用不可忽视,如果只监控一台机器,可以先用轻量方案,比如直接用 Uptime Kuma 这类自带界面和告警的开源工具,连 Prometheus 都不用装。
无论选哪条路线,脚本方案里的设计经验——指标维度、阈值基线、去重逻辑、告警消息格式——都可以原封不动地带过去。平台只是把“采集、存储、展示、通知”四件事做得更自动化,你对业务进程的理解,才是监控系统真正的核心资产。
最后说一点体会。监控和告警这类系统,价值不在于配置了多少个监控项、发了多少条告警,而在于它稳定运行的时间里,你有多少次没被它打扰。那些收不到告警的安静夜晚,才是它真正在好好工作。如果你第一次搭,挑一个最重要的业务进程,先把脚本跑起来,把日志写起来,把告警调通,观察一周再说。一个守不住的夜班,比任何花哨的方案都更能教会你判断阈值、过滤噪音、区分真假异常。