编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

  • Shell-Bash

    • Shell & Bash 从0到1实战专栏
    • Shell 入门与变量
    • 流程控制与函数
    • 数据与 IO 处理
    • grep 搜索实战
    • sed 与 awk 编程
    • 文件查找与统计
    • 日志监控与告警
      • 7.1 日志分析
        • 7.1.1 常见日志格式解析
        • 7.1.2 访问量统计——PV/UV/QPS/带宽
        • 7.1.3 Top IP 分析与异常流量检测
        • 7.1.4 状态码分布与异常监控
        • 7.1.5 响应时间分析——揪出慢请求
        • 7.1.6 错误日志提取、分类与告警
      • 7.2 系统监控
        • 7.2.1 CPU 监控——使用率/负载/进程排行
        • 7.2.2 内存监控——使用率/OOM 风险/Swap
        • 7.2.3 磁盘监控——空间/inode/IO
        • 7.2.4 网络监控——流量/连接/端口
        • 7.2.5 进程存活检测与自动重启
        • 7.2.6 定时采集与资源趋势分析
      • 7.3 综合告警系统实战
        • 7.3.1 告警策略设计——多级阈值与冷却机制
        • 7.3.2 告警通知——邮件/钉钉/企业微信
        • 7.3.3 一体化监控告警脚本——从采集到通知
        • 7.3.4 新手陷阱 Top 5
        • 7.3.5 综合思考题
    • 备份进程与磁盘
    • 用户与服务管理
    • 网络调度与部署
    • 调试与脚本规范
    • 安全与兼容处理
    • 性能与打包分发
  • 工具脚本

  • 自动化脚本

  • ScriptHub
  • Shell-Bash
杨充
2021-10-28
目录

日志监控与告警

# 第 7 章 日志监控与告警

# 目录介绍

  • 7.1 日志分析
    • 7.1.1 常见日志格式解析
    • 7.1.2 访问量统计——PV/UV/QPS/带宽
    • 7.1.3 Top IP 分析与异常流量检测
    • 7.1.4 状态码分布与异常监控
    • 7.1.5 响应时间分析——揪出慢请求
    • 7.1.6 错误日志提取、分类与告警
  • 7.2 系统监控
    • 7.2.1 CPU 监控——使用率/负载/进程排行
    • 7.2.2 内存监控——使用率/OOM 风险/Swap
    • 7.2.3 磁盘监控——空间/inode/IO
    • 7.2.4 网络监控——流量/连接/端口
    • 7.2.5 进程存活检测与自动重启
    • 7.2.6 定时采集与资源趋势分析
  • 7.3 综合告警系统实战
    • 7.3.1 告警策略设计——多级阈值与冷却机制
    • 7.3.2 告警通知——邮件/钉钉/企业微信
    • 7.3.3 一体化监控告警脚本——从采集到通知
    • 7.3.4 新手陷阱 Top 5
    • 7.3.5 综合思考题

# 7.1 日志分析

# 7.1.1 常见日志格式解析

能看懂日志格式是分析的基础。以下是运维中最常见的几种:

#!/bin/bash

# ===== 1. Nginx / Apache 访问日志(Combined 格式)=====
# 示例行:
# 192.168.1.100 - - [10/Jun/2025:14:30:00 +0800] "GET /api/users HTTP/1.1" 200 1234 "https://example.com" "Mozilla/5.0"
#  $1             $2 $3  $4                     $5            $6      $7   $8  $9                 $10

# 字段解析:
#  $1 = 客户端 IP
#  $2 = 远程登录名(通常为 -)
#  $3 = 远程用户名(通常为 -)
#  $4 = 时间戳 [日/月/年:时:分:秒 时区]
#  $5 = 请求行 = 方法 URL 协议
#  $6 = HTTP 方法(GET/POST...)
#  $7 = 请求 URL 路径
#  $8 = HTTP 协议版本
#  $9 = HTTP 状态码(200/301/404/500...)
#  $10= 响应字节数
#  $11= Referer
#  $12= User-Agent

# ===== Nginx access.log 一行解析脚本 =====
cat > parse_nginx_log.sh << 'EOF'
#!/bin/bash
LOG_FILE="${1:-/var/log/nginx/access.log}"

awk '{
    # 提取时间戳
    gsub(/[\[\]]/, "", $4)     # 去掉方括号 [ ]
    timestamp = $4 " " $5      # 时间 + 时区

    # 提取请求信息
    gsub(/"/, "", $6)          # 去掉引号
    method = $6
    url = $7

    # 提取关键指标
    ip = $1
    status = $9
    bytes = $10

    # 输出解析结果
    printf "[%s] %s %-7s %s → HTTP %d, %d bytes\n",
        timestamp, ip, method, url, status, bytes
}' "$LOG_FILE"
EOF

# ===== 2. Apache 错误日志格式 =====
# [Mon Jun 10 14:30:00.123456 2025] [core:error] [pid 12345] [client 1.2.3.4:56789] AH00124: Request exceeded the limit

# ===== 3. Syslog / 系统日志格式 =====
# Jun 10 14:30:00 server01 sshd[12345]: Failed password for root from 1.2.3.4 port 22 ssh2

# ===== 4. 应用日志(常见自定义格式)=====
# 2025-06-10 14:30:00.123 [ERROR] [main-thread] com.example.Service - Connection timeout after 30000ms
#   $1          $2       $3     $4            $5                 $6

# ===== 实战:通用日志字段提取函数 =====
extract_log_fields() {
    local format="$1"      # nginx / syslog / app
    local line="$2"

    case "$format" in
        nginx)
            echo "$line" | awk '{print "IP:",$1," 状态:",$9," URL:",$7," 字节:",$10}'
            ;;
        syslog)
            echo "$line" | awk '{print "时间:",$1,$2,$3," 主机:",$4," 进程:",$5," 内容:",substr($0, index($0,$6))}'
            ;;
        app)
            echo "$line" | awk -F'[][]' '{
                split($1, t, " ")
                print "时间:", t[1], t[2], " 级别:", $2, " 消息:", $4
            }'
            ;;
    esac
}

# 7.1.2 访问量统计——PV/UV/QPS/带宽

#!/bin/bash

LOG="/var/log/nginx/access.log"

# ===== PV (Page View) —— 总请求量 =====
echo "=== PV 统计 ==="
wc -l "$LOG"
# 输出:1234567 /var/log/nginx/access.log

# 按小时统计 PV(看当天流量高峰)
awk '{split($4, t, ":"); hour=t[2]; count[hour]++}
END {
    for (h=0; h<=23; h++) printf "%02d:00 %6d\n", h, count[h]+0
}' "$LOG" | sort -t':' -k1n

# ===== UV (Unique Visitor) —— 独立 IP 数 =====
echo "=== UV 统计 ==="
awk '{print $1}' "$LOG" | sort -u | wc -l

# 按小时 UV(更精细)
awk '{split($4, t, ":"); hour=t[2]; uv[hour][$1]=1}
END {
    for (h=0; h<=23; h++) {
        count=0; for (ip in uv[h]) count++
        printf "%02d:00 %6d\n", h, count
    }
}' "$LOG" | sort -t':' -k1n

# ===== QPS (Query Per Second) —— 每秒请求数 =====
echo "=== QPS 统计 ==="
# 统计最近 1 分钟的 QPS
tail -1000 "$LOG" | awk -v interval=60 '{
    split($4, t, ":")
    timestamp = t[2]":"t[3]":"t[4]
    count[timestamp]++
}
END {
    total = 0; n = 0
    for (ts in count) { total += count[ts]; n++ }
    printf "最近 %d 条日志,平均 QPS: %.2f\n", 1000, total/(n*1.0)
}'

# 更精确的 QPS(按秒统计)
awk '{
    split($4, t, ":")
    sec = sprintf("%s:%s:%s:%s", t[2], t[3], t[4], substr(t[5],1,2))
    qps[sec]++
}
END {
    for (s in qps) if (qps[s] > 100) print s, qps[s]
}' "$LOG" | sort -k2 -rn | head -10
# 输出 QPS 超过 100 的时刻(用于定位瞬时高峰)

# ===== 带宽 / 流量统计 =====
echo "=== 流量统计 ==="
awk '{
    split($4, t, ":")
    hour = t[2]+0
    bytes_total[hour] += $10
    count[hour]++
}
END {
    printf "%-8s %12s %12s %12s\n", "小时", "请求数", "流量(MB)", "平均(KB/请求)"
    for (h=0; h<=23; h++) {
        if (count[h] > 0) {
            printf "%02d:00   %10d %10.2f %10.2f\n",
                h, count[h], bytes_total[h]/1048576,
                (bytes_total[h]/count[h])/1024
        }
    }
}' "$LOG" | sort -t':' -k1n

# ===== 一行汇总:今日核心指标 =====
awk 'BEGIN { split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", m)
    for (i in m) month_num[m[i]] = sprintf("%02d", i) }
{
    ip_count[$1]++
    if ($9 >= 500) err5xx++
    else if ($9 >= 400) err4xx++
    success++
    total_bytes += $10
    # 提取当前日期
    split($4, t, "/"); split(t[1], date, " ")
    today = date[2] "-" month_num[date[1]] "-" strftime("%Y")
}
END {
    printf "===== %s 日志日报 =====\n", today
    printf "总请求: %d\n", NR
    printf "独立IP: %d\n", length(ip_count)
    printf "成功: %d (%.1f%%)\n", success, success/NR*100
    printf "4xx: %d (%.1f%%)\n", err4xx+0, err4xx/NR*100
    printf "5xx: %d (%.1f%%)\n", err5xx+0, err5xx/NR*100
    printf "总流量: %.2f MB\n", total_bytes/1048576
}' "$LOG"

# 7.1.3 Top IP 分析与异常流量检测

#!/bin/bash

LOG="/var/log/nginx/access.log"

# ===== Top 10 访问 IP =====
awk '{print $1}' "$LOG" | sort | uniq -c | sort -rn | head -10

# ===== 异常 IP 检测:请求量远超平均的 IP =====
awk '{ip[$1]++; total++}
END {
    avg = total / length(ip)
    printf "总独立IP: %d, 平均每IP请求: %.1f\n\n", length(ip), avg
    printf "%-20s %8s %8s %8s\n", "IP", "请求数", "占比%", "倍数"
    for (i in ip) {
        if (ip[i] > avg * 3) {     # 超过平均 3 倍 = 异常
            printf "%-20s %8d %7.1f%% %7.1fx\n", i, ip[i], ip[i]/total*100, ip[i]/avg
        }
    }
}' "$LOG"

# ===== 检测恶意请求特征 =====
# 1. 单一 IP 大量 404(目录扫描)
awk '$9 == 404 {ip[$1]++}
END {
    for (i in ip) if (ip[i] > 50) printf "疑似扫描 IP: %s (404=%d次)\n", i, ip[i]
}' "$LOG"

# 2. 单一 IP 大量 5xx(攻击导致服务异常)
awk '$9 >= 500 {ip[$1]++; err[$1][$9]++}
END {
    for (i in ip) if (ip[i] > 20) {
        printf "异常 IP: %s (5xx=%d次)", i, ip[i]
        for (code in err[i]) printf " HTTP %d=%d", code, err[i][code]
        printf "\n"
    }
}' "$LOG"

# 3. User-Agent 异常(爬虫/工具扫描)
awk '{
    # 提取 User-Agent(Combined 格式的第 12 个字段)
    match($0, /"[^"]*"$/)
    ua = substr($0, RSTART+1, RLENGTH-2)
    ua_count[ua]++
}
END {
    for (u in ua_count) {
        if (ua_count[u] > 100) printf "%d 次 - %s\n", ua_count[u], u
    }
}' "$LOG" | sort -rn | head -10

# 7.1.4 状态码分布与异常监控

#!/bin/bash

LOG="/var/log/nginx/access.log"

# ===== 状态码分布统计 =====
awk '{code[$9]++; total++}
END {
    printf "%-10s %8s %8s\n", "状态码", "数量", "占比"
    printf "%-10s %8s %8s\n", "------", "----", "----"
    for (c in code) {
        printf "HTTP %-5s %8d %7.1f%%\n", c, code[c], code[c]/total*100
    }
}' "$LOG" | sort

# ===== 按小时看状态码变化(找故障时间点)=====
awk '{
    split($4, t, ":")
    hour = t[2]+0
    codes[hour][$9]++
}
END {
    printf "%-7s", "Hour"
    for (c=200; c<=504; c++) if (c==200||c==301||c==302||c==304||c==400||c==403||c==404||c==500||c==502||c==503||c==504) printf "%7d", c
    printf "\n"
    for (h=0; h<=23; h++) {
        printf "%02d:00 ", h
        for (c=200; c<=504; c++)
            if (c==200||c==301||c==302||c==304||c==400||c==403||c==404||c==500||c==502||c==503||c==504)
                printf "%7d", codes[h][c]+0
        printf "\n"
    }
}' "$LOG"

# ===== 5xx 错误实时监控(配合 tail -f)=====
# 使用方式:tail -f access.log | ./monitor_5xx.sh
cat > monitor_5xx.sh << 'EOF'
#!/bin/bash
THRESHOLD=10           # 每分钟超过 10 个 5xx 就告警
INTERVAL=60            # 统计周期(秒)
COUNT_FILE="/tmp/5xx_count_$$"

while read -r line; do
    status=$(echo "$line" | awk '{print $9}')
    if [[ "$status" -ge 500 ]] 2>/dev/null; then
        echo "$(date +%s)" >> "$COUNT_FILE"
    fi
done < <(tail -f /var/log/nginx/access.log) &
MONITOR_PID=$!

# 定时检查
while true; do
    sleep "$INTERVAL"
    now=$(date +%s)
    cutoff=$((now - INTERVAL))
    count=$(awk -v c="$cutoff" '$1 > c' "$COUNT_FILE" 2>/dev/null | wc -l)

    if [[ "$count" -ge "$THRESHOLD" ]]; then
        echo "[$(date)] ⚠️ 告警:过去 ${INTERVAL}s 内出现 $count 个 5xx 错误!"
        # 重置计数文件
        : > "$COUNT_FILE"
    fi
done

trap "kill $MONITOR_PID 2>/dev/null; rm -f $COUNT_FILE" EXIT
EOF

# 7.1.5 响应时间分析——揪出慢请求

Nginx 可配置 $request_time(处理时间)和 $upstream_response_time(上游响应时间),需先在 log_format 中添加:

log_format timed '$remote_addr - [$time_local] "$request" $status $body_bytes_sent '
                  '"$http_referer" "$http_user_agent" $request_time $upstream_response_time';
#!/bin/bash

LOG="/var/log/nginx/timed_access.log"
# 假设最后两列是 $request_time 和 $upstream_response_time

# ===== Top 10 慢请求 =====
awk '{print $(NF-1), $7}' "$LOG" | sort -rn | head -10

# ===== 响应时间分桶统计 =====
awk '{
    rt = $(NF-1) + 0
    if (rt < 0.1)     bucket["0~100ms"]++
    else if (rt < 0.5) bucket["100~500ms"]++
    else if (rt < 1)   bucket["0.5~1s"]++
    else if (rt < 3)   bucket["1~3s"]++
    else if (rt < 5)   bucket["3~5s"]++
    else               bucket["5s+"]++
    total++
}
END {
    for (b in bucket) printf "%-12s %8d %6.1f%%\n", b, bucket[b], bucket[b]/total*100
}' "$LOG" | sort -k2 -rn

# ===== 按 URL 统计平均响应时间 =====
awk '{
    url = $7
    rt = $(NF-1) + 0
    url_count[url]++
    url_rt_total[url] += rt
    if (rt > url_rt_max[url]) url_rt_max[url] = rt
}
END {
    printf "%-50s %8s %8s %10s\n", "URL", "次数", "平均(秒)", "最大(秒)"
    for (u in url_count) {
        if (url_count[u] >= 10) {   # 至少被请求 10 次才统计
            avg = url_rt_total[u] / url_count[u]
            printf "%-50s %8d %8.3f %10.3f\n", u, url_count[u], avg, url_rt_max[u]
        }
    }
}' "$LOG" | sort -k3 -rn | head -20

# ===== 检测上游响应慢($upstream_response_time > 阈值)=====
awk '$(NF) > 3 {    # 上游响应超过 3 秒
    print $1, $4, $7, "上游:", $(NF), "总耗时:", $(NF-1)
}' "$LOG"

# 7.1.6 错误日志提取、分类与告警

#!/bin/bash

ERROR_LOG="/var/log/nginx/error.log"

# ===== 提取所有错误并分类 =====
# 按错误类型分组统计
awk '{
    # 提取错误级别(第一个方括号中的内容)
    match($0, /\[([a-z]+)\]/, arr)
    level = arr[1]

    # 提取简要信息
    msg = $0
    gsub(/^.*\] /, "", msg)    # 去掉前缀
    msg = substr(msg, 1, 80)   # 截断

    errors[level]++
    if (errors[level] <= 3) samples[level] = samples[level] "\n  " msg
}
END {
    for (l in errors) {
        printf "[%s] %d 次\n%s\n\n", toupper(l), errors[l], samples[l]
    }
}' "$ERROR_LOG"

# ===== 错误趋势:每分钟错误数 =====
awk '{
    match($0, /[0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}/)
    minute = substr($0, RSTART, 16)
    count[minute]++
}
END {
    for (m in count) print m, count[m]
}' "$ERROR_LOG" | sort | tail -30

# ===== 应用中 OOM / 超时 / 连接失败 分类 =====
cat > classify_errors.sh << 'EOF'
#!/bin/bash
LOG="${1:-/var/log/app/error.log}"

declare -A PATTERNS=(
    ["OOM/内存溢出"]="OutOfMemory|java\.lang\.OutOfMemoryError|OOM killer"
    ["连接超时"]="timeout|timed out|Connection refused"
    ["数据库错误"]="SQLException|Connection pool|too many connections"
    ["磁盘空间"]="No space left|Disk quota"
    ["权限错误"]="Permission denied|Access denied"
    ["第三方API失败"]="api.*error|HTTP \d{3}.*error"
    ["空指针/非法参数"]="NullPointerException|IllegalArgument"
    ["死锁"]="Deadlock|Lock wait timeout"
)

echo "===== 错误分类统计 ====="
for category in "${!PATTERNS[@]}"; do
    count=$(grep -cP "${PATTERNS[$category]}" "$LOG" 2>/dev/null)
    if [[ "$count" -gt 0 ]]; then
        printf "%-20s %d 次\n" "$category" "$count"
    fi
done
EOF

# 7.2 系统监控

# 7.2.1 CPU 监控——使用率/负载/进程排行

#!/bin/bash

# ===== CPU 使用率(实时)=====
cpu_usage() {
    # 读取 /proc/stat 计算 CPU 使用率(用户态 + 系统态 + IO 等待)
    local idle1 idle2 total1 total2

    read -r _ user1 nice1 system1 idle1 iowait1 irq1 softirq1 steal1 _ < /proc/stat
    idle1=$((idle1 + iowait1))
    total1=$((user1 + nice1 + system1 + idle1 + iowait1 + irq1 + softirq1 + steal1))

    sleep 1

    read -r _ user2 nice2 system2 idle2 iowait2 irq2 softirq2 steal2 _ < /proc/stat
    idle2=$((idle2 + iowait2))
    total2=$((user2 + nice2 + system2 + idle2 + iowait2 + irq2 + softirq2 + steal2))

    local diff_idle=$((idle2 - idle1))
    local diff_total=$((total2 - total1))

    echo "scale=2; (1 - $diff_idle / $diff_total) * 100" | bc
}

echo "CPU 使用率: $(cpu_usage)%"

# ===== 系统负载(Load Average)=====
check_load() {
    local load1 load5 load15 cores
    read -r load1 load5 load15 _ < /proc/loadavg
    cores=$(nproc)

    echo "Load: 1min=${load1}  5min=${load5}  15min=${load15}  (CPU核数: ${cores})"

    # 负载告警:1min 负载 > CPU 核数
    if (( $(echo "$load1 > $cores * 1.5" | bc -l) )); then
        echo "⚠️ 1 分钟负载 $load1 超过核心数 $cores 的 1.5 倍!"
    fi
}
check_load

# ===== Top 5 CPU 进程 =====
ps aux --sort=-%cpu | head -6 | awk '{
    if (NR == 1) { printf "%-8s %-20s %6s %6s %s\n", "PID", "进程", "CPU%", "MEM%", "命令" }
    else { printf "%-8s %-20s %5.1f%% %5.1f%% %s\n", $2, substr($11,1,20), $3, $4, substr($0, index($0,$11)) }
}'

# ===== 持续 CPU 监控脚本 =====
cat > cpu_monitor.sh << 'SCRIPT'
#!/bin/bash
THRESHOLD=80                    # CPU 使用率阈值(%)
COOLDOWN=300                    # 冷却时间(秒),避免重复告警
LAST_ALERT_FILE="/tmp/last_cpu_alert"

while true; do
    # 获取 CPU 使用率(取 top 输出)
    cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
    cpu=${cpu%.*}                # 去掉小数

    if [[ "$cpu" -ge "$THRESHOLD" ]]; then
        now=$(date +%s)
        last_alert=$(cat "$LAST_ALERT_FILE" 2>/dev/null || echo 0)

        if (( now - last_alert > COOLDOWN )); then
            echo "[$(date)] ⚠️ CPU 使用率 ${cpu}%,超过阈值 ${THRESHOLD}%"
            echo "$now" > "$LAST_ALERT_FILE"
            # 记录 Top 进程快照
            ps aux --sort=-%cpu | head -6 >> /var/log/cpu_alert.log
        fi
    fi
    sleep 10
done
SCRIPT

# 7.2.2 内存监控——使用率/OOM 风险/Swap

#!/bin/bash

# ===== 内存概览 =====
free -h | awk '{
    if (NR==1) print "===== 内存概览 ====="
    if (NR==2) printf "总内存: %8s  已用: %8s  可用: %8s\n", $2, $3, $7
    if (NR==3) printf "Swap:    %8s  已用: %8s  空闲: %8s\n", $2, $3, $4
}'

# ===== 详细内存使用 =====
check_memory() {
    local total used avail percent

    # 从 /proc/meminfo 精确读取
    total=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
    avail=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
    used=$((total - avail))
    percent=$((used * 100 / total))

    echo "内存使用: ${percent}% (已用: $((used/1024))MB / 总共: $((total/1024))MB)"

    # OOM 风险告警:可用内存 < 10%
    if [[ "$percent" -ge 90 ]]; then
        echo "⚠️ 严重:可用内存不足 10%,存在 OOM 风险!"

        # 列出 OOM Score 最高的进程(最有可能被 kill 的)
        echo ""
        echo "OOM Killer 候选进程(Score 最高 Top 5):"
        for pid in $(ps aux --sort=-%mem | awk 'NR>1{print $2}' | head -5); do
            if [[ -f /proc/$pid/oom_score ]]; then
                score=$(cat /proc/$pid/oom_score)
                name=$(cat /proc/$pid/comm 2>/dev/null)
                echo "  PID:$pid  Score:$score  Name:$name"
            fi
        done
    fi
}
check_memory

# ===== Swap 使用监控 =====
check_swap() {
    local total used percent
    total=$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo)
    used=$(awk '/^SwapFree:/ {print $2}' /proc/meminfo)
    used=$((total - used))
    percent=$((used * 100 / total))

    echo "Swap 使用: ${percent}%"

    # Swap 使用 > 50% 意味着物理内存不足
    if [[ "$percent" -ge 50 ]]; then
        echo "⚠️ Swap 使用率 ${percent}%,物理内存可能不足,建议排查"
    fi
}
check_swap

# ===== Top 5 内存进程 =====
ps aux --sort=-%mem | head -6 | awk '{
    if (NR==1) printf "%-8s %6s %-20s %s\n", "PID", "MEM%", "进程", "RSS(MB)"
    else printf "%-8s %5.1f%% %-20s %6d\n", $2, $4, substr($11,1,20), $6/1024
}'

# 7.2.3 磁盘监控——空间/inode/IO

#!/bin/bash

# ===== 磁盘空间概览 =====
df -h | awk 'NR==1 || /^\/dev\//'            # 只显示物理磁盘

# ===== 磁盘使用率告警 =====
check_disk() {
    local threshold=80
    df -h | awk -v th="$threshold" 'NR>1 && /^\/dev\// {
        gsub(/%/, "", $5)
        if ($5+0 >= th) {
            printf "⚠️ 磁盘 %-20s 使用率: %d%%  (已用:%s / 总共:%s)\n",
                $6, $5, $3, $2
        }
    }'
}
check_disk

# ===== Inode 使用率检测 =====
# Inode 耗尽也会导致"磁盘满"的错误(No space left on device)
df -i | awk 'NR>1 && /^\/dev\// {
    gsub(/%/, "", $5)
    if ($5+0 >= 80) {
        printf "⚠️ Inode 使用率过高:%s %d%%\n", $6, $5
    }
}'

# ===== 磁盘 I/O 监控 =====
# 使用 iostat 查看 IO 负载
iostat -x 1 2 | awk 'END {
    printf "%-10s %8s %8s %8s %8s\n", "设备", "r/s", "w/s", "await", "util%"
}
NR > 6 && $1 ~ /^sd|^nvme/ {
    printf "%-10s %8.1f %8.1f %8.1f %7.1f%%\n", $1, $4, $5, $10, $14
}'
# await > 50ms 或 util% > 80% 说明磁盘 IO 瓶颈

# ===== 查找大文件/大目录 =====
find_large() {
    local dir="${1:-/}"
    echo "=== Top 10 大目录(${dir})==="
    du -sh "$dir"* 2>/dev/null | sort -hr | head -10

    echo ""
    echo "=== Top 10 大文件(${dir})==="
    find "$dir" -type f -size +100M -exec ls -lh {} \; 2>/dev/null \
        | awk '{print $5, $NF}' | sort -hr | head -10
}
# find_large /var

# ===== 磁盘增长趋势 =====
# 每小时记录一次,观察增长速率
cat > disk_trend.sh << 'SCRIPT'
#!/bin/bash
LOG="/var/log/disk_usage.log"

echo "$(date '+%Y-%m-%d %H:%M:%S') $(df -h / | awk 'NR==2{print $3, $5}')" >> "$LOG"

# 分析最近 24 小时增长率
if [[ $(wc -l < "$LOG") -ge 2 ]]; then
    tail -24 "$LOG" | awk '{
        gsub(/%/, "", $4)
        if (NR == 1) first = $4
        last = $4
    }
    END {
        growth = last - first
        printf "24小时增长: %.1f%% (从 %.1f%% 到 %.1f%%)\n", growth, first, last
        if (growth > 5) print "⚠️ 磁盘增长过快!"
    }'
fi
SCRIPT

# 7.2.4 网络监控——流量/连接/端口

#!/bin/bash

# ===== 网络接口流量 =====
# 读取 /proc/net/dev 统计收发流量
network_traffic() {
    local iface="${1:-eth0}"
    awk -v iface="$iface" '$1 ~ iface":" {
        printf "接收: %.2f MB  |  发送: %.2f MB\n", $2/1048576, $10/1048576
    }' /proc/net/dev
}
network_traffic eth0
# 注意:这是累计值,需两次采样相减算速率

# ===== 连接状态统计 =====
echo "=== TCP 连接状态 ==="
ss -tan | awk 'NR>1 {state[$1]++}
END {
    for (s in state) printf "%-12s %d\n", s, state[s]
}' | sort -k2 -rn

# 重点关注 TIME_WAIT(过多说明短连接多)和 CLOSE_WAIT(应用未正确关闭连接)
echo ""
ss -tan state time-wait | wc -l | xargs echo "TIME_WAIT:"
ss -tan state close-wait | wc -l | xargs echo "CLOSE_WAIT:"
ss -tan state established | wc -l | xargs echo "ESTABLISHED:"

# ===== 端口监听检查 =====
check_ports() {
    local required_ports=("80" "443" "22" "3306" "6379")
    echo "=== 端口检查 ==="
    for port in "${required_ports[@]}"; do
        if ss -tlnp | grep -q ":$port "; then
            echo "✅ 端口 $port 正常监听"
        else
            echo "❌ 端口 $port 未监听!"
        fi
    done
}
check_ports

# ===== 对外连接数 Top IP =====
ss -tnp | awk 'NR>1 {
    split($5, dst, ":")
    ip = dst[1]
    if (ip !~ /^127|^::1|^\*$/) count[ip]++
}
END {
    for (i in count) printf "%-20s %d\n", i, count[i]
}' | sort -k2 -rn | head -10

# ===== 网络连通性检测 =====
check_connectivity() {
    local targets=("8.8.8.8" "1.1.1.1" "baidu.com")
    for target in "${targets[@]}"; do
        if ping -c 1 -W 2 "$target" > /dev/null 2>&1; then
            echo "✅ $target 可达"
        else
            echo "❌ $target 不通"
        fi
    done
}
# check_connectivity

# 7.2.5 进程存活检测与自动重启

#!/bin/bash

# ===== 单个进程检测 =====
check_and_restart() {
    local process_name="$1"
    local start_cmd="$2"

    if pgrep -x "$process_name" > /dev/null; then
        echo "✅ $process_name 运行中 (PID: $(pgrep -x "$process_name" | tr '\n' ' '))"
        return 0
    else
        echo "❌ $process_name 已停止,尝试重启..."
        eval "$start_cmd"
        sleep 3
        if pgrep -x "$process_name" > /dev/null; then
            echo "✅ $process_name 重启成功"
            return 0
        else
            echo "💀 $process_name 重启失败!发送告警..."
            return 1
        fi
    fi
}

# check_and_restart "nginx" "systemctl start nginx"
# check_and_restart "redis-server" "systemctl start redis"

# ===== 进程守护脚本(while+sleep 模式)=====
cat > process_guard.sh << 'SCRIPT'
#!/bin/bash
# 用法:./process_guard.sh nginx "systemctl start nginx" 10
#      每隔 10 秒检测 nginx,挂了就重启

PROCESS="$1"
START_CMD="$2"
INTERVAL="${3:-30}"

while true; do
    if ! pgrep -x "$PROCESS" > /dev/null; then
        echo "[$(date)] $PROCESS 挂了,正在重启..." >> /var/log/guard.log
        eval "$START_CMD"
        sleep 3
        if pgrep -x "$PROCESS" > /dev/null; then
            echo "[$(date)] $PROCESS 重启成功" >> /var/log/guard.log
        else
            echo "[$(date)] 💀 $PROCESS 重启失败!" >> /var/log/guard.log
        fi
    fi
    sleep "$INTERVAL"
done
SCRIPT

# ===== 批量检测关键服务 =====
cat > service_health_check.sh << 'SCRIPT'
#!/bin/bash
# 拷贝为 /etc/cron.d/health_check 实现每 5 分钟检测

declare -A SERVICES=(
    ["nginx"]="systemctl restart nginx"
    ["php-fpm"]="systemctl restart php-fpm"
    ["mysqld"]="systemctl restart mysqld"
    ["redis-server"]="systemctl restart redis"
)

for svc in "${!SERVICES[@]}"; do
    if systemctl is-active --quiet "$svc"; then
        :
    else
        echo "[$(date)] $svc 服务异常,尝试重启" >> /var/log/service_guard.log
        ${SERVICES[$svc]}
    fi
done
SCRIPT

# ===== 检测进程是否假死(端口监听但无响应)=====
check_port_response() {
    local port="$1"
    local timeout=3
    local url="${2:-http://localhost:$port/}"

    if timeout "$timeout" curl -sf "$url" > /dev/null 2>&1; then
        echo "✅ 端口 $port 响应正常"
        return 0
    else
        echo "❌ 端口 $port 无响应(进程可能假死)"
        return 1
    fi
}

# 7.2.6 定时采集与资源趋势分析

#!/bin/bash

# ===== 资源采集脚本(cron 每 1 分钟执行一次)=====
# crontab: * * * * * /path/to/collect_metrics.sh

cat > collect_metrics.sh << 'SCRIPT'
#!/bin/bash
DATA_DIR="/var/log/metrics"
mkdir -p "$DATA_DIR"
TS=$(date '+%Y-%m-%d %H:%M:%S')

# CPU 使用率
cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print 100 - $8}')

# 内存使用率
mem_total=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
mem_avail=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
mem_used=$(( (mem_total - mem_avail) * 100 / mem_total ))

# 磁盘使用率
disk=$(df / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')

# 负载
load=$(awk '{print $1}' /proc/loadavg)

# 写入 CSV
echo "$TS,$cpu,${mem_used},$disk,$load" >> "$DATA_DIR/system.csv"

# 只保留最近 7 天的数据
find "$DATA_DIR" -name "*.csv" -mtime +7 -delete
SCRIPT

# ===== 趋势分析脚本 =====
cat > trend_analysis.sh << 'SCRIPT'
#!/bin/bash
DATA="$1"

echo "=== 过去 24 小时趋势 ==="
awk -F',' '{
    cpu += $2; mem += $3; disk += $4; load += $5; n++
}
END {
    printf "CPU 平均:   %.1f%%\n", cpu/n
    printf "内存平均:   %.1f%%\n", mem/n
    printf "磁盘平均:   %.1f%%\n", disk/n
    printf "负载平均:   %.2f\n", load/n
}' "$DATA"

echo ""
echo "=== 峰值 ==="
awk -F',' '{
    if ($2 > max_cpu)  { max_cpu = $2;  cpu_ts = $1 }
    if ($3 > max_mem)  { max_mem = $3;  mem_ts = $1 }
    if ($4 > max_disk) { max_disk = $4; disk_ts = $1 }
    if ($5 > max_load) { max_load = $5; load_ts = $1 }
}
END {
    printf "CPU 峰值:   %.1f%%  (%s)\n", max_cpu, cpu_ts
    printf "内存峰值:   %.1f%%  (%s)\n", max_mem, mem_ts
    printf "磁盘峰值:   %.1f%%  (%s)\n", max_disk, disk_ts
    printf "负载峰值:   %.2f  (%s)\n", max_load, load_ts
}' "$DATA"
SCRIPT

# 7.3 综合告警系统实战

# 7.3.1 告警策略设计——多级阈值与冷却机制

一个成熟的告警系统需要三个核心机制:

#!/bin/bash

# ===== 告警级别定义 =====
# WARN   = 预警,关注但不紧急(如 CPU 使用 70%)
# ERROR  = 需要立即处理(如 CPU 使用 90%)
# FATAL  = 致命,可能影响业务(如磁盘满、服务宕机)

# ===== 冷却机制 =====
# 同一个告警在冷却期内不重复发送(避免告警风暴)
COOLDOWN_WARN=600       # 10 分钟
COOLDOWN_ERROR=300      # 5 分钟
COOLDOWN_FATAL=60       # 1 分钟(致命告警需要持续提醒)

ALERT_STATE_DIR="/tmp/alert_state"
mkdir -p "$ALERT_STATE_DIR"

# ===== 通用告警函数 =====
send_alert() {
    local level="$1"       # WARN / ERROR / FATAL
    local target="$2"      # 告警目标(CPU/MEM/DISK...)
    local value="$3"       # 当前值
    local threshold="$4"   # 阈值
    local message="$5"     # 详细消息
    local now=$(date +%s)
    local state_file="$ALERT_STATE_DIR/${target}_${level}"

    # 检查冷却
    local cooldown_var="COOLDOWN_${level}"
    local cooldown=${!cooldown_var}
    local last_alert=$(cat "$state_file" 2>/dev/null || echo 0)

    if (( now - last_alert < cooldown )); then
        return  # 冷却中,不发送
    fi

    # 更新状态文件
    echo "$now" > "$state_file"

    # 输出告警
    echo "[$(date)] ${level} [${target}] 当前:${value} 阈值:${threshold} - ${message}"
    # 这里接入通知通道(见下一节)
}

# ===== 综合检查 =====
comprehensive_check() {
    # CPU
    local cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print 100 - $8}')
    cpu=${cpu%.*}
    if [[ "$cpu" -ge 95 ]]; then
        send_alert "FATAL" "CPU" "$cpu" "95" "CPU 使用率过高,服务可能卡死"
    elif [[ "$cpu" -ge 85 ]]; then
        send_alert "ERROR" "CPU" "$cpu" "85" "CPU 持续高负载"
    elif [[ "$cpu" -ge 70 ]]; then
        send_alert "WARN" "CPU" "$cpu" "70" "CPU 使用率偏高"
    fi

    # 内存
    local mem_total=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
    local mem_avail=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
    local mem_percent=$(( (mem_total - mem_avail) * 100 / mem_total ))
    if [[ "$mem_percent" -ge 95 ]]; then
        send_alert "FATAL" "MEM" "${mem_percent}%" "95" "可用内存严重不足,OOM 风险"
    elif [[ "$mem_percent" -ge 85 ]]; then
        send_alert "ERROR" "MEM" "${mem_percent}%" "85" "内存使用率过高"
    fi

    # 磁盘
    local disk=$(df / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
    if [[ "$disk" -ge 95 ]]; then
        send_alert "FATAL" "DISK" "${disk}%" "95" "磁盘即将写满!"
    elif [[ "$disk" -ge 85 ]]; then
        send_alert "ERROR" "DISK" "${disk}%" "85" "磁盘空间不足"
    elif [[ "$disk" -ge 75 ]]; then
        send_alert "WARN" "DISK" "${disk}%" "75" "磁盘使用率偏高"
    fi

    # 负载
    local load=$(awk '{print $1}' /proc/loadavg)
    local cores=$(nproc)
    if (( $(echo "$load > $cores * 2" | bc -l) )); then
        send_alert "ERROR" "LOAD" "$load" "$((cores*2))" "系统负载过高"
    elif (( $(echo "$load > $cores" | bc -l) )); then
        send_alert "WARN" "LOAD" "$load" "$cores" "负载超过核心数"
    fi
}

# comprehensive_check

# 7.3.2 告警通知——邮件/钉钉/企业微信

#!/bin/bash

# ===== 1. 邮件告警(通过 mail/mailx 命令)=====
send_email_alert() {
    local subject="$1"
    local body="$2"
    local recipients="${3:-admin@example.com}"

    echo "$body" | mail -s "$subject" "$recipients"
    # 或使用 sendmail / msmtp 等 MTA
}

# sendmail 方式(更可靠,特别是服务器上)
send_email_sendmail() {
    local to="$1"
    local subject="$2"
    local body="$3"

    /usr/sbin/sendmail -t << EMAIL_EOF
To: $to
Subject: $subject
Content-Type: text/plain; charset=utf-8

$body
EMAIL_EOF
}

# ===== 2. 钉钉机器人告警 =====
DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"

send_dingtalk() {
    local title="$1"
    local content="$2"
    local level="${3:-WARN}"

    # 根据级别选择颜色
    case "$level" in
        FATAL) color="#FF0000" ;;
        ERROR) color="#FF6600" ;;
        WARN)  color="#FFAA00" ;;
        *)     color="#0088CC" ;;
    esac

    local json=$(cat << JSON
{
    "msgtype": "markdown",
    "markdown": {
        "title": "$title",
        "text": "## ${level} <font color=${color}>${title}</font>\n\n${content}\n\n> 发送时间: $(date '+%Y-%m-%d %H:%M:%S')"
    }
}
JSON
)
    curl -s -X POST -H "Content-Type: application/json" -d "$json" "$DINGTALK_WEBHOOK" > /dev/null
}

# ===== 3. 企业微信机器人告警 =====
WECOM_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"

send_wecom() {
    local content="$1"
    local level="${2:-WARN}"

    # 根据级别造前缀 emoji
    case "$level" in
        FATAL) prefix="💀【致命】" ;;
        ERROR) prefix="🚨【错误】" ;;
        WARN)  prefix="⚠️ 【预警】" ;;
        *)     prefix="ℹ️ 【信息】" ;;
    esac

    local json="{\"msgtype\":\"text\",\"text\":{\"content\":\"${prefix}\n${content}\n\n时间: $(date '+%Y-%m-%d %H:%M:%S')\"}}"
    curl -s -X POST -H "Content-Type: application/json" -d "$json" "$WECOM_WEBHOOK" > /dev/null
}

# ===== 通用通知路由器(自动选渠道)=====
notify() {
    local title="$1"
    local body="$2"
    local level="$3"

    # 致命告警:所有渠道都发
    # 错误告警:钉钉 + 邮件
    # 预警:仅钉钉

    case "$level" in
        FATAL)
            send_dingtalk "$title" "$body" "FATAL"
            send_wecom "$title" "$body" "FATAL"
            send_email_sendmail "admin@example.com" "[FATAL] $title" "$body"
            ;;
        ERROR)
            send_dingtalk "$title" "$body" "ERROR"
            send_email_sendmail "admin@example.com" "[ERROR] $title" "$body"
            ;;
        WARN|*)
            send_dingtalk "$title" "$body" "WARN"
            ;;
    esac
}

# 7.3.3 一体化监控告警脚本——从采集到通知

#!/bin/bash
# ============================================
# 一体化监控告警脚本
# 用法:crontab 中配置 */5 * * * * /path/to/monitor.sh
# 推荐频率:每 5 分钟一次
# ============================================

set -euo pipefail

# ---- 配置区(修改为你自己的值)----
CONFIG_FILE="${HOME}/.monitor.conf"
if [[ -f "$CONFIG_FILE" ]]; then
    source "$CONFIG_FILE"
fi

# 阈值设置
CPU_WARN=80
CPU_ERROR=90
MEM_WARN=85
MEM_ERROR=95
DISK_WARN=80
DISK_ERROR=90
LOAD_RATIO=1.5       # 负载超过核心数 * 此值的倍数

# 告警冷却(秒)
COOLDOWN=300          # 5 分钟

# Webhook
DING_WEBHOOK="${DING_WEBHOOK:-}"
WECOM_WEBHOOK="${WECOM_WEBHOOK:-}"
EMAIL_TO="${EMAIL_TO:-root@localhost}"

# 日志
LOG_FILE="/var/log/system_monitor.log"
ALERT_DIR="/tmp/monitor_alerts"
mkdir -p "$ALERT_DIR"

# ---- 工具函数 ----
log()          { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; echo "$*"; }
get_metric()   { cat "$ALERT_DIR/${1}.last" 2>/dev/null || echo ""; }
set_metric()   { echo "$2" > "$ALERT_DIR/${1}.last"; }

is_cooldown() {
    local key="$1"
    local ts_file="$ALERT_DIR/${key}.ts"
    local now=$(date +%s)
    local last=$(cat "$ts_file" 2>/dev/null || echo 0)
    if (( now - last < COOLDOWN )); then return 0; fi
    echo "$now" > "$ts_file"
    return 1
}

# ---- 通知通道 ----
ding_notify() {
    [[ -z "$DING_WEBHOOK" ]] && return
    curl -s -X POST -H "Content-Type: application/json" \
        -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[$(hostname)] $1\"}}" \
        "$DING_WEBHOOK" > /dev/null
}

wecom_notify() {
    [[ -z "$WECOM_WEBHOOK" ]] && return
    curl -s -X POST -H "Content-Type: application/json" \
        -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[$(hostname)] $1\"}}" \
        "$WECOM_WEBHOOK" > /dev/null
}

alert() {
    local level="$1"; shift
    local msg="$*"

    log "[${level}] $msg"

    # 冷却检查(FATAL 不停发)
    if [[ "$level" != "FATAL" ]]; then
        local key=$(echo "$msg" | md5sum | cut -c1-8)
        is_cooldown "$key" && return
    fi

    # 发送通知
    case "$level" in
        FATAL) ding_notify "💀 [致命] $msg"; wecom_notify "💀 [致命] $msg" ;;
        ERROR) ding_notify "🚨 [错误] $msg" ;;
        WARN)  ding_notify "⚠️  [预警] $msg" ;;
    esac
}

# ---- 采集函数 ----
collect_cpu() {
    local cpu=$(top -bn1 2>/dev/null | grep "Cpu(s)" | awk '{print 100 - $8}' 2>/dev/null || echo "0")
    cpu=${cpu%.*}
    local prev=$(get_metric "cpu")

    set_metric "cpu" "$cpu"

    if [[ "$cpu" -ge "$CPU_ERROR" ]]; then
        alert "ERROR" "CPU 使用率 ${cpu}%,阈值 ${CPU_ERROR}%"
    elif [[ "$cpu" -ge "$CPU_WARN" ]]; then
        alert "WARN" "CPU 使用率 ${cpu}%,阈值 ${CPU_WARN}%"
    fi
    log "  CPU: ${cpu}%"
}

collect_memory() {
    local total=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
    local avail=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
    local percent=$(( (total - avail) * 100 / total ))
    set_metric "mem" "$percent"

    if [[ "$percent" -ge "$MEM_ERROR" ]]; then
        alert "ERROR" "内存使用 ${percent}%,阈值 ${MEM_ERROR}%"
    elif [[ "$percent" -ge "$MEM_WARN" ]]; then
        alert "WARN" "内存使用 ${percent}%,阈值 ${MEM_WARN}%"
    fi
    log "  MEM: ${percent}%"
}

collect_disk() {
    while IFS= read -r line; do
        local device=$(echo "$line" | awk '{print $1}')
        local percent=$(echo "$line" | awk '{gsub(/%/,"",$5); print $5}')
        local mount=$(echo "$line" | awk '{print $6}')

        if [[ "$percent" -ge "$DISK_ERROR" ]]; then
            alert "ERROR" "磁盘 ${mount} 使用 ${percent}%,阈值 ${DISK_ERROR}%"
        elif [[ "$percent" -ge "$DISK_WARN" ]]; then
            alert "WARN" "磁盘 ${mount} 使用 ${percent}%,阈值 ${DISK_WARN}%"
        fi
        log "  DISK ${mount}: ${percent}%"
    done < <(df -h | awk 'NR>1 && /^\/dev\//')
}

collect_load() {
    local load=$(awk '{print $1}' /proc/loadavg)
    local cores=$(nproc)
    set_metric "load" "$load"

    local threshold=$(echo "$cores * $LOAD_RATIO" | bc)
    if (( $(echo "$load > $threshold" | bc -l) )); then
        alert "WARN" "负载 ${load},核心数 ${cores},阈值 ${threshold}"
    fi
    log "  LOAD: ${load} (cores: ${cores})"
}

collect_services() {
    local services=("nginx" "php-fpm" "mysqld" "redis-server" "sshd" "docker")
    for svc in "${services[@]}"; do
        if systemctl is-active --quiet "$svc" 2>/dev/null; then
            :
        else
            if systemctl list-unit-files "$svc.service" &>/dev/null; then
                alert "ERROR" "服务 $svc 未运行!尝试重启..."
                systemctl restart "$svc" 2>/dev/null || \
                    alert "FATAL" "服务 $svc 重启失败!"
            fi
        fi
    done
}

# ---- 主流程 ----
main() {
    log "===== 开始巡检 ====="
    collect_load
    collect_cpu
    collect_memory
    collect_disk
    collect_services
    log "===== 巡检结束 ====="
}

main

# 7.3.4 新手陷阱 Top 5

陷阱 1:top -bn1 第一次不准

# ❌ top 第一次采样可能不准确
top -bn1 | grep "Cpu(s)"

# ✅ 取第 2 次迭代或者用 /proc/stat 直接算
top -bn2 | grep "Cpu(s)" | tail -1
# 或用 mpstat:mpstat 1 1 | awk 'END{print 100-$NF}'

陷阱 2:内存监控用错指标

# ❌ 用 free 的 "available" 行看可用内存
# MemFree 不等于真实可用(Linux 会用空闲内存做缓存)

# ❌ 错误:认为 buff/cache 是"已用内存"
free -m | awk 'NR==2{printf "%d%%\n", $3*100/$2}'

# ✅ 用 MemAvailable(精确反映可用于新进程的内存)
awk '/^MemAvailable:/ {avail=$2} /^MemTotal:/ {total=$2}
     END {printf "%d%%\n", (total-avail)*100/total}' /proc/meminfo

陷阱 3:df 和 du 统计不一致

# df 显示磁盘已满,但 du 统计文件远远没到
# 原因:已删除但仍被进程持有的文件(deleted but open)

# 检查是否有大量被删除但仍占用的文件
lsof | grep deleted | wc -l
lsof | grep deleted | awk '{sum+=$7} END {printf "%.2f MB\n", sum/1048576}'
# 重启持有这些文件的进程即可释放空间

陷阱 4:crontab 环境变量不同

# ❌ crontab 执行脚本时 PATH 极短,很多命令找不到
# 脚本里用绝对路径或先 source profile

# ✅ 在脚本开头明确设置
#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
source /etc/profile 2>/dev/null || true

陷阱 5:告警脚本本身占用资源

# ❌ 监控脚本内部有死循环、递归、未限制频率的日志写入
# 结果:监控脚本变成系统最大负担

# ✅ 设置资源限制
ulimit -t 30         # CPU 时间限制 30 秒
ulimit -v 262144     # 虚拟内存限制 256MB
# 或用 timeout 包裹:timeout 30 monitor.sh

# 7.3.5 综合思考题

  1. 日志关联分析:如何从 Nginx 访问日志 + 错误日志中,关联找出导致 502 错误的真正元凶(比如上游 PHP-FPM 进程在那一秒正好超时)?

  2. OOM 预防:写一个脚本,当可用内存低于 10% 时,主动找出占用内存最多的进程,发送告警并记录 fast 快照(/proc//smaps 摘要),以便事后分析。

  3. 磁盘增长率预警:如何在磁盘使用率 70% 时,根据最近 7 天的增长率预测"磁盘将在多少天后满",提前发出预警?

  4. 告警抑制链:设计一个逻辑——当"网络不通"告警触发时,自动抑制"服务不可达"类型的下游告警,避免告警风暴。

  5. 综合脚本:编写一个 health_check.sh,检测以下项并生成 JSON 格式的健康报告:

    • 磁盘使用率 / Inode 使用率
    • 内存使用率 / Swap 使用率
    • CPU 使用率 / 负载
    • 关键端口(80/443/3306/6379)监听状态
    • 关键进程(nginx/mysql/redis/php-fpm)存活状态
    • 最近的 5xx 错误数(过去 5 分钟)
#Shell#运维
上次更新: 2026/06/28, 17:55:19
文件查找与统计
备份进程与磁盘

← 文件查找与统计 备份进程与磁盘→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式