这两天我们有个服务出现了很严重的内存泄漏问题,本来按说既然是内存泄漏问题那我们就找到内存泄漏原因然后解决掉就行了,但是因为那个服务不是我写的,然后现在又很忙抽不出那么多时间,所以就先用临时的方法补救一下,写个监测脚本发现不对就自动重启
脚本内容如下(脚本名称:monitor.sh)
注意我这里是根据 CPU 使用率判断的,因为这个服务即使内存泄漏很严重但是服务本身没有挂掉还在运行,只是请求进不去了
下面脚本加了一些内容,因为我发现有的服务器上环境变量不对启动不了 jar 包,还有就是有的程序启动的时候会有一段时间 CPU 占比非常大,所以又加了一个冷却时间,防止一直重启
#!/bin/bash
APP_HOME="/data/application/app"
JAR_NAME="app-1.0.jar"
LOG_FILE="$APP_HOME/monitor.log"
RESTART_RECORD="$APP_HOME/.last_restart_time"
# 设置 Java 环境
export JAVA_HOME=/data/midsoftware/ali_jdk_8.16.17
export PATH=$JAVA_HOME/bin:$PATH
# 进入应用目录
cd $APP_HOME || { echo "Failed to cd $APP_HOME"; exit 1; }
PID=$(ps aux | grep "$JAR_NAME" | grep -v grep | awk '{print $2}')
CURRENT_TIME=$(date +%s)
COOLDOWN=150 # 150秒冷却时间
LAST_RESTART=0
if [ -f "$RESTART_RECORD" ]; then
LAST_RESTART=$(cat "$RESTART_RECORD")
fi
if [ -z "$PID" ]; then
# 服务未运行,启动
sh startup.sh
echo "$(date '+%F %T') Service not running, starting..." >> $LOG_FILE
date +%s > "$RESTART_RECORD" # 记录启动时间
else
# 检查 CPU
# CPU=$(ps -p $PID -o %cpu= | awk '{print int($1)}')
CPU=$(top -b -n1 -p $PID | tail -n +8 | awk '{print int($9)}')
echo "$(date '+%F %T') PID=$PID CPU=${CPU}%" >> $LOG_FILE
# 注意这个80,指的是单核的80,如果你是8核的满载是800,这里可以加大
CPU_THRESHOLD=80
TIME_DIFF=$(( CURRENT_TIME - LAST_RESTART ))
if [ "$CPU" -gt "$CPU_THRESHOLD" ]; then
if [ "$TIME_DIFF" -lt "$COOLDOWN" ]; then
echo "$(date '+%F %T') CPU exceed $CPU_THRESHOLD%, but within cooldown period, skipping restart." >> $LOG_FILE
else
echo "$(date '+%F %T') CPU exceed $CPU_THRESHOLD%, restarting service..." >> $LOG_FILE
sh shutdown.sh
sleep 5
sh startup.sh
echo "$(date '+%F %T') Restarted service" >> $LOG_FILE
date +%s > "$RESTART_RECORD" # 更新重启时间
fi
fi
fi
加上权限
chmod +x /data/application/app/monitor.sh
然后我们把它加到定时任务里面去
crontab -e
在里面加入一行(注意自己的脚本路径和名称)
* * * * * /data/application/app/monitor.sh >> /data/application/app/monitor.log 2>&1
上面的是每分钟执行一次,服务器上的定时任务最小的粒度就是一分钟了,如果想更细的粒度比如每十秒检查一次可以写成下面这样的
* * * * * /data/application/app/monitor.sh >> /data/application/app/monitor.log 2>&1
* * * * * sleep 10; /data/application/app/monitor.sh >> /data/application/app/monitor.log 2>&1
* * * * * sleep 20; /data/application/app/monitor.sh >> /data/application/app/monitor.log 2>&1
* * * * * sleep 30; /data/application/app/monitor.sh >> /data/application/app/monitor.log 2>&1
* * * * * sleep 40; /data/application/app/monitor.sh >> /data/application/app/monitor.log 2>&1
* * * * * sleep 50; /data/application/app/monitor.sh >> /data/application/app/monitor.log 2>&1
检查一下看是否添加成功,如果下面命令里面能看到你家的那条记录就说明成功了
crontab -l
等一两分钟我们再去看下日志(注意自己的路径)
tail -f /data/application/app/monitor.log
能看到下面这样的
root@ecs-v2:/data/application/app# tail -f /data/application/app/monitor.log
2025-08-22 09:36:01 PID=1736316 CPU=22%
2025-08-22 09:37:01 PID=1736316 CPU=22%
2025-08-22 09:38:01 PID=1736316 CPU=22%
2025-08-22 09:39:01 PID=1736316 CPU=22%
2025-08-22 09:40:01 PID=1736316 CPU=22%
还有那种服务直接挂了重启的脚本,其它的和上面一样,加入到定时任务里面就可以了
#!/bin/bash
APP_HOME=/data/application/app
JAR_NAME=app-1.0.jar
START_SCRIPT=$APP_HOME/startup.sh
# 找到进程 PID
PID=$(ps aux | grep $JAR_NAME | grep -v grep | awk '{print $2}')
cd $APP_HOME
if [ -z "$PID" ]; then
sh $START_SCRIPT
echo "服务已挂掉,已自动重启,时间:`date '+%Y-%m-%d %T'`" >> $APP_HOME/restart_record.log
fi
2025.9.17补充
这几条一直在排查这个内存泄漏问题,中间调整了这个脚本,加了一些检测的功能,这里一并放出来
#!/bin/bash
APP_HOME="/data/application/web-report-controller"
JAR_NAME="web-report-controller-1.0.jar"
LOG_FILE="$APP_HOME/monitor.log"
RESTART_RECORD="$APP_HOME/.last_restart_time"
DUMP_DIR="$APP_HOME/dumps"
# 设置 Java 环境
export JAVA_HOME=/data/midsoftware/ali_jdk_8.16.17
export PATH=$JAVA_HOME/bin:$PATH
mkdir -p "$DUMP_DIR"
cd $APP_HOME || { echo "Failed to cd $APP_HOME"; exit 1; }
PID=$(ps aux | grep "$JAR_NAME" | grep -v grep | awk '{print $2}')
CURRENT_TIME=$(date +%s)
COOLDOWN=150
LAST_RESTART=0
if [ -f "$RESTART_RECORD" ]; then
LAST_RESTART=$(cat "$RESTART_RECORD")
fi
if [ -z "$PID" ]; then
sh startup.sh
echo "$(date '+%F %T') Service not running, starting..." >> $LOG_FILE
date +%s > "$RESTART_RECORD"
else
# 使用 /proc 文件系统获取准确的内存信息
if [ -f "/proc/$PID/status" ]; then
RSS_KB=$(grep VmRSS /proc/$PID/status | awk '{print $2}')
VSZ_KB=$(grep VmSize /proc/$PID/status | awk '{print $2}')
RSS_MB=$((RSS_KB / 1024))
VSZ_MB=$((VSZ_KB / 1024))
# 使用top获取实时CPU
CPU=$(top -b -n1 -p $PID 2>/dev/null | tail -1 | awk '{print int($9)}')
# 使用ps获取内存百分比
MEM=$(ps -p $PID -o %mem --no-headers 2>/dev/null | awk '{print int($1)}')
echo "$(date '+%F %T') PID=$PID CPU=${CPU}% MEM=${MEM}% RSS=${RSS_MB}MB VSZ=${VSZ_MB}MB" >> $LOG_FILE
# 注意这个300,指的是单核的80,如果你是8核的满载是800,这里可以加大
CPU_THRESHOLD=300
TIME_DIFF=$(( CURRENT_TIME - LAST_RESTART ))
if [ "$CPU" -gt "$CPU_THRESHOLD" ]; then
JSTACK_FILE="$DUMP_DIR/jstack_${PID}_$(date +%F_%H-%M-%S).log"
jstack $PID > "$JSTACK_FILE" 2>&1
echo "$(date '+%F %T') High CPU detected, jstack saved to $JSTACK_FILE" >> $LOG_FILE
if [ "$TIME_DIFF" -lt "$COOLDOWN" ]; then
echo "$(date '+%F %T') CPU exceed ${CPU_THRESHOLD}%, but within cooldown period, skipping restart." >> $LOG_FILE
else
echo "$(date '+%F %T') CPU exceed ${CPU_THRESHOLD}%, restarting service..." >> $LOG_FILE
sh shutdown.sh
sleep 5
sh startup.sh
echo "$(date '+%F %T') Restarted service" >> $LOG_FILE
date +%s > "$RESTART_RECORD"
fi
fi
else
echo "$(date '+%F %T') PID $PID not found in /proc, process may have terminated" >> $LOG_FILE
fi
fi
里面会输出这样日志,还会生成 jstack_4019781_2025-09-17_01-29-21.log 线程状态记录,根据生成的这些文件我这边已经找到了问题原因,就是 GC 太频繁导致的,里面一堆的 GC 线程在执行

456

被折叠的 条评论
为什么被折叠?



