背景:
在1.0(https://blog.csdn.net/xdmxmf0/article/details/156645896?spm=1001.2014.3001.5501)的基础上,增加对springboot微服务的GC监控。前期登录服务器发现进程还在,查看日志也在正常输出,但是,但是,nacos服务注册页面却发现不了。于是一看gclog目录下,发现竟然有.hprof文件。
和1.0的区别:
1、1.0是不需要进行改造的,只要创建相关目录,拿走即用。
2、2.0是有条件的
1、配置了GC相关的参数,尤其是GC后的.hprof,文件位置,尽量都放在一个目录下。
2、脚本内容需要加入.hprof的目录。
触发条件:
.hprof文件通常在以下三种情况下生成:
1. 内存溢出自动生成(最常见)
这是你的脚本主要监控的场景。
- 时机:当 Java 应用程序的内存(堆内存)耗尽,无法再分配新对象,并且抛出
java.lang.OutOfMemoryError: Java heap space时。 - 前提条件:你的 SpringBoot 启动参数中必须包含了以下配置:
-XX:+HeapDumpOnOutOfMemoryError(告诉 JVM:当 OOM 时,生成堆转储文件)-XX:HeapDumpPath=/data/mdm2.0/deploy/logs/gclog/(告诉 JVM:文件存到哪里,如果不指定路径,默认存在项目根目录下,文件名通常是java_pid<pid>.hprof)
- 文件特征:此时生成的文件瞬间大小会很大(接近配置的
-Xmx大小),生成过程可能会导致应用“假死”几秒到几十秒。
2. 触发 Full GC 后生成(如果配置了相关参数)
有些系统为了排查内存泄漏,会在发生长时间 Full GC 时 dump 内存。
- 时机:当系统发生一次耗时较长的 Full GC(旧年代 GC)。
- 前提条件:启动参数中配置了
-XX:+DumpHeapAtFullGC。 - 注意:这个参数比较少见,容易产生大量 dump 文件撑爆磁盘,通常不建议在生产开启,除非有极强的排查需求。
3. 人工手动生成(运维排查期间)
- 时机:运维人员发现系统 CPU 飙高、响应变慢,怀疑内存泄漏,但还没到 OOM 崩溃的程度。
- 操作方式:
- 使用
jps找到 Java 进程 ID。 - 使用命令
jmap -dump:format=b,file=service.hprof <pid>。 - 或者在 Arthas 等工具中使用
heapdump命令。
- 使用
- 特征:这种文件名通常是自定义的(如
manual_dump_2023.hprof),不一定是-dump.hprof格式。
思路:
- 在满足上述条件的情况下,监控gc目录下的
.hprof。 - 如有新生成的,则进行名称处理,进行邮件告警。
- 给这个文件打标记,防止多次邮件告警通知。
- 监控日志,写入到日志文件,便于后期分析记录,审计等。
开干:
创建可执行文件 system_monitor2.0.sh
#!/bin/bash # 系统监控告警脚本 # 检测CPU、内存、磁盘使用率,以及*分钟内登录失败次数,超过阈值时发送邮件告警 # 2.0加入监控服务是否GC,若GC则邮件告警 # 配置参数 CPU_THRESHOLD=85 # CPU使用率阈值(%) MEM_THRESHOLD=80 # 内存使用率阈值(%) DISK_THRESHOLD=80 # 磁盘使用率阈值(%) LOGIN_FAIL_THRESHOLD=5 # 登录失败次数阈值 TIME_WINDOW=5 # 时间窗口(分钟) EMAIL="*****@qq.com" # 告警接收邮箱 SUBJECT="系统资源告警 - $(hostname)" LOG_FILE="/data/dt/script/monitor/$(hostname)_$(date +%Y%m%d)_sys.out" #2.0 GC监控的参数 LOG_DIR="/data/deploy/logs/gclog" # 记录已处理文件的标记目录,防止重复发送邮件 PROCESSED_FLAG_DIR="/tmp/gc_monitor_processed" # 同时输出到屏幕和日志 log() { local msg="[$(date '+%Y-%m-%d %H:%M:%S')]$1" echo "$msg" | tee -a "$LOG_FILE" } send_email() { printf "%s\n" "$2" | /usr/bin/mail -s "$1" "$EMAIL" log "[邮件发送] $1" } check_cpu() { echo "--- CPU 检查 ---" | tee -a "$LOG_FILE" local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print 100 -$8}' | cut -d'.' -f1) log "当前 CPU 使用率: ${cpu_usage}% (阈值:${CPU_THRESHOLD}%)" if [[ "$cpu_usage" =~ ^[0-9]+$ ]] && [ "$cpu_usage" -gt "$CPU_THRESHOLD" ]; then local msg="警告: CPU使用率为 ${cpu_usage}%,超过阈值${CPU_THRESHOLD}%,请及时关注并处理!" send_email "$SUBJECT" "$msg" else log "状态: 正常" fi } check_memory() { echo "--- 内存 检查 ---" | tee -a "$LOG_FILE" # 计算内存使用率 local mem_usage=$(free | awk '/Mem:/ {printf "%d", ($3/$2)*100}') log "当前 内存 使用率: ${mem_usage}% (阈值:${MEM_THRESHOLD}%)" if [ "$mem_usage" -gt "$MEM_THRESHOLD" ]; then local msg="警告: 内存使用率为 ${mem_usage}%,超过阈值${MEM_THRESHOLD}%,请及时关注并处理!" send_email "$SUBJECT" "$msg" else log "状态: 正常" fi } check_disk() { echo "--- 磁盘 检查 ---" | tee -a "$LOG_FILE" local alert_triggered=0 # 使用进程替换 while read output; do local usep=$(echo "$output" | awk '{print $1}' | cut -d'%' -f1) local partition=$(echo "$output" | awk '{print $2}') if [[ "$usep" =~ ^[0-9]+$ ]]; then log "分区 $partition 使用率:${usep}%" if [ "$usep" -ge "$DISK_THRESHOLD" ]; then local msg="警告: 磁盘分区 $partition 使用率为${usep}%,超过阈值 ${DISK_THRESHOLD}%,请及时关注并处理!" send_email "$SUBJECT" "$msg" alert_triggered=1 fi fi done < <(df -h | grep -vE '^Filesystem|tmpfs|cdrom|overlay' | awk '{ print $5 " "$1 }') if [ "$alert_triggered" -eq 0 ]; then log "状态: 所有分区正常" fi } check_login_failures() { echo "--- 安全 检查 (登录失败) ---" | tee -a "$LOG_FILE" local fail_count=0 start_time=$(date +%s -d "${TIME_WINDOW} minutes ago") if [ -f /var/log/secure ]; then fail_count=$(grep "Failed password" /var/log/secure 2>/dev/null | awk -v min="$start_time" '{ cmd = "date +%s -d \""$1" "$2" "$3"\""; cmd | getline log_ts; close(cmd); if (log_ts >= min) print; }' | wc -l) fi log "最近 ${TIME_WINDOW} 分钟内检测到登录失败次数:${fail_count} (阈值:${LOGIN_FAIL_THRESHOLD})" if [ "$fail_count" -ge "$LOGIN_FAIL_THRESHOLD" ]; then local msg="安全警告: 检测到登录失败 ${fail_count} 次,超过阈值${LOGIN_FAIL_THRESHOLD} 次" send_email "$SUBJECT" "$msg" else log "状态: 安全" fi } # 如果标记目录不存在,则创建 mkdir -p "$PROCESSED_FLAG_DIR" # 检查监控目录是否存在 if [ ! -d "$LOG_DIR" ]; then echo "监控目录不存: $LOG_DIR" exit 1 fi check_gc(){ echo "--- 服务GC 检查 ---" | tee -a "$LOG_FILE" # --- 初始化计数器,用于记录本次循环是否处理了新文件 --- local new_file_count=0 while read -r hprof_file; do filename=$(basename "$hprof_file") temp_name=${filename%.hprof} service_name=$(echo "$temp_name" | sed 's/-dump$//') flag_file="$PROCESSED_FLAG_DIR/${filename}.sent" if [ ! -f "$flag_file" ]; then # 发现新文件,计数器 +1 ((new_file_count++)) log "检测到新的 hprof 文件: ${hprof_file},正在发送邮件..." # 获取文件信息 file_size=$(du -sh "$hprof_file" | cut -f1) mod_time=$(stat -c %y "$hprof_file") # 构建邮件内容 (使用 local 变量,虽然在while里无所谓,但好习惯) local msg="检测到服务发生内存溢出(OOM)并生成了堆转储文件。 服务名称: ${service_name} 文件路径: ${hprof_file} 文件大小: ${file_size} 生成时间: ${mod_time} 请尽快登录服务器进行分析! " send_email "$SUBJECT" "$msg" if [ $? -eq 0 ]; then log "邮件发送成功。" touch "$flag_file" else log "邮件发送失败,请检查邮件服务配置。" fi fi # 注意这里结尾的写法:done < <(command) done < <(find "$LOG_DIR" -name "*.hprof" -type f) # --- 循环结束后的判断逻辑 --- if [ "$new_file_count" -eq 0 ]; then log "所有服务运行正常,未检测到新的堆转储文件。" fi } # ---------------- 主程序 ---------------- main() { echo " " | tee -a "$LOG_FILE" echo "========================================" | tee -a "$LOG_FILE" log "MDM2.0 $(hostname) 操作系统健康巡检报告" log "检查时间:$(date '+%Y-%m-%d %H:%M:%S')" log "当前IP: $(hostname -I | awk '{print $1}')" echo "========================================" | tee -a "$LOG_FILE" check_cpu echo " " | tee -a "$LOG_FILE" check_memory echo " " | tee -a "$LOG_FILE" check_disk echo " " | tee -a "$LOG_FILE" check_login_failures echo " " | tee -a "$LOG_FILE" check_gc echo " " | tee -a "$LOG_FILE" echo "========================================" | tee -a "$LOG_FILE" log "检查结束" echo "========================================" | tee -a "$LOG_FILE" } main设置定时任务,略
查看执行结果
查看邮件告警
其他:
该脚本的可塑性较高,灵活配置,根据实际需要进行改造即可。