前言
最近突然收到告警,发现服务频繁挂掉。怀着一丝不安,登录 Linux 服务器上,熟练地敲下那个最经典的命令:free -h。当看到输出结果的那一刻,Swap 分区使用率 100%。
这是一个典型的服务器内存问题的开端,也是我最近亲身经历的一次排查之旅。这不仅仅是一个找到并杀死一个高内存进程那么简单的故事,而是一个层层深入,最终发现系统监控配置不当的经典案例。这篇文章将完整复盘整个排查过程,希望能给遇到同样问题的朋友们带来一些启发。
第一步:发现问题,触目惊心的 Swap 占用
一切的开始,都源于 free -h 的输出。
[root@lj2508com ~]# free -h
total used free shared buff/cache available
Mem: 2.9G 2.4G 79M 9.8M 450M 355M
Swap: 3.0G 3.0G 28K
分析:
- 物理内存(Mem):总共 2.9G,可用(available)只剩下 355M,情况不容乐观。
- 交换空间(Swap):总共 3.0G,已用 3.0G,使用率 100%!
Swap 被用满是一个极其危险的信号。当物理内存不足时,系统会将一部分不常用的内存数据换到硬盘(Swap 分区)上。硬盘的读写速度与内存相差百倍千倍,大量使用 Swap 会导致系统性能急剧下降,操作卡顿,应用响应缓慢。很明显,服务器正在承受巨大的内存压力。
第二步:定位进程,是谁在“吃”内存?
确定了内存不足后,下一步就是揪出消耗内存的进程。这时候,top 命令就是我们最好的工具。
在终端输入 top,等待几秒数据稳定后,按下 Shift + M,让进程按内存使用率排序。
[root@lj2508com ~]# top
top - 19:48:12 up 298 days, 21:15, 1 user, load average: 0.02, 0.14, 0.20
Tasks: 105 total, 1 running, 104 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.9 us, 1.2 sy, 0.0 ni, 94.8 id, 0.0 wa, 0.0 hi, 0.2 si, 0.0 st
KiB Mem : 3066732 total, 75524 free, 2525108 used, 466100 buff/cache
KiB Swap: 3145724 total, 20 free, 3145704 used. 362704 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5632 root 20 0 3540280 235800 5880 S 1.0 7.7 10:59.88 java
39797 root 20 0 3540124 172772 5344 S 0.7 5.6 23:54.73 java
73728 root 20 0 3540140 170928 4520 S 1.3 5.6 37:16.44 java
69521 root 20 0 3619856 155944 4920 S 0.3 5.1 72:04.02 java
38696 root 20 0 3544256 154996 4436 S 1.3 5.1 103:37.94 java
... (省略更多java进程) ...
分析:
一目了然!排名靠前的全是 java 进程。虽然单个进程占用率(%MEM)在 5%-7% 之间,看起来不是特别夸张,但架不住数量多。粗略一加,光是屏幕上看到的这些 java 进程,就吃掉了超过 70% 的物理内存。
第三步:深挖根源,Java 背后究竟是谁?
java 只是一个执行环境,我们需要知道它到底在运行什么程序。ps -ef 命令能帮我们看到进程的完整启动命令。
[root@lj2508com ~]# ps -ef | grep java
root 586 1 0 Aug06 ? 01:12:49 /opt/jdk-22.0.1/bin/java -jar /root/email-2.1.2-SNAPSHOT.jar
root 5632 1 1 03:21 ? 00:11:00 /opt/jdk-22.0.1/bin/java -jar /root/email-2.1.2-SNAPSHOT.jar
root 7924 1 0 Aug08 ? 01:07:02 /opt/jdk-22.0.1/bin/java -jar /root/email-2.1.2-SNAPSHOT.jar
root 8440 1 0 Aug10 ? 01:56:41 /opt/jdk-22.0.1/bin/java -jar /root/email-2.1.2-SNAPSHOT.jar
root 37511 1 0 Aug12 ? 01:29:30 /opt/jdk-22.0.1/bin/java -jar /root/email-2.1.2-SNAPSHOT.jar
... (省略更多) ...
分析:
这是决定性的证据!
- 身份确认:所有
java进程运行的都是同一个程序:/root/email-2.1.2-SNAPSHOT.jar。 - 启动时间:请看日期列(Aug06, Aug08, Aug10…),这些进程是在过去很多天里,一天天被启动起来的。
根本原因:一个名为 email-2.1.2-SNAPSHOT.jar 的应用被重复启动,而旧的进程从未被关闭。日积累,这些“僵尸”进程最终耗尽了服务器的所有资源。
第四步:追溯原因,Monit 的“好心办坏事”
最初,我们以为这是一个配置错误的 cron 定时任务。但最终确认,真凶是 Monit——一个进程监控工具。
Monit 的工作机制是持续监控一个服务是否健康,如果不健康,就自动重启它。问题出在了“重启”策略上:
- 错误的健康检查:
Monit配置了一个(可能已经失效的)健康检查。当检查失败时,Monit就认为服务挂了。 - 有缺陷的重启:
Monit执行的“重启”命令,仅仅是重新执行了一遍java -jar ...的启动命令,而没有先去杀死那个可能只是“假死”或“响应缓慢”的旧进程。
这就形成了一个恶性循环: Monit 检查失败 -> Monit 启动新进程 -> 服务器负载更高 -> Monit 启动更多新进程…
第五步:解决问题,从紧急处理到长治久安
1. 紧急处理:清理进程
首先,用 pkill 命令清理掉所有残留的进程,让服务器喘口气。
# -f 参数会匹配完整的命令行,确保我们只杀死目标应用
pkill -f email-2.1.2-SNAPSHOT.jar
执行后,再看 free -h,内存和 Swap 应该都已恢复正常。
2. 根治问题:修正 Monit 配置
为了实现“确保服务一直且只有一个在运行”的目标,我们需要一份更健壮的 Monit 配置。
# 配置文件: /etc/monit.d/email_service
# 监控名为 email-service 的服务
# 使用 matching "..." 来通过 ps 命令查找进程
check process email-service matching "email-2.1.2-SNAPSHOT.jar"
# 定义「启动」命令
# 使用 nohup ... & 让程序在后台运行,并将日志输出到文件
start program = "/bin/bash -c 'nohup /opt/jdk-22.0.1/bin/java -jar /root/email-2.1.2-SNAPSHOT.jar > /root/email_app.log 2>&1 &'"
# 定义「停止」命令
# pkill -f 会杀死所有匹配的进程,用于重启服务前的清理
stop program = "/bin/bash -c 'pkill -f email-2.1.2-SNAPSHOT.jar'"
这份配置利用 matching 实现了核心目标:当进程不存在时,自动拉起。当需要重启时,stop program 会先清理掉所有旧进程,再由 start program 启动新进程,从而避免了进程累积。
更优方案提示:虽然
matching能解决问题,但业界更推荐使用 PID 文件 (with pidfile) 的方式来管理服务。PID 文件能让 Monit 精确地“认准”由它自己启动的那个进程,实现了更严格的“一对一”管理,是杜绝此类问题的最佳实践。
总结与反思
这次排查是一次非常典型的案例,它告诉我们:
- Swap 占用是重要线索:它往往是内存问题最直观的表象。
- 工具要层层递进:从
free(宏观) ->top(定位) ->ps(深挖),一步步缩小排查范围。 - 自动化不等于智能化:像
Monit这样的自动化工具非常强大,但也需要被正确地配置。错误的“重启”策略有时比服务宕机本身更具破坏性。
评论区